mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:40:49 +00:00
fix: expose telegram topic names in agent context (#65973) (thanks @ptahdunbar)
* feat(telegram): expose forum topic names in agent context
Telegram Bot API does not provide a method to look up forum topic names
by thread ID. This adds an in-memory LRU cache that learns topic names
from service messages (forum_topic_created, forum_topic_edited,
forum_topic_closed, forum_topic_reopened) and seeds from
reply_to_message.forum_topic_created as a fallback for pre-existing
topics.
The resolved topic name is surfaced as:
- TopicName in MsgContext (available to {{TopicName}} in templates)
- topic_name in the agent prompt metadata block
- topicName in plugin hook event metadata
Includes unit tests for the topic-name-cache module (11 tests including
eviction and read-recency).
Known limitation: cache is in-memory only; after a restart it falls back
to the creation-time name until a rename event is observed.
* refactor(telegram): distill topic name flow
* fix: expose telegram topic names in agent context (#65973) (thanks @ptahdunbar)
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
@@ -6,6 +6,8 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Telegram/forum topics: surface human topic names in agent context, prompt metadata, and plugin hook metadata by learning names from Telegram forum service messages. (#65973) Thanks @ptahdunbar.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/context engines: run opt-in turn maintenance as idle-aware background work so the next foreground turn no longer waits on proactive maintenance. (#65233) thanks @100yenadmin
|
||||
|
||||
@@ -143,6 +143,24 @@ describe("buildTelegramMessageContext group sessions without forum", () => {
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:telegram:group:-1001234567890:topic:99");
|
||||
expect(ctx?.ctxPayload?.MessageThreadId).toBe(99);
|
||||
});
|
||||
|
||||
it("surfaces topic name from reply_to_message forum metadata", async () => {
|
||||
const ctx = await buildContext({
|
||||
message_id: 3,
|
||||
chat: { id: -1001234567890, type: "supergroup", title: "Test Forum", is_forum: true },
|
||||
date: 1700000002,
|
||||
text: "@bot hello",
|
||||
message_thread_id: 99,
|
||||
from: { id: 42, first_name: "Alice" },
|
||||
reply_to_message: {
|
||||
message_id: 2,
|
||||
forum_topic_created: { name: "Deployments", icon_color: 0x6fb9f0 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(ctx?.ctxPayload?.TopicName).toBe("Deployments");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildTelegramMessageContext direct peer routing", () => {
|
||||
|
||||
@@ -105,6 +105,7 @@ export async function buildTelegramInboundContextPayload(params: {
|
||||
options?: TelegramMessageContextOptions;
|
||||
dmAllowFrom?: Array<string | number>;
|
||||
effectiveGroupAllow?: NormalizedAllowFrom;
|
||||
topicName?: string;
|
||||
sessionRuntime?: TelegramMessageContextSessionRuntimeOverrides;
|
||||
}): Promise<{
|
||||
ctxPayload: FinalizedTelegramInboundContext;
|
||||
@@ -139,6 +140,7 @@ export async function buildTelegramInboundContextPayload(params: {
|
||||
options,
|
||||
dmAllowFrom,
|
||||
effectiveGroupAllow,
|
||||
topicName,
|
||||
sessionRuntime: sessionRuntimeOverride,
|
||||
} = params;
|
||||
const replyTarget = describeReplyTarget(msg);
|
||||
@@ -349,6 +351,7 @@ export async function buildTelegramInboundContextPayload(params: {
|
||||
CommandSource: options?.commandSource,
|
||||
MessageThreadId: threadSpec.id,
|
||||
IsForum: isForum,
|
||||
TopicName: isForum && topicName ? topicName : undefined,
|
||||
OriginatingChannel: "telegram" as const,
|
||||
OriginatingTo: `telegram:${chatId}`,
|
||||
});
|
||||
|
||||
@@ -2,10 +2,8 @@ import type { ReactionTypeEmoji } from "@grammyjs/types";
|
||||
import {
|
||||
resolveAckReaction,
|
||||
shouldAckReaction as shouldAckReactionGate,
|
||||
type StatusReactionController,
|
||||
} from "openclaw/plugin-sdk/channel-feedback";
|
||||
import { logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import type { TelegramDirectConfig, TelegramGroupConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { deriveLastRoutePolicy } from "openclaw/plugin-sdk/routing";
|
||||
import { DEFAULT_ACCOUNT_ID, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
@@ -35,6 +33,7 @@ import {
|
||||
resolveTelegramReactionVariant,
|
||||
resolveTelegramStatusReactionEmojis,
|
||||
} from "./status-reaction-variants.js";
|
||||
import { getTopicName, updateTopicName } from "./topic-name-cache.js";
|
||||
|
||||
export type {
|
||||
BuildTelegramMessageContextParams,
|
||||
@@ -56,6 +55,15 @@ type TelegramReactionApi = (
|
||||
messageId: number,
|
||||
reactions: Array<{ type: "emoji"; emoji: ReactionTypeEmoji["emoji"] }>,
|
||||
) => Promise<unknown>;
|
||||
type TelegramStatusReactionController = {
|
||||
setQueued: () => void | Promise<void>;
|
||||
setThinking: () => void | Promise<void>;
|
||||
setTool: (name: string) => void | Promise<void>;
|
||||
setCompacting: () => void | Promise<void>;
|
||||
cancelPending: () => void;
|
||||
setError: () => void | Promise<void>;
|
||||
setDone: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
export type TelegramMessageContext = {
|
||||
ctxPayload: TelegramMessageContextPayload["ctxPayload"];
|
||||
@@ -83,7 +91,7 @@ export type TelegramMessageContext = {
|
||||
ackReactionPromise: Promise<boolean> | null;
|
||||
reactionApi: TelegramReactionApi | null;
|
||||
removeAckAfterReply: boolean;
|
||||
statusReactionController: StatusReactionController | null;
|
||||
statusReactionController: TelegramStatusReactionController | null;
|
||||
accountId: string;
|
||||
};
|
||||
|
||||
@@ -140,6 +148,45 @@ export const buildTelegramMessageContext = async ({
|
||||
const resolvedThreadId = threadSpec.scope === "forum" ? threadSpec.id : undefined;
|
||||
const replyThreadId = threadSpec.id;
|
||||
const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined;
|
||||
let topicName: string | undefined;
|
||||
if (isForum && resolvedThreadId != null) {
|
||||
const ftCreated = msg.forum_topic_created;
|
||||
const ftEdited = msg.forum_topic_edited;
|
||||
const ftClosed = msg.forum_topic_closed;
|
||||
const ftReopened = msg.forum_topic_reopened;
|
||||
|
||||
if (ftCreated?.name) {
|
||||
updateTopicName(chatId, resolvedThreadId, {
|
||||
name: ftCreated.name,
|
||||
iconColor: ftCreated.icon_color,
|
||||
iconCustomEmojiId: ftCreated.icon_custom_emoji_id,
|
||||
closed: false,
|
||||
});
|
||||
} else if (ftEdited?.name) {
|
||||
updateTopicName(chatId, resolvedThreadId, {
|
||||
name: ftEdited.name,
|
||||
iconCustomEmojiId: ftEdited.icon_custom_emoji_id,
|
||||
});
|
||||
} else if (ftClosed) {
|
||||
updateTopicName(chatId, resolvedThreadId, { closed: true });
|
||||
} else if (ftReopened) {
|
||||
updateTopicName(chatId, resolvedThreadId, { closed: false });
|
||||
}
|
||||
|
||||
topicName = getTopicName(chatId, resolvedThreadId);
|
||||
if (!topicName) {
|
||||
const replyFtCreated = msg.reply_to_message?.forum_topic_created;
|
||||
if (replyFtCreated?.name) {
|
||||
updateTopicName(chatId, resolvedThreadId, {
|
||||
name: replyFtCreated.name,
|
||||
iconColor: replyFtCreated.icon_color,
|
||||
iconCustomEmojiId: replyFtCreated.icon_custom_emoji_id,
|
||||
});
|
||||
topicName = replyFtCreated.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const threadIdForConfig = resolvedThreadId ?? dmThreadId;
|
||||
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, threadIdForConfig);
|
||||
// Use direct config dmPolicy override if available for DMs
|
||||
@@ -219,7 +266,7 @@ export const buildTelegramMessageContext = async ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const requireTopic = (groupConfig as TelegramDirectConfig | undefined)?.requireTopic;
|
||||
const requireTopic = groupConfig?.requireTopic;
|
||||
const topicRequiredButMissing = !isGroup && requireTopic === true && dmThreadId == null;
|
||||
if (topicRequiredButMissing) {
|
||||
logVerbose(`Blocked telegram DM ${chatId}: requireTopic=true but no topic present`);
|
||||
@@ -330,7 +377,7 @@ export const buildTelegramMessageContext = async ({
|
||||
const requireMention = firstDefined(
|
||||
activationOverride,
|
||||
topicConfig?.requireMention,
|
||||
(groupConfig as TelegramGroupConfig | undefined)?.requireMention,
|
||||
groupConfig?.requireMention,
|
||||
baseRequireMention,
|
||||
);
|
||||
|
||||
@@ -413,46 +460,49 @@ export const buildTelegramMessageContext = async ({
|
||||
? (runtime?.createStatusReactionController ??
|
||||
(await loadTelegramMessageContextRuntime()).createStatusReactionController)
|
||||
: null;
|
||||
const statusReactionController: StatusReactionController | null = createStatusReactionController
|
||||
? createStatusReactionController({
|
||||
enabled: true,
|
||||
adapter: {
|
||||
setReaction: async (emoji: string) => {
|
||||
if (reactionApi) {
|
||||
if (!allowedStatusReactionEmojisPromise) {
|
||||
allowedStatusReactionEmojisPromise = resolveTelegramAllowedEmojiReactions({
|
||||
chat: msg.chat,
|
||||
chatId,
|
||||
getChat: getChatApi ?? undefined,
|
||||
}).catch((err) => {
|
||||
logVerbose(
|
||||
`telegram status-reaction available_reactions lookup failed for chat ${chatId}: ${String(err)}`,
|
||||
);
|
||||
return null;
|
||||
const statusReactionController: TelegramStatusReactionController | null =
|
||||
createStatusReactionController
|
||||
? createStatusReactionController({
|
||||
enabled: true,
|
||||
adapter: {
|
||||
setReaction: async (emoji: string) => {
|
||||
if (reactionApi) {
|
||||
if (!allowedStatusReactionEmojisPromise) {
|
||||
allowedStatusReactionEmojisPromise = resolveTelegramAllowedEmojiReactions({
|
||||
chat: msg.chat,
|
||||
chatId,
|
||||
getChat: getChatApi ?? undefined,
|
||||
}).catch((err) => {
|
||||
logVerbose(
|
||||
`telegram status-reaction available_reactions lookup failed for chat ${chatId}: ${String(err)}`,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
const allowedStatusReactionEmojis = await allowedStatusReactionEmojisPromise;
|
||||
const resolvedEmoji = resolveTelegramReactionVariant({
|
||||
requestedEmoji: emoji,
|
||||
variantsByRequestedEmoji: statusReactionVariantsByEmoji,
|
||||
allowedEmojiReactions: allowedStatusReactionEmojis,
|
||||
});
|
||||
if (!resolvedEmoji) {
|
||||
return;
|
||||
}
|
||||
await reactionApi(chatId, msg.message_id, [
|
||||
{ type: "emoji", emoji: resolvedEmoji },
|
||||
]);
|
||||
}
|
||||
const allowedStatusReactionEmojis = await allowedStatusReactionEmojisPromise;
|
||||
const resolvedEmoji = resolveTelegramReactionVariant({
|
||||
requestedEmoji: emoji,
|
||||
variantsByRequestedEmoji: statusReactionVariantsByEmoji,
|
||||
allowedEmojiReactions: allowedStatusReactionEmojis,
|
||||
});
|
||||
if (!resolvedEmoji) {
|
||||
return;
|
||||
}
|
||||
await reactionApi(chatId, msg.message_id, [{ type: "emoji", emoji: resolvedEmoji }]);
|
||||
}
|
||||
},
|
||||
// Telegram replaces atomically — no removeReaction needed
|
||||
},
|
||||
// Telegram replaces atomically — no removeReaction needed
|
||||
},
|
||||
initialEmoji: ackReaction,
|
||||
emojis: resolvedStatusReactionEmojis,
|
||||
timing: statusReactionsConfig?.timing,
|
||||
onError: (err) => {
|
||||
logVerbose(`telegram status-reaction error for chat ${chatId}: ${String(err)}`);
|
||||
},
|
||||
})
|
||||
: null;
|
||||
initialEmoji: ackReaction,
|
||||
emojis: resolvedStatusReactionEmojis,
|
||||
timing: statusReactionsConfig?.timing,
|
||||
onError: (err) => {
|
||||
logVerbose(`telegram status-reaction error for chat ${chatId}: ${String(err)}`);
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
// When status reactions are enabled, setQueued() replaces the simple ack reaction
|
||||
const ackReactionPromise: Promise<boolean> | null = statusReactionController
|
||||
@@ -505,6 +555,7 @@ export const buildTelegramMessageContext = async ({
|
||||
dmAllowFrom,
|
||||
effectiveGroupAllow,
|
||||
commandAuthorized: bodyResult.commandAuthorized,
|
||||
topicName,
|
||||
sessionRuntime,
|
||||
});
|
||||
|
||||
|
||||
94
extensions/telegram/src/topic-name-cache.test.ts
Normal file
94
extensions/telegram/src/topic-name-cache.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, it, beforeEach } from "vitest";
|
||||
import {
|
||||
clearTopicNameCache,
|
||||
getTopicEntry,
|
||||
getTopicName,
|
||||
topicNameCacheSize,
|
||||
updateTopicName,
|
||||
} from "./topic-name-cache.js";
|
||||
|
||||
describe("topic-name-cache", () => {
|
||||
beforeEach(() => {
|
||||
clearTopicNameCache();
|
||||
});
|
||||
|
||||
it("stores and retrieves a topic name", () => {
|
||||
updateTopicName(-100123, 42, { name: "Deployments" });
|
||||
expect(getTopicName(-100123, 42)).toBe("Deployments");
|
||||
});
|
||||
|
||||
it("returns undefined for unknown topics", () => {
|
||||
expect(getTopicName(-100123, 99)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("handles renames via forum_topic_edited (overwrites previous name)", () => {
|
||||
updateTopicName(-100123, 42, { name: "Deployments" });
|
||||
updateTopicName(-100123, 42, { name: "CI/CD" });
|
||||
expect(getTopicName(-100123, 42)).toBe("CI/CD");
|
||||
});
|
||||
|
||||
it("preserves name when patching only closed status", () => {
|
||||
updateTopicName(-100123, 42, { name: "Deployments" });
|
||||
updateTopicName(-100123, 42, { closed: true });
|
||||
expect(getTopicName(-100123, 42)).toBe("Deployments");
|
||||
expect(getTopicEntry(-100123, 42)?.closed).toBe(true);
|
||||
});
|
||||
|
||||
it("marks topic as reopened", () => {
|
||||
updateTopicName(-100123, 42, { name: "Deployments", closed: true });
|
||||
updateTopicName(-100123, 42, { closed: false });
|
||||
expect(getTopicEntry(-100123, 42)?.closed).toBe(false);
|
||||
});
|
||||
|
||||
it("stores icon metadata", () => {
|
||||
updateTopicName(-100123, 42, {
|
||||
name: "Design",
|
||||
iconColor: 0x6fb9f0,
|
||||
iconCustomEmojiId: "emoji123",
|
||||
});
|
||||
const entry = getTopicEntry(-100123, 42);
|
||||
expect(entry?.iconColor).toBe(0x6fb9f0);
|
||||
expect(entry?.iconCustomEmojiId).toBe("emoji123");
|
||||
});
|
||||
|
||||
it("does not store entries with empty name and no prior entry", () => {
|
||||
updateTopicName(-100123, 42, { closed: true });
|
||||
expect(getTopicName(-100123, 42)).toBeUndefined();
|
||||
expect(topicNameCacheSize()).toBe(0);
|
||||
});
|
||||
|
||||
it("updates timestamps on write", async () => {
|
||||
updateTopicName(-100123, 42, { name: "A" });
|
||||
const t1 = getTopicEntry(-100123, 42)?.updatedAt ?? 0;
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
updateTopicName(-100123, 42, { name: "B" });
|
||||
const t2 = getTopicEntry(-100123, 42)?.updatedAt ?? 0;
|
||||
expect(t2).toBeGreaterThan(t1);
|
||||
});
|
||||
|
||||
it("works with string chatId and threadId", () => {
|
||||
updateTopicName("-100123", "42", { name: "StringKeys" });
|
||||
expect(getTopicName("-100123", "42")).toBe("StringKeys");
|
||||
});
|
||||
|
||||
it("evicts the oldest entry when cache exceeds 2048", () => {
|
||||
for (let i = 0; i < 2049; i++) {
|
||||
updateTopicName(-100000, i, { name: `Topic ${i}` });
|
||||
}
|
||||
expect(topicNameCacheSize()).toBe(2048);
|
||||
expect(getTopicName(-100000, 0)).toBeUndefined();
|
||||
expect(getTopicName(-100000, 2048)).toBe("Topic 2048");
|
||||
});
|
||||
|
||||
it("refreshes recency on read so active topics survive eviction", async () => {
|
||||
updateTopicName(-100000, 1, { name: "Active" });
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
for (let i = 2; i <= 2048; i++) {
|
||||
updateTopicName(-100000, i, { name: `Topic ${i}` });
|
||||
}
|
||||
getTopicName(-100000, 1);
|
||||
updateTopicName(-100000, 9999, { name: "Newcomer" });
|
||||
expect(getTopicName(-100000, 1)).toBe("Active");
|
||||
expect(topicNameCacheSize()).toBe(2048);
|
||||
});
|
||||
});
|
||||
79
extensions/telegram/src/topic-name-cache.ts
Normal file
79
extensions/telegram/src/topic-name-cache.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
const MAX_ENTRIES = 2_048;
|
||||
|
||||
export type TopicEntry = {
|
||||
name: string;
|
||||
iconColor?: number;
|
||||
iconCustomEmojiId?: string;
|
||||
closed?: boolean;
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
const cache = new Map<string, TopicEntry>();
|
||||
|
||||
function cacheKey(chatId: number | string, threadId: number | string): string {
|
||||
return `${chatId}:${threadId}`;
|
||||
}
|
||||
|
||||
function evictOldest(): void {
|
||||
if (cache.size <= MAX_ENTRIES) {
|
||||
return;
|
||||
}
|
||||
let oldestKey: string | undefined;
|
||||
let oldestTime = Infinity;
|
||||
for (const [key, entry] of cache) {
|
||||
if (entry.updatedAt < oldestTime) {
|
||||
oldestTime = entry.updatedAt;
|
||||
oldestKey = key;
|
||||
}
|
||||
}
|
||||
if (oldestKey) {
|
||||
cache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTopicName(
|
||||
chatId: number | string,
|
||||
threadId: number | string,
|
||||
patch: Partial<Omit<TopicEntry, "updatedAt">>,
|
||||
): void {
|
||||
const key = cacheKey(chatId, threadId);
|
||||
const existing = cache.get(key);
|
||||
const merged: TopicEntry = {
|
||||
name: patch.name ?? existing?.name ?? "",
|
||||
iconColor: patch.iconColor ?? existing?.iconColor,
|
||||
iconCustomEmojiId: patch.iconCustomEmojiId ?? existing?.iconCustomEmojiId,
|
||||
closed: patch.closed ?? existing?.closed,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
if (!merged.name) {
|
||||
return;
|
||||
}
|
||||
cache.set(key, merged);
|
||||
evictOldest();
|
||||
}
|
||||
|
||||
export function getTopicName(
|
||||
chatId: number | string,
|
||||
threadId: number | string,
|
||||
): string | undefined {
|
||||
const entry = cache.get(cacheKey(chatId, threadId));
|
||||
if (entry) {
|
||||
entry.updatedAt = Date.now();
|
||||
}
|
||||
return entry?.name;
|
||||
}
|
||||
|
||||
export function getTopicEntry(
|
||||
chatId: number | string,
|
||||
threadId: number | string,
|
||||
): TopicEntry | undefined {
|
||||
return cache.get(cacheKey(chatId, threadId));
|
||||
}
|
||||
|
||||
export function clearTopicNameCache(): void {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
export function topicNameCacheSize(): number {
|
||||
return cache.size;
|
||||
}
|
||||
@@ -280,6 +280,20 @@ describe("buildInboundUserContextPrefix", () => {
|
||||
expect(text).toContain('"conversation_label": "ops-room"');
|
||||
});
|
||||
|
||||
it("includes topic_name for forum chats", () => {
|
||||
const text = buildInboundUserContextPrefix({
|
||||
ChatType: "group",
|
||||
IsForum: true,
|
||||
MessageThreadId: 42,
|
||||
TopicName: "Deployments",
|
||||
} as TemplateContext);
|
||||
|
||||
const conversationInfo = parseConversationInfoPayload(text);
|
||||
expect(conversationInfo["topic_id"]).toBe("42");
|
||||
expect(conversationInfo["topic_name"]).toBe("Deployments");
|
||||
expect(conversationInfo["is_forum"]).toBe(true);
|
||||
});
|
||||
|
||||
it("includes sender identifier in conversation info", () => {
|
||||
const text = buildInboundUserContextPrefix({
|
||||
ChatType: "group",
|
||||
|
||||
@@ -196,6 +196,7 @@ export function buildInboundUserContextPrefix(
|
||||
ctx.MessageThreadId != null
|
||||
? (normalizePromptMetadataString(String(ctx.MessageThreadId)) ?? undefined)
|
||||
: undefined,
|
||||
topic_name: normalizePromptMetadataString(ctx.TopicName) ?? undefined,
|
||||
is_forum: ctx.IsForum === true ? true : undefined,
|
||||
is_group_chat: !isDirect ? true : undefined,
|
||||
was_mentioned: ctx.WasMentioned === true ? true : undefined,
|
||||
|
||||
@@ -167,6 +167,8 @@ export type MsgContext = {
|
||||
NativeDirectUserId?: string;
|
||||
/** Telegram forum supergroup marker. */
|
||||
IsForum?: boolean;
|
||||
/** Human-readable Telegram forum topic name (cached from service messages). */
|
||||
TopicName?: string;
|
||||
/** Warning: DM has topics enabled but this message is not in a topic. */
|
||||
TopicRequiredButMissing?: boolean;
|
||||
/**
|
||||
|
||||
@@ -132,7 +132,7 @@ describe("message hook mappers", () => {
|
||||
});
|
||||
|
||||
it("maps canonical inbound context to plugin/internal received payloads", () => {
|
||||
const canonical = deriveInboundMessageHookContext(makeInboundCtx());
|
||||
const canonical = deriveInboundMessageHookContext(makeInboundCtx({ TopicName: "Deployments" }));
|
||||
|
||||
expect(toPluginMessageContext(canonical)).toEqual({
|
||||
channelId: "demo-chat",
|
||||
@@ -147,6 +147,7 @@ describe("message hook mappers", () => {
|
||||
messageId: "msg-1",
|
||||
senderName: "User One",
|
||||
threadId: 42,
|
||||
topicName: "Deployments",
|
||||
}),
|
||||
});
|
||||
expect(toInternalMessageReceivedContext(canonical)).toEqual({
|
||||
@@ -160,6 +161,7 @@ describe("message hook mappers", () => {
|
||||
metadata: expect.objectContaining({
|
||||
senderUsername: "userone",
|
||||
senderE164: "+15551234567",
|
||||
topicName: "Deployments",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,7 @@ export type CanonicalInboundMessageHookContext = {
|
||||
channelName?: string;
|
||||
isGroup: boolean;
|
||||
groupId?: string;
|
||||
topicName?: string;
|
||||
};
|
||||
|
||||
export type CanonicalSentMessageHookContext = {
|
||||
@@ -131,6 +132,7 @@ export function deriveInboundMessageHookContext(
|
||||
channelName: ctx.GroupChannel,
|
||||
isGroup,
|
||||
groupId: isGroup ? conversationId : undefined,
|
||||
topicName: ctx.TopicName,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -266,6 +268,7 @@ export function toPluginInboundClaimEvent(
|
||||
guildId: canonical.guildId,
|
||||
channelName: canonical.channelName,
|
||||
groupId: canonical.groupId,
|
||||
topicName: canonical.topicName,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -291,6 +294,7 @@ export function toPluginMessageReceivedEvent(
|
||||
senderE164: canonical.senderE164,
|
||||
guildId: canonical.guildId,
|
||||
channelName: canonical.channelName,
|
||||
topicName: canonical.topicName,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -328,6 +332,7 @@ export function toInternalMessageReceivedContext(
|
||||
senderE164: canonical.senderE164,
|
||||
guildId: canonical.guildId,
|
||||
channelName: canonical.channelName,
|
||||
topicName: canonical.topicName,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user