One message per problem: a reminder replaces, a blip says nothing (issues 373 and 369)

The operator stopped believing the mesh's notifications: one problem stood
twice on the desk (a reminder beside the message it followed, an escalation
then a reminder 31 s later), a unit failing again at each apply was said
again 22 times, and a question's buttons outlived it on a reminder the
ask's end never reached.

- messenger: a reminder or escalation is said in place of the problem's
  message (Replace under its handle), alone, never as a digest line; a
  problem first said in a digest gets its own message next time. A
  reminder counts from when it was last said. A clearing is said once it
  stays cleared 2 min (10 min once it came back); back before that, the
  message continues without a word. Cleared before ever said and back:
  said. An ask's expiry is said with its day.
- desk-channel: a send or show under a handle already shown replaces its
  notification by the notifier's replace id.
- telegram: the same deletes the earlier message and sends the new one,
  ringing as its kind rings; one it cannot delete loses its buttons.
This commit is contained in:
jochen
2026-10-10 14:22:42 +02:00
parent 6ec02b6eb1
commit 3345ff9317
17 changed files with 569 additions and 55 deletions
+24 -2
View File
@@ -10,7 +10,8 @@ package main
// handle). Up to 4096 characters arrive whole; longer is cut and the cut said (D1). A warning or a
// clearing arrives without sound, an urgent message or a question rings (D4). A rate limit is waited out
// as told (D5); a refusal is a permanent failure, said to the router, never retried; an edit to the same
// text is a success (D6). A message that cannot be delivered after three tries is said as failed.
// text is a success (D6). A message that cannot be delivered after three tries is said as failed. A
// `send` or `show` under a handle already shown replaces that message: deleted, and the new one sent.
// - **What arrives** is long-polled: outbound only. A tapped button is said as a `choice` with the sender
// the service authenticated, and acknowledged on the phone with the router's word on it — what was
// recorded, or why it was not — which comes as a reply to the tap; without one in TapWait, with words that
@@ -232,11 +233,19 @@ func (c *Channel) Take(subject string, body json.RawMessage) error {
words, buttons := text(m), c.buttons(m)
silent := m.Silent || (!m.Urgent && len(buttons) == 0)
var place *Placed
if verb == asks.Edit {
if verb == asks.Edit || !m.Secret {
if place, err = c.Store.Placed(m.Handle); err != nil {
return err // the state is away: offered again
}
}
if verb != asks.Edit && place != nil {
// **A message said again under a handle already shown replaces it** (issue 373): a reminder or an
// escalation is one message per problem, not one more beside it. An edit here notifies nobody, so the
// earlier message is deleted and this one sent, ringing as its kind rings; one the service will not
// delete loses its buttons and says where the newer one is.
c.replaced(*place)
place = nil
}
for try := 1; ; try++ {
var id int64
switch {
@@ -274,6 +283,19 @@ func (c *Channel) Take(subject string, body json.RawMessage) error {
}
}
// replaced takes an earlier message out of the chat: deleted, or where the service refuses that (a message
// older than two days), its buttons taken away and the newer message named.
func (c *Channel) replaced(p Placed) {
err := c.Bot.Delete(p.Chat, p.ID)
if err == nil {
return
}
c.Logf("[telegram] an earlier message could not be deleted (%v); its buttons are taken away instead", err)
if err := c.Bot.Edit(p.Chat, p.ID, "Replaced by a newer message below.", nil); err != nil && !gone(err) {
c.Logf("[telegram] an earlier message could not be marked replaced either: %v", err)
}
}
func (c *Channel) newMessage(handle string, chat int64, words string, buttons [][2]string, silent bool, id *int64) error {
got, err := c.Bot.Send(chat, words, buttons, silent)
if err != nil {
@@ -580,3 +580,48 @@ func TestAToastIsCutAtTelegramsBound(t *testing.T) {
t.Errorf("toast %q", got)
}
}
// Issue 373: a reminder or an escalation is sent under the handle of the message it follows, and replaces it:
// the earlier one leaves the chat and the new one rings as its kind rings. One the service will not delete
// 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(asks.Show, asks.Message{Handle: "m1", Title: "Urgent: ace silent", Urgent: true, To: "42",
Buttons: []asks.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(asks.Send, asks.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(asks.Show, asks.Message{Handle: "m1", Title: "Still open: ace silent", Urgent: true, To: "42",
Buttons: []asks.Button{{Label: "Silence for a week", Ticket: "fedcba9876543210fedcba9876543210"}}})
if err := c.Take(subject, body); err != nil {
t.Fatal(err)
}
del := f.of("deleteMessage")
if len(del) != 1 || del[0].Body["message_id"] != float64(first) {
t.Fatalf("the earlier message was not deleted: %+v", del)
}
sent := f.of("sendMessage")
if len(sent) != 3 || sent[2].Body["disable_notification"] != false || st.placed["m1"].ID == first {
t.Fatalf("the reminder was not sent ringing in its place: %d sends, placed %d", len(sent), st.placed["m1"].ID)
}
// The service refuses the delete: the earlier message loses its buttons instead.
f.answer["deleteMessage"] = func(map[string]any) (int, string) {
return 400, `{"ok":false,"error_code":400,"description":"Bad Request: message can't be deleted"}`
}
second := st.placed["m1"].ID
subject, body = work(asks.Send, asks.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") {
t.Fatalf("an undeletable earlier message kept its buttons: %+v", ed)
}
if kb, _ := json.Marshal(ed[0].Body["reply_markup"]); string(kb) != `{"inline_keyboard":[]}` {
t.Errorf("its buttons stayed: %s", kb)
}
}