Skip to content
Merged
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
20 changes: 20 additions & 0 deletions extensions/telegram/src/bot-message-context.body.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
// Telegram plugin module implements bot message context.body behavior.
import {
buildMentionRegexes,
classifyChannelInboundEvent,
formatLocationText,
implicitMentionKindWhen,
logInboundDrop,
matchesMentionWithExplicit,
resolveInboundMentionDecision,
resolveUnmentionedGroupInboundPolicy,
type BuildChannelInboundEventContextParams,
type BuildMentionRegexesOptions,
type InboundEventKind,
type NormalizedLocation,
} from "openclaw/plugin-sdk/channel-inbound";
import { resolveChannelGroupPolicy } from "openclaw/plugin-sdk/channel-policy";
import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
import { isAbortRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type {
TelegramDirectConfig,
Expand Down Expand Up @@ -71,6 +75,7 @@ export type TelegramInboundBodyResult = {
commandAuthorized: boolean;
effectiveWasMentioned: boolean;
mentionFacts: TelegramMentionFacts;
inboundEventKind: InboundEventKind;
canDetectMention: boolean;
shouldBypassMention: boolean;
hasControlCommand: boolean;
Expand Down Expand Up @@ -415,6 +420,20 @@ export async function resolveTelegramInboundBody(params: {
},
});
const effectiveWasMentioned = mentionDecision.effectiveWasMentioned;
const commandSource =
options?.commandSource ??
(commandAuthorized && hasControlCommandInMessage ? "text" : undefined);
const inboundEventKind = classifyChannelInboundEvent({
conversation: { kind: isGroup ? "group" : "direct" },
unmentionedGroupPolicy: resolveUnmentionedGroupInboundPolicy({
cfg,
agentId: routeAgentId,
}),
wasMentioned: effectiveWasMentioned,
hasControlCommand: hasControlCommandInMessage,
hasAbortRequest: isAbortRequestText(rawBody, { botUsername }),
commandSource,
});
if (isGroup && requireMention && canDetectMention && mentionDecision.shouldSkip) {
logger.info({ chatId, reason: "no-mention" }, "skipping group message");
recordTelegramGroupHistoryEntry({
Expand Down Expand Up @@ -479,6 +498,7 @@ export async function resolveTelegramInboundBody(params: {
historyKey,
commandAuthorized,
effectiveWasMentioned,
inboundEventKind,
mentionFacts: resolveTelegramMentionFacts({
canDetectMention,
effectiveWasMentioned,
Expand Down
79 changes: 64 additions & 15 deletions extensions/telegram/src/bot-message-context.dm-threads.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,34 @@ type SessionRuntimeModule = typeof import("./bot-message-context.session.runtime
type RecordInboundSessionFn = SessionRuntimeModule["recordInboundSession"];
type ResolveStorePathFn = SessionRuntimeModule["resolveStorePath"];

const { recordInboundSessionMock, resolveStorePathMock } = vi.hoisted(() => ({
recordInboundSessionMock: vi.fn<RecordInboundSessionFn>(async () => undefined),
resolveStorePathMock: vi.fn<ResolveStorePathFn>(() => "/tmp/openclaw-session-store.json"),
}));
const { inboundBodyResult, recordInboundSessionMock, resolveStorePathMock } = vi.hoisted(() => {
const createInboundBodyResult = () => ({
bodyText: "hello",
rawBody: "hello",
historyKey: undefined,
commandAuthorized: false,
effectiveWasMentioned: true,
inboundEventKind: "user_request" as const,
mentionFacts: {
canDetectMention: false,
wasMentioned: true,
explicitlyMentionedBot: false,
effectiveWasMentioned: true,
requireMention: false,
shouldSkip: false,
},
canDetectMention: false,
shouldBypassMention: false,
hasControlCommand: false,
stickerCacheHit: false,
locationData: undefined,
});
return {
inboundBodyResult: { value: createInboundBodyResult(), reset: createInboundBodyResult },
recordInboundSessionMock: vi.fn<RecordInboundSessionFn>(async () => undefined),
resolveStorePathMock: vi.fn<ResolveStorePathFn>(() => "/tmp/openclaw-session-store.json"),
};
});

vi.mock("./bot-message-context.session.runtime.js", async () => {
const actual = await vi.importActual<typeof import("./bot-message-context.session.runtime.js")>(
Expand All @@ -28,17 +52,7 @@ vi.mock("./bot-message-context.session.runtime.js", async () => {
});

vi.mock("./bot-message-context.body.js", () => ({
resolveTelegramInboundBody: async () => ({
bodyText: "hello",
rawBody: "hello",
historyKey: undefined,
commandAuthorized: false,
effectiveWasMentioned: true,
canDetectMention: false,
shouldBypassMention: false,
stickerCacheHit: false,
locationData: undefined,
}),
resolveTelegramInboundBody: async () => inboundBodyResult.value,
}));

const { buildTelegramMessageContextForTest } =
Expand All @@ -49,6 +63,7 @@ const { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } =
beforeEach(() => {
clearRuntimeConfigSnapshot();
resetTopicNameCacheForTest();
inboundBodyResult.value = inboundBodyResult.reset();
});

afterEach(() => {
Expand Down Expand Up @@ -188,6 +203,40 @@ describe("buildTelegramMessageContext group sessions without forum", () => {
expect(ctx.ctxPayload.MessageThreadId).toBeUndefined();
});

it("carries the body-layer inbound event kind instead of restamping from copied mention booleans", async () => {
inboundBodyResult.value = {
...inboundBodyResult.reset(),
effectiveWasMentioned: false,
inboundEventKind: "user_request",
mentionFacts: {
canDetectMention: true,
wasMentioned: true,
explicitlyMentionedBot: true,
mentionSource: "explicit_bot",
effectiveWasMentioned: true,
requireMention: false,
shouldSkip: false,
},
};

const ctx = await buildTelegramMessageContextForTest({
cfg: { messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } } },
message: {
message_id: 7,
chat: { id: -1001234567890, type: "supergroup", title: "Test Group" },
date: 1700000000,
text: "@bot hello",
entities: [{ type: "mention", offset: 0, length: "@bot".length }],
from: { id: 42, first_name: "Alice" },
},
resolveGroupActivation: () => false,
resolveGroupRequireMention: () => false,
});

expect(ctx?.ctxPayload.InboundEventKind).toBe("user_request");
expect(ctx?.ctxPayload.ExplicitlyMentionedBot).toBe(true);
});

it("keeps same session for regular group with and without message_thread_id", async () => {
const ctxWithThread = await buildContext({
message_id: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ vi.mock("./bot-message-context.body.js", () => ({
historyKey: undefined,
commandAuthorized: false,
effectiveWasMentioned: true,
inboundEventKind: "user_request",
mentionFacts: {
canDetectMention: false,
wasMentioned: true,
effectiveWasMentioned: true,
requireMention: false,
shouldSkip: false,
},
canDetectMention: false,
shouldBypassMention: false,
hasControlCommand: false,
stickerCacheHit: false,
locationData: undefined,
}),
Expand Down
40 changes: 24 additions & 16 deletions extensions/telegram/src/bot-message-context.reactions.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
// Telegram tests cover bot message context.reactions plugin behavior.
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { TelegramInboundBodyResult } from "./bot-message-context.body.js";
import type { BuildTelegramMessageContextParams } from "./bot-message-context.types.js";

type InboundBodyMock = (arg: unknown) => Promise<{
bodyText: string;
rawBody: string;
historyKey: undefined;
commandAuthorized: boolean;
effectiveWasMentioned: boolean;
canDetectMention: boolean;
shouldBypassMention: boolean;
stickerCacheHit: boolean;
locationData: undefined;
}>;

const inboundBodyMock = vi.hoisted(() =>
vi.fn<InboundBodyMock>(async () => ({
type InboundBodyMock = (arg: unknown) => Promise<TelegramInboundBodyResult>;

const { createInboundBodyResult, inboundBodyMock } = vi.hoisted(() => {
const buildInboundBodyResult = (
inboundEventKind: TelegramInboundBodyResult["inboundEventKind"] = "user_request",
): TelegramInboundBodyResult => ({
bodyText: "hello",
rawBody: "hello",
historyKey: undefined,
commandAuthorized: false,
effectiveWasMentioned: false,
inboundEventKind,
mentionFacts: {
canDetectMention: true,
wasMentioned: false,
effectiveWasMentioned: false,
requireMention: false,
shouldSkip: false,
},
canDetectMention: true,
shouldBypassMention: false,
hasControlCommand: false,
stickerCacheHit: false,
locationData: undefined,
})),
);
});

return {
createInboundBodyResult: buildInboundBodyResult,
inboundBodyMock: vi.fn<InboundBodyMock>(async () => buildInboundBodyResult()),
};
});

vi.mock("./bot-message-context.body.js", () => ({
resolveTelegramInboundBody: (arg: unknown) => inboundBodyMock(arg),
Expand Down Expand Up @@ -66,6 +73,7 @@ describe("buildTelegramMessageContext reactions", () => {
it("does not create ack or status reactions for room events", async () => {
const setMessageReaction = vi.fn(async () => undefined);
const { createStatusReactionController } = createStatusReactionControllerStub();
inboundBodyMock.mockResolvedValueOnce(createInboundBodyResult("room_event"));

const ctx = await buildTelegramMessageContextForTest({
message: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,27 @@ describe("buildTelegramMessageContext requireMention precedence", () => {
expect(ctx?.ctxPayload.InboundEventKind).toBe("room_event");
});

it("keeps explicit bot mentions as user requests in always-on room-event groups", async () => {
const ctx = await buildTelegramMessageContextForTest({
cfg: { messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } } },
message: {
...buildForumMessage(),
text: "@bot status",
entities: [{ type: "mention", offset: 0, length: "@bot".length }],
},
resolveGroupActivation: () => false,
resolveGroupRequireMention: () => false,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: false },
topicConfig: undefined,
}),
});

expect(ctx?.ctxPayload.InboundEventKind).toBe("user_request");
expect(ctx?.ctxPayload.WasMentioned).toBe(true);
expect(ctx?.ctxPayload.ExplicitlyMentionedBot).toBe(true);
});

it("keeps ambient abort phrases as user requests", async () => {
const ctx = await buildTelegramMessageContextForTest({
cfg: { messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } } },
Expand Down
21 changes: 3 additions & 18 deletions extensions/telegram/src/bot-message-context.session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import {
type BuildChannelInboundEventContextParams,
type BuildChannelInboundEventContextAsyncParams,
type BuiltChannelInboundEventContext,
classifyChannelInboundEvent,
formatInboundEnvelope,
resolveUnmentionedGroupInboundPolicy,
resolveEnvelopeFormatOptions,
toLocationContext,
type NormalizedLocation,
type InboundEventKind,
} from "openclaw/plugin-sdk/channel-inbound";
import { isAbortRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
import { normalizeCommandBody } from "openclaw/plugin-sdk/command-surface";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type {
Expand Down Expand Up @@ -195,6 +193,7 @@ export async function buildTelegramInboundContextPayload(params: {
groupConfig?: TelegramGroupConfig | TelegramDirectConfig;
topicConfig?: TelegramTopicConfig;
effectiveWasMentioned: boolean;
inboundEventKind: InboundEventKind;
groupRequireMention: boolean;
mentionFacts: TelegramMentionFacts;
hasControlCommand: boolean;
Expand Down Expand Up @@ -246,6 +245,7 @@ export async function buildTelegramInboundContextPayload(params: {
groupConfig,
topicConfig,
effectiveWasMentioned,
inboundEventKind,
groupRequireMention,
mentionFacts,
hasControlCommand,
Expand Down Expand Up @@ -430,22 +430,7 @@ export async function buildTelegramInboundContextPayload(params: {
const commandSource =
options?.commandSource ??
(commandAuthorized && hasControlCommand ? ("text" as const) : undefined);
const unmentionedGroupPolicy = resolveUnmentionedGroupInboundPolicy({
cfg,
agentId: route.agentId,
});
const hasAbortRequest = isAbortRequestText(rawBody, {
botUsername: normalizeOptionalLowercaseString(primaryCtx.me?.username),
});
const conversationKind = isGroup ? "group" : "direct";
const inboundEventKind = classifyChannelInboundEvent({
conversation: { kind: conversationKind },
unmentionedGroupPolicy,
wasMentioned: effectiveWasMentioned,
hasControlCommand,
hasAbortRequest,
commandSource,
});
let watermarkedGroupHistoryEntries: HistoryEntry[] | undefined;
let groupHistoryPromptEntries: HistoryEntry[] = [];
if (hasGroupHistoryContext && historyKey && historyLimit > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const inboundBodyMock = vi.hoisted(() =>
historyKey: undefined,
commandAuthorized: false,
effectiveWasMentioned: false,
inboundEventKind: "user_request",
mentionFacts: {
canDetectMention: true,
wasMentioned: false,
Expand Down
1 change: 1 addition & 0 deletions extensions/telegram/src/bot-message-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ export const buildTelegramMessageContext = async ({
groupConfig,
topicConfig,
effectiveWasMentioned: bodyResult.effectiveWasMentioned,
inboundEventKind: bodyResult.inboundEventKind,
groupRequireMention: Boolean(groupRequireMention),
mentionFacts: bodyResult.mentionFacts,
hasControlCommand: bodyResult.hasControlCommand,
Expand Down
Loading