Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4051,6 +4051,46 @@ describe("Discord controller flows", () => {
expect(result).toEqual({ handled: false });
});

it("claims inbound Telegram topic messages when the event carries chat id plus thread id", async () => {
const { controller } = await createControllerHarness();
await (controller as any).store.upsertBinding({
conversation: {
channel: "telegram",
accountId: "default",
conversationId: "123:topic:456",
parentConversationId: "123",
},
sessionKey: "session-1",
threadId: "thread-1",
workspaceDir: "/repo/openclaw",
updatedAt: Date.now(),
});
const startTurn = vi.fn(() => ({
result: Promise.resolve({
threadId: "thread-1",
text: "hello",
}),
getThreadId: () => "thread-1",
queueMessage: vi.fn(async () => true),
interrupt: vi.fn(async () => {}),
isAwaitingInput: () => false,
submitPendingInput: vi.fn(async () => false),
submitPendingInputPayload: vi.fn(async () => false),
}));
(controller as any).client.startTurn = startTurn;

const result = await controller.handleInboundClaim({
content: "There?",
channel: "telegram",
accountId: "default",
conversationId: "123",
threadId: 456,
});

expect(result).toEqual({ handled: true });
expect(startTurn).toHaveBeenCalled();
});

it("uses a raw Discord channel id for the typing lease on inbound claims", async () => {
const { controller, discordTypingStart } = await createControllerHarness();
await (controller as any).store.upsertBinding({
Expand Down
45 changes: 38 additions & 7 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,34 @@ function toConversationTargetFromCommand(ctx: PluginCommandContext): Conversatio
return null;
}

function normalizeTelegramThreadId(threadId: string | number | undefined): number | undefined {
if (typeof threadId === "number") {
return Number.isFinite(threadId) ? threadId : undefined;
}
if (typeof threadId === "string") {
const parsed = Number(threadId);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
}

function buildTelegramTopicConversationId(
conversationId: string | undefined,
threadId: number | undefined,
): string | undefined {
if (!conversationId) {
return undefined;
}
if (threadId == null) {
return conversationId;
}
const topicSeparator = ":topic:";
const baseConversationId = conversationId.includes(topicSeparator)
? conversationId.split(topicSeparator)[0]
: conversationId;
return `${baseConversationId}:topic:${threadId}`;
}

function toConversationTargetFromInbound(event: {
channel: string;
accountId?: string;
Expand All @@ -573,6 +601,7 @@ function toConversationTargetFromInbound(event: {
}
const channel = event.channel.trim().toLowerCase();
const conversationIdRaw = event.conversationId?.trim();
const telegramThreadId = normalizeTelegramThreadId(event.threadId);
const conversationId =
channel === "discord"
? (() => {
Expand All @@ -588,10 +617,18 @@ function toConversationTargetFromInbound(event: {
const isChannel = Boolean(event.parentConversationId?.trim() || event.isGroup || guildId);
return `${isChannel ? "channel" : "user"}:${normalized}`;
})()
: channel === "telegram"
? buildTelegramTopicConversationId(
normalizeTelegramChatId(conversationIdRaw),
telegramThreadId,
)
: event.conversationId;
const parentConversationId =
channel === "discord"
? normalizeDiscordConversationId(event.parentConversationId)
: channel === "telegram" && telegramThreadId != null
? normalizeTelegramChatId(event.parentConversationId) ??
normalizeTelegramChatId(conversationIdRaw)?.split(":topic:")[0]
: event.parentConversationId;
if (!conversationId) {
return null;
Expand All @@ -604,13 +641,7 @@ function toConversationTargetFromInbound(event: {
threadId:
channel === "discord"
? undefined
: typeof event.threadId === "number"
? event.threadId
: typeof event.threadId === "string"
? Number.isFinite(Number(event.threadId))
? Number(event.threadId)
: undefined
: undefined,
: telegramThreadId,
};
}

Expand Down