Refuse to post the same payment line twice, at the sender

Members were seeing the same payout announced twice in the proof channel
and the shared payments topic, and asking whether someone had been paid
twice. The money was never wrong — one transfer, one contract event — but
a payment feed is the last place where a second receipt is acceptable,
because it makes people doubt the ledger.

The indexer's durable announce-once key is the real fix and shipped in
85a6056. This adds the layer behind it: sendTelegramTo refuses to post the
same text to the same chat within 15 minutes, and logs when it does.
Distinct events can never collide — every line carries its own member ids,
amount and transaction hash.

Deliberately at the sender rather than the emitter, so it holds no matter
which upstream path replays an event.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-18 09:37:10 -05:00
parent 85a6056354
commit e43b812f97
+25
View File
@@ -163,9 +163,34 @@ function sendTelegram(text, topicId, replyMarkup) {
}
// Same bot, any chat: the team forum, or the public payment-proof channel
// (config.telegramProofChatId — the bot just has to be an admin there).
// Last-resort duplicate guard, at the point of send.
//
// 2026-09-18: members saw the same payout line posted twice in the proof channel and the
// shared payments topic — "did this person get paid twice?" The money was always right; only
// the announcement doubled. The indexer now has a durable announce-once key per on-chain
// event, which is the real fix, but a payment feed is the last place to rely on a single
// layer: anything upstream that replays an event would put a second receipt in front of
// members and make them doubt the ledger. So the sender itself refuses to post the same text
// to the same chat twice within the window. Distinct events never collide — every line
// carries its own ids, amount and transaction hash.
const TG_RECENT = new Map();
const TG_DEDUPE_MS = 15 * 60 * 1000;
function tgSeenRecently(chatId, topicId, text) {
const k = String(chatId) + '|' + String(topicId || '') + '|' + text;
const now = Date.now();
if (TG_RECENT.size > 500) for (const [kk, ts] of TG_RECENT) if (now - ts > TG_DEDUPE_MS) TG_RECENT.delete(kk);
const prev = TG_RECENT.get(k);
if (prev && now - prev < TG_DEDUPE_MS) return true;
TG_RECENT.set(k, now);
return false;
}
function sendTelegramTo(chatId, text, topicId, replyMarkup, parseMode) {
const c = getConfig();
if (!c.telegramBotToken || !chatId) return;
if (tgSeenRecently(chatId, topicId, text)) {
console.warn('telegram duplicate suppressed:', String(text).replace(/\s+/g, ' ').slice(0, 90));
return;
}
// No link previews: payout/team-build posts carry Polygonscan links, and the
// preview card tripled the height of every message in the group.
const payload = { chat_id: chatId, text, disable_web_page_preview: true };