fix(telegram): reopen a deleted support topic instead of losing it to General
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
Telegram silently routes a copyMessage aimed at a deleted forum topic into the chat's General topic with NO error, so the bot's error-based recreate never fired — the user's next message landed in General with no card. Confirmed against the prod bot logs: a relay after a topic deletion logged neither "relay to topic failed" nor "topic gone, reopening". Probe topic liveness before reusing it: re-applying the info card's reply markup is a no-op that errors only when the card (hence the topic) is gone, so the bot detects the deletion and reopens the topic + card rather than relying on the (absent) copy error. The post-copy recreate stays as a race backstop.
This commit is contained in:
@@ -38,6 +38,9 @@ type supportAPI struct {
|
||||
deleted [][]int
|
||||
editedMsgIDs []string
|
||||
answers []string
|
||||
// editErr, when set, makes editMessageReplyMarkup return that Telegram error
|
||||
// description (simulating a deleted info card / topic).
|
||||
editErr string
|
||||
}
|
||||
|
||||
func newSupportAPI(adminIDs ...int64) *supportAPI {
|
||||
@@ -77,6 +80,10 @@ func (s *supportAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.deleted = append(s.deleted, ids)
|
||||
io.WriteString(w, `{"ok":true,"result":true}`)
|
||||
case strings.HasSuffix(path, "/editMessageReplyMarkup"):
|
||||
if s.editErr != "" {
|
||||
fmt.Fprintf(w, `{"ok":false,"error_code":400,"description":%q}`, s.editErr)
|
||||
return
|
||||
}
|
||||
s.editedMsgIDs = append(s.editedMsgIDs, r.FormValue("message_id"))
|
||||
io.WriteString(w, `{"ok":true,"result":{"message_id":1}}`)
|
||||
case strings.HasSuffix(path, "/answerCallbackQuery"):
|
||||
@@ -181,6 +188,36 @@ func TestSupportSubsequentReusesTopic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSupportTopicRecreatedWhenDeleted covers the case the operator hit in prod:
|
||||
// after they delete a user's whole topic, Telegram routes a copy aimed at it into
|
||||
// General with no error, so the bot must proactively detect the dead topic (the card
|
||||
// probe fails) and reopen it instead of silently losing the message to General.
|
||||
func TestSupportTopicRecreatedWhenDeleted(t *testing.T) {
|
||||
api := newSupportAPI()
|
||||
api.editErr = "message to edit not found" // the operators deleted the topic + its card
|
||||
b, st := newSupportBot(t, api)
|
||||
// Seed a topic that has since been deleted in Telegram.
|
||||
if err := st.SetTopic(7, 999, 4999, "Ann", "", "annlee"); err != nil {
|
||||
t.Fatalf("seed topic: %v", err)
|
||||
}
|
||||
|
||||
b.handleSupportUserMessage(context.Background(), userMsg(7, "still there?"))
|
||||
|
||||
if api.topicsOpened != 1 {
|
||||
t.Fatalf("topics opened = %d, want 1 (reopened after deletion)", api.topicsOpened)
|
||||
}
|
||||
rec, _ := st.Get(7)
|
||||
if rec.TopicID != 1000 || rec.HeaderMsgID != 5000 {
|
||||
t.Errorf("store = topic %d / header %d, want the reopened 1000 / 5000", rec.TopicID, rec.HeaderMsgID)
|
||||
}
|
||||
if len(api.copies) != 1 || api.copies[0].threadID != "1000" {
|
||||
t.Errorf("relay copies = %+v, want one into the reopened topic 1000", api.copies)
|
||||
}
|
||||
if _, ok := st.ByTopic(999); ok {
|
||||
t.Error("stale topic 999 still indexed after reopen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportBlockedUserDropped(t *testing.T) {
|
||||
api := newSupportAPI()
|
||||
b, st := newSupportBot(t, api)
|
||||
|
||||
Reference in New Issue
Block a user