messenger, desk-channel, telegram, mesh-config take the channel benches' shapes from the operator-channel contract and its client (B5d; ADR 0634 §1, §6; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -35,7 +35,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/channel"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
)
|
||||
|
||||
// Kind is this channel's kind on both benches.
|
||||
@@ -59,19 +60,19 @@ const StandingEvery = 5 * time.Minute
|
||||
|
||||
// Standing is what the desk says of itself: the bench's word, and when a named machine's notifier last lost
|
||||
// the answers its notifications waited for (its bundle started again). The router then says again what is
|
||||
// still open and unanswered (issue 384). It belongs in channel.Standing once the SDK carries it; until then the
|
||||
// still open and unanswered (issue 384). It belongs in oc.Standing once the SDK carries it; until then the
|
||||
// field travels as JSON the router reads under the same name. Zero (as "0001-01-01T00:00:00Z") until then.
|
||||
type Standing struct {
|
||||
channel.Standing
|
||||
oc.Standing
|
||||
AnswersLostAt time.Time `json:"answers-lost-at"`
|
||||
}
|
||||
|
||||
// Shown is one of the router's messages as the desk shows it: the message, and its notification on each
|
||||
// machine.
|
||||
type Shown struct {
|
||||
Message channel.Message `json:"message"`
|
||||
On map[string]int `json:"on"`
|
||||
Sent time.Time `json:"sent"`
|
||||
Message oc.Message `json:"message"`
|
||||
On map[string]int `json:"on"`
|
||||
Sent time.Time `json:"sent"`
|
||||
}
|
||||
|
||||
// Store keeps what is shown, by the router's handle.
|
||||
@@ -146,7 +147,7 @@ func (d *Desk) Take(subject string, body json.RawMessage) error {
|
||||
d.Logf("[desk-channel] work on %s is not this kind's; taken and dropped", subject)
|
||||
return nil
|
||||
}
|
||||
var m channel.Message
|
||||
var m oc.Message
|
||||
if err := json.Unmarshal(body, &m); err != nil || m.Handle == "" {
|
||||
d.Logf("[desk-channel] work on %s could not be read; taken and dropped", subject)
|
||||
return nil
|
||||
@@ -158,8 +159,8 @@ func (d *Desk) Take(subject string, body json.RawMessage) error {
|
||||
// would appear later, to whoever is at the screen then.
|
||||
seeing, why := d.seeing()
|
||||
if len(seeing) == 0 {
|
||||
f := channel.Failed{Handle: m.Handle, Why: "not shown: " + why, At: d.Now()}
|
||||
return d.Publish(channel.IntakeSubject(channel.FailedWhat, Kind), f, "failed."+m.Handle+"."+strconv.FormatInt(d.Now().UnixNano(), 10))
|
||||
f := oc.Failed{Handle: m.Handle, Why: "not shown: " + why, At: d.Now()}
|
||||
return d.Publish(occ.IntakeSubject(occ.FailedWhat, Kind), f, "failed."+m.Handle+"."+strconv.FormatInt(d.Now().UnixNano(), 10))
|
||||
}
|
||||
machines = seeing
|
||||
}
|
||||
@@ -176,7 +177,7 @@ func (d *Desk) Take(subject string, body json.RawMessage) error {
|
||||
// shown (a reminder, an escalation), replace the notification it had on each machine — dunst's replace id —
|
||||
// so a problem is one notification updated in place, never one more beside it. An edit stays quiet where
|
||||
// it was shown; a message said again is shown anew where it was not.
|
||||
edit := parts[4] == channel.Edit
|
||||
edit := parts[4] == occ.Edit
|
||||
var before *Shown
|
||||
switch {
|
||||
case edit:
|
||||
@@ -226,8 +227,8 @@ func (d *Desk) Take(subject string, body json.RawMessage) error {
|
||||
if len(machines) == 0 {
|
||||
why = "no machine to show it on: the setting desktop-machines names none"
|
||||
}
|
||||
f := channel.Failed{Handle: m.Handle, Why: why, At: d.Now()}
|
||||
if err := d.Publish(channel.IntakeSubject(channel.FailedWhat, Kind), f, "failed."+m.Handle+"."+strconv.FormatInt(d.Now().UnixNano(), 10)); err != nil {
|
||||
f := oc.Failed{Handle: m.Handle, Why: why, At: d.Now()}
|
||||
if err := d.Publish(occ.IntakeSubject(occ.FailedWhat, Kind), f, "failed."+m.Handle+"."+strconv.FormatInt(d.Now().UnixNano(), 10)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -322,7 +323,7 @@ func sameShown(a, b Shown) bool {
|
||||
}
|
||||
|
||||
// show puts one message on one machine's notifier, in place of `replace` when it names one.
|
||||
func (d *Desk) show(machine string, m channel.Message, replace int) (int, error) {
|
||||
func (d *Desk) show(machine string, m oc.Message, replace int) (int, error) {
|
||||
urgency := "normal"
|
||||
switch {
|
||||
case m.Urgent:
|
||||
@@ -376,12 +377,12 @@ func (d *Desk) Chosen(event, machine, ticket string) {
|
||||
}
|
||||
handle, shown := d.find(ticket)
|
||||
if shown == nil {
|
||||
_, _ = d.show(machine, channel.Message{Title: "That answer was not taken",
|
||||
_, _ = d.show(machine, oc.Message{Title: "That answer was not taken",
|
||||
Body: "It belongs to no message shown here any more. Nothing was done.", Silent: true}, 0)
|
||||
return
|
||||
}
|
||||
if _, here := shown.On[machine]; !here {
|
||||
_, _ = d.show(machine, channel.Message{Title: "That answer was not taken",
|
||||
_, _ = d.show(machine, oc.Message{Title: "That answer was not taken",
|
||||
Body: "It was not shown on " + machine + ". Nothing was done.", Silent: true}, 0)
|
||||
return
|
||||
}
|
||||
@@ -395,11 +396,11 @@ func (d *Desk) Chosen(event, machine, ticket string) {
|
||||
}
|
||||
d.clickedOn[id] = machine
|
||||
d.mu.Unlock()
|
||||
c := channel.Choice{ID: id, Ticket: ticket, Handle: handle, At: d.Now(),
|
||||
Sender: channel.Sender{Identity: machine, Display: machine, Authenticated: false}}
|
||||
if err := d.Publish(channel.IntakeSubject(channel.Chosen, Kind), c, id); err != nil {
|
||||
c := oc.Choice{ID: id, Ticket: ticket, Handle: handle, At: d.Now(),
|
||||
Sender: oc.Sender{Identity: machine, Display: machine, Authenticated: false}}
|
||||
if err := d.Publish(occ.IntakeSubject(occ.Chosen, Kind), c, id); err != nil {
|
||||
d.Logf("[desk-channel] a choice could not be said to the router: %v", err)
|
||||
_, _ = d.show(machine, channel.Message{Title: "That answer could not be taken now",
|
||||
_, _ = d.show(machine, oc.Message{Title: "That answer could not be taken now",
|
||||
Body: "The mesh did not take it. Try again in a minute.", Silent: true}, 0)
|
||||
}
|
||||
}
|
||||
@@ -508,7 +509,7 @@ func (d *Desk) seeing() ([]string, string) {
|
||||
// (the review of 2026-10-09). The router shows no link's code while the desk is not ready.
|
||||
func (d *Desk) Standing() Standing {
|
||||
d.init()
|
||||
s := Standing{Standing: channel.Standing{At: d.Now()}, AnswersLostAt: d.answersLostAt()}
|
||||
s := Standing{Standing: oc.Standing{At: d.Now()}, AnswersLostAt: d.answersLostAt()}
|
||||
if _, why := d.seeing(); why != "" {
|
||||
s.Why = why
|
||||
return s
|
||||
@@ -542,7 +543,7 @@ func (d *Desk) SayStanding() {
|
||||
if same {
|
||||
return
|
||||
}
|
||||
if err := d.Publish(channel.IntakeSubject(channel.StandingWhat, Kind), s, "standing."+strconv.FormatInt(s.At.UnixNano(), 10)); err != nil {
|
||||
if err := d.Publish(occ.IntakeSubject(occ.StandingWhat, Kind), s, "standing."+strconv.FormatInt(s.At.UnixNano(), 10)); err != nil {
|
||||
d.Logf("[desk-channel] could not say whether it can send: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/channel"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
)
|
||||
|
||||
type memShown map[string]Shown
|
||||
@@ -62,10 +63,10 @@ func newDesk(t *testing.T, machines ...string) (*Desk, *[]notified, map[string][
|
||||
return d, &shown, said, store
|
||||
}
|
||||
|
||||
func take(t *testing.T, d *Desk, verb string, m channel.Message) {
|
||||
func take(t *testing.T, d *Desk, verb string, m oc.Message) {
|
||||
t.Helper()
|
||||
raw, _ := json.Marshal(m)
|
||||
if err := d.Take(channel.ChannelSubject(verb, Kind), raw); err != nil {
|
||||
if err := d.Take(occ.ChannelSubject(verb, Kind), raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -74,16 +75,16 @@ func take(t *testing.T, d *Desk, verb string, m channel.Message) {
|
||||
// says a click as a choice the router judges — only on a machine the message was shown on.
|
||||
func TestAClickOnAMachineTheMessageWasShownOnIsSaidToTheRouter(t *testing.T) {
|
||||
d, shown, said, _ := newDesk(t, "laptop", "away-machine")
|
||||
take(t, d, channel.Show, channel.Message{Handle: "m1", Title: "3 failed services", Body: "Needs you: <mend> them.",
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
take(t, d, occ.Show, oc.Message{Handle: "m1", Title: "3 failed services", Body: "Needs you: <mend> them.",
|
||||
Buttons: []oc.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
if len(*shown) != 1 || (*shown)[0].machine != "laptop" || (*shown)[0].args["expire_ms"] != 0 ||
|
||||
(*shown)[0].args["body"] != "Needs you: <mend> them." {
|
||||
t.Fatalf("shown %+v", *shown)
|
||||
}
|
||||
d.Chosen("e1", "laptop", "t1")
|
||||
d.Chosen("e1", "laptop", "t1") // a redelivery
|
||||
got := said[channel.IntakeSubject(channel.Chosen, Kind)]
|
||||
var c channel.Choice
|
||||
got := said[occ.IntakeSubject(occ.Chosen, Kind)]
|
||||
var c oc.Choice
|
||||
if len(got) != 1 || json.Unmarshal(got[0], &c) != nil || c.Ticket != "t1" || c.Handle != "m1" ||
|
||||
c.Sender.Identity != "laptop" || c.Sender.Authenticated {
|
||||
t.Fatalf("said %s", got)
|
||||
@@ -91,14 +92,14 @@ func TestAClickOnAMachineTheMessageWasShownOnIsSaidToTheRouter(t *testing.T) {
|
||||
// Elsewhere it is refused, there, and nothing is said to the router.
|
||||
d.Chosen("e2", "workstation", "t1")
|
||||
d.Chosen("e3", "laptop", "forged")
|
||||
if len(said[channel.IntakeSubject(channel.Chosen, Kind)]) != 1 {
|
||||
if len(said[occ.IntakeSubject(occ.Chosen, Kind)]) != 1 {
|
||||
t.Error("a click not shown there was said")
|
||||
}
|
||||
if last := (*shown)[len(*shown)-1]; last.machine != "laptop" || last.args["summary"] != "That answer was not taken" {
|
||||
t.Errorf("the refusal was not shown where it was chosen: %+v", last)
|
||||
}
|
||||
// The router's reply to the click is shown on the machine it was made on.
|
||||
take(t, d, channel.Send, channel.Message{Handle: "r1", Title: "Details: 3 failed services", Reply: c.ID})
|
||||
take(t, d, occ.Send, oc.Message{Handle: "r1", Title: "Details: 3 failed services", Reply: c.ID})
|
||||
if last := (*shown)[len(*shown)-1]; last.machine != "laptop" || last.args["summary"] != "Details: 3 failed services" {
|
||||
t.Errorf("%+v", last)
|
||||
}
|
||||
@@ -107,8 +108,8 @@ func TestAClickOnAMachineTheMessageWasShownOnIsSaidToTheRouter(t *testing.T) {
|
||||
// The note follows the body after a blank line: the desk has no smaller text, and leaves nothing out (issue 639).
|
||||
func TestTheNoteFollowsTheBody(t *testing.T) {
|
||||
d, shown, _, _ := newDesk(t, "laptop")
|
||||
take(t, d, channel.Show, channel.Message{Handle: "m1", Title: "q", Body: "The summary.", Note: "Asked by mesh-knowledge at 16:05.",
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
take(t, d, occ.Show, oc.Message{Handle: "m1", Title: "q", Body: "The summary.", Note: "Asked by mesh-knowledge at 16:05.",
|
||||
Buttons: []oc.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
if last := (*shown)[len(*shown)-1]; last.args["body"] != "The summary.\n\nAsked by mesh-knowledge at 16:05." {
|
||||
t.Errorf("the body: %q", last.args["body"])
|
||||
}
|
||||
@@ -116,8 +117,8 @@ func TestTheNoteFollowsTheBody(t *testing.T) {
|
||||
|
||||
func TestAnEditReplacesTheNotification(t *testing.T) {
|
||||
d, shown, _, _ := newDesk(t, "laptop")
|
||||
take(t, d, channel.Show, channel.Message{Handle: "m1", Title: "q", Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
take(t, d, channel.Edit, channel.Message{Handle: "m1", Title: "Silence for a week: chosen"})
|
||||
take(t, d, occ.Show, oc.Message{Handle: "m1", Title: "q", Buttons: []oc.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
take(t, d, occ.Edit, oc.Message{Handle: "m1", Title: "Silence for a week: chosen"})
|
||||
if last := (*shown)[len(*shown)-1]; last.args["replace_id"] != 1 || last.args["actions"] != nil {
|
||||
t.Errorf("the edit: %+v", last.args)
|
||||
}
|
||||
@@ -134,17 +135,17 @@ func TestANotifierThatStartedAgainIsShownNothingFromMemory(t *testing.T) {
|
||||
d, shown, said, store := newDesk(t, "laptop")
|
||||
d.Paused("laptop", false)
|
||||
d.SayStanding()
|
||||
take(t, d, channel.Show, channel.Message{Handle: "open", Title: "2 failed services on shanks",
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t-open"}}})
|
||||
take(t, d, channel.Show, channel.Message{Handle: "silenced", Title: "Agent-home shrank",
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t-silenced"}}})
|
||||
take(t, d, channel.Edit, channel.Message{Handle: "silenced", Title: "Silenced until Saturday: Agent-home shrank", Silent: true})
|
||||
take(t, d, channel.Show, channel.Message{Handle: "resolved", Title: "Questions for you not delivered",
|
||||
Buttons: []channel.Button{{Label: "Details", Ticket: "t-resolved"}}})
|
||||
take(t, d, channel.Edit, channel.Message{Handle: "resolved", Title: "Resolved: questions for you are delivered", Silent: true})
|
||||
take(t, d, occ.Show, oc.Message{Handle: "open", Title: "2 failed services on shanks",
|
||||
Buttons: []oc.Button{{Label: "Silence for a week", Ticket: "t-open"}}})
|
||||
take(t, d, occ.Show, oc.Message{Handle: "silenced", Title: "Agent-home shrank",
|
||||
Buttons: []oc.Button{{Label: "Silence for a week", Ticket: "t-silenced"}}})
|
||||
take(t, d, occ.Edit, oc.Message{Handle: "silenced", Title: "Silenced until Saturday: Agent-home shrank", Silent: true})
|
||||
take(t, d, occ.Show, oc.Message{Handle: "resolved", Title: "Questions for you not delivered",
|
||||
Buttons: []oc.Button{{Label: "Details", Ticket: "t-resolved"}}})
|
||||
take(t, d, occ.Edit, oc.Message{Handle: "resolved", Title: "Resolved: questions for you are delivered", Silent: true})
|
||||
openID := store["open"].On["laptop"]
|
||||
before := len(*shown)
|
||||
standings := len(said[channel.IntakeSubject(channel.StandingWhat, Kind)])
|
||||
standings := len(said[occ.IntakeSubject(occ.StandingWhat, Kind)])
|
||||
later := d.Now().Add(time.Hour)
|
||||
d.Now = func() time.Time { return later }
|
||||
d.Paused("laptop", false)
|
||||
@@ -157,25 +158,25 @@ func TestANotifierThatStartedAgainIsShownNothingFromMemory(t *testing.T) {
|
||||
if store["open"].On["laptop"] != openID {
|
||||
t.Fatalf("the open message's notification was forgotten: %+v", store["open"].On)
|
||||
}
|
||||
got := said[channel.IntakeSubject(channel.StandingWhat, Kind)]
|
||||
got := said[occ.IntakeSubject(occ.StandingWhat, Kind)]
|
||||
var s Standing
|
||||
if len(got) != standings+1 || json.Unmarshal(got[len(got)-1], &s) != nil || !s.Ready || !s.AnswersLostAt.Equal(later) {
|
||||
t.Fatalf("the standing after the start: %d said, last %s", len(got)-standings, got[len(got)-1])
|
||||
}
|
||||
// The router says the open one again under its handle: in place of its notification, with its answers.
|
||||
take(t, d, channel.Show, channel.Message{Handle: "open", Title: "2 failed services on shanks",
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t-open"}}})
|
||||
take(t, d, occ.Show, oc.Message{Handle: "open", Title: "2 failed services on shanks",
|
||||
Buttons: []oc.Button{{Label: "Silence for a week", Ticket: "t-open"}}})
|
||||
last := (*shown)[len(*shown)-1]
|
||||
if len(*shown) != before+1 || last.args["replace_id"] != openID || last.args["actions"] == nil {
|
||||
t.Fatalf("the open message said again: %+v", last.args)
|
||||
}
|
||||
d.Chosen("e2", "laptop", "t-open")
|
||||
if len(said[channel.IntakeSubject(channel.Chosen, Kind)]) != 1 {
|
||||
if len(said[occ.IntakeSubject(occ.Chosen, Kind)]) != 1 {
|
||||
t.Error("a click on the message said again was not said to the router")
|
||||
}
|
||||
// The same start event again (a redelivery) does nothing more.
|
||||
standings = len(said[channel.IntakeSubject(channel.StandingWhat, Kind)])
|
||||
if n := d.Started("e-start", "laptop"); n != 0 || len(said[channel.IntakeSubject(channel.StandingWhat, Kind)]) != standings {
|
||||
standings = len(said[occ.IntakeSubject(occ.StandingWhat, Kind)])
|
||||
if n := d.Started("e-start", "laptop"); n != 0 || len(said[occ.IntakeSubject(occ.StandingWhat, Kind)]) != standings {
|
||||
t.Errorf("a redelivered start was acted on again: %d", n)
|
||||
}
|
||||
// A desk none of whose notifiers started again says a zero time, which the router reads as none.
|
||||
@@ -187,8 +188,8 @@ func TestANotifierThatStartedAgainIsShownNothingFromMemory(t *testing.T) {
|
||||
|
||||
func TestNoMachineThatShowsItIsAFailureSaidToTheRouter(t *testing.T) {
|
||||
d, _, said, _ := newDesk(t, "away-machine")
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "x"})
|
||||
if f := said[channel.IntakeSubject(channel.FailedWhat, Kind)]; len(f) != 1 || !strings.Contains(string(f[0]), "no session answers") ||
|
||||
take(t, d, occ.Send, oc.Message{Handle: "m1", Title: "x"})
|
||||
if f := said[occ.IntakeSubject(occ.FailedWhat, Kind)]; len(f) != 1 || !strings.Contains(string(f[0]), "no session answers") ||
|
||||
strings.Contains(string(f[0]), `"permanent":true`) {
|
||||
t.Errorf("%s", f)
|
||||
}
|
||||
@@ -229,7 +230,7 @@ func TestItPromisesNoVerifiedSender(t *testing.T) {
|
||||
func TestASecretMessageIsShownAndNotKept(t *testing.T) {
|
||||
d, shown, _, store := newDesk(t, "laptop")
|
||||
d.Paused("laptop", false)
|
||||
take(t, d, channel.Send, channel.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true})
|
||||
take(t, d, occ.Send, oc.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true})
|
||||
if len(*shown) != 1 {
|
||||
t.Fatal("not shown")
|
||||
}
|
||||
@@ -252,8 +253,8 @@ func TestASecretIsShownOnlyWhereTheNotifierIsHeardUnpausedAndNeverKept(t *testin
|
||||
if s := d.Standing(); !s.Ready {
|
||||
t.Fatalf("not ready with laptop heard unpaused: %+v", s)
|
||||
}
|
||||
code := channel.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true}
|
||||
take(t, d, channel.Send, code)
|
||||
code := oc.Message{Handle: "code", Title: "Link a Telegram account as you", Body: "123 456", Secret: true}
|
||||
take(t, d, occ.Send, code)
|
||||
if len(*shown) != 1 || (*shown)[0].machine != "laptop" || (*shown)[0].args["secret"] != true {
|
||||
t.Fatalf("the code was shown as %+v", *shown)
|
||||
}
|
||||
@@ -262,11 +263,11 @@ func TestASecretIsShownOnlyWhereTheNotifierIsHeardUnpausedAndNeverKept(t *testin
|
||||
}
|
||||
// Paused: not shown anywhere, said as failed, and the desk is not ready.
|
||||
d.Paused("laptop", true)
|
||||
take(t, d, channel.Send, code)
|
||||
take(t, d, occ.Send, code)
|
||||
if len(*shown) != 1 {
|
||||
t.Fatalf("the code was shown on a paused notifier: %+v", *shown)
|
||||
}
|
||||
if f := said[channel.IntakeSubject(channel.FailedWhat, Kind)]; len(f) != 1 || !strings.Contains(string(f[0]), "paused") {
|
||||
if f := said[occ.IntakeSubject(occ.FailedWhat, Kind)]; len(f) != 1 || !strings.Contains(string(f[0]), "paused") {
|
||||
t.Errorf("not said as failed: %s", f)
|
||||
}
|
||||
if s := d.Standing(); s.Ready || !strings.Contains(s.Why, "paused") {
|
||||
@@ -284,7 +285,7 @@ func TestASecretIsShownOnlyWhereTheNotifierIsHeardUnpausedAndNeverKept(t *testin
|
||||
t.Error("a machine not named made the desk ready")
|
||||
}
|
||||
// A message that is not secret still goes to every named machine, paused or not.
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m9", Title: "x"})
|
||||
take(t, d, occ.Send, oc.Message{Handle: "m9", Title: "x"})
|
||||
if len(*shown) != 3 {
|
||||
t.Errorf("an ordinary message was held back: %+v", *shown)
|
||||
}
|
||||
@@ -330,8 +331,8 @@ func TestItRunsAsItsOwnAccount(t *testing.T) {
|
||||
// was not shown; a message under a new handle replaces nothing.
|
||||
func TestAMessageSaidAgainUnderItsHandleReplacesItsNotification(t *testing.T) {
|
||||
d, shown, _, store := newDesk(t, "laptop", "desktop")
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "2 failed services on shanks"})
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m2", Title: "another problem"})
|
||||
take(t, d, occ.Send, oc.Message{Handle: "m1", Title: "2 failed services on shanks"})
|
||||
take(t, d, occ.Send, oc.Message{Handle: "m2", Title: "another problem"})
|
||||
if len(*shown) != 4 {
|
||||
t.Fatalf("shown %d", len(*shown))
|
||||
}
|
||||
@@ -343,8 +344,8 @@ func TestAMessageSaidAgainUnderItsHandleReplacesItsNotification(t *testing.T) {
|
||||
first := store["m1"].On
|
||||
// The desk now names a third machine as well.
|
||||
d.Machines = func() []string { return []string{"laptop", "desktop", "third"} }
|
||||
take(t, d, channel.Show, channel.Message{Handle: "m1", Title: "Still open: 2 failed services on shanks", Urgent: true,
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
take(t, d, occ.Show, oc.Message{Handle: "m1", Title: "Still open: 2 failed services on shanks", Urgent: true,
|
||||
Buttons: []oc.Button{{Label: "Silence for a week", Ticket: "t1"}}})
|
||||
again := (*shown)[4:]
|
||||
if len(again) != 3 {
|
||||
t.Fatalf("said again on %d machines", len(again))
|
||||
@@ -397,18 +398,18 @@ func TestAFailedReplaceKeepsTheNotificationItWasToReplace(t *testing.T) {
|
||||
d, shown, _, store := newDesk(t, "laptop")
|
||||
down := true
|
||||
d.Store = downStore{store, &down}
|
||||
raised, _ := json.Marshal(channel.Message{Handle: "m9", Title: "A new build cannot be delivered", Urgent: true})
|
||||
if err := d.Take(channel.ChannelSubject(channel.Send, Kind), raised); err != nil {
|
||||
raised, _ := json.Marshal(oc.Message{Handle: "m9", Title: "A new build cannot be delivered", Urgent: true})
|
||||
if err := d.Take(occ.ChannelSubject(occ.Send, Kind), raised); err != nil {
|
||||
t.Fatalf("the raise was not taken: %v", err)
|
||||
}
|
||||
cleared, _ := json.Marshal(channel.Message{Handle: "m9", Title: "Cleared: a new build can be delivered"})
|
||||
if err := d.Take(channel.ChannelSubject(channel.Edit, Kind), cleared); err != nil {
|
||||
cleared, _ := json.Marshal(oc.Message{Handle: "m9", Title: "Cleared: a new build can be delivered"})
|
||||
if err := d.Take(occ.ChannelSubject(occ.Edit, Kind), cleared); err != nil {
|
||||
t.Fatalf("the clearing was not taken: %v", err)
|
||||
}
|
||||
d.KeepUnkept() // the minute's standing, store still down
|
||||
down = !back
|
||||
d.KeepUnkept()
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m10", Title: "another"})
|
||||
take(t, d, occ.Send, oc.Message{Handle: "m10", Title: "another"})
|
||||
if len(*shown) != 3 || (*shown)[1].args["replace_id"] != 1 || (*shown)[1].args["summary"] != "Cleared: a new build can be delivered" {
|
||||
t.Fatalf("the clearing did not replace the urgent notification: %+v", *shown)
|
||||
}
|
||||
@@ -423,7 +424,7 @@ func TestAFailedReplaceKeepsTheNotificationItWasToReplace(t *testing.T) {
|
||||
}
|
||||
|
||||
d, shown, said, store := newDesk(t, "laptop", "desktop")
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "2 failed services on shanks"})
|
||||
take(t, d, occ.Send, oc.Message{Handle: "m1", Title: "2 failed services on shanks"})
|
||||
first := store["m1"].On
|
||||
notify := d.Notify
|
||||
d.Notify = func(machine string, args map[string]any) (json.RawMessage, error) {
|
||||
@@ -432,13 +433,13 @@ func TestAFailedReplaceKeepsTheNotificationItWasToReplace(t *testing.T) {
|
||||
}
|
||||
return notify(machine, args)
|
||||
}
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "Still open: 2 failed services on shanks"})
|
||||
take(t, d, occ.Send, oc.Message{Handle: "m1", Title: "Still open: 2 failed services on shanks"})
|
||||
if got := store["m1"].On["desktop"]; got != first["desktop"] {
|
||||
t.Fatalf("the desktop's notification %d was forgotten: %v", first["desktop"], store["m1"].On)
|
||||
}
|
||||
d.Notify = func(string, map[string]any) (json.RawMessage, error) { return nil, errors.New("away") }
|
||||
take(t, d, channel.Send, channel.Message{Handle: "m1", Title: "Now urgent: 2 failed services on shanks"})
|
||||
if len(said[channel.IntakeSubject(channel.FailedWhat, Kind)]) != 1 {
|
||||
take(t, d, occ.Send, oc.Message{Handle: "m1", Title: "Now urgent: 2 failed services on shanks"})
|
||||
if len(said[occ.IntakeSubject(occ.FailedWhat, Kind)]) != 1 {
|
||||
t.Fatalf("shown nowhere, and not said as failed")
|
||||
}
|
||||
_ = shown
|
||||
|
||||
@@ -12,3 +12,8 @@ require (
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
@@ -37,7 +37,6 @@ import (
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
ch "git.novox.be/novox/mesh-sdk/go/channel"
|
||||
"git.novox.be/novox/mesh-sdk/go/outward"
|
||||
)
|
||||
|
||||
@@ -428,7 +427,7 @@ func approverReady(bus AskBus) string {
|
||||
}
|
||||
channels := 0
|
||||
for _, k := range kinds {
|
||||
if k.Seat != ch.ChannelSeat {
|
||||
if k.Seat != occ.ChannelSeat {
|
||||
continue
|
||||
}
|
||||
channels++
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
ch "git.novox.be/novox/mesh-sdk/go/channel"
|
||||
)
|
||||
|
||||
// standIn is the operator channel's router and a channel that proves who answered, in memory: it takes the asks
|
||||
@@ -37,7 +36,7 @@ type standIn struct {
|
||||
|
||||
func newStandIn() *standIn {
|
||||
return &standIn{asks: map[string]oc.Ask{}, records: map[string][]byte{}, kinds: []stdio.KindHeld{
|
||||
{Seat: ch.ChannelSeat, Kind: "telegram", Module: "telegram", Node: "novox",
|
||||
{Seat: occ.ChannelSeat, Kind: "telegram", Module: "telegram", Node: "novox",
|
||||
Capabilities: []string{"choice", "exact-render", "verified-sender"}}}}
|
||||
}
|
||||
|
||||
@@ -459,7 +458,7 @@ func TestAProposalIsJudgedAndRefusedBeforeTheOperatorIsAsked(t *testing.T) {
|
||||
t.Fatalf("a refused proposal was asked: %v", r.subjects)
|
||||
}
|
||||
|
||||
r.kinds = []stdio.KindHeld{{Seat: ch.ChannelSeat, Kind: "desktop", Capabilities: []string{"choice"}}}
|
||||
r.kinds = []stdio.KindHeld{{Seat: occ.ChannelSeat, Kind: "desktop", Capabilities: []string{"choice"}}}
|
||||
if _, err := c.Propose(Call{Action: "propose", Key: "font-size", Value: 13.0, Caller: "g", Why: "w"}); err == nil ||
|
||||
!strings.Contains(err.Error(), "no channel that proves who answered") {
|
||||
t.Errorf("no approving channel: %v", err)
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ch "git.novox.be/novox/mesh-sdk/go/channel"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
)
|
||||
|
||||
// Action is one answer a condition names: a label, and the seat verb its producer calls when it is chosen.
|
||||
@@ -132,7 +132,7 @@ func pruneGrants(grants map[string]Grant, kind string, now time.Time, keep int)
|
||||
// Chosen takes an answer a channel of a kind said it was given: an ask's option is judged and, when it
|
||||
// stands, closes the ask with a warrant for its asker (asks.go); Details are shown where they were asked.
|
||||
// Anything else is refused, and said there. The same answer is acted on once.
|
||||
func (h *Holder) Chosen(kind string, c ch.Choice) (string, error) {
|
||||
func (h *Holder) Chosen(kind string, c oc.Choice) (string, error) {
|
||||
// The root-free word is asked before the work is taken (the confirmation review of 2026-10-09):
|
||||
// up to RootFreeWithin, which no ask, answer or link should wait behind. Asked fresh here, for this answer,
|
||||
// and judged with in it.
|
||||
@@ -255,7 +255,7 @@ func firstLineOf(s string) string {
|
||||
|
||||
// Failed takes a channel's word that a message did not arrive: the channel is said to be failing, and the
|
||||
// record it carried is tried again — unless the failure is permanent, which is said and not repeated.
|
||||
func (h *Holder) Failed(kind string, f ch.Failed) {
|
||||
func (h *Holder) Failed(kind string, f oc.Failed) {
|
||||
h.work.Lock()
|
||||
defer h.work.Unlock()
|
||||
h.noteChannel(kind, errorf("%s", f.Why))
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
ch "git.novox.be/novox/mesh-sdk/go/channel"
|
||||
)
|
||||
|
||||
// The router of novox/hq ADR 0259: it holds every ask, judges each answer and issues a warrant; it
|
||||
@@ -252,9 +251,9 @@ func link(h *Holder, store *memAsks, c *clock, identity string, at time.Time) {
|
||||
Linked: at, ApproveFrom: at.Add(LinkedApproveAfter)}
|
||||
}
|
||||
|
||||
func choiceOf(ticket, identity string, at time.Time) ch.Choice {
|
||||
return ch.Choice{ID: "c-" + ticket + "-" + identity, Ticket: ticket, At: at,
|
||||
Sender: ch.Sender{Identity: identity, Display: "Jo", Authenticated: true}}
|
||||
func choiceOf(ticket, identity string, at time.Time) oc.Choice {
|
||||
return oc.Choice{ID: "c-" + ticket + "-" + identity, Ticket: ticket, At: at,
|
||||
Sender: oc.Sender{Identity: identity, Display: "Jo", Authenticated: true}}
|
||||
}
|
||||
|
||||
func ticketFor(m Message, label string) string {
|
||||
@@ -737,7 +736,7 @@ func TestAnAnswerReplayedLongAfterIsNotActedOn(t *testing.T) {
|
||||
|
||||
func TestLinkingShowsACodeOnTheDeskAndLinksOnlyWhoAsked(t *testing.T) {
|
||||
h, tg, dt, c, store, _ := newRouter(t)
|
||||
asked := ch.LinkAsked{ID: "l1", At: c.now(), Sender: ch.Sender{Identity: "42", Display: "Jo", Authenticated: true}}
|
||||
asked := oc.LinkAsked{ID: "l1", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}}
|
||||
h.Link("telegram", asked)
|
||||
if len(dt.sends) != 1 || !strings.HasPrefix(dt.sends[0].Title, "Link a Telegram account as you") {
|
||||
t.Fatalf("no code on the desk: %+v", dt.sends)
|
||||
@@ -750,13 +749,13 @@ func TestLinkingShowsACodeOnTheDeskAndLinksOnlyWhoAsked(t *testing.T) {
|
||||
t.Errorf("the code is kept in clear: %+v", p)
|
||||
}
|
||||
// Another account sending the code is refused; a wrong code is refused; the right one links.
|
||||
if a := h.Prove("telegram", ch.Code{Sender: ch.Sender{Identity: "666", Authenticated: true}, Code: code}); a.Accepted {
|
||||
if a := h.Prove("telegram", oc.Code{Sender: oc.Sender{Identity: "666", Authenticated: true}, Code: code}); a.Accepted {
|
||||
t.Error("another account linked with the code")
|
||||
}
|
||||
if a := h.Prove("telegram", ch.Code{Sender: ch.Sender{Identity: "42", Authenticated: true}, Code: "000000x"}); a.Accepted {
|
||||
if a := h.Prove("telegram", oc.Code{Sender: oc.Sender{Identity: "42", Authenticated: true}, Code: "000000x"}); a.Accepted {
|
||||
t.Error("a wrong code linked")
|
||||
}
|
||||
a := h.Prove("telegram", ch.Code{Sender: ch.Sender{Identity: "42", Authenticated: true}, Code: code, Purpose: "link"})
|
||||
a := h.Prove("telegram", oc.Code{Sender: oc.Sender{Identity: "42", Authenticated: true}, Code: code, Purpose: "link"})
|
||||
if !a.Accepted || !strings.Contains(a.Words, "approve from 13:00") {
|
||||
t.Fatalf("the right code: %+v (code %q)", a, code)
|
||||
}
|
||||
@@ -771,7 +770,7 @@ func TestLinkingShowsACodeOnTheDeskAndLinksOnlyWhoAsked(t *testing.T) {
|
||||
}
|
||||
}
|
||||
// A second account cannot link while one is.
|
||||
h.Link("telegram", ch.LinkAsked{ID: "l2", At: c.now(), Sender: ch.Sender{Identity: "666", Authenticated: true}})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l2", At: c.now(), Sender: oc.Sender{Identity: "666", Authenticated: true}})
|
||||
if _, pending := store.links["telegram"]; pending {
|
||||
t.Error("a second link was begun while one is linked")
|
||||
}
|
||||
@@ -779,21 +778,21 @@ func TestLinkingShowsACodeOnTheDeskAndLinksOnlyWhoAsked(t *testing.T) {
|
||||
|
||||
func TestALinkCodeExpiresAndFiveWrongTriesEndIt(t *testing.T) {
|
||||
h, _, _, c, store, _ := newRouter(t)
|
||||
jo := ch.Sender{Identity: "42", Authenticated: true}
|
||||
h.Link("telegram", ch.LinkAsked{ID: "l1", At: c.now(), Sender: jo})
|
||||
jo := oc.Sender{Identity: "42", Authenticated: true}
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l1", At: c.now(), Sender: jo})
|
||||
for i := 0; i < LinkTries; i++ {
|
||||
h.Prove("telegram", ch.Code{Sender: jo, Code: "nope"})
|
||||
h.Prove("telegram", oc.Code{Sender: jo, Code: "nope"})
|
||||
}
|
||||
if _, pending := store.links["telegram"]; pending {
|
||||
t.Error("five wrong codes did not end the link")
|
||||
}
|
||||
h.Link("telegram", ch.LinkAsked{ID: "l2", At: c.now(), Sender: jo})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l2", At: c.now(), Sender: jo})
|
||||
c.pass(LinkCodeFor)
|
||||
if a := h.Prove("telegram", ch.Code{Sender: jo, Code: "123456"}); a.Accepted || !strings.Contains(a.Words, "expired") {
|
||||
if a := h.Prove("telegram", oc.Code{Sender: jo, Code: "123456"}); a.Accepted || !strings.Contains(a.Words, "expired") {
|
||||
t.Errorf("%+v", a)
|
||||
}
|
||||
// The desk cannot be linked: it proves nothing about who clicked.
|
||||
h.Link("desktop", ch.LinkAsked{ID: "l3", At: c.now(), Sender: ch.Sender{Identity: "g14"}})
|
||||
h.Link("desktop", oc.LinkAsked{ID: "l3", At: c.now(), Sender: oc.Sender{Identity: "g14"}})
|
||||
if _, pending := store.links["desktop"]; pending {
|
||||
t.Error("the desk began a link")
|
||||
}
|
||||
@@ -849,9 +848,9 @@ func TestARelinkWithinADayWaitsForTheReplacedAccount(t *testing.T) {
|
||||
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
||||
_, _ = h.Unlink("telegram")
|
||||
_, _ = h.Chosen("telegram", choiceOf(ticketFor(tg.sends[len(tg.sends)-1], "Unlink"), "42", c.now()))
|
||||
h.Link("telegram", ch.LinkAsked{ID: "l9", At: c.now(), Sender: ch.Sender{Identity: "666", Display: "Eve", Authenticated: true}})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l9", At: c.now(), Sender: oc.Sender{Identity: "666", Display: "Eve", Authenticated: true}})
|
||||
code := codeOf(t, dt.sends[len(dt.sends)-1])
|
||||
a := h.Prove("telegram", ch.Code{Sender: ch.Sender{Identity: "666", Authenticated: true}, Code: code})
|
||||
a := h.Prove("telegram", oc.Code{Sender: oc.Sender{Identity: "666", Authenticated: true}, Code: code})
|
||||
if !a.Accepted || !strings.Contains(a.Words, "waits for that account's approval") || len(store.ids) != 0 {
|
||||
t.Fatalf("linked at once within a day of an unlink: %+v %v", a, store.ids)
|
||||
}
|
||||
@@ -979,12 +978,12 @@ func TestAnAnswerNeedsAChannelThatRendersExactly(t *testing.T) {
|
||||
// At most three links an hour per kind, and the code is shown only where the words stay private.
|
||||
func TestLinksAreBoundedAndTheCodeShownOnlyInPrivate(t *testing.T) {
|
||||
h, tg, dt, c, store, _ := newRouter(t)
|
||||
jo := ch.Sender{Identity: "42", Display: "Jo", Authenticated: true}
|
||||
jo := oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}
|
||||
for i := 0; i < LinksAnHour; i++ {
|
||||
h.Link("telegram", ch.LinkAsked{ID: fmt.Sprint("l", i), At: c.now(), Sender: jo})
|
||||
h.Link("telegram", oc.LinkAsked{ID: fmt.Sprint("l", i), At: c.now(), Sender: jo})
|
||||
}
|
||||
shown := len(dt.sends)
|
||||
h.Link("telegram", ch.LinkAsked{ID: "l9", At: c.now(), Sender: jo})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l9", At: c.now(), Sender: jo})
|
||||
if len(dt.sends) != shown || !strings.Contains(tg.sends[len(tg.sends)-1].Body, "Try again in an hour") {
|
||||
t.Error("a fourth link this hour showed a code")
|
||||
}
|
||||
@@ -995,7 +994,7 @@ func TestLinksAreBoundedAndTheCodeShownOnlyInPrivate(t *testing.T) {
|
||||
h2.Bench.SetKinds([]KindInfo{
|
||||
{Kind: "telegram", Module: "telegram", Capabilities: []string{"choice", "verified-sender", "exact-render", "reaches-away"}},
|
||||
{Kind: "desktop", Module: "desk-channel", Capabilities: []string{"choice", "exact-render"}}})
|
||||
h2.Link("telegram", ch.LinkAsked{ID: "l1", At: c2.now(), Sender: jo})
|
||||
h2.Link("telegram", oc.LinkAsked{ID: "l1", At: c2.now(), Sender: jo})
|
||||
if _, pending := store2.links["telegram"]; pending || !strings.Contains(tg2.sends[len(tg2.sends)-1].Body, "No private place") {
|
||||
t.Error("a code was shown where the words are not private")
|
||||
}
|
||||
@@ -1013,7 +1012,7 @@ func TestAWarningTheDeskCouldNotShowGoesAway(t *testing.T) {
|
||||
if len(dt.sends) != 1 {
|
||||
t.Fatalf("the desk was sent %d", len(dt.sends))
|
||||
}
|
||||
h.Failed("desktop", ch.Failed{Handle: "g14=1", Why: "no session answers", At: c.now()})
|
||||
h.Failed("desktop", oc.Failed{Handle: "g14=1", Why: "no session answers", At: c.now()})
|
||||
c.pass(BurstWindow)
|
||||
h.Flush()
|
||||
if len(tg.sends) == 0 {
|
||||
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
ch "git.novox.be/novox/mesh-sdk/go/channel"
|
||||
)
|
||||
|
||||
// ErrChanged is a compare-and-set that lost: another writer was first.
|
||||
@@ -1055,7 +1054,7 @@ func (h *Holder) toldLine(rec AskRecord) string {
|
||||
|
||||
// confirmChoice says, where an answer was given and in reply to it, that it was recorded: on Telegram the
|
||||
// tap's own acknowledgement (issue 372). Called only once the ask's end is recorded.
|
||||
func (h *Holder) confirmChoice(kind string, c ch.Choice, label, asker string, told bool) {
|
||||
func (h *Holder) confirmChoice(kind string, c oc.Choice, label, asker string, told bool) {
|
||||
words := "You chose " + label + ": recorded"
|
||||
switch {
|
||||
case asker == SelfAsker:
|
||||
@@ -1070,7 +1069,7 @@ func (h *Holder) confirmChoice(kind string, c ch.Choice, label, asker string, to
|
||||
}
|
||||
|
||||
// confirmAgain says, in reply to the same answer given again, that it was already recorded.
|
||||
func (h *Holder) confirmAgain(kind string, c ch.Choice, label string, at time.Time) {
|
||||
func (h *Holder) confirmAgain(kind string, c oc.Choice, label string, at time.Time) {
|
||||
err := h.showOn(kind, Message{Title: "You already chose " + label + " at " + at.In(h.zone()).Format("15:04") + ": recorded.",
|
||||
Quiet: true, Reply: c.ID})
|
||||
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "confirmed again " + label, Outcome: outcomeOf(err)})
|
||||
@@ -1174,7 +1173,7 @@ func joinOr(labels []string) string {
|
||||
|
||||
// judge is an answer to an ask's option, chosen on a kind: refused at the first failure, in words said on
|
||||
// the kind; otherwise the ask closed and the warrant published (ADR 0259 §5).
|
||||
func (h *Holder) judge(kind string, c ch.Choice, key string, root func(KindInfo) string) (string, error) {
|
||||
func (h *Holder) judge(kind string, c oc.Choice, key string, root func(KindInfo) string) (string, error) {
|
||||
e := h.askOf(key)
|
||||
if e == nil {
|
||||
return h.refuseChoice(kind, c, "it belongs to no ask this router holds")
|
||||
@@ -1290,7 +1289,7 @@ func (h *Holder) judge(kind string, c ch.Choice, key string, root func(KindInfo)
|
||||
// refuseStranger refuses an answer from an account that is not the operator's, and tells the operator, by name
|
||||
// and number, on the account that is linked: a question's buttons reached somebody else (a forwarded message,
|
||||
// or a phone in other hands). Nothing is said to the other account.
|
||||
func (h *Holder) refuseStranger(kind string, c ch.Choice) (string, error) {
|
||||
func (h *Holder) refuseStranger(kind string, c oc.Choice) (string, error) {
|
||||
why := "this account is not linked as the operator"
|
||||
h.Logf("[messenger] an answer on %s from %s was refused: %s", kind, c.Sender.Identity, why)
|
||||
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "answer", Outcome: "refused: " + why})
|
||||
@@ -1306,7 +1305,7 @@ func (h *Holder) refuseStranger(kind string, c ch.Choice) (string, error) {
|
||||
}
|
||||
|
||||
// refuseChoice says on the kind that an answer was not taken, and why.
|
||||
func (h *Holder) refuseChoice(kind string, c ch.Choice, why string) (string, error) {
|
||||
func (h *Holder) refuseChoice(kind string, c oc.Choice, why string) (string, error) {
|
||||
h.Logf("[messenger] an answer on %s from %s was refused: %s", kind, c.Sender.Identity, why)
|
||||
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "answer", Outcome: "refused: " + why})
|
||||
h.showOn(kind, Message{Title: "That answer was not taken", Body: capital(why) + ". Nothing was done.",
|
||||
@@ -1326,15 +1325,15 @@ func (h *Holder) showOnHandle(kind string, m Message) (string, error) {
|
||||
if h.Publish == nil {
|
||||
return "", errors.New("this router cannot reach the bus")
|
||||
}
|
||||
out := ch.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply, To: m.To,
|
||||
out := oc.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply, To: m.To,
|
||||
Secret: m.Secret}
|
||||
if out.To == "" && h.Bench != nil && h.Bench.To != nil {
|
||||
out.To = h.Bench.To(kind)
|
||||
}
|
||||
for _, a := range m.Actions {
|
||||
out.Buttons = append(out.Buttons, ch.Button{Label: a.Label, Ticket: a.Token})
|
||||
out.Buttons = append(out.Buttons, oc.Button{Label: a.Label, Ticket: a.Token})
|
||||
}
|
||||
err := h.Publish(ch.ChannelSubject(ch.Show, kind), out, out.Handle)
|
||||
err := h.Publish(occ.ChannelSubject(occ.Show, kind), out, out.Handle)
|
||||
h.noteChannel(kind, err)
|
||||
return out.Handle, err
|
||||
}
|
||||
@@ -1351,7 +1350,7 @@ func (h *Holder) showOn(kind string, m Message) error {
|
||||
if h.Publish == nil {
|
||||
return errors.New("this router cannot reach the bus")
|
||||
}
|
||||
out := ch.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply}
|
||||
out := oc.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply}
|
||||
if h.Bench != nil && h.Bench.To != nil {
|
||||
out.To = h.Bench.To(kind)
|
||||
}
|
||||
@@ -1360,9 +1359,9 @@ func (h *Holder) showOn(kind string, m Message) error {
|
||||
}
|
||||
out.Secret = m.Secret
|
||||
for _, a := range m.Actions {
|
||||
out.Buttons = append(out.Buttons, ch.Button{Label: a.Label, Ticket: a.Token})
|
||||
out.Buttons = append(out.Buttons, oc.Button{Label: a.Label, Ticket: a.Token})
|
||||
}
|
||||
err := h.Publish(ch.ChannelSubject(ch.Send, kind), out, out.Handle)
|
||||
err := h.Publish(occ.ChannelSubject(occ.Send, kind), out, out.Handle)
|
||||
h.noteChannel(kind, err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
ch "git.novox.be/novox/mesh-sdk/go/channel"
|
||||
"github.com/nats-io/nats-server/v2/server"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
@@ -98,7 +98,7 @@ func (r *busRuntime) bucket(name string) jetstream.KeyValue {
|
||||
func (r *busRuntime) setKinds(caps map[string][]string) {
|
||||
var out []map[string]any
|
||||
for kind, c := range caps {
|
||||
out = append(out, map[string]any{"seat": ch.ChannelSeat, "kind": kind, "module": kind, "node": "test", "capabilities": c})
|
||||
out = append(out, map[string]any{"seat": occ.ChannelSeat, "kind": kind, "module": kind, "node": "test", "capabilities": c})
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.kinds = out
|
||||
@@ -236,7 +236,7 @@ func (r *busRuntime) runtime(id json.RawMessage, method string, params json.RawM
|
||||
}
|
||||
return keys, err
|
||||
case "mesh/subscribe":
|
||||
if strings.HasPrefix(p.Pattern, ch.IntakeSeat) {
|
||||
if strings.HasPrefix(p.Pattern, occ.IntakeSeat) {
|
||||
r.mu.Lock()
|
||||
r.subscribed = true
|
||||
r.mu.Unlock()
|
||||
@@ -279,7 +279,7 @@ func (r *busRuntime) forward() {
|
||||
}); err != nil {
|
||||
r.t.Fatal(err)
|
||||
}
|
||||
if _, err := r.nc.Subscribe("mesh.seat."+ch.IntakeSeat+".event.>", func(m *nats.Msg) {
|
||||
if _, err := r.nc.Subscribe("mesh.seat."+occ.IntakeSeat+".event.>", func(m *nats.Msg) {
|
||||
key := strings.Replace(strings.TrimPrefix(m.Subject, "mesh.seat."), ".event", "", 1)
|
||||
_, _ = r.request("mesh/event", map[string]any{"envelope": map[string]any{"key": key, "node": "test", "body": json.RawMessage(m.Data)}})
|
||||
}); err != nil {
|
||||
@@ -329,7 +329,7 @@ func TestAnApprovalAskIsHeldThenOfferedWhenAChannelCanCarryItOverTheBus(t *testi
|
||||
if _, err := r.nc.Subscribe(oc.SubjectDecided("mesh-delivery"), collect(&told)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := r.nc.Subscribe(ch.ChannelSubject(ch.Show, "telegram"), collect(&shown)); err != nil {
|
||||
if _, err := r.nc.Subscribe(occ.ChannelSubject(occ.Show, "telegram"), collect(&shown)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.forward()
|
||||
@@ -340,7 +340,7 @@ func TestAnApprovalAskIsHeldThenOfferedWhenAChannelCanCarryItOverTheBus(t *testi
|
||||
return r.subscribed && r.taking && r.kindsRead > 0
|
||||
})
|
||||
standing, _ := json.Marshal(Standing{Ready: true, At: time.Now().UTC()})
|
||||
if err := r.nc.Publish(ch.IntakeSubject(ch.StandingWhat, "telegram"), standing); err != nil {
|
||||
if err := r.nc.Publish(occ.IntakeSubject(occ.StandingWhat, "telegram"), standing); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
ch "git.novox.be/novox/mesh-sdk/go/channel"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -25,7 +24,7 @@ import (
|
||||
type conditionBed struct {
|
||||
r *busRuntime
|
||||
mu sync.Mutex
|
||||
desk []ch.Message
|
||||
desk []oc.Message
|
||||
acks []string // the answer of the router to each work item: "" when acknowledged
|
||||
}
|
||||
|
||||
@@ -40,8 +39,8 @@ func newConditionBedWith(t *testing.T, settings string) *conditionBed {
|
||||
b := &conditionBed{r: newBusRuntime(t)}
|
||||
b.r.settings = settings
|
||||
b.r.setKinds(map[string][]string{"desktop": {"choice", "edit"}})
|
||||
if _, err := b.r.nc.Subscribe("mesh.seat."+ch.ChannelSeat+".accept.*.desktop", func(m *nats.Msg) {
|
||||
var msg ch.Message
|
||||
if _, err := b.r.nc.Subscribe("mesh.seat."+occ.ChannelSeat+".accept.*.desktop", func(m *nats.Msg) {
|
||||
var msg oc.Message
|
||||
if json.Unmarshal(m.Data, &msg) == nil {
|
||||
b.mu.Lock()
|
||||
b.desk = append(b.desk, msg)
|
||||
@@ -83,7 +82,7 @@ func newConditionBedWith(t *testing.T, settings string) *conditionBed {
|
||||
return b.r.subscribed
|
||||
})
|
||||
standing, _ := json.Marshal(Standing{Ready: true, At: time.Now().UTC()})
|
||||
if err := b.r.nc.Publish(ch.IntakeSubject(ch.StandingWhat, "desktop"), standing); err != nil {
|
||||
if err := b.r.nc.Publish(occ.IntakeSubject(occ.StandingWhat, "desktop"), standing); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
@@ -93,7 +92,7 @@ func newConditionBedWith(t *testing.T, settings string) *conditionBed {
|
||||
// forwardEvents hands the bundle the channels' word, as bus_test.go's forward does for it.
|
||||
func (b *conditionBed) forwardEvents(t *testing.T) {
|
||||
t.Helper()
|
||||
if _, err := b.r.nc.Subscribe("mesh.seat."+ch.IntakeSeat+".event.>", func(m *nats.Msg) {
|
||||
if _, err := b.r.nc.Subscribe("mesh.seat."+occ.IntakeSeat+".event.>", func(m *nats.Msg) {
|
||||
key := strings.Replace(strings.TrimPrefix(m.Subject, "mesh.seat."), ".event", "", 1)
|
||||
_, _ = b.r.request("mesh/event", map[string]any{"envelope": map[string]any{"key": key, "node": "test", "body": json.RawMessage(m.Data)}})
|
||||
}); err != nil {
|
||||
@@ -128,10 +127,10 @@ func (b *conditionBed) record(key string) *Record {
|
||||
return &rec
|
||||
}
|
||||
|
||||
func (b *conditionBed) told() []ch.Message {
|
||||
func (b *conditionBed) told() []oc.Message {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return append([]ch.Message(nil), b.desk...)
|
||||
return append([]oc.Message(nil), b.desk...)
|
||||
}
|
||||
|
||||
func (b *conditionBed) acknowledged(n int) bool {
|
||||
|
||||
@@ -32,7 +32,6 @@ import (
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
ch "git.novox.be/novox/mesh-sdk/go/channel"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -187,7 +186,7 @@ func (h *Holder) offerDeskUnlink(id Identity) bool {
|
||||
|
||||
// Link takes a link asked for on a kind: a code shown where only the operator sees it, or a refusal said on
|
||||
// the kind.
|
||||
func (h *Holder) Link(kind string, l ch.LinkAsked) string {
|
||||
func (h *Holder) Link(kind string, l oc.LinkAsked) string {
|
||||
h.warmRootFree(false) // outside the work: a question it asks reads a recent word
|
||||
h.work.Lock()
|
||||
defer h.work.Unlock()
|
||||
@@ -278,11 +277,11 @@ func (h *Holder) Link(kind string, l ch.LinkAsked) string {
|
||||
}
|
||||
|
||||
// Prove answers a code sent on a kind: the link it confirms, or why not. Never said in an event.
|
||||
func (h *Holder) Prove(kind string, c ch.Code) ch.ProofAnswer {
|
||||
func (h *Holder) Prove(kind string, c oc.Code) oc.ProofAnswer {
|
||||
h.warmRootFree(false) // outside the work: a question it asks reads a recent word
|
||||
h.work.Lock()
|
||||
defer h.work.Unlock()
|
||||
no := func(words string) ch.ProofAnswer { return ch.ProofAnswer{Accepted: false, Words: words} }
|
||||
no := func(words string) oc.ProofAnswer { return oc.ProofAnswer{Accepted: false, Words: words} }
|
||||
if c.Purpose != "" && c.Purpose != "link" {
|
||||
return no("The mesh takes no code for that yet.")
|
||||
}
|
||||
@@ -341,12 +340,12 @@ func (h *Holder) Prove(kind string, c ch.Code) ch.ProofAnswer {
|
||||
h.announce(Message{Title: "A new " + Display(kind) + " account asks to be linked as you",
|
||||
Body: fmt.Sprintf("%s (number %s), less than a day after an unlink. It waits for the account it replaces, "+
|
||||
"or for root at the router's machine.", p.Display, p.Identity), Urgent: true})
|
||||
return ch.ProofAnswer{Accepted: true, Words: "The code is right. Because an account was unlinked here less " +
|
||||
return oc.ProofAnswer{Accepted: true, Words: "The code is right. Because an account was unlinked here less " +
|
||||
"than a day ago, the link waits for that account's approval, or for you at the router's machine."}
|
||||
}
|
||||
h.linkNow(id, "with the code shown on the desk")
|
||||
from := now.Add(LinkedApproveAfter).In(h.zone()).Format("15:04")
|
||||
return ch.ProofAnswer{Accepted: true, Words: "Linked. You can answer acknowledgements now, and approve from " + from + "."}
|
||||
return oc.ProofAnswer{Accepted: true, Words: "Linked. You can answer acknowledgements now, and approve from " + from + "."}
|
||||
}
|
||||
|
||||
// Identities is every linked account, by kind.
|
||||
@@ -386,7 +385,7 @@ func sixDigits() (string, error) {
|
||||
}
|
||||
|
||||
// decodeLink reads a link asked for.
|
||||
func decodeLink(body json.RawMessage) (ch.LinkAsked, error) {
|
||||
var l ch.LinkAsked
|
||||
func decodeLink(body json.RawMessage) (oc.LinkAsked, error) {
|
||||
var l oc.LinkAsked
|
||||
return l, json.Unmarshal(body, &l)
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
|
||||
_ "time/tzdata" // the operator's zone is known whatever this machine has installed
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
ch "git.novox.be/novox/mesh-sdk/go/channel"
|
||||
)
|
||||
|
||||
func errorf(format string, a ...any) error { return fmt.Errorf(format, a...) }
|
||||
@@ -493,7 +493,7 @@ func run(h *Holder, l *listening) {
|
||||
// which the bus grants that kind's holder alone.
|
||||
func kindOf(key string) (what, kind string, ok bool) {
|
||||
parts := strings.Split(key, ".")
|
||||
if len(parts) != 3 || parts[0] != ch.IntakeSeat {
|
||||
if len(parts) != 3 || parts[0] != occ.IntakeSeat {
|
||||
return "", "", false
|
||||
}
|
||||
return parts[1], parts[2], true
|
||||
@@ -508,8 +508,8 @@ func listenForChannels(h *Holder) {
|
||||
return nil
|
||||
}
|
||||
switch what {
|
||||
case ch.Chosen:
|
||||
var c ch.Choice
|
||||
case occ.Chosen:
|
||||
var c oc.Choice
|
||||
if err := json.Unmarshal(e.Body, &c); err != nil || c.Ticket == "" {
|
||||
logf("[messenger] an answer from %s could not be read: %s", kind, firstLineOf(string(e.Body)))
|
||||
return nil
|
||||
@@ -518,19 +518,19 @@ func listenForChannels(h *Holder) {
|
||||
c.ID = e.Headers["x-event-id"]
|
||||
}
|
||||
_, _ = h.Chosen(kind, c)
|
||||
case ch.Link:
|
||||
case occ.Link:
|
||||
l, err := decodeLink(e.Body)
|
||||
if err != nil {
|
||||
logf("[messenger] a link asked on %s could not be read", kind)
|
||||
return nil
|
||||
}
|
||||
h.Link(kind, l)
|
||||
case ch.FailedWhat:
|
||||
var f ch.Failed
|
||||
case occ.FailedWhat:
|
||||
var f oc.Failed
|
||||
if json.Unmarshal(e.Body, &f) == nil {
|
||||
h.Failed(kind, f)
|
||||
}
|
||||
case ch.StandingWhat:
|
||||
case occ.StandingWhat:
|
||||
// Read as this module's Standing: the bench's word, and answers-lost-at beside it (issue 384).
|
||||
var s Standing
|
||||
if json.Unmarshal(e.Body, &s) == nil {
|
||||
@@ -541,7 +541,7 @@ func listenForChannels(h *Holder) {
|
||||
return nil
|
||||
}
|
||||
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
||||
if err := stdio.Subscribe(ch.IntakeSeat+".#", handle); err == nil {
|
||||
if err := stdio.Subscribe(occ.IntakeSeat+".#", handle); err == nil {
|
||||
logf("[messenger] listening for what the channels say")
|
||||
break
|
||||
} else {
|
||||
@@ -580,14 +580,14 @@ func answerProofs(h *Holder) {
|
||||
handle := func(subject string, body json.RawMessage) (any, error) {
|
||||
parts := strings.Split(subject, ".")
|
||||
kind := parts[len(parts)-1]
|
||||
var c ch.Code
|
||||
var c oc.Code
|
||||
if err := json.Unmarshal(body, &c); err != nil {
|
||||
return ch.ProofAnswer{Words: "The code could not be read."}, nil
|
||||
return oc.ProofAnswer{Words: "The code could not be read."}, nil
|
||||
}
|
||||
return h.Prove(kind, c), nil
|
||||
}
|
||||
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
||||
if err := stdio.SeatAnswer(ch.ProofSubject(ch.CodeProof, "*"), handle); err == nil {
|
||||
if err := stdio.SeatAnswer(occ.ProofSubject(occ.CodeProof, "*"), handle); err == nil {
|
||||
logf("[messenger] answering the codes the channels are sent")
|
||||
break
|
||||
} else {
|
||||
@@ -614,7 +614,7 @@ func followKinds(h *Holder) {
|
||||
} else {
|
||||
var out []KindInfo
|
||||
for _, k := range kinds {
|
||||
if k.Seat == ch.ChannelSeat {
|
||||
if k.Seat == occ.ChannelSeat {
|
||||
out = append(out, KindInfo{Kind: k.Kind, Module: k.Module, Node: k.Node, Capabilities: k.Capabilities})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
ch "git.novox.be/novox/mesh-sdk/go/channel"
|
||||
)
|
||||
|
||||
// The manifest says what the code does (novox/hq ADR 0259): the seats it declares — the operator's, and
|
||||
@@ -72,9 +72,9 @@ func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { //
|
||||
want := []seat{
|
||||
{Name: oc.Seat, Scope: "mesh", Serves: verbs, Accepts: []string{"ask", "cancel", "condition"}, Emits: []string{"decided"},
|
||||
ByCaller: []string{"ask", "cancel", "condition", "decided"}, Records: []string{"asks"}},
|
||||
{Name: ch.ChannelSeat, Scope: "mesh", Kinded: true, Accepts: []string{ch.Show, ch.Edit, ch.Send}},
|
||||
{Name: ch.IntakeSeat, Scope: "mesh", Kinded: true,
|
||||
Emits: []string{ch.Chosen, ch.Link, ch.FailedWhat, ch.StandingWhat}, Proofs: []string{ch.CodeProof}},
|
||||
{Name: occ.ChannelSeat, Scope: "mesh", Kinded: true, Accepts: []string{occ.Show, occ.Edit, occ.Send}},
|
||||
{Name: occ.IntakeSeat, Scope: "mesh", Kinded: true,
|
||||
Emits: []string{occ.Chosen, occ.Link, occ.FailedWhat, occ.StandingWhat}, Proofs: []string{occ.CodeProof}},
|
||||
}
|
||||
if !reflect.DeepEqual(m.Seats, want) {
|
||||
t.Fatalf("seats %+v", m.Seats)
|
||||
@@ -82,7 +82,7 @@ func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { //
|
||||
if len(m.Claims) != 1 || m.Claims[0].Name != oc.Seat || !reflect.DeepEqual(m.Claims[0].Serves, verbs) {
|
||||
t.Fatalf("claims %+v", m.Claims)
|
||||
}
|
||||
if !reflect.DeepEqual(m.Uses, []string{ch.ChannelSeat}) {
|
||||
if !reflect.DeepEqual(m.Uses, []string{occ.ChannelSeat}) {
|
||||
t.Fatalf("uses %v", m.Uses)
|
||||
}
|
||||
if !reflect.DeepEqual(m.Consumes, []string{
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
chn "git.novox.be/novox/mesh-sdk/go/channel"
|
||||
)
|
||||
|
||||
// H3: approval rests on the controller's positive, recent word that no agent can become root where the router
|
||||
@@ -206,11 +205,11 @@ func TestALinksCodeIsSecretOnEveryPath(t *testing.T) {
|
||||
bench.Stand("telegram", Standing{Ready: true, At: c.now()})
|
||||
bench.Stand("desktop", Standing{Ready: true, At: c.now()})
|
||||
var mu sync.Mutex
|
||||
put := map[string][]chn.Message{}
|
||||
put := map[string][]oc.Message{}
|
||||
publish := func(subject string, body any, id string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if m, ok := body.(chn.Message); ok {
|
||||
if m, ok := body.(oc.Message); ok {
|
||||
put[subject] = append(put[subject], m)
|
||||
}
|
||||
return nil
|
||||
@@ -219,8 +218,8 @@ func TestALinksCodeIsSecretOnEveryPath(t *testing.T) {
|
||||
Desk: &SeatChannel{Bench: bench, Role: RoleDesk, Publish: publish}, Bench: bench, Publish: publish,
|
||||
Asks: newMemAsks(), Node: "anchor", Now: c.now, Logf: t.Logf, Zone: func() *time.Location { return time.UTC }}
|
||||
h.init()
|
||||
h.Link("telegram", chn.LinkAsked{ID: "l1", At: c.now(), Sender: chn.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
|
||||
shown := put[chn.ChannelSubject(chn.Send, "desktop")]
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l1", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
|
||||
shown := put[occ.ChannelSubject(occ.Send, "desktop")]
|
||||
if len(shown) != 1 || !strings.Contains(shown[0].Body, "send this code") {
|
||||
t.Fatalf("the code was not shown on the desk: %+v", put)
|
||||
}
|
||||
@@ -307,9 +306,9 @@ func TestALinkIsNotMadeWhileAnotherAccountIsLinked(t *testing.T) {
|
||||
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
||||
_, _ = h.Unlink("telegram")
|
||||
_, _ = h.Chosen("telegram", choiceOf(ticketFor(tg.sends[len(tg.sends)-1], "Unlink"), "42", c.now()))
|
||||
h.Link("telegram", chn.LinkAsked{ID: "l9", At: c.now(), Sender: chn.Sender{Identity: "666", Display: "Eve", Authenticated: true}})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l9", At: c.now(), Sender: oc.Sender{Identity: "666", Display: "Eve", Authenticated: true}})
|
||||
code := codeOf(t, dt.sends[len(dt.sends)-1])
|
||||
_ = h.Prove("telegram", chn.Code{Sender: chn.Sender{Identity: "666", Authenticated: true}, Code: code})
|
||||
_ = h.Prove("telegram", oc.Code{Sender: oc.Sender{Identity: "666", Authenticated: true}, Code: code})
|
||||
asked := tg.sends[len(tg.sends)-1]
|
||||
// Meanwhile root linked another account (allowed at the machine).
|
||||
store.ids[identityKey("telegram", "77")] = Identity{Kind: "telegram", Identity: "77", Linked: c.now(), ApproveFrom: c.now().Add(time.Hour)}
|
||||
@@ -326,7 +325,7 @@ func TestAnUnlinkedAccountDoesNotComeBackByACodeWithinADay(t *testing.T) {
|
||||
_, _ = h.Unlink("telegram")
|
||||
_, _ = h.Chosen("telegram", choiceOf(ticketFor(tg.sends[len(tg.sends)-1], "Unlink"), "42", c.now()))
|
||||
shown := len(dt.sends)
|
||||
h.Link("telegram", chn.LinkAsked{ID: "l1", At: c.now(), Sender: chn.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l1", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
|
||||
if _, pending := store.links["telegram"]; pending {
|
||||
t.Error("a link was begun for the account unlinked an hour ago")
|
||||
}
|
||||
@@ -345,7 +344,7 @@ func TestAnUnlinkedAccountDoesNotComeBackByACodeWithinADay(t *testing.T) {
|
||||
func TestAnAccountInItsFirstHourIsUnlinkedFromTheDesk(t *testing.T) {
|
||||
h, tg, dt, c, store, w := newRouter(t)
|
||||
link(h, store, c, "666", c.now().Add(-10*time.Minute)) // an account the operator did not link
|
||||
h.Link("telegram", chn.LinkAsked{ID: "l1", At: c.now(), Sender: chn.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l1", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
|
||||
if reply := tg.sends[len(tg.sends)-1]; !strings.Contains(reply.Body, "unlink it at your desk") {
|
||||
t.Fatalf("the sender was not told how: %q", reply.Body)
|
||||
}
|
||||
@@ -367,7 +366,7 @@ func TestAnAccountInItsFirstHourIsUnlinkedFromTheDesk(t *testing.T) {
|
||||
// Past its hour, no question goes to the desk.
|
||||
h2, tg2, dt2, c2, store2, _ := newRouter(t)
|
||||
link(h2, store2, c2, "666", c2.now().Add(-2*time.Hour))
|
||||
h2.Link("telegram", chn.LinkAsked{ID: "l1", At: c2.now(), Sender: chn.Sender{Identity: "42", Authenticated: true}})
|
||||
h2.Link("telegram", oc.LinkAsked{ID: "l1", At: c2.now(), Sender: oc.Sender{Identity: "42", Authenticated: true}})
|
||||
for _, m := range dt2.sends {
|
||||
if labelsOf(m) == "Unlink it" {
|
||||
t.Error("the desk was asked to unlink an account past its first hour")
|
||||
@@ -383,7 +382,7 @@ func TestAnAccountInItsFirstHourIsUnlinkedFromTheDesk(t *testing.T) {
|
||||
func TestNoLinkIsBegunWhileTheDeskCannotShowTheCode(t *testing.T) {
|
||||
h, tg, dt, c, store, _ := newRouter(t)
|
||||
h.Bench.Stand("desktop", Standing{Ready: false, Why: "notifications are paused on the desk", At: c.now()})
|
||||
h.Link("telegram", chn.LinkAsked{ID: "l1", At: c.now(), Sender: chn.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l1", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
|
||||
if _, pending := store.links["telegram"]; pending {
|
||||
t.Error("a link was begun")
|
||||
}
|
||||
@@ -399,12 +398,12 @@ func TestNoLinkIsBegunWhileTheDeskCannotShowTheCode(t *testing.T) {
|
||||
// operator's link, and a restart forgets no count.
|
||||
func TestLinksAreBoundedPerAccountAndTheCountIsKept(t *testing.T) {
|
||||
h, tg, dt, c, store, _ := newRouter(t)
|
||||
eve := chn.Sender{Identity: "666", Display: "Eve", Authenticated: true}
|
||||
eve := oc.Sender{Identity: "666", Display: "Eve", Authenticated: true}
|
||||
for i := 0; i < LinksAnHour; i++ {
|
||||
h.Link("telegram", chn.LinkAsked{ID: "e" + string(rune('0'+i)), At: c.now(), Sender: eve})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "e" + string(rune('0'+i)), At: c.now(), Sender: eve})
|
||||
}
|
||||
shown := len(dt.sends)
|
||||
h.Link("telegram", chn.LinkAsked{ID: "jo", At: c.now(), Sender: chn.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "jo", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
|
||||
if len(dt.sends) != shown+1 || !strings.Contains(dt.sends[len(dt.sends)-1].Body, "(number 42)") {
|
||||
t.Fatalf("a stranger's tries stopped the operator's link: %q", tg.sends[len(tg.sends)-1].Body)
|
||||
}
|
||||
@@ -414,7 +413,7 @@ func TestLinksAreBoundedPerAccountAndTheCountIsKept(t *testing.T) {
|
||||
c2.pass(c.now().Sub(c2.now()))
|
||||
stand(h2, c2)
|
||||
before := len(dt2.sends)
|
||||
h2.Link("telegram", chn.LinkAsked{ID: "e9", At: c2.now(), Sender: eve})
|
||||
h2.Link("telegram", oc.LinkAsked{ID: "e9", At: c2.now(), Sender: eve})
|
||||
if len(dt2.sends) != before || !strings.Contains(tg2.sends[len(tg2.sends)-1].Body, "Try again in an hour") {
|
||||
t.Error("a restart forgot the count")
|
||||
}
|
||||
@@ -463,7 +462,7 @@ func TestTwoAnswersAtOnceMakeOneWarrant(t *testing.T) {
|
||||
func TestAnUnlinkFromTheDeskInTheFirstHourBarsNothing(t *testing.T) {
|
||||
h, tg, dt, c, store, _ := newRouter(t)
|
||||
link(h, store, c, "42", c.now().Add(-10*time.Minute)) // the operator's own, linked just now
|
||||
h.Link("telegram", chn.LinkAsked{ID: "l1", At: c.now(), Sender: chn.Sender{Identity: "666", Display: "X", Authenticated: true}})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l1", At: c.now(), Sender: oc.Sender{Identity: "666", Display: "X", Authenticated: true}})
|
||||
asked := dt.sends[len(dt.sends)-1]
|
||||
ch := choiceOf(ticketFor(asked, "Unlink it"), "g14", c.now()) // a click nobody can prove was the operator's
|
||||
ch.Sender.Authenticated = false
|
||||
@@ -479,7 +478,7 @@ func TestAnUnlinkFromTheDeskInTheFirstHourBarsNothing(t *testing.T) {
|
||||
// The operator sends /start again: a code is shown at once, and the code links without waiting for anybody.
|
||||
c.pass(time.Minute)
|
||||
stand(h, c)
|
||||
h.Link("telegram", chn.LinkAsked{ID: "l2", At: c.now(), Sender: chn.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l2", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
|
||||
code := ""
|
||||
for _, m := range dt.sends {
|
||||
if strings.Contains(m.Body, "number 42") && strings.Contains(m.Body, "send this code") {
|
||||
@@ -489,7 +488,7 @@ func TestAnUnlinkFromTheDeskInTheFirstHourBarsNothing(t *testing.T) {
|
||||
if code == "" {
|
||||
t.Fatalf("no code was shown to relink the operator's account; said %q", tg.sends[len(tg.sends)-1].Body)
|
||||
}
|
||||
if a := h.Prove("telegram", chn.Code{Sender: chn.Sender{Identity: "42", Authenticated: true}, Code: code, Purpose: "link"}); !a.Accepted ||
|
||||
if a := h.Prove("telegram", oc.Code{Sender: oc.Sender{Identity: "42", Authenticated: true}, Code: code, Purpose: "link"}); !a.Accepted ||
|
||||
!strings.Contains(a.Words, "Linked") {
|
||||
t.Fatalf("the operator's code was not taken at once: %+v", a)
|
||||
}
|
||||
@@ -549,7 +548,7 @@ func TestTheRootFreeWordIsNotAskedWhileTheWorkIsHeld(t *testing.T) {
|
||||
<-asking
|
||||
linked := make(chan struct{})
|
||||
go func() {
|
||||
h.Link("telegram", chn.LinkAsked{ID: "l9", At: c.now(), Sender: chn.Sender{Identity: "77", Authenticated: true}})
|
||||
h.Link("telegram", oc.LinkAsked{ID: "l9", At: c.now(), Sender: oc.Sender{Identity: "77", Authenticated: true}})
|
||||
close(linked)
|
||||
}()
|
||||
select {
|
||||
|
||||
@@ -24,7 +24,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
ch "git.novox.be/novox/mesh-sdk/go/channel"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
)
|
||||
|
||||
// StandingFresh is how long a channel's word on whether it can send is believed: a channel says it at
|
||||
@@ -277,16 +278,16 @@ func (c *SeatChannel) putOn(kind, handle string, m Message, edit bool) error {
|
||||
if c.Publish == nil {
|
||||
return errors.New("this router cannot reach the bus")
|
||||
}
|
||||
verb := ch.Send
|
||||
verb := occ.Send
|
||||
switch {
|
||||
case edit:
|
||||
verb = ch.Edit
|
||||
verb = occ.Edit
|
||||
case len(m.Actions) > 0:
|
||||
verb = ch.Show
|
||||
verb = occ.Show
|
||||
}
|
||||
// Secret travels whatever the path: a link's code is kept by no channel (the review of 2026-10-09 found it
|
||||
// dropped here, where the desk's own role sends).
|
||||
out := ch.Message{Handle: handle, Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply,
|
||||
out := oc.Message{Handle: handle, Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply,
|
||||
Secret: m.Secret}
|
||||
if c.Bench.To != nil {
|
||||
out.To = c.Bench.To(kind)
|
||||
@@ -295,9 +296,9 @@ func (c *SeatChannel) putOn(kind, handle string, m Message, edit bool) error {
|
||||
out.To = m.To
|
||||
}
|
||||
for _, a := range m.Actions {
|
||||
out.Buttons = append(out.Buttons, ch.Button{Label: a.Label, Ticket: a.Token})
|
||||
out.Buttons = append(out.Buttons, oc.Button{Label: a.Label, Ticket: a.Token})
|
||||
}
|
||||
return c.Publish(ch.ChannelSubject(verb, kind), out, handle+"."+fmt.Sprint(time.Now().UnixNano()))
|
||||
return c.Publish(occ.ChannelSubject(verb, kind), out, handle+"."+fmt.Sprint(time.Now().UnixNano()))
|
||||
}
|
||||
|
||||
func newHandle() string {
|
||||
|
||||
@@ -39,7 +39,8 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/channel"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
)
|
||||
|
||||
// Kind is this channel's kind on both benches.
|
||||
@@ -134,7 +135,7 @@ type Channel struct {
|
||||
meAt time.Time
|
||||
me string
|
||||
meErr string
|
||||
said channel.Standing
|
||||
said oc.Standing
|
||||
saidAt time.Time
|
||||
lastSent time.Time
|
||||
lastErr string
|
||||
@@ -167,7 +168,7 @@ func (c *Channel) init() {
|
||||
|
||||
// buttons are a message's buttons as the API takes them: a label and the ticket, which must fit the 64
|
||||
// bytes Telegram allows; one that does not is left out and said.
|
||||
func (c *Channel) buttons(m channel.Message) [][2]string {
|
||||
func (c *Channel) buttons(m oc.Message) [][2]string {
|
||||
var out [][2]string
|
||||
for _, b := range m.Buttons {
|
||||
if len(b.Ticket) == 0 || len(b.Ticket) > 64 {
|
||||
@@ -181,7 +182,7 @@ func (c *Channel) buttons(m channel.Message) [][2]string {
|
||||
|
||||
// chatFor is where a message goes: the linked account the router names, else the chat of the arrival it
|
||||
// replies to. Neither is a permanent failure: nobody is linked, and nobody asked.
|
||||
func (c *Channel) chatFor(m channel.Message) (int64, error) {
|
||||
func (c *Channel) chatFor(m oc.Message) (int64, error) {
|
||||
if m.To != "" {
|
||||
id, err := strconv.ParseInt(m.To, 10, 64)
|
||||
if err != nil {
|
||||
@@ -208,14 +209,14 @@ func (c *Channel) Take(subject string, body json.RawMessage) error {
|
||||
return nil
|
||||
}
|
||||
verb := parts[4]
|
||||
var m channel.Message
|
||||
var m oc.Message
|
||||
if err := json.Unmarshal(body, &m); err != nil || m.Handle == "" {
|
||||
c.Logf("[telegram] work on %s could not be read; taken and dropped", subject)
|
||||
return nil
|
||||
}
|
||||
// The router's word on a tap, said where the tap was made: the tap's own acknowledgement (issue 372). A
|
||||
// word of one line is that alone; one with more to it is also said in the chat, where it stays.
|
||||
if verb == channel.Send && m.Reply != "" && !m.Secret && c.settleTap(m.Reply, toast(m)) && strings.TrimSpace(m.Body) == "" {
|
||||
if verb == occ.Send && m.Reply != "" && !m.Secret && c.settleTap(m.Reply, toast(m)) && strings.TrimSpace(m.Body) == "" {
|
||||
c.mu.Lock()
|
||||
c.lastSent, c.lastErr = c.Now(), ""
|
||||
c.delivered++
|
||||
@@ -235,7 +236,7 @@ func (c *Channel) Take(subject string, body json.RawMessage) error {
|
||||
// delete loses its buttons and says a newer message follows. A send that fails leaves the earlier one.
|
||||
var place, earlier *Placed
|
||||
switch {
|
||||
case verb == channel.Edit:
|
||||
case verb == occ.Edit:
|
||||
if place, err = c.Store.Placed(m.Handle); err != nil {
|
||||
return err // the state is away: offered again
|
||||
}
|
||||
@@ -320,8 +321,8 @@ func (c *Channel) fail(handle string, err error, forGood bool) {
|
||||
c.failed++
|
||||
c.mu.Unlock()
|
||||
c.Logf("[telegram] a message was not delivered: %v", err)
|
||||
f := channel.Failed{Handle: handle, Why: err.Error(), Permanent: forGood, At: c.Now()}
|
||||
if perr := c.Publish(channel.IntakeSubject(channel.FailedWhat, Kind), f, "failed."+handle+"."+strconv.FormatInt(c.Now().UnixNano(), 10)); perr != nil {
|
||||
f := oc.Failed{Handle: handle, Why: err.Error(), Permanent: forGood, At: c.Now()}
|
||||
if perr := c.Publish(occ.IntakeSubject(occ.FailedWhat, Kind), f, "failed."+handle+"."+strconv.FormatInt(c.Now().UnixNano(), 10)); perr != nil {
|
||||
c.Logf("[telegram] and the router could not be told: %v", perr)
|
||||
}
|
||||
}
|
||||
@@ -332,8 +333,8 @@ func private(from *User, chat Chat) bool {
|
||||
return from != nil && !from.IsBot && chat.Type == "private" && chat.ID == from.ID
|
||||
}
|
||||
|
||||
func senderOf(u User) channel.Sender {
|
||||
return channel.Sender{Identity: strconv.FormatInt(u.ID, 10), Display: u.Display(), Authenticated: true}
|
||||
func senderOf(u User) oc.Sender {
|
||||
return oc.Sender{Identity: strconv.FormatInt(u.ID, 10), Display: u.Display(), Authenticated: true}
|
||||
}
|
||||
|
||||
// Handle takes one update.
|
||||
@@ -368,8 +369,8 @@ func (c *Channel) Handle(u Update) {
|
||||
c.mu.Lock()
|
||||
c.taps[id] = cb.ID
|
||||
c.mu.Unlock()
|
||||
ch := channel.Choice{ID: id, Ticket: cb.Data, Sender: senderOf(cb.From), At: now}
|
||||
if err := c.Publish(channel.IntakeSubject(channel.Chosen, Kind), ch, id); err != nil {
|
||||
ch := oc.Choice{ID: id, Ticket: cb.Data, Sender: senderOf(cb.From), At: now}
|
||||
if err := c.Publish(occ.IntakeSubject(occ.Chosen, Kind), ch, id); err != nil {
|
||||
c.Logf("[telegram] a tap could not be said to the router: %v", err)
|
||||
words := "The mesh could not take this answer now. Nothing was done. Try again in a minute."
|
||||
c.settleTap(id, words)
|
||||
@@ -386,8 +387,8 @@ func (c *Channel) Handle(u Update) {
|
||||
c.remember(id, m.Chat.ID)
|
||||
switch t := strings.TrimSpace(m.Text); {
|
||||
case t == "/start" || strings.HasPrefix(t, "/start "):
|
||||
l := channel.LinkAsked{ID: id, Sender: senderOf(*m.From), At: now}
|
||||
if err := c.Publish(channel.IntakeSubject(channel.Link, Kind), l, id); err != nil {
|
||||
l := oc.LinkAsked{ID: id, Sender: senderOf(*m.From), At: now}
|
||||
if err := c.Publish(occ.IntakeSubject(occ.Link, Kind), l, id); err != nil {
|
||||
c.reply(m.Chat.ID, "The mesh could not take this now. Try again in a minute.")
|
||||
}
|
||||
case codeShape.MatchString(t):
|
||||
@@ -396,12 +397,12 @@ func (c *Channel) Handle(u Update) {
|
||||
if err := c.Bot.Delete(m.Chat.ID, m.MessageID); err != nil {
|
||||
c.Logf("[telegram] a code could not be deleted from the chat: %v", err)
|
||||
}
|
||||
raw, err := c.Prove(channel.ProofSubject(channel.CodeProof, Kind), channel.Code{Sender: senderOf(*m.From), Code: code, Purpose: "link"})
|
||||
raw, err := c.Prove(occ.ProofSubject(occ.CodeProof, Kind), oc.Code{Sender: senderOf(*m.From), Code: code, Purpose: "link"})
|
||||
if err != nil {
|
||||
c.reply(m.Chat.ID, "The mesh could not check the code now. Try again in a minute.")
|
||||
return
|
||||
}
|
||||
var a channel.ProofAnswer
|
||||
var a oc.ProofAnswer
|
||||
if json.Unmarshal(raw, &a) != nil || a.Words == "" {
|
||||
a.Words = "The mesh's answer could not be read."
|
||||
}
|
||||
@@ -432,7 +433,7 @@ func (c *Channel) settleTap(arrival, words string) bool {
|
||||
|
||||
// toast is a message as a tap's acknowledgement shows it: its title and the first line of its body, cut at
|
||||
// ToastLength.
|
||||
func toast(m channel.Message) string {
|
||||
func toast(m oc.Message) string {
|
||||
t := strings.TrimSpace(m.Title)
|
||||
if first, _, _ := strings.Cut(strings.TrimSpace(m.Body), "\n"); first != "" {
|
||||
t += ": " + first
|
||||
@@ -502,10 +503,10 @@ func (c *Channel) Poll(wait time.Duration) error {
|
||||
}
|
||||
|
||||
// Standing is whether the channel can send now: the token works and the updates arrive.
|
||||
func (c *Channel) Standing() channel.Standing {
|
||||
func (c *Channel) Standing() oc.Standing {
|
||||
c.init()
|
||||
now := c.Now()
|
||||
s := channel.Standing{EditsSilently: true, At: now}
|
||||
s := oc.Standing{EditsSilently: true, At: now}
|
||||
if _, err := c.Bot.token(); err != nil {
|
||||
s.Why = err.Error()
|
||||
return s
|
||||
@@ -545,7 +546,7 @@ func (c *Channel) SayStanding() {
|
||||
if same {
|
||||
return
|
||||
}
|
||||
if err := c.Publish(channel.IntakeSubject(channel.StandingWhat, Kind), s, "standing."+strconv.FormatInt(s.At.UnixNano(), 10)); err != nil {
|
||||
if err := c.Publish(occ.IntakeSubject(occ.StandingWhat, Kind), s, "standing."+strconv.FormatInt(s.At.UnixNano(), 10)); err != nil {
|
||||
c.Logf("[telegram] could not say whether it can send: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/channel"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
)
|
||||
|
||||
const goodToken = "123456789:AAEhBP0av28P4XFQnIuR-o-7Xnz1kkUzW3g"
|
||||
@@ -184,15 +185,15 @@ func (s *said) waited() {
|
||||
}
|
||||
}
|
||||
|
||||
func work(verb string, m channel.Message) (string, json.RawMessage) {
|
||||
func work(verb string, m oc.Message) (string, json.RawMessage) {
|
||||
raw, _ := json.Marshal(m)
|
||||
return channel.ChannelSubject(verb, Kind), raw
|
||||
return occ.ChannelSubject(verb, Kind), raw
|
||||
}
|
||||
|
||||
func TestAQuestionIsShownWithItsTicketsAndRingsAndAWarningIsSilent(t *testing.T) {
|
||||
c, f, _, st := newChannel(t, goodToken)
|
||||
subject, body := work(channel.Show, channel.Message{Handle: "m1", Title: "Delivery of hq held", Body: "Needs you: release it.",
|
||||
To: "42", Buttons: []channel.Button{{Label: "Release", Ticket: "0123456789abcdef0123456789abcdef"}}})
|
||||
subject, body := work(occ.Show, oc.Message{Handle: "m1", Title: "Delivery of hq held", Body: "Needs you: release it.",
|
||||
To: "42", Buttons: []oc.Button{{Label: "Release", Ticket: "0123456789abcdef0123456789abcdef"}}})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -212,12 +213,12 @@ func TestAQuestionIsShownWithItsTicketsAndRingsAndAWarningIsSilent(t *testing.T)
|
||||
if st.placed["m1"].ID == 0 {
|
||||
t.Error("where it is was not kept")
|
||||
}
|
||||
subject, body = work(channel.Send, channel.Message{Handle: "m2", Title: "Resolved: hq works again", To: "42"})
|
||||
subject, body = work(occ.Send, oc.Message{Handle: "m2", Title: "Resolved: hq works again", To: "42"})
|
||||
_ = c.Take(subject, body)
|
||||
if f.of("sendMessage")[1].Body["disable_notification"] != true {
|
||||
t.Error("a clearing rang (D4)")
|
||||
}
|
||||
subject, body = work(channel.Send, channel.Message{Handle: "m3", Title: "Urgent: ace silent", Urgent: true, To: "42"})
|
||||
subject, body = work(occ.Send, oc.Message{Handle: "m3", Title: "Urgent: ace silent", Urgent: true, To: "42"})
|
||||
_ = c.Take(subject, body)
|
||||
if f.of("sendMessage")[2].Body["disable_notification"] != false {
|
||||
t.Error("an urgent message was silent (D4)")
|
||||
@@ -226,7 +227,7 @@ func TestAQuestionIsShownWithItsTicketsAndRingsAndAWarningIsSilent(t *testing.T)
|
||||
|
||||
func TestALongMessageIsCutAndTheCutSaid(t *testing.T) {
|
||||
c, f, _, _ := newChannel(t, goodToken)
|
||||
subject, body := work(channel.Send, channel.Message{Handle: "m1", Title: "t", Body: strings.Repeat("x", 5000), To: "42"})
|
||||
subject, body := work(occ.Send, oc.Message{Handle: "m1", Title: "t", Body: strings.Repeat("x", 5000), To: "42"})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -243,10 +244,10 @@ func TestALongMessageIsCutAndTheCutSaid(t *testing.T) {
|
||||
// nor injects. The tap on it is still compared by the plain words, which is what Telegram gives back.
|
||||
func TestAMessageIsRenderedInHTMLWithItsMarkupEscaped(t *testing.T) {
|
||||
c, f, s, _ := newChannel(t, goodToken)
|
||||
subject, body := work(channel.Show, channel.Message{Handle: "q1", Title: "Set mounts on <shanks> & co?",
|
||||
subject, body := work(occ.Show, oc.Message{Handle: "q1", Title: "Set mounts on <shanks> & co?",
|
||||
Body: "Changes two settings.\n+ shares: media=/storage/media\n~ sources: a=b (was: c)",
|
||||
Note: "Asked by mesh-controller at 15:11.\nFingerprint 1234 5678 9abc.",
|
||||
To: "42", Buttons: []channel.Button{{Label: "Approve", Ticket: "tkt"}}})
|
||||
To: "42", Buttons: []oc.Button{{Label: "Approve", Ticket: "tkt"}}})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -264,7 +265,7 @@ func TestAMessageIsRenderedInHTMLWithItsMarkupEscaped(t *testing.T) {
|
||||
Message: &Msg{MessageID: shown, Chat: Chat{ID: 42, Type: "private"},
|
||||
Text: "Set mounts on <shanks> & co?\nChanges two settings.\n+ shares: media=/storage/media\n~ sources: a=b (was: c)\n" +
|
||||
"Asked by mesh-controller at 15:11.\nFingerprint 1234 5678 9abc."}}})
|
||||
if got := s.of(channel.IntakeSubject(channel.Chosen, Kind)); len(got) != 1 {
|
||||
if got := s.of(occ.IntakeSubject(occ.Chosen, Kind)); len(got) != 1 {
|
||||
t.Fatalf("the tap on the rendered message was not taken: %s", got)
|
||||
}
|
||||
// Words changed by another holder of the token are still told apart.
|
||||
@@ -272,11 +273,11 @@ func TestAMessageIsRenderedInHTMLWithItsMarkupEscaped(t *testing.T) {
|
||||
Message: &Msg{MessageID: shown, Chat: Chat{ID: 42, Type: "private"},
|
||||
Text: "Set mounts on <shanks> & co?\nChanges three settings.\n+ shares: media=/storage/media\n~ sources: a=b (was: c)\n" +
|
||||
"Asked by mesh-controller at 15:11.\nFingerprint 1234 5678 9abc."}}})
|
||||
if got := s.of(channel.IntakeSubject(channel.Chosen, Kind)); len(got) != 1 {
|
||||
if got := s.of(occ.IntakeSubject(occ.Chosen, Kind)); len(got) != 1 {
|
||||
t.Fatalf("a tap on changed words was taken: %s", got)
|
||||
}
|
||||
// The edit says what was chosen, one line with the headline, and is in the same mode.
|
||||
subject, body = work(channel.Edit, channel.Message{Handle: "q1", Title: "You chose Approve at 15:06 on Telegram · Set mounts on <shanks> & co?",
|
||||
subject, body = work(occ.Edit, oc.Message{Handle: "q1", Title: "You chose Approve at 15:06 on Telegram · Set mounts on <shanks> & co?",
|
||||
Note: "Told to mesh-controller, which acts on it itself.", To: "42"})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -302,7 +303,7 @@ func TestATapOnAMessageShownBeforeIsStillTaken(t *testing.T) {
|
||||
}
|
||||
c.Handle(Update{UpdateID: 1, Callback: &Callback{ID: "cb1", Data: "tkt", From: User{ID: 42, FirstName: "Jo"},
|
||||
Message: &Msg{MessageID: 9, Chat: Chat{ID: 42, Type: "private"}, Text: words}}})
|
||||
if got := s.of(channel.IntakeSubject(channel.Chosen, Kind)); len(got) != 1 {
|
||||
if got := s.of(occ.IntakeSubject(occ.Chosen, Kind)); len(got) != 1 {
|
||||
t.Fatalf("the tap on a message shown before was not taken: %s", got)
|
||||
}
|
||||
}
|
||||
@@ -313,8 +314,8 @@ func TestAnEditToTheSameTextIsASuccessAndAGoneMessageIsSentAgain(t *testing.T) {
|
||||
f.answer["editMessageText"] = func(map[string]any) (int, string) {
|
||||
return 400, `{"ok":false,"error_code":400,"description":"Bad Request: message is not modified"}`
|
||||
}
|
||||
subject, body := work(channel.Edit, channel.Message{Handle: "m1", Title: "same", To: "42"})
|
||||
if err := c.Take(subject, body); err != nil || len(f.of("sendMessage")) != 0 || len(s.of(channel.IntakeSubject(channel.FailedWhat, Kind))) != 0 {
|
||||
subject, body := work(occ.Edit, oc.Message{Handle: "m1", Title: "same", To: "42"})
|
||||
if err := c.Take(subject, body); err != nil || len(f.of("sendMessage")) != 0 || len(s.of(occ.IntakeSubject(occ.FailedWhat, Kind))) != 0 {
|
||||
t.Fatalf("an identical edit was not a success (D6): %v", err)
|
||||
}
|
||||
f.answer["editMessageText"] = func(map[string]any) (int, string) {
|
||||
@@ -341,21 +342,21 @@ func TestARefusalIsPermanentARateLimitIsWaitedOutAndNeitherCarriesTheToken(t *te
|
||||
}
|
||||
return 403, `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user ` + goodToken + `"}`
|
||||
}
|
||||
subject, body := work(channel.Send, channel.Message{Handle: "m1", Title: "x", To: "42"})
|
||||
subject, body := work(occ.Send, oc.Message{Handle: "m1", Title: "x", To: "42"})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(slept) != 1 || slept[0] != 7*time.Second || tries != 2 {
|
||||
t.Errorf("the rate limit was not waited out as told (D5): slept %v, tries %d", slept, tries)
|
||||
}
|
||||
failed := s.of(channel.IntakeSubject(channel.FailedWhat, Kind))
|
||||
failed := s.of(occ.IntakeSubject(occ.FailedWhat, Kind))
|
||||
if len(failed) != 1 || !strings.Contains(string(failed[0]), `"permanent":true`) || strings.Contains(string(failed[0]), goodToken) {
|
||||
t.Fatalf("the refusal: %s (D6)", failed)
|
||||
}
|
||||
// No linked account and nothing to reply to: said as failed for good, never retried.
|
||||
subject, body = work(channel.Send, channel.Message{Handle: "m2", Title: "x"})
|
||||
subject, body = work(occ.Send, oc.Message{Handle: "m2", Title: "x"})
|
||||
_ = c.Take(subject, body)
|
||||
if f := s.of(channel.IntakeSubject(channel.FailedWhat, Kind)); len(f) != 2 || !strings.Contains(string(f[1]), "no account is linked") {
|
||||
if f := s.of(occ.IntakeSubject(occ.FailedWhat, Kind)); len(f) != 2 || !strings.Contains(string(f[1]), "no account is linked") {
|
||||
t.Errorf("%s", f)
|
||||
}
|
||||
}
|
||||
@@ -365,10 +366,10 @@ func TestATapIsAcknowledgedAtOnceAndSaidWithItsAuthenticatedSender(t *testing.T)
|
||||
shown := showQuestion(t, c, f)
|
||||
c.Handle(Update{UpdateID: 1, Callback: &Callback{ID: "cb1", Data: "tkt", From: User{ID: 42, FirstName: "Jo"},
|
||||
Message: &Msg{MessageID: shown, Chat: Chat{ID: 42, Type: "private"}, Text: "Release it?\nThe delivery is held."}}})
|
||||
got := s.of(channel.IntakeSubject(channel.Chosen, Kind))
|
||||
var ch channel.Choice
|
||||
got := s.of(occ.IntakeSubject(occ.Chosen, Kind))
|
||||
var ch oc.Choice
|
||||
if len(got) != 1 || json.Unmarshal(got[0], &ch) != nil || ch.Ticket != "tkt" ||
|
||||
!reflect.DeepEqual(ch.Sender, channel.Sender{Identity: "42", Display: "Jo", Authenticated: true}) {
|
||||
!reflect.DeepEqual(ch.Sender, oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}) {
|
||||
t.Fatalf("said %s", got)
|
||||
}
|
||||
// In a group, or from another bot, nothing is said: it is nobody's own chat with the bot.
|
||||
@@ -376,7 +377,7 @@ func TestATapIsAcknowledgedAtOnceAndSaidWithItsAuthenticatedSender(t *testing.T)
|
||||
Message: &Msg{Chat: Chat{ID: -100, Type: "group"}}}})
|
||||
c.Handle(Update{UpdateID: 3, Callback: &Callback{ID: "cb3", Data: "tkt", From: User{ID: 9, IsBot: true},
|
||||
Message: &Msg{Chat: Chat{ID: 9, Type: "private"}}}})
|
||||
if len(s.of(channel.IntakeSubject(channel.Chosen, Kind))) != 1 {
|
||||
if len(s.of(occ.IntakeSubject(occ.Chosen, Kind))) != 1 {
|
||||
t.Error("a tap outside the account's own chat was said")
|
||||
}
|
||||
}
|
||||
@@ -385,13 +386,13 @@ func TestStartAsksALinkAndACodeIsAProofDeletedFromTheChat(t *testing.T) {
|
||||
c, f, s, _ := newChannel(t, goodToken)
|
||||
jo := &User{ID: 42, FirstName: "Jo", Username: "jo"}
|
||||
c.Handle(Update{UpdateID: 10, Message: &Msg{MessageID: 5, From: jo, Chat: Chat{ID: 42, Type: "private"}, Text: "/start"}})
|
||||
var l channel.LinkAsked
|
||||
got := s.of(channel.IntakeSubject(channel.Link, Kind))
|
||||
var l oc.LinkAsked
|
||||
got := s.of(occ.IntakeSubject(occ.Link, Kind))
|
||||
if len(got) != 1 || json.Unmarshal(got[0], &l) != nil || l.Sender.Identity != "42" || l.Sender.Display != "Jo (@jo)" {
|
||||
t.Fatalf("link %s", got)
|
||||
}
|
||||
// The router's answer to the link goes to the chat it came from, though nobody is linked yet.
|
||||
subject, body := work(channel.Send, channel.Message{Handle: "r1", Title: "Send the code shown on your desk", Reply: l.ID})
|
||||
subject, body := work(occ.Send, oc.Message{Handle: "r1", Title: "Send the code shown on your desk", Reply: l.ID})
|
||||
if err := c.Take(subject, body); err != nil || len(f.of("sendMessage")) != 1 || f.of("sendMessage")[0].Body["chat_id"] != float64(42) {
|
||||
t.Fatalf("the reply did not reach the chat: %v %v", err, f.of("sendMessage"))
|
||||
}
|
||||
@@ -439,7 +440,7 @@ func TestStandingSaysWhatIsMissingAndNeverTheToken(t *testing.T) {
|
||||
}
|
||||
c.SayStanding()
|
||||
c.SayStanding()
|
||||
if n := len(said.of(channel.IntakeSubject(channel.StandingWhat, Kind))); n != 1 {
|
||||
if n := len(said.of(occ.IntakeSubject(occ.StandingWhat, Kind))); n != 1 {
|
||||
t.Errorf("standing said %d times without a change", n)
|
||||
}
|
||||
}
|
||||
@@ -449,7 +450,7 @@ func TestPollTakesTheUpdatesAndKeepsTheOffsetAndSaysAConflict(t *testing.T) {
|
||||
f.answer["getUpdates"] = func(map[string]any) (int, string) {
|
||||
return 200, `{"ok":true,"result":[{"update_id":41,"message":{"message_id":1,"from":{"id":42},"chat":{"id":42,"type":"private"},"text":"/start"}}]}`
|
||||
}
|
||||
if err := c.Poll(time.Second); err != nil || st.offset != 42 || len(s.of(channel.IntakeSubject(channel.Link, Kind))) != 1 {
|
||||
if err := c.Poll(time.Second); err != nil || st.offset != 42 || len(s.of(occ.IntakeSubject(occ.Link, Kind))) != 1 {
|
||||
t.Fatalf("%v offset %d", err, st.offset)
|
||||
}
|
||||
f.answer["getUpdates"] = func(map[string]any) (int, string) {
|
||||
@@ -466,8 +467,8 @@ func TestPollTakesTheUpdatesAndKeepsTheOffsetAndSaysAConflict(t *testing.T) {
|
||||
// showQuestion shows one question with a button to the linked account, and answers its message's id.
|
||||
func showQuestion(t *testing.T, c *Channel, f *fakeAPI) int64 {
|
||||
t.Helper()
|
||||
subject, body := work(channel.Show, channel.Message{Handle: "q1", Title: "Release it?", Body: "The delivery is held.",
|
||||
To: "42", Buttons: []channel.Button{{Label: "Release", Ticket: "tkt"}}})
|
||||
subject, body := work(occ.Show, oc.Message{Handle: "q1", Title: "Release it?", Body: "The delivery is held.",
|
||||
To: "42", Buttons: []oc.Button{{Label: "Release", Ticket: "tkt"}}})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -493,7 +494,7 @@ func TestATapOnAMessageShowingOtherWordsIsRefused(t *testing.T) {
|
||||
}
|
||||
tap("cb-other", shown, "Silence it for a week?\nNothing changes.")
|
||||
tap("cb-unknown", shown+100, "Release it?\nThe delivery is held.")
|
||||
if got := s.of(channel.IntakeSubject(channel.Chosen, Kind)); len(got) != 0 {
|
||||
if got := s.of(occ.IntakeSubject(occ.Chosen, Kind)); len(got) != 0 {
|
||||
t.Fatalf("a tap on other words was said: %s", got)
|
||||
}
|
||||
answers := f.of("answerCallbackQuery")
|
||||
@@ -502,16 +503,16 @@ func TestATapOnAMessageShowingOtherWordsIsRefused(t *testing.T) {
|
||||
}
|
||||
// The same words, as Telegram gives them back (its line endings, its outer spaces), are taken.
|
||||
tap("cb-same", shown, " Release it?\r\nThe delivery is held. ")
|
||||
if got := s.of(channel.IntakeSubject(channel.Chosen, Kind)); len(got) != 1 {
|
||||
if got := s.of(occ.IntakeSubject(occ.Chosen, Kind)); len(got) != 1 {
|
||||
t.Fatalf("the tap on the words shown was not said: %s", got)
|
||||
}
|
||||
// An edit moves what was shown: the old words are then refused.
|
||||
subject, body := work(channel.Edit, channel.Message{Handle: "q1", Title: "Released", Body: "by you", To: "42"})
|
||||
subject, body := work(occ.Edit, oc.Message{Handle: "q1", Title: "Released", Body: "by you", To: "42"})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tap("cb-old", shown, "Release it?\nThe delivery is held.")
|
||||
if got := s.of(channel.IntakeSubject(channel.Chosen, Kind)); len(got) != 1 {
|
||||
if got := s.of(occ.IntakeSubject(occ.Chosen, Kind)); len(got) != 1 {
|
||||
t.Errorf("a tap on words since replaced was said")
|
||||
}
|
||||
if d, _ := st.Shown(42, shown); d == "" {
|
||||
@@ -536,7 +537,7 @@ func TestATapIsAcknowledgedWithTheRoutersWordOnIt(t *testing.T) {
|
||||
t.Fatalf("the tap was acknowledged before the router said anything: %v", got)
|
||||
}
|
||||
sentBefore := len(f.of("sendMessage"))
|
||||
subject, body := work(channel.Send, channel.Message{Handle: "r1", Title: "You chose Release: recorded, and mesh-delivery was told.",
|
||||
subject, body := work(occ.Send, oc.Message{Handle: "r1", Title: "You chose Release: recorded, and mesh-delivery was told.",
|
||||
Silent: true, Reply: "tg-tap-cb1", To: "42"})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -569,7 +570,7 @@ func TestARefusedTapSaysWhyOnThePhoneAndInTheChat(t *testing.T) {
|
||||
shown := showQuestion(t, c, f)
|
||||
tapOn(c, shown, "cb1")
|
||||
sentBefore := len(f.of("sendMessage"))
|
||||
subject, body := work(channel.Send, channel.Message{Handle: "r1", Title: "That answer was not taken",
|
||||
subject, body := work(occ.Send, oc.Message{Handle: "r1", Title: "That answer was not taken",
|
||||
Body: "The question was already answered or has ended. Nothing was done.", Silent: true, Reply: "tg-tap-cb1", To: "42"})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -595,7 +596,7 @@ func TestATapTheRouterIsSlowOnClaimsNothingAndItsWordComesInTheChat(t *testing.T
|
||||
t.Fatalf("a slow router's tap was acknowledged with %v", got)
|
||||
}
|
||||
sentBefore := len(f.of("sendMessage"))
|
||||
subject, body := work(channel.Send, channel.Message{Handle: "r1", Title: "You chose Release: recorded.", Reply: "tg-tap-cb1", To: "42"})
|
||||
subject, body := work(occ.Send, oc.Message{Handle: "r1", Title: "You chose Release: recorded.", Reply: "tg-tap-cb1", To: "42"})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -607,7 +608,7 @@ func TestATapTheRouterIsSlowOnClaimsNothingAndItsWordComesInTheChat(t *testing.T
|
||||
return 400, `{"ok":false,"error_code":400,"description":"Bad Request: query is too old and response timeout expired or query ID is invalid"}`
|
||||
}
|
||||
tapOn(c, shown, "cb2")
|
||||
subject, body = work(channel.Send, channel.Message{Handle: "r2", Title: "You chose Release: recorded.", Reply: "tg-tap-cb2", To: "42"})
|
||||
subject, body = work(occ.Send, oc.Message{Handle: "r2", Title: "You chose Release: recorded.", Reply: "tg-tap-cb2", To: "42"})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -629,7 +630,7 @@ func TestATapNotSaidToTheRouterIsSaidSoAndASecretIsNeverAToast(t *testing.T) {
|
||||
}
|
||||
c.Publish = func(string, any, string) error { return nil }
|
||||
tapOn(c, shown, "cb2")
|
||||
subject, body := work(channel.Send, channel.Message{Handle: "r1", Title: "Your code", Body: "123456", Secret: true,
|
||||
subject, body := work(occ.Send, oc.Message{Handle: "r1", Title: "Your code", Body: "123456", Secret: true,
|
||||
Reply: "tg-tap-cb2", To: "42"})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -643,11 +644,11 @@ func TestATapNotSaidToTheRouterIsSaidSoAndASecretIsNeverAToast(t *testing.T) {
|
||||
|
||||
// The toast is the title and the first line of the body, cut at Telegram's bound.
|
||||
func TestAToastIsCutAtTelegramsBound(t *testing.T) {
|
||||
long := toast(channel.Message{Title: strings.Repeat("a", 300)})
|
||||
long := toast(oc.Message{Title: strings.Repeat("a", 300)})
|
||||
if utf8.RuneCountInString(long) != ToastLength || !strings.HasSuffix(long, "…") {
|
||||
t.Errorf("a long toast is %d runes", utf8.RuneCountInString(long))
|
||||
}
|
||||
if got := toast(channel.Message{Title: "Details: x", Body: "first\nsecond"}); got != "Details: x: first" {
|
||||
if got := toast(oc.Message{Title: "Details: x", Body: "first\nsecond"}); got != "Details: x: first" {
|
||||
t.Errorf("toast %q", got)
|
||||
}
|
||||
}
|
||||
@@ -657,19 +658,19 @@ func TestAToastIsCutAtTelegramsBound(t *testing.T) {
|
||||
// loses its buttons. A new handle is a new message, and nothing is deleted.
|
||||
func TestAMessageSaidAgainUnderItsHandleReplacesTheEarlierOne(t *testing.T) {
|
||||
c, f, _, st := newChannel(t, goodToken)
|
||||
subject, body := work(channel.Show, channel.Message{Handle: "m1", Title: "Urgent: ace silent", Urgent: true, To: "42",
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "0123456789abcdef0123456789abcdef"}}})
|
||||
subject, body := work(occ.Show, oc.Message{Handle: "m1", Title: "Urgent: ace silent", Urgent: true, To: "42",
|
||||
Buttons: []oc.Button{{Label: "Silence for a week", Ticket: "0123456789abcdef0123456789abcdef"}}})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := st.placed["m1"].ID
|
||||
subject, body = work(channel.Send, channel.Message{Handle: "m2", Title: "another problem", To: "42"})
|
||||
subject, body = work(occ.Send, oc.Message{Handle: "m2", Title: "another problem", To: "42"})
|
||||
_ = c.Take(subject, body)
|
||||
if len(f.of("deleteMessage")) != 0 {
|
||||
t.Fatal("a new handle deleted a message")
|
||||
}
|
||||
subject, body = work(channel.Show, channel.Message{Handle: "m1", Title: "Still open: ace silent", Urgent: true, To: "42",
|
||||
Buttons: []channel.Button{{Label: "Silence for a week", Ticket: "fedcba9876543210fedcba9876543210"}}})
|
||||
subject, body = work(occ.Show, oc.Message{Handle: "m1", Title: "Still open: ace silent", Urgent: true, To: "42",
|
||||
Buttons: []oc.Button{{Label: "Silence for a week", Ticket: "fedcba9876543210fedcba9876543210"}}})
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -686,7 +687,7 @@ func TestAMessageSaidAgainUnderItsHandleReplacesTheEarlierOne(t *testing.T) {
|
||||
return 400, `{"ok":false,"error_code":400,"description":"Bad Request: message can't be deleted"}`
|
||||
}
|
||||
second := st.placed["m1"].ID
|
||||
subject, body = work(channel.Send, channel.Message{Handle: "m1", Title: "Now urgent: ace silent", Urgent: true, To: "42"})
|
||||
subject, body = work(occ.Send, oc.Message{Handle: "m1", Title: "Now urgent: ace silent", Urgent: true, To: "42"})
|
||||
_ = c.Take(subject, body)
|
||||
ed := f.of("editMessageText")
|
||||
if len(ed) != 1 || ed[0].Body["message_id"] != float64(second) || !strings.Contains(ed[0].Body["text"].(string), "Replaced") {
|
||||
@@ -705,7 +706,7 @@ func TestAReplacementThatCannotBeSentLeavesTheEarlierMessage(t *testing.T) {
|
||||
f.answer["sendMessage"] = func(map[string]any) (int, string) {
|
||||
return 400, `{"ok":false,"error_code":400,"description":"Bad Request: chat not found"}`
|
||||
}
|
||||
subject, body := work(channel.Send, channel.Message{Handle: "m1", Title: "Still open: ace silent", Urgent: true, To: "42"})
|
||||
subject, body := work(occ.Send, oc.Message{Handle: "m1", Title: "Still open: ace silent", Urgent: true, To: "42"})
|
||||
_ = c.Take(subject, body)
|
||||
if len(f.of("deleteMessage")) != 0 || st.placed["m1"].ID != 7 {
|
||||
t.Fatalf("the earlier message was taken away though its replacement was not sent: %+v", f.of("deleteMessage"))
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/channel"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
)
|
||||
|
||||
// ParseMode is the formatting the Bot API is told the text is in.
|
||||
@@ -40,7 +40,7 @@ type shown struct{ title, body, note string }
|
||||
// first: the note carries what each answer does and every value an approval sets, so it is kept whole as long as
|
||||
// the title and the note fit; only then is the note cut, and that said too. The bound is on the words, as
|
||||
// Telegram's is on the text once its markup is read.
|
||||
func partsOf(m channel.Message) shown {
|
||||
func partsOf(m oc.Message) shown {
|
||||
s := shown{title: strings.TrimSpace(m.Title), body: strings.TrimSpace(m.Body), note: strings.TrimSpace(m.Note)}
|
||||
over := utf8.RuneCountInString(s.plain()) - MaxLength
|
||||
if over <= 0 {
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/channel"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
)
|
||||
|
||||
// adr0343 is the ask for ADR 0343 as the router now sends it to this channel (novox/hq issue 639): the knowledge
|
||||
// base's brief as its title and body, the rest in its note. On 2026-10-13 the same ask arrived as about thirty plain
|
||||
// lines — every one of its thirteen clauses on a line of its own — and the operator would not read it.
|
||||
var adr0343 = channel.Message{Handle: "h-343", To: "42",
|
||||
var adr0343 = oc.Message{Handle: "h-343", To: "42",
|
||||
Title: "Your agents' memory follows you to every machine",
|
||||
Body: "What an agent learns in a project's memory is copied to every machine that runs the agent, so a session " +
|
||||
"anywhere starts with it. A file changed on two machines keeps both versions and warns you; secrets in memory files " +
|
||||
@@ -22,13 +23,13 @@ var adr0343 = channel.Message{Handle: "h-343", To: "42",
|
||||
"Approve: the record is accepted and its clauses are in force. Decline: the record stays proposed; nothing of it is in force.\n" +
|
||||
"ADR 0343, proposed by fix-367@g14; the knowledge base's own review approved it. Fingerprint a59f 361b f60e cb5e d237 a837.\n" +
|
||||
"Read its 13 clauses with the knowledge base's get, number 343.",
|
||||
Buttons: []channel.Button{{Label: "Approve", Ticket: "t-approve"}, {Label: "Decline", Ticket: "t-decline"}}}
|
||||
Buttons: []oc.Button{{Label: "Approve", Ticket: "t-approve"}, {Label: "Decline", Ticket: "t-decline"}}}
|
||||
|
||||
// The ADR 0343 ask fits a phone's glance: a bold headline, the summary and its points, then the rest apart and
|
||||
// collapsed — at most twelve lines in all, and never a clause.
|
||||
func TestTheADR0343AskFitsAGlance(t *testing.T) {
|
||||
c, f, _, _ := newChannel(t, goodToken)
|
||||
subject, body := work(channel.Show, adr0343)
|
||||
subject, body := work(occ.Show, adr0343)
|
||||
if err := c.Take(subject, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -64,7 +65,7 @@ func TestTheADR0343AskFitsAGlance(t *testing.T) {
|
||||
// The note is kept whole when the message is too long: the body is cut first and the cut said; only a note that
|
||||
// cannot fit beside the title is cut, and that said too (the review of mesh-catalog #278).
|
||||
func TestALongBodyIsCutBeforeTheNote(t *testing.T) {
|
||||
m := channel.Message{Title: "t", Body: strings.Repeat("b", 5000), Note: "+ shares: media=/storage/media\nFingerprint 1234."}
|
||||
m := oc.Message{Title: "t", Body: strings.Repeat("b", 5000), Note: "+ shares: media=/storage/media\nFingerprint 1234."}
|
||||
s := partsOf(m)
|
||||
if s.note != m.Note || !strings.HasSuffix(s.body, cutNote) || len([]rune(s.plain())) != MaxLength {
|
||||
t.Errorf("note %q, body ends %q, %d characters", s.note, s.body[len(s.body)-20:], len([]rune(s.plain())))
|
||||
@@ -78,7 +79,7 @@ func TestALongBodyIsCutBeforeTheNote(t *testing.T) {
|
||||
|
||||
// A '<', '>' or '&' in the note is escaped like any other part: it neither breaks the blockquote nor injects.
|
||||
func TestTheNoteIsEscaped(t *testing.T) {
|
||||
got := partsOf(channel.Message{Title: "t", Note: "Asked by <b>x</b> & y </blockquote><i>"}).html()
|
||||
got := partsOf(oc.Message{Title: "t", Note: "Asked by <b>x</b> & y </blockquote><i>"}).html()
|
||||
want := "<b>t</b>\n\n<blockquote expandable>Asked by <b>x</b> & y </blockquote><i></blockquote>"
|
||||
if got != want {
|
||||
t.Errorf("%q", got)
|
||||
@@ -89,7 +90,7 @@ func TestTheNoteIsEscaped(t *testing.T) {
|
||||
// the end of the body (the asker's Details) is cut; the headline and the answer are never.
|
||||
func TestAnAnsweredAskOverTheBoundKeepsItsHeadlineAndItsAnswer(t *testing.T) {
|
||||
answer := "You chose Approve at 20:03 on Telegram.\nTold to mesh-knowledge, which acts on it itself."
|
||||
m := channel.Message{Handle: "h-714", To: "42", Title: "Fixes reach every machine at once, unchecked",
|
||||
m := oc.Message{Handle: "h-714", To: "42", Title: "Fixes reach every machine at once, unchecked",
|
||||
Body: "A fix goes to every machine in one step.\n• No machine goes first\n" + strings.Repeat("Details line. ", 400),
|
||||
Note: answer}
|
||||
s := partsOf(m)
|
||||
|
||||
@@ -12,3 +12,8 @@ require (
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user