fix: dedupe inbound messages across providers

This commit is contained in:
Peter Steinberger
2026-01-11 00:12:17 +01:00
parent bd2002010c
commit 7c76561569
18 changed files with 353 additions and 53 deletions

View File

@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
const useSpy = vi.fn();
const middlewareUseSpy = vi.fn();
@@ -16,6 +17,10 @@ const apiStub: ApiStub = {
sendChatAction: sendChatActionSpy,
};
beforeEach(() => {
resetInboundDedupe();
});
vi.mock("grammy", () => ({
Bot: class {
api = apiStub;

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
import * as replyModule from "../auto-reply/reply.js";
import { createTelegramBot, getTelegramSequentialKey } from "./bot.js";
import { resolveTelegramFetch } from "./fetch.js";
@@ -124,6 +125,7 @@ const getOnHandler = (event: string) => {
describe("createTelegramBot", () => {
beforeEach(() => {
resetInboundDedupe();
loadConfig.mockReturnValue({
telegram: { dmPolicy: "open", allowFrom: ["*"] },
});

View File

@@ -45,6 +45,7 @@ import {
updateLastRoute,
} from "../config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
import { createDedupeCache } from "../infra/dedupe.js";
import { formatErrorMessage } from "../infra/errors.js";
import { recordProviderActivity } from "../infra/provider-activity.js";
import { getChildLogger } from "../logging.js";
@@ -120,32 +121,11 @@ const buildTelegramUpdateKey = (ctx: TelegramUpdateKeyContext) => {
return undefined;
};
const shouldSkipTelegramUpdate = (
cache: Map<string, { ts: number }>,
key?: string,
) => {
if (!key) return false;
const now = Date.now();
const existing = cache.get(key);
if (existing && now - existing.ts < RECENT_TELEGRAM_UPDATE_TTL_MS) {
return true;
}
if (existing) cache.delete(key);
cache.set(key, { ts: now });
if (cache.size > RECENT_TELEGRAM_UPDATE_MAX) {
for (const [cachedKey, entry] of cache) {
if (now - entry.ts > RECENT_TELEGRAM_UPDATE_TTL_MS) {
cache.delete(cachedKey);
}
}
while (cache.size > RECENT_TELEGRAM_UPDATE_MAX) {
const oldestKey = cache.keys().next().value as string | undefined;
if (!oldestKey) break;
cache.delete(oldestKey);
}
}
return false;
};
const createTelegramUpdateDedupe = () =>
createDedupeCache({
ttlMs: RECENT_TELEGRAM_UPDATE_TTL_MS,
maxSize: RECENT_TELEGRAM_UPDATE_MAX,
});
/** Telegram Location object */
interface TelegramLocation {
@@ -233,10 +213,10 @@ export function createTelegramBot(opts: TelegramBotOptions) {
bot.api.config.use(apiThrottler());
bot.use(sequentialize(getTelegramSequentialKey));
const recentUpdates = new Map<string, { ts: number }>();
const recentUpdates = createTelegramUpdateDedupe();
const shouldSkipUpdate = (ctx: TelegramUpdateKeyContext) => {
const key = buildTelegramUpdateKey(ctx);
const skipped = shouldSkipTelegramUpdate(recentUpdates, key);
const skipped = recentUpdates.check(key);
if (skipped && key && shouldLogVerbose()) {
logVerbose(`telegram dedupe: skipped ${key}`);
}
@@ -388,7 +368,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
primaryCtx: TelegramContext,
allMedia: Array<{ path: string; contentType?: string }>,
storeAllowFrom: string[],
options?: { forceWasMentioned?: boolean },
options?: { forceWasMentioned?: boolean; messageIdOverride?: string },
) => {
const msg = primaryCtx.message;
recordProviderActivity({
@@ -720,7 +700,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
SenderUsername: senderUsername || undefined,
Provider: "telegram",
Surface: "telegram",
MessageSid: String(msg.message_id),
MessageSid: options?.messageIdOverride ?? String(msg.message_id),
ReplyToId: replyTarget?.id,
ReplyToBody: replyTarget?.body,
ReplyToSender: replyTarget?.sender,
@@ -1163,7 +1143,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
{ message: syntheticMessage, me: ctx.me, getFile },
[],
storeAllowFrom,
{ forceWasMentioned: true },
{ forceWasMentioned: true, messageIdOverride: callback.id },
);
} catch (err) {
runtime.error?.(danger(`callback handler failed: ${String(err)}`));