messenger, telegram: a second tap on the recorded answer is said as recorded, and the fallback claims nothing (issue 372 review)
mesh/merge-gate pass: builds messenger, telegram → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds messenger, telegram → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
The review found that a double tap while the phone's spinner turned would be answered 'That answer was not taken', reading as if the approval failed; it is now 'You already chose Approve at 13:30: recorded.' and does nothing more. The fallback toast no longer promises a change that may not come (a stranger's tap), the wait outlasts the router's 10 s wait for the controller's root word, the channel's own refusals stay in the chat too, and the store's error words stay in the journal.
This commit is contained in:
@@ -387,10 +387,17 @@ func TestAnApproveFromTheLinkedAccountIsAWarrantForTheAskerAlone(t *testing.T) {
|
||||
if out, _ := h.Chosen("telegram", choiceOf(release, "42", c.now())); out != "chosen" {
|
||||
t.Errorf("a redelivery of the same answer was acted on again: %s", out)
|
||||
}
|
||||
// The same answer from the same account, as a new tap, is what was recorded: said so, nothing more done
|
||||
// (issue 372). From another account it is refused.
|
||||
again := choiceOf(release, "42", c.now())
|
||||
again.ID = "another"
|
||||
if out, err := h.Chosen("telegram", again); err == nil || out != "refused" {
|
||||
t.Errorf("a used ticket was taken: %s", out)
|
||||
if out, err := h.Chosen("telegram", again); err != nil || out != "chosen" ||
|
||||
!strings.HasPrefix(tg.sends[len(tg.sends)-1].Title, "You already chose Release at ") {
|
||||
t.Errorf("the same answer again was not said as recorded: %s %v %q", out, err, tg.sends[len(tg.sends)-1].Title)
|
||||
}
|
||||
stranger := choiceOf(release, "666", c.now())
|
||||
if out, err := h.Chosen("telegram", stranger); err == nil || out != "refused" {
|
||||
t.Errorf("a used ticket was taken from another account: %s", out)
|
||||
}
|
||||
stop := ticketFor(tg.sends[0], "Stop")
|
||||
if out, err := h.Chosen("telegram", choiceOf(stop, "42", c.now())); err == nil || !strings.Contains(err.Error(), "already answered") {
|
||||
|
||||
@@ -755,6 +755,13 @@ func (h *Holder) confirmChoice(kind string, c asks.Choice, label, asker string,
|
||||
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "confirmed " + label, Outcome: outcomeOf(err)})
|
||||
}
|
||||
|
||||
// confirmAgain says, in reply to the same answer given again, that it was already recorded.
|
||||
func (h *Holder) confirmAgain(kind string, c asks.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)})
|
||||
}
|
||||
|
||||
// sweepAsks ends asks past their expiry, and tells again an asker not yet told. Called every minute.
|
||||
func (h *Holder) sweepAsks(now time.Time) {
|
||||
h.mu.Lock()
|
||||
@@ -865,6 +872,14 @@ func (h *Holder) judge(kind string, c asks.Choice, key string, root func(KindInf
|
||||
}
|
||||
now := h.Now()
|
||||
t, issued := rec.Tickets[c.Ticket]
|
||||
if w := rec.Warrant; issued && rec.State == string(asks.OutcomeChosen) && w != nil && w.Option == t.Option &&
|
||||
w.By != nil && w.By.Kind == kind && w.By.Identity == c.Sender.Identity && (c.Sender.Authenticated || !h.Bench.Promises(kind, "verified-sender")) {
|
||||
// The same answer again from the same account — a second tap while the first was being confirmed, or one
|
||||
// redelivered after a restart: it is what was recorded, so it is said so, not refused as if it failed
|
||||
// (issue 372). Nothing more is done.
|
||||
h.confirmAgain(kind, c, w.Label, rec.Closed)
|
||||
return "chosen", nil
|
||||
}
|
||||
switch {
|
||||
case !issued:
|
||||
return h.refuseChoice(kind, c, "it belongs to no ask this router holds")
|
||||
@@ -948,7 +963,9 @@ func (h *Holder) judge(kind string, c asks.Choice, key string, root func(KindInf
|
||||
if errors.Is(err, ErrChanged) {
|
||||
return h.refuseChoice(kind, c, "the question was already answered")
|
||||
}
|
||||
return h.refuseChoice(kind, c, "it could not be recorded: "+err.Error())
|
||||
// The store's own words stay in the journal: they are not the operator's to read on the phone.
|
||||
h.Logf("[messenger] the answer to %s's ask %s could not be recorded: %v", rec.Asker, rec.Ask.ID, err)
|
||||
return h.refuseChoice(kind, c, "it could not be recorded")
|
||||
}
|
||||
h.record(Sent{At: now, Channel: kind, Key: rec.Record, What: "answer " + o.Label, Outcome: "warrant to " + rec.Asker})
|
||||
h.Logf("[messenger] %s: a warrant for %s's ask %s", w.Says(), rec.Asker, rec.Ask.ID)
|
||||
|
||||
@@ -136,10 +136,10 @@ func TestARefusedAnswerSaysWhyInReplyToTheTap(t *testing.T) {
|
||||
settle(h, c)
|
||||
release := ticketFor(tg.sends[0], "Release")
|
||||
_, _ = h.Chosen("telegram", choiceOf(release, "42", c.now()))
|
||||
again := choiceOf(release, "42", c.now())
|
||||
again := choiceOf(ticketFor(tg.sends[0], "Stop"), "42", c.now())
|
||||
again.ID = "tg-tap-again"
|
||||
if out, _ := h.Chosen("telegram", again); out != "refused" {
|
||||
t.Fatalf("a used ticket was taken: %s", out)
|
||||
t.Fatalf("a second answer to an answered question was taken: %s", out)
|
||||
}
|
||||
last := tg.sends[len(tg.sends)-1]
|
||||
if last.Reply != "tg-tap-again" || last.Title != "That answer was not taken" ||
|
||||
@@ -150,3 +150,36 @@ func TestARefusedAnswerSaysWhyInReplyToTheTap(t *testing.T) {
|
||||
t.Error("the refusal carries what may not leave")
|
||||
}
|
||||
}
|
||||
|
||||
// A second tap on the answer just recorded — the phone's spinner still turning, or a redelivery after a restart
|
||||
// — is said as recorded, not refused as if it failed, and nothing more is done (issue 372).
|
||||
func TestTheSameAnswerTappedTwiceIsSaidAsRecorded(t *testing.T) {
|
||||
h, tg, _, c, store, w := newRouter(t)
|
||||
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
||||
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
|
||||
settle(h, c)
|
||||
release := ticketFor(tg.sends[0], "Release")
|
||||
first := choiceOf(release, "42", c.now())
|
||||
first.ID = "tg-tap-1"
|
||||
if out, err := h.Chosen("telegram", first); err != nil || out != "chosen" {
|
||||
t.Fatalf("%s %v", out, err)
|
||||
}
|
||||
edits := len(tg.edits)
|
||||
second := choiceOf(release, "42", c.now())
|
||||
second.ID = "tg-tap-2"
|
||||
if out, err := h.Chosen("telegram", second); err != nil || out != "chosen" {
|
||||
t.Fatalf("the second tap: %s %v", out, err)
|
||||
}
|
||||
last := tg.sends[len(tg.sends)-1]
|
||||
if last.Reply != "tg-tap-2" || last.Title != "You already chose Release at 12:00: recorded." {
|
||||
t.Errorf("the second tap was answered %+v", last)
|
||||
}
|
||||
for _, m := range tg.sends {
|
||||
if m.Title == "That answer was not taken" {
|
||||
t.Errorf("a tap on the answer recorded was refused: %+v", m)
|
||||
}
|
||||
}
|
||||
if len(w.of("mesh-delivery")) != 1 || len(tg.edits) != edits {
|
||||
t.Error("the second tap did something more")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ as the operator.
|
||||
was not taken — and this channel shows that as the tap's own acknowledgement, the short line Telegram
|
||||
shows over the chat. A word with more to it (a refusal's reason, Details) is also said in the chat, where it
|
||||
stays. Nothing is acknowledged before the router's word, so the phone never says "recorded" for what was
|
||||
not; without a word in 8 seconds the tap is acknowledged with words that claim nothing, and the word when it
|
||||
not; without a word in 13 seconds the tap is acknowledged with words that claim nothing, and the word when it
|
||||
comes is said in the chat. A secret is never an acknowledgement.
|
||||
- **A code** (six digits, the one shown on the desk when linking) is asked of the router as a proof, by
|
||||
request and reply: never in an event, never kept, and deleted from the chat at once.
|
||||
|
||||
@@ -271,7 +271,7 @@ func (b *Bot) Edit(chat, id int64, text string, buttons [][2]string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// AnswerTap ends the phone's spinner for a tapped button, at once.
|
||||
// AnswerTap ends the phone's spinner for a tapped button, showing words over the chat when there are any.
|
||||
func (b *Bot) AnswerTap(id, text string) error {
|
||||
body := map[string]any{"callback_query_id": id}
|
||||
if text != "" {
|
||||
|
||||
@@ -54,9 +54,11 @@ const (
|
||||
StandingEvery = 5 * time.Minute
|
||||
// TapWait is how long a tap's acknowledgement waits for the router's word on it (issue 372): the phone
|
||||
// shows its spinner meanwhile, and Telegram takes an acknowledgement only for a short while.
|
||||
TapWait = 8 * time.Second
|
||||
// It is longer than the router may wait for the controller's word on root before it judges an answer
|
||||
// (messenger's RootFreeWithin, 10 seconds), so a slow controller still leaves the word to the toast.
|
||||
TapWait = 13 * time.Second
|
||||
// TapPending is a tap's acknowledgement when the router said nothing within TapWait: it claims nothing.
|
||||
TapPending = "Passed to the mesh, which has not decided yet. The question changes when it has."
|
||||
TapPending = "Passed to the mesh; nothing is confirmed yet."
|
||||
// ToastLength is what a tap's acknowledgement shows (Telegram's own bound).
|
||||
ToastLength = 200
|
||||
// FixedLine is what anything the bot does not take is answered with.
|
||||
@@ -324,7 +326,9 @@ func (c *Channel) Handle(u Update) {
|
||||
// the token changed, or one it never showed, answers nothing (the review of 2026-10-09).
|
||||
if want, err := c.Store.Shown(cb.Message.Chat.ID, cb.Message.MessageID); err != nil || want == "" ||
|
||||
want != wordsDigest(cb.Message.Text) {
|
||||
c.answerTap(cb.ID, "That answer was not taken: this message does not show what the mesh put there. Nothing was done.")
|
||||
words := "That answer was not taken: this message does not show what the mesh put there. Nothing was done."
|
||||
c.answerTap(cb.ID, words)
|
||||
c.reply(cb.Message.Chat.ID, words) // a toast goes; the chat keeps why
|
||||
c.Logf("[telegram] a tap on a message not showing what this channel put there was refused")
|
||||
return
|
||||
}
|
||||
@@ -340,7 +344,9 @@ func (c *Channel) Handle(u Update) {
|
||||
ch := asks.Choice{ID: id, Ticket: cb.Data, Sender: senderOf(cb.From), At: now}
|
||||
if err := c.Publish(asks.IntakeSubject(asks.Chosen, Kind), ch, id); err != nil {
|
||||
c.Logf("[telegram] a tap could not be said to the router: %v", err)
|
||||
c.settleTap(id, "The mesh could not take this answer now. Nothing was done. Try again in a minute.")
|
||||
words := "The mesh could not take this answer now. Nothing was done. Try again in a minute."
|
||||
c.settleTap(id, words)
|
||||
c.reply(cb.Message.Chat.ID, words)
|
||||
return
|
||||
}
|
||||
c.After(TapWait, func() { c.settleTap(id, TapPending) })
|
||||
@@ -546,7 +552,7 @@ func (c *Channel) Status() map[string]any {
|
||||
out["rules"] = []string{
|
||||
"it shows what the router sends and says what was chosen; whether the sender is the operator is the router's to judge, from the account linked there (novox/hq ADR 0259)",
|
||||
"a button carries only the router's one-time ticket",
|
||||
"only a private chat with the account itself counts; a tap is acknowledged with the router's word on it — what was recorded, or why not — or, without one in 8 seconds, with words that claim nothing",
|
||||
"only a private chat with the account itself counts; a tap is acknowledged with the router's word on it — what was recorded, or why not — or, without one in 13 seconds, with words that claim nothing",
|
||||
"/start asks to link this account; the code shown on the desk is checked by request and reply, never in an event, and deleted from the chat",
|
||||
"updates are long-polled: outbound only, no route into the mesh",
|
||||
"up to 4096 characters arrive whole; longer is cut and the cut said",
|
||||
|
||||
Reference in New Issue
Block a user