mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
Discord/Telegram: emit edit system events (#22310)
This commit is contained in:
@@ -4,23 +4,36 @@ import {
|
||||
MessageCreateListener,
|
||||
MessageReactionAddListener,
|
||||
MessageReactionRemoveListener,
|
||||
MessageUpdateListener,
|
||||
PresenceUpdateListener,
|
||||
type User,
|
||||
} from "@buape/carbon";
|
||||
import type { DmPolicy, GroupPolicy } from "../../config/types.base.js";
|
||||
import { danger } from "../../globals.js";
|
||||
import { formatDurationSeconds } from "../../infra/format-time/format-duration.ts";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { readChannelAllowFromStore } from "../../pairing/pairing-store.js";
|
||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||
import {
|
||||
allowListMatches,
|
||||
isDiscordGroupAllowedByPolicy,
|
||||
normalizeDiscordAllowList,
|
||||
normalizeDiscordSlug,
|
||||
resolveDiscordChannelConfigWithFallback,
|
||||
resolveDiscordGuildEntry,
|
||||
resolveDiscordMemberAccessState,
|
||||
resolveGroupDmAllow,
|
||||
shouldEmitDiscordReactionNotification,
|
||||
} from "./allow-list.js";
|
||||
import { formatDiscordReactionEmoji, formatDiscordUserTag } from "./format.js";
|
||||
import { resolveDiscordChannelInfo } from "./message-utils.js";
|
||||
import {
|
||||
formatDiscordReactionEmoji,
|
||||
formatDiscordUserTag,
|
||||
resolveDiscordSystemLocation,
|
||||
} from "./format.js";
|
||||
import { resolveDiscordChannelInfo, resolveDiscordMessageChannelId } from "./message-utils.js";
|
||||
import { setPresence } from "./presence-cache.js";
|
||||
import { resolveDiscordThreadChannel, resolveDiscordThreadParentInfo } from "./threading.js";
|
||||
|
||||
type LoadedConfig = ReturnType<typeof import("../../config/config.js").loadConfig>;
|
||||
type RuntimeEnv = import("../../runtime.js").RuntimeEnv;
|
||||
@@ -30,6 +43,8 @@ export type DiscordMessageEvent = Parameters<MessageCreateListener["handle"]>[0]
|
||||
|
||||
export type DiscordMessageHandler = (data: DiscordMessageEvent, client: Client) => Promise<void>;
|
||||
|
||||
export type DiscordMessageUpdateEvent = Parameters<MessageUpdateListener["handle"]>[0];
|
||||
|
||||
type DiscordReactionEvent = Parameters<MessageReactionAddListener["handle"]>[0];
|
||||
|
||||
type DiscordReactionListenerParams = {
|
||||
@@ -41,6 +56,16 @@ type DiscordReactionListenerParams = {
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
type DiscordMessageUpdateListenerParams = DiscordReactionListenerParams & {
|
||||
dmEnabled: boolean;
|
||||
dmPolicy: DmPolicy;
|
||||
allowFrom?: string[];
|
||||
groupPolicy: GroupPolicy;
|
||||
groupDmEnabled: boolean;
|
||||
groupDmChannels?: string[];
|
||||
allowBots: boolean;
|
||||
};
|
||||
|
||||
const DISCORD_SLOW_LISTENER_THRESHOLD_MS = 30_000;
|
||||
const discordEventQueueLog = createSubsystemLogger("discord/event-queue");
|
||||
|
||||
@@ -103,6 +128,22 @@ export class DiscordMessageListener extends MessageCreateListener {
|
||||
}
|
||||
}
|
||||
|
||||
export class DiscordMessageUpdateListener extends MessageUpdateListener {
|
||||
constructor(private params: DiscordMessageUpdateListenerParams) {
|
||||
super();
|
||||
}
|
||||
|
||||
async handle(data: DiscordMessageUpdateEvent, client: Client) {
|
||||
await runDiscordMessageUpdateHandler({
|
||||
data,
|
||||
client,
|
||||
handlerParams: this.params,
|
||||
listener: this.constructor.name,
|
||||
event: this.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DiscordReactionListener extends MessageReactionAddListener {
|
||||
constructor(private params: DiscordReactionListenerParams) {
|
||||
super();
|
||||
@@ -137,6 +178,30 @@ export class DiscordReactionRemoveListener extends MessageReactionRemoveListener
|
||||
}
|
||||
}
|
||||
|
||||
async function runDiscordMessageUpdateHandler(params: {
|
||||
data: DiscordMessageUpdateEvent;
|
||||
client: Client;
|
||||
handlerParams: DiscordMessageUpdateListenerParams;
|
||||
listener: string;
|
||||
event: string;
|
||||
}): Promise<void> {
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
await handleDiscordMessageUpdateEvent({
|
||||
data: params.data,
|
||||
client: params.client,
|
||||
handlerParams: params.handlerParams,
|
||||
});
|
||||
} finally {
|
||||
logSlowDiscordListener({
|
||||
logger: params.handlerParams.logger,
|
||||
listener: params.listener,
|
||||
event: params.event,
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function runDiscordReactionHandler(params: {
|
||||
data: DiscordReactionEvent;
|
||||
client: Client;
|
||||
@@ -167,6 +232,223 @@ async function runDiscordReactionHandler(params: {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDiscordMessageUpdateEvent(params: {
|
||||
data: DiscordMessageUpdateEvent;
|
||||
client: Client;
|
||||
handlerParams: DiscordMessageUpdateListenerParams;
|
||||
}) {
|
||||
const { data, client, handlerParams } = params;
|
||||
try {
|
||||
const message = data.message;
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
const editedTimestamp =
|
||||
message.editedTimestamp ??
|
||||
(data as { edited_timestamp?: string | null }).edited_timestamp ??
|
||||
null;
|
||||
if (!editedTimestamp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const author =
|
||||
message.author ?? (message as { rawData?: { author?: User | null } }).rawData?.author;
|
||||
const authorId = author?.id ? String(author.id) : "";
|
||||
if (handlerParams.botUserId && authorId && authorId === handlerParams.botUserId) {
|
||||
return;
|
||||
}
|
||||
if (author?.bot && !handlerParams.allowBots) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageChannelId = resolveDiscordMessageChannelId({
|
||||
message,
|
||||
eventChannelId: data.channel_id,
|
||||
});
|
||||
if (!messageChannelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channelInfo = await resolveDiscordChannelInfo(client, messageChannelId);
|
||||
const isGuildMessage = Boolean(data.guild_id);
|
||||
if (!channelInfo && !isGuildMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDirectMessage = channelInfo?.type === ChannelType.DM;
|
||||
const isGroupDm = channelInfo?.type === ChannelType.GroupDM;
|
||||
|
||||
if (isDirectMessage) {
|
||||
if (!handlerParams.dmEnabled) {
|
||||
return;
|
||||
}
|
||||
if (handlerParams.dmPolicy === "disabled") {
|
||||
return;
|
||||
}
|
||||
if (!authorId) {
|
||||
return;
|
||||
}
|
||||
if (handlerParams.dmPolicy !== "open") {
|
||||
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
|
||||
const effectiveAllowFrom = [...(handlerParams.allowFrom ?? []), ...storeAllowFrom];
|
||||
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, [
|
||||
"discord:",
|
||||
"user:",
|
||||
"pk:",
|
||||
]);
|
||||
if (!allowList) {
|
||||
return;
|
||||
}
|
||||
const authorTag = author ? formatDiscordUserTag(author as User) : undefined;
|
||||
const allowed = allowListMatches(allowList, {
|
||||
id: authorId,
|
||||
name: author?.username ?? undefined,
|
||||
tag: authorTag,
|
||||
});
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isGroupDm) {
|
||||
if (!handlerParams.groupDmEnabled) {
|
||||
return;
|
||||
}
|
||||
const channelName = channelInfo?.name ?? undefined;
|
||||
const displayChannelName = channelName ?? messageChannelId;
|
||||
const displayChannelSlug = displayChannelName ? normalizeDiscordSlug(displayChannelName) : "";
|
||||
const groupDmAllowed = resolveGroupDmAllow({
|
||||
channels: handlerParams.groupDmChannels,
|
||||
channelId: messageChannelId,
|
||||
channelName: displayChannelName,
|
||||
channelSlug: displayChannelSlug,
|
||||
});
|
||||
if (!groupDmAllowed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let threadParentId: string | undefined;
|
||||
let threadParentName: string | undefined;
|
||||
const threadChannel = resolveDiscordThreadChannel({
|
||||
isGuildMessage,
|
||||
message,
|
||||
channelInfo,
|
||||
messageChannelId,
|
||||
});
|
||||
if (threadChannel) {
|
||||
const parentInfo = await resolveDiscordThreadParentInfo({
|
||||
client,
|
||||
threadChannel,
|
||||
channelInfo,
|
||||
});
|
||||
threadParentId = parentInfo.id;
|
||||
threadParentName = parentInfo.name;
|
||||
}
|
||||
|
||||
const guildInfo = isGuildMessage
|
||||
? resolveDiscordGuildEntry({
|
||||
guild: data.guild ?? undefined,
|
||||
guildEntries: handlerParams.guildEntries,
|
||||
})
|
||||
: null;
|
||||
if (
|
||||
isGuildMessage &&
|
||||
handlerParams.guildEntries &&
|
||||
Object.keys(handlerParams.guildEntries).length > 0 &&
|
||||
!guildInfo
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channelName = channelInfo?.name ?? threadChannel?.name ?? undefined;
|
||||
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
|
||||
const parentSlug = threadParentName ? normalizeDiscordSlug(threadParentName) : "";
|
||||
const channelConfig = isGuildMessage
|
||||
? resolveDiscordChannelConfigWithFallback({
|
||||
guildInfo,
|
||||
channelId: messageChannelId,
|
||||
channelName,
|
||||
channelSlug,
|
||||
parentId: threadParentId,
|
||||
parentName: threadParentName,
|
||||
parentSlug,
|
||||
scope: threadChannel ? "thread" : "channel",
|
||||
})
|
||||
: null;
|
||||
|
||||
if (isGuildMessage && channelConfig?.enabled === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channelAllowlistConfigured =
|
||||
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
|
||||
const channelAllowed = channelConfig?.allowed !== false;
|
||||
if (
|
||||
isGuildMessage &&
|
||||
!isDiscordGroupAllowedByPolicy({
|
||||
groupPolicy: handlerParams.groupPolicy,
|
||||
guildAllowlisted: Boolean(guildInfo),
|
||||
channelAllowlistConfigured,
|
||||
channelAllowed,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (isGuildMessage && channelConfig?.allowed === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const memberRoles = (data as { member?: { roles?: string[] } }).member?.roles;
|
||||
const memberRoleIds = Array.isArray(memberRoles)
|
||||
? memberRoles.map((roleId) => String(roleId))
|
||||
: [];
|
||||
|
||||
const senderTag = author ? formatDiscordUserTag(author as User) : undefined;
|
||||
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
|
||||
channelConfig,
|
||||
guildInfo,
|
||||
memberRoleIds,
|
||||
sender: {
|
||||
id: authorId,
|
||||
name: author?.username ?? undefined,
|
||||
tag: senderTag,
|
||||
},
|
||||
});
|
||||
if (isGuildMessage && hasAccessRestrictions && !memberAllowed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const route = resolveAgentRoute({
|
||||
cfg: handlerParams.cfg,
|
||||
channel: "discord",
|
||||
accountId: handlerParams.accountId,
|
||||
guildId: data.guild_id ?? undefined,
|
||||
memberRoleIds,
|
||||
peer: {
|
||||
kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
|
||||
id: isDirectMessage ? authorId : messageChannelId,
|
||||
},
|
||||
parentPeer: threadParentId ? { kind: "channel", id: threadParentId } : undefined,
|
||||
});
|
||||
|
||||
const location = resolveDiscordSystemLocation({
|
||||
isDirectMessage,
|
||||
isGroupDm,
|
||||
guild: data.guild ?? undefined,
|
||||
channelName: channelName ?? messageChannelId,
|
||||
});
|
||||
const text = `Discord message edited in ${location}.`;
|
||||
enqueueSystemEvent(text, {
|
||||
sessionKey: route.sessionKey,
|
||||
contextKey: `discord:message:edited:${messageChannelId}:${message.id}`,
|
||||
});
|
||||
} catch (err) {
|
||||
handlerParams.logger.error(danger(`discord message update handler failed: ${String(err)}`));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDiscordReactionEvent(params: {
|
||||
data: DiscordReactionEvent;
|
||||
client: Client;
|
||||
|
||||
113
src/discord/monitor/message-update.test.ts
Normal file
113
src/discord/monitor/message-update.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { ChannelType } from "@buape/carbon";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { DiscordMessageUpdateListener, type DiscordMessageUpdateEvent } from "./listeners.js";
|
||||
import { __resetDiscordChannelInfoCacheForTest } from "./message-utils.js";
|
||||
|
||||
vi.mock("../../infra/system-events.js", () => ({
|
||||
enqueueSystemEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("DiscordMessageUpdateListener", () => {
|
||||
const enqueueSystemEventMock = vi.mocked(enqueueSystemEvent);
|
||||
|
||||
beforeEach(() => {
|
||||
enqueueSystemEventMock.mockReset();
|
||||
__resetDiscordChannelInfoCacheForTest();
|
||||
});
|
||||
|
||||
it("enqueues system event for edited DMs", async () => {
|
||||
const cfg = { channels: { discord: {} } } as OpenClawConfig;
|
||||
const listener = new DiscordMessageUpdateListener({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
runtime: { error: vi.fn() } as unknown as import("../../runtime.js").RuntimeEnv,
|
||||
botUserId: "bot-1",
|
||||
guildEntries: undefined,
|
||||
logger: { error: vi.fn(), warn: vi.fn() } as unknown as ReturnType<
|
||||
typeof import("../../logging/subsystem.js").createSubsystemLogger
|
||||
>,
|
||||
dmEnabled: true,
|
||||
dmPolicy: "open",
|
||||
allowFrom: [],
|
||||
groupPolicy: "open",
|
||||
groupDmEnabled: false,
|
||||
groupDmChannels: undefined,
|
||||
allowBots: false,
|
||||
});
|
||||
|
||||
const message = {
|
||||
id: "msg-1",
|
||||
channelId: "dm-1",
|
||||
editedTimestamp: "2026-02-20T00:00:00.000Z",
|
||||
author: { id: "user-1", username: "Ada", discriminator: "0001", bot: false },
|
||||
} as unknown as import("@buape/carbon").Message;
|
||||
|
||||
const client = {
|
||||
fetchChannel: vi.fn(async () => ({ type: ChannelType.DM })),
|
||||
} as unknown as import("@buape/carbon").Client;
|
||||
|
||||
await listener.handle(
|
||||
{
|
||||
channel_id: "dm-1",
|
||||
message,
|
||||
} as DiscordMessageUpdateEvent,
|
||||
client,
|
||||
);
|
||||
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
|
||||
"Discord message edited in DM.",
|
||||
expect.objectContaining({
|
||||
contextKey: "discord:message:edited:dm-1:msg-1",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips system event when guild allowlist blocks sender", async () => {
|
||||
const cfg = { channels: { discord: {} } } as OpenClawConfig;
|
||||
const listener = new DiscordMessageUpdateListener({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
runtime: { error: vi.fn() } as unknown as import("../../runtime.js").RuntimeEnv,
|
||||
botUserId: "bot-1",
|
||||
guildEntries: {
|
||||
"guild-1": { users: ["user-allowed"] },
|
||||
},
|
||||
logger: { error: vi.fn(), warn: vi.fn() } as unknown as ReturnType<
|
||||
typeof import("../../logging/subsystem.js").createSubsystemLogger
|
||||
>,
|
||||
dmEnabled: true,
|
||||
dmPolicy: "open",
|
||||
allowFrom: [],
|
||||
groupPolicy: "open",
|
||||
groupDmEnabled: false,
|
||||
groupDmChannels: undefined,
|
||||
allowBots: false,
|
||||
});
|
||||
|
||||
const message = {
|
||||
id: "msg-2",
|
||||
channelId: "channel-1",
|
||||
editedTimestamp: "2026-02-20T00:00:00.000Z",
|
||||
author: { id: "user-blocked", username: "Ada", discriminator: "0001", bot: false },
|
||||
} as unknown as import("@buape/carbon").Message;
|
||||
|
||||
const client = {
|
||||
fetchChannel: vi.fn(async () => ({ type: ChannelType.GuildText })),
|
||||
} as unknown as import("@buape/carbon").Client;
|
||||
|
||||
await listener.handle(
|
||||
{
|
||||
channel_id: "channel-1",
|
||||
guild_id: "guild-1",
|
||||
guild: { id: "guild-1", name: "Test Guild" },
|
||||
member: { roles: [] },
|
||||
message,
|
||||
} as DiscordMessageUpdateEvent,
|
||||
client,
|
||||
);
|
||||
|
||||
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -59,6 +59,7 @@ import { createDiscordGatewayPlugin } from "./gateway-plugin.js";
|
||||
import { registerGateway, unregisterGateway } from "./gateway-registry.js";
|
||||
import {
|
||||
DiscordMessageListener,
|
||||
DiscordMessageUpdateListener,
|
||||
DiscordPresenceListener,
|
||||
DiscordReactionListener,
|
||||
DiscordReactionRemoveListener,
|
||||
@@ -605,6 +606,24 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
});
|
||||
|
||||
registerDiscordListener(client.listeners, new DiscordMessageListener(messageHandler, logger));
|
||||
registerDiscordListener(
|
||||
client.listeners,
|
||||
new DiscordMessageUpdateListener({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
runtime,
|
||||
botUserId,
|
||||
guildEntries,
|
||||
logger,
|
||||
dmEnabled,
|
||||
dmPolicy,
|
||||
allowFrom,
|
||||
groupPolicy,
|
||||
groupDmEnabled,
|
||||
groupDmChannels,
|
||||
allowBots: discordCfg.allowBots ?? false,
|
||||
}),
|
||||
);
|
||||
registerDiscordListener(
|
||||
client.listeners,
|
||||
new DiscordReactionListener({
|
||||
|
||||
@@ -453,6 +453,140 @@ export const registerTelegramHandlers = ({
|
||||
return false;
|
||||
};
|
||||
|
||||
const buildTelegramEditSenderLabel = (msg: Message) => {
|
||||
const senderChat = msg.sender_chat;
|
||||
const senderName =
|
||||
[msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() ||
|
||||
msg.from?.username ||
|
||||
senderChat?.title ||
|
||||
senderChat?.username;
|
||||
const senderUsername = msg.from?.username ?? senderChat?.username;
|
||||
const senderUsernameLabel = senderUsername ? `@${senderUsername}` : undefined;
|
||||
let senderLabel = senderName;
|
||||
if (senderName && senderUsernameLabel) {
|
||||
senderLabel = `${senderName} (${senderUsernameLabel})`;
|
||||
} else if (!senderName && senderUsernameLabel) {
|
||||
senderLabel = senderUsernameLabel;
|
||||
}
|
||||
const senderId = msg.from?.id ?? senderChat?.id;
|
||||
if (!senderLabel && senderId != null) {
|
||||
senderLabel = `id:${senderId}`;
|
||||
}
|
||||
return senderLabel || "unknown";
|
||||
};
|
||||
|
||||
const handleTelegramEditedMessage = async (params: {
|
||||
ctx: TelegramUpdateKeyContext;
|
||||
msg: Message;
|
||||
requireConfiguredGroup: boolean;
|
||||
}) => {
|
||||
try {
|
||||
if (shouldSkipUpdate(params.ctx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = params.msg;
|
||||
if (msg.from?.is_bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = msg.chat.id;
|
||||
const isChannelPost = msg.chat.type === "channel";
|
||||
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup" || isChannelPost;
|
||||
const isForum = msg.chat.is_forum === true;
|
||||
const messageThreadId = msg.message_thread_id;
|
||||
const senderId =
|
||||
msg.from?.id != null
|
||||
? String(msg.from.id)
|
||||
: msg.sender_chat?.id != null
|
||||
? String(msg.sender_chat.id)
|
||||
: String(chatId);
|
||||
const senderUsername = msg.from?.username ?? msg.sender_chat?.username ?? "";
|
||||
const groupAllowContext = await resolveTelegramGroupAllowFromContext({
|
||||
chatId,
|
||||
accountId,
|
||||
isForum,
|
||||
messageThreadId,
|
||||
groupAllowFrom,
|
||||
resolveTelegramGroupConfig,
|
||||
});
|
||||
const {
|
||||
resolvedThreadId,
|
||||
storeAllowFrom,
|
||||
groupConfig,
|
||||
topicConfig,
|
||||
effectiveGroupAllow,
|
||||
hasGroupAllowOverride,
|
||||
} = groupAllowContext;
|
||||
|
||||
if (params.requireConfiguredGroup && (!groupConfig || groupConfig.enabled === false)) {
|
||||
logVerbose(`Blocked telegram channel ${chatId} (channel disabled)`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
shouldSkipGroupMessage({
|
||||
isGroup,
|
||||
chatId,
|
||||
chatTitle: msg.chat.title,
|
||||
resolvedThreadId,
|
||||
senderId,
|
||||
senderUsername,
|
||||
effectiveGroupAllow,
|
||||
hasGroupAllowOverride,
|
||||
groupConfig,
|
||||
topicConfig,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isGroup) {
|
||||
const dmPolicy = telegramCfg.dmPolicy ?? "pairing";
|
||||
if (dmPolicy === "disabled") {
|
||||
return;
|
||||
}
|
||||
const effectiveDmAllow = normalizeAllowFromWithStore({
|
||||
allowFrom: telegramCfg.allowFrom,
|
||||
storeAllowFrom,
|
||||
});
|
||||
if (dmPolicy !== "open") {
|
||||
const allowed = isAllowlistAuthorized(effectiveDmAllow, senderId, senderUsername);
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
|
||||
const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId });
|
||||
const route = resolveAgentRoute({
|
||||
cfg: loadConfig(),
|
||||
channel: "telegram",
|
||||
accountId,
|
||||
peer: { kind: isGroup ? "group" : "direct", id: peerId },
|
||||
parentPeer,
|
||||
});
|
||||
const sessionKey = route.sessionKey;
|
||||
const senderLabel = buildTelegramEditSenderLabel(msg);
|
||||
const chatLabel = isGroup
|
||||
? msg.chat.title?.trim() || (isChannelPost ? "Telegram channel" : "Telegram group")
|
||||
: senderLabel !== "unknown"
|
||||
? `DM with ${senderLabel}`
|
||||
: "DM";
|
||||
const text = `Telegram message edited in ${chatLabel}.`;
|
||||
enqueueSystemEvent(text, {
|
||||
sessionKey,
|
||||
contextKey: `telegram:message:edited:${chatId}:${resolvedThreadId ?? "main"}:${
|
||||
msg.message_id
|
||||
}`,
|
||||
});
|
||||
logVerbose(`telegram: edit event enqueued: ${text}`);
|
||||
} catch (err) {
|
||||
runtime.error?.(danger(`telegram edit handler failed: ${String(err)}`));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle emoji reactions to messages.
|
||||
bot.on("message_reaction", async (ctx) => {
|
||||
try {
|
||||
@@ -544,6 +678,35 @@ export const registerTelegramHandlers = ({
|
||||
runtime.error?.(danger(`telegram reaction handler failed: ${String(err)}`));
|
||||
}
|
||||
});
|
||||
|
||||
bot.on("edited_message", async (ctx) => {
|
||||
const msg =
|
||||
(ctx as { editedMessage?: Message }).editedMessage ?? ctx.update?.edited_message ?? undefined;
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
await handleTelegramEditedMessage({
|
||||
ctx,
|
||||
msg,
|
||||
requireConfiguredGroup: false,
|
||||
});
|
||||
});
|
||||
|
||||
bot.on("edited_channel_post", async (ctx) => {
|
||||
const msg =
|
||||
(ctx as { editedChannelPost?: Message }).editedChannelPost ??
|
||||
ctx.update?.edited_channel_post ??
|
||||
undefined;
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
await handleTelegramEditedMessage({
|
||||
ctx,
|
||||
msg,
|
||||
requireConfiguredGroup: true,
|
||||
});
|
||||
});
|
||||
|
||||
const processInboundMessage = async (params: {
|
||||
ctx: TelegramContext;
|
||||
msg: Message;
|
||||
|
||||
@@ -746,6 +746,43 @@ describe("createTelegramBot", () => {
|
||||
expect(reactionHandler).toBeDefined();
|
||||
});
|
||||
|
||||
it("enqueues system event for edited messages", async () => {
|
||||
onSpy.mockReset();
|
||||
enqueueSystemEventSpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
telegram: { dmPolicy: "open" },
|
||||
},
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("edited_message") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
const editedMessage = {
|
||||
chat: { id: 1234, type: "private" },
|
||||
message_id: 88,
|
||||
from: { id: 9, first_name: "Ada", username: "ada_bot" },
|
||||
date: 1736380800,
|
||||
text: "edited",
|
||||
};
|
||||
|
||||
await handler({
|
||||
update: { update_id: 550, edited_message: editedMessage },
|
||||
editedMessage,
|
||||
});
|
||||
|
||||
expect(enqueueSystemEventSpy).toHaveBeenCalledTimes(1);
|
||||
expect(enqueueSystemEventSpy).toHaveBeenCalledWith(
|
||||
"Telegram message edited in DM with Ada (@ada_bot).",
|
||||
expect.objectContaining({
|
||||
contextKey: expect.stringContaining("telegram:message:edited:1234:main:88"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("enqueues system event for reaction", async () => {
|
||||
onSpy.mockReset();
|
||||
enqueueSystemEventSpy.mockReset();
|
||||
|
||||
Reference in New Issue
Block a user