diff --git a/modules/desk-channel/cmd/desk-channel/desk.go b/modules/desk-channel/cmd/desk-channel/desk.go index 1033ebe41..050d9635d 100644 --- a/modules/desk-channel/cmd/desk-channel/desk.go +++ b/modules/desk-channel/cmd/desk-channel/desk.go @@ -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 } diff --git a/modules/desk-channel/cmd/desk-channel/desk_test.go b/modules/desk-channel/cmd/desk-channel/desk_test.go index 27ecd0b67..e41b4cf64 100644 --- a/modules/desk-channel/cmd/desk-channel/desk_test.go +++ b/modules/desk-channel/cmd/desk-channel/desk_test.go @@ -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: 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: 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 diff --git a/modules/desk-channel/go.mod b/modules/desk-channel/go.mod index c9e731b5e..d941f7e5c 100644 --- a/modules/desk-channel/go.mod +++ b/modules/desk-channel/go.mod @@ -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 +) diff --git a/modules/mesh-config/cmd/mesh-config/proposals.go b/modules/mesh-config/cmd/mesh-config/proposals.go index dcd880957..7307c49ed 100644 --- a/modules/mesh-config/cmd/mesh-config/proposals.go +++ b/modules/mesh-config/cmd/mesh-config/proposals.go @@ -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++ diff --git a/modules/mesh-config/cmd/mesh-config/proposals_test.go b/modules/mesh-config/cmd/mesh-config/proposals_test.go index 40482e4b5..e41c281d3 100644 --- a/modules/mesh-config/cmd/mesh-config/proposals_test.go +++ b/modules/mesh-config/cmd/mesh-config/proposals_test.go @@ -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) diff --git a/modules/messenger/cmd/messenger/answers.go b/modules/messenger/cmd/messenger/answers.go index 5856d935c..57344ad00 100644 --- a/modules/messenger/cmd/messenger/answers.go +++ b/modules/messenger/cmd/messenger/answers.go @@ -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)) diff --git a/modules/messenger/cmd/messenger/answers_test.go b/modules/messenger/cmd/messenger/answers_test.go index a0d42ad63..b02a26ec4 100644 --- a/modules/messenger/cmd/messenger/answers_test.go +++ b/modules/messenger/cmd/messenger/answers_test.go @@ -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 { diff --git a/modules/messenger/cmd/messenger/asks.go b/modules/messenger/cmd/messenger/asks.go index e3e897159..ef45f15f3 100644 --- a/modules/messenger/cmd/messenger/asks.go +++ b/modules/messenger/cmd/messenger/asks.go @@ -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 } diff --git a/modules/messenger/cmd/messenger/bus_test.go b/modules/messenger/cmd/messenger/bus_test.go index 0cf07b0d3..10bcc2a59 100644 --- a/modules/messenger/cmd/messenger/bus_test.go +++ b/modules/messenger/cmd/messenger/bus_test.go @@ -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) diff --git a/modules/messenger/cmd/messenger/condition_bus_test.go b/modules/messenger/cmd/messenger/condition_bus_test.go index 796942cb7..24d147a01 100644 --- a/modules/messenger/cmd/messenger/condition_bus_test.go +++ b/modules/messenger/cmd/messenger/condition_bus_test.go @@ -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 { diff --git a/modules/messenger/cmd/messenger/link.go b/modules/messenger/cmd/messenger/link.go index ef554b32e..a32d6eb6d 100644 --- a/modules/messenger/cmd/messenger/link.go +++ b/modules/messenger/cmd/messenger/link.go @@ -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) } diff --git a/modules/messenger/cmd/messenger/main.go b/modules/messenger/cmd/messenger/main.go index 12e9f8358..9c034ac71 100644 --- a/modules/messenger/cmd/messenger/main.go +++ b/modules/messenger/cmd/messenger/main.go @@ -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}) } } diff --git a/modules/messenger/cmd/messenger/manifest_test.go b/modules/messenger/cmd/messenger/manifest_test.go index a7c6e3bfe..ffb17331b 100644 --- a/modules/messenger/cmd/messenger/manifest_test.go +++ b/modules/messenger/cmd/messenger/manifest_test.go @@ -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{ diff --git a/modules/messenger/cmd/messenger/review_test.go b/modules/messenger/cmd/messenger/review_test.go index 4039be837..1bcac1d5b 100644 --- a/modules/messenger/cmd/messenger/review_test.go +++ b/modules/messenger/cmd/messenger/review_test.go @@ -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 { diff --git a/modules/messenger/cmd/messenger/seatchannel.go b/modules/messenger/cmd/messenger/seatchannel.go index afbfceab5..cee88e3d6 100644 --- a/modules/messenger/cmd/messenger/seatchannel.go +++ b/modules/messenger/cmd/messenger/seatchannel.go @@ -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 { diff --git a/modules/telegram/cmd/telegram/channel.go b/modules/telegram/cmd/telegram/channel.go index 847d74c87..5b14a8ee4 100644 --- a/modules/telegram/cmd/telegram/channel.go +++ b/modules/telegram/cmd/telegram/channel.go @@ -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 } diff --git a/modules/telegram/cmd/telegram/channel_test.go b/modules/telegram/cmd/telegram/channel_test.go index b08c0d45d..d7c87526b 100644 --- a/modules/telegram/cmd/telegram/channel_test.go +++ b/modules/telegram/cmd/telegram/channel_test.go @@ -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 & co?", + subject, body := work(occ.Show, oc.Message{Handle: "q1", Title: "Set mounts on & 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 & 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 & 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 & co?", + subject, body = work(occ.Edit, oc.Message{Handle: "q1", Title: "You chose Approve at 15:06 on Telegram · Set mounts on & 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")) diff --git a/modules/telegram/cmd/telegram/render.go b/modules/telegram/cmd/telegram/render.go index e978f6d6a..032225f2b 100644 --- a/modules/telegram/cmd/telegram/render.go +++ b/modules/telegram/cmd/telegram/render.go @@ -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 { diff --git a/modules/telegram/cmd/telegram/render_test.go b/modules/telegram/cmd/telegram/render_test.go index 8ef471705..cd08b9444 100644 --- a/modules/telegram/cmd/telegram/render_test.go +++ b/modules/telegram/cmd/telegram/render_test.go @@ -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 x & y "}).html() + got := partsOf(oc.Message{Title: "t", Note: "Asked by x & y "}).html() want := "t\n\n
Asked by <b>x</b> & y </blockquote><i>
" 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) diff --git a/modules/telegram/go.mod b/modules/telegram/go.mod index 9ea94ea9f..deee5ca7b 100644 --- a/modules/telegram/go.mod +++ b/modules/telegram/go.mod @@ -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 +)