clawgram 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1233 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createChannelPlugin = void 0;
4
+ const core_1 = require("openclaw/plugin-sdk/core");
5
+ const channel_runtime_1 = require("openclaw/plugin-sdk/channel-runtime");
6
+ const param_readers_1 = require("openclaw/plugin-sdk/param-readers");
7
+ const tool_send_1 = require("openclaw/plugin-sdk/tool-send");
8
+ const direct_dm_1 = require("openclaw/plugin-sdk/direct-dm");
9
+ const channel_inbound_1 = require("openclaw/plugin-sdk/channel-inbound");
10
+ const channel_reply_pipeline_1 = require("openclaw/plugin-sdk/channel-reply-pipeline");
11
+ const inbound_envelope_1 = require("openclaw/plugin-sdk/inbound-envelope");
12
+ const inbound_reply_dispatch_1 = require("openclaw/plugin-sdk/inbound-reply-dispatch");
13
+ const channel_pairing_1 = require("openclaw/plugin-sdk/channel-pairing");
14
+ const events_1 = require("telegram/events");
15
+ const gramjs_client_1 = require("./gramjs-client");
16
+ const normalize_1 = require("./normalize");
17
+ const history_1 = require("./history");
18
+ const joins_1 = require("./joins");
19
+ const group_reply_address_1 = require("./group-reply-address");
20
+ const group_visible_reply_guard_1 = require("./group-visible-reply-guard");
21
+ const helpers_1 = require("./helpers");
22
+ const proxy_config_1 = require("./proxy-config");
23
+ const constants_1 = require("./constants");
24
+ const actionLog = (0, core_1.createSubsystemLogger)("channels/clawgram");
25
+ /**
26
+ * Read scope as configured for the account. Left `undefined` when the key is
27
+ * absent so `isChatReadable` can tell "not configured" from "configured empty" —
28
+ * the first means no restriction, the second denies everything.
29
+ */
30
+ function readAccountReadChats(account) {
31
+ const raw = account?.readChats;
32
+ if (raw === undefined || raw === null)
33
+ return undefined;
34
+ const entries = Array.isArray(raw) ? raw : [raw];
35
+ return entries.map((entry) => String(entry).trim()).filter(Boolean);
36
+ }
37
+ function resolveAccountReadChats(cfg, accountId) {
38
+ return readAccountReadChats(cfg?.channels?.["clawgram"]?.accounts?.[accountId]);
39
+ }
40
+ function parseOptionalThreadId(value) {
41
+ if (typeof value === "number") {
42
+ return Number.isFinite(value) ? Math.trunc(value) : undefined;
43
+ }
44
+ if (typeof value !== "string") {
45
+ return undefined;
46
+ }
47
+ const trimmed = value.trim();
48
+ if (!trimmed || !/^\d+$/.test(trimmed)) {
49
+ return undefined;
50
+ }
51
+ const parsed = Number.parseInt(trimmed, 10);
52
+ return Number.isFinite(parsed) ? parsed : undefined;
53
+ }
54
+ const createChannelPlugin = (runtimes) => {
55
+ const resolveRuntimeAccountId = (cfg, preferred) => {
56
+ const configured = (0, helpers_1.resolveConfiguredAccountId)(cfg, preferred);
57
+ if (configured && runtimes.has(configured)) {
58
+ return configured;
59
+ }
60
+ if (preferred?.trim()) {
61
+ return preferred.trim();
62
+ }
63
+ return configured ?? runtimes.keys().next().value;
64
+ };
65
+ return {
66
+ id: "clawgram",
67
+ meta: {
68
+ id: "clawgram",
69
+ label: "Clawgram",
70
+ selectionLabel: "Clawgram (GramJS)",
71
+ docsPath: "/channels/clawgram",
72
+ blurb: "Connect your personal Telegram account to OpenClaw via MTProto. Your AI assistant responds as you.",
73
+ aliases: ["tguserbot"],
74
+ },
75
+ capabilities: {
76
+ chatTypes: ["direct", "group"],
77
+ reactions: true,
78
+ threads: true,
79
+ media: true,
80
+ nativeCommands: false,
81
+ blockStreaming: false,
82
+ },
83
+ agentPrompt: {
84
+ messageToolHints: () => [
85
+ "Use clawgram to send Telegram replies from the connected personal account.",
86
+ "When replying in the current Telegram chat, omit `to`/`target` and clawgram will send to the current conversation automatically.",
87
+ "Explicit targets may be @username, numeric Telegram user id, phone/contact resolvable by Telegram, group chat ids, or clawgram:<target>.",
88
+ "For Telegram forum topics, send to the group chat id and pass the topic id separately as `threadId`.",
89
+ ],
90
+ messageToolCapabilities: () => [
91
+ "clawgram can reply in the current Telegram conversation when no explicit target is provided.",
92
+ "clawgram can send text messages to direct chats and groups from the connected personal account.",
93
+ "clawgram supports Telegram forum topics via the `threadId` parameter on group sends.",
94
+ ],
95
+ },
96
+ config: {
97
+ listAccountIds(cfg) {
98
+ const accounts = cfg?.channels?.["clawgram"]?.accounts;
99
+ if (!accounts || typeof accounts !== "object") {
100
+ return [];
101
+ }
102
+ return Object.keys(accounts);
103
+ },
104
+ resolveAccount(cfg, accountId) {
105
+ const account = cfg?.channels?.["clawgram"]?.accounts?.[accountId];
106
+ return {
107
+ apiId: Number(account?.apiId),
108
+ apiHash: String(account?.apiHash ?? ""),
109
+ sessionString: String(account?.sessionString ?? ""),
110
+ allowFrom: (0, helpers_1.resolveAllowFrom)(account?.allowFrom),
111
+ groups: (0, helpers_1.resolveGroups)(account?.groups),
112
+ readChats: readAccountReadChats(account),
113
+ enabled: account?.enabled,
114
+ accountId,
115
+ proxy: (0, proxy_config_1.resolveProxyConfig)(account?.proxy),
116
+ };
117
+ },
118
+ },
119
+ gateway: {
120
+ startAccount: async (ctx) => {
121
+ const { account, accountId, channelRuntime, cfg, log } = ctx;
122
+ if (!channelRuntime) {
123
+ throw new Error("clawgram: channelRuntime is required");
124
+ }
125
+ if (runtimes.has(accountId)) {
126
+ log?.warn?.("clawgram stale runtime detected, reconnecting", { accountId });
127
+ await runtimes.get(accountId)?.stop().catch(() => undefined);
128
+ runtimes.delete(accountId);
129
+ }
130
+ const gram = new gramjs_client_1.GramJsClientManager(account);
131
+ await gram.start();
132
+ runtimes.set(accountId, gram);
133
+ const pairing = (0, channel_pairing_1.createChannelPairingController)({
134
+ // The controller only reads core.channel.pairing, but its parameter is typed
135
+ // as the full PluginRuntime, and ctx (hence channelRuntime) is untyped.
136
+ core: { channel: channelRuntime },
137
+ channel: "clawgram",
138
+ accountId,
139
+ });
140
+ const me = await gram.getMe();
141
+ const selfId = me?.id ? String(me.id) : undefined;
142
+ const selfUsername = (0, helpers_1.resolveActiveUsername)(me);
143
+ const selfLabel = (0, helpers_1.toDisplayName)({
144
+ username: selfUsername,
145
+ firstName: typeof me?.firstName === "string" ? me.firstName : undefined,
146
+ lastName: typeof me?.lastName === "string" ? me.lastName : undefined,
147
+ fallback: selfId,
148
+ });
149
+ log?.info?.("clawgram connected ------------------------------------------", {
150
+ accountId,
151
+ selfId,
152
+ username: selfUsername,
153
+ proxy: gram.getProxySummary(),
154
+ });
155
+ const client = gram.getClient();
156
+ const eventBuilder = new events_1.NewMessage({});
157
+ const eventHandler = async (event) => {
158
+ try {
159
+ const rawMessage = event?.message;
160
+ const rawPeerUserId = rawMessage?.peerId?.userId;
161
+ const rawPeerChatId = rawMessage?.peerId?.chatId;
162
+ const rawPeerChannelId = rawMessage?.peerId?.channelId;
163
+ const directLike = rawPeerUserId !== undefined ||
164
+ (typeof rawMessage?.chatId === "number" && rawMessage.chatId > 0);
165
+ if (directLike) {
166
+ log?.info?.("clawgram raw direct-like event", {
167
+ accountId,
168
+ messageId: String(rawMessage?.id ?? ""),
169
+ chatId: String(rawMessage?.chatId ?? ""),
170
+ peerUserId: String(rawPeerUserId ?? ""),
171
+ peerChatId: String(rawPeerChatId ?? ""),
172
+ peerChannelId: String(rawPeerChannelId ?? ""),
173
+ senderId: String(rawMessage?.senderId ?? rawMessage?.fromId?.userId ?? ""),
174
+ out: rawMessage?.out === true,
175
+ textLength: typeof rawMessage?.message === "string" ? rawMessage.message.length : typeof rawMessage?.text === "string" ? rawMessage.text.length : 0,
176
+ });
177
+ }
178
+ const normalized = (0, normalize_1.normalizeTelegramEvent)(event, accountId);
179
+ if (!normalized) {
180
+ if (directLike) {
181
+ log?.info?.("clawgram normalize returned null", {
182
+ accountId,
183
+ messageId: String(rawMessage?.id ?? ""),
184
+ chatId: String(rawMessage?.chatId ?? ""),
185
+ peerUserId: String(rawPeerUserId ?? ""),
186
+ });
187
+ }
188
+ return;
189
+ }
190
+ const directReplyTarget = normalized.chatType === "direct"
191
+ ? undefined
192
+ : await (0, helpers_1.resolveReplyTarget)(rawMessage);
193
+ const senderProfile = normalized.chatType === "direct"
194
+ ? await (0, helpers_1.resolveSenderProfileWithTimeout)(rawMessage, {
195
+ senderId: normalized.senderId,
196
+ client,
197
+ }, 1500)
198
+ : await (0, helpers_1.resolveSenderProfile)(rawMessage, {
199
+ senderId: normalized.senderId,
200
+ client,
201
+ });
202
+ const replyTarget = normalized.chatType === "direct"
203
+ ? normalized.chatId
204
+ : await (0, helpers_1.resolveChatTarget)(rawMessage);
205
+ if (replyTarget) {
206
+ normalized.replyTarget = replyTarget;
207
+ }
208
+ if (!normalized.senderUsername && senderProfile.username) {
209
+ normalized.senderUsername = senderProfile.username;
210
+ }
211
+ if (!normalized.senderDisplay && senderProfile.display) {
212
+ normalized.senderDisplay = senderProfile.display;
213
+ }
214
+ if (normalized.isOutgoing) {
215
+ if (normalized.chatType === "direct") {
216
+ log?.info?.("clawgram skipping outgoing direct event", {
217
+ accountId,
218
+ chatId: normalized.chatId,
219
+ messageId: normalized.messageId,
220
+ senderId: normalized.senderId,
221
+ });
222
+ }
223
+ return;
224
+ }
225
+ if (normalized.chatType === "channel") {
226
+ log?.info?.("clawgram skipping channel inbound", {
227
+ accountId,
228
+ chatId: normalized.chatId,
229
+ chatType: normalized.chatType,
230
+ messageId: normalized.messageId,
231
+ });
232
+ return;
233
+ }
234
+ const text = normalized.text?.trim();
235
+ if (!text) {
236
+ log?.info?.("clawgram skipping empty inbound text", {
237
+ accountId,
238
+ chatId: normalized.chatId,
239
+ messageId: normalized.messageId,
240
+ });
241
+ return;
242
+ }
243
+ const senderId = normalized.senderId ?? normalized.chatId;
244
+ const isTelegramServiceDirect = normalized.chatType === "direct" &&
245
+ (normalized.chatId === "777000" || senderId === "777000");
246
+ const isSavedMessagesDirect = normalized.chatType === "direct" &&
247
+ Boolean(selfId) &&
248
+ normalized.chatId === selfId &&
249
+ senderId === selfId;
250
+ if (isTelegramServiceDirect) {
251
+ log?.info?.("clawgram skipping Telegram service direct chat", {
252
+ accountId,
253
+ chatId: normalized.chatId,
254
+ messageId: normalized.messageId,
255
+ senderId,
256
+ });
257
+ return;
258
+ }
259
+ if (isSavedMessagesDirect) {
260
+ log?.info?.("clawgram skipping Saved Messages direct chat", {
261
+ accountId,
262
+ chatId: normalized.chatId,
263
+ messageId: normalized.messageId,
264
+ senderId,
265
+ selfId,
266
+ });
267
+ return;
268
+ }
269
+ const senderUsername = normalized.senderUsername;
270
+ const senderLabel = normalized.senderDisplay || normalized.senderUsername || senderId;
271
+ const conversationTarget = normalized.chatType === "direct"
272
+ ? normalized.chatId
273
+ : normalized.replyTarget ?? normalized.chatId;
274
+ const conversationFallbackTargets = [
275
+ normalized.chatType === "direct" ? directReplyTarget : undefined,
276
+ normalized.chatType === "direct" ? normalized.replyTarget : undefined,
277
+ normalized.chatType === "direct" && normalized.senderUsername ? `@${normalized.senderUsername}` : undefined,
278
+ normalized.chatId,
279
+ ].filter((target, index, items) => {
280
+ if (!target || target === conversationTarget) {
281
+ return false;
282
+ }
283
+ return items.findIndex((candidate) => candidate === target) === index;
284
+ });
285
+ const sendTextToConversation = async (args) => {
286
+ const targets = [conversationTarget, ...conversationFallbackTargets];
287
+ let lastError;
288
+ for (const target of targets) {
289
+ try {
290
+ return await gram.sendText({
291
+ target,
292
+ text: args.text,
293
+ replyToMessageId: args.replyToMessageId,
294
+ messageThreadId: args.messageThreadId,
295
+ });
296
+ }
297
+ catch (error) {
298
+ lastError = error;
299
+ }
300
+ }
301
+ throw lastError;
302
+ };
303
+ const accountConfig = cfg?.channels?.["clawgram"]?.accounts?.[accountId] ??
304
+ cfg?.channels?.["clawgram"] ??
305
+ {};
306
+ const directAllowFrom = (0, helpers_1.resolveAllowFrom)(accountConfig?.allowFrom ?? account?.allowFrom);
307
+ const groups = (0, helpers_1.resolveGroups)(accountConfig?.groups ?? account?.groups);
308
+ const dmPolicy = "open";
309
+ if (normalized.chatType === "group") {
310
+ const groupConfig = (0, helpers_1.resolveGroupConfig)(groups, normalized.chatId);
311
+ if (!groupConfig) {
312
+ log?.info?.("clawgram skipping group not present in groups config", {
313
+ accountId,
314
+ chatId: normalized.chatId,
315
+ messageId: normalized.messageId,
316
+ });
317
+ return;
318
+ }
319
+ if (groupConfig.enabled === false) {
320
+ log?.info?.("clawgram skipping disabled group", {
321
+ accountId,
322
+ chatId: normalized.chatId,
323
+ messageId: normalized.messageId,
324
+ });
325
+ return;
326
+ }
327
+ if (!(0, helpers_1.isSenderAllowed)({
328
+ allowFrom: groupConfig.allowFrom,
329
+ senderId,
330
+ senderUsername: normalized.senderUsername,
331
+ })) {
332
+ log?.info?.("clawgram blocking inbound group sender by allowFrom", {
333
+ accountId,
334
+ chatId: normalized.chatId,
335
+ messageId: normalized.messageId,
336
+ senderId,
337
+ username: normalized.senderUsername,
338
+ allowFrom: groupConfig.allowFrom,
339
+ });
340
+ return;
341
+ }
342
+ const scopedGroupPeerId = (0, helpers_1.buildScopedGroupPeerId)(accountId, normalized.chatId);
343
+ const { route: inboundRoute, buildEnvelope } = (0, inbound_envelope_1.resolveInboundRouteEnvelopeBuilderWithRuntime)({
344
+ cfg,
345
+ channel: "clawgram",
346
+ accountId,
347
+ peer: {
348
+ kind: "group",
349
+ id: scopedGroupPeerId,
350
+ },
351
+ runtime: channelRuntime,
352
+ sessionStore: cfg?.session?.store,
353
+ });
354
+ // channelRuntime comes from the untyped ctx, so the generic route type falls
355
+ // back to the minimal RouteLike. The runtime value is a ResolvedAgentRoute.
356
+ const route = inboundRoute;
357
+ const wasMentioned = (0, helpers_1.hasTelegramMention)({
358
+ cfg,
359
+ agentId: route.agentId,
360
+ selfUsername,
361
+ text,
362
+ message: rawMessage,
363
+ });
364
+ const wasReplyToSelf = await (0, helpers_1.isReplyToSelfMessage)(rawMessage, selfId);
365
+ const mentionDecision = (0, channel_inbound_1.resolveInboundMentionDecision)({
366
+ facts: {
367
+ canDetectMention: true,
368
+ wasMentioned,
369
+ hasAnyMention: /(^|\s)@[a-zA-Z0-9_]{5,}\b/.test(text),
370
+ },
371
+ policy: {
372
+ isGroup: true,
373
+ requireMention: groupConfig.groupPolicy === "mention",
374
+ allowTextCommands: false,
375
+ hasControlCommand: false,
376
+ commandAuthorized: true,
377
+ },
378
+ });
379
+ log?.info?.("clawgram group mention gate", {
380
+ accountId,
381
+ chatId: normalized.chatId,
382
+ messageId: normalized.messageId,
383
+ selfUsername,
384
+ mentionedFlag: rawMessage?.mentioned === true,
385
+ hasEntities: Array.isArray(rawMessage?.entities) ? rawMessage.entities.length : 0,
386
+ wasMentioned,
387
+ wasReplyToSelf,
388
+ shouldSkip: mentionDecision.shouldSkip,
389
+ text,
390
+ });
391
+ if (groupConfig.groupPolicy === "mention" && mentionDecision.shouldSkip && !wasReplyToSelf) {
392
+ log?.info?.("clawgram skipping group message without mention", {
393
+ accountId,
394
+ chatId: normalized.chatId,
395
+ messageId: normalized.messageId,
396
+ senderId,
397
+ });
398
+ return;
399
+ }
400
+ const { storePath, body } = buildEnvelope({
401
+ channel: "Telegram",
402
+ from: senderLabel,
403
+ body: text,
404
+ timestamp: normalized.timestamp,
405
+ });
406
+ const conversationRouteTarget = (0, helpers_1.buildConversationTarget)(normalized.chatId);
407
+ const ctxPayload = channelRuntime.reply.finalizeInboundContext({
408
+ Body: body,
409
+ BodyForAgent: text,
410
+ RawBody: text,
411
+ CommandBody: text,
412
+ From: conversationRouteTarget,
413
+ To: conversationRouteTarget,
414
+ SessionKey: route.sessionKey,
415
+ AccountId: route.accountId ?? accountId,
416
+ ChatType: "group",
417
+ ConversationLabel: senderLabel,
418
+ SenderId: senderId,
419
+ SenderUsername: normalized.senderUsername,
420
+ SenderName: normalized.senderDisplay,
421
+ GroupId: normalized.chatId,
422
+ GroupSubject: normalized.chatId,
423
+ WasMentioned: mentionDecision.effectiveWasMentioned || wasReplyToSelf,
424
+ WasReplyToSelf: wasReplyToSelf,
425
+ Provider: "telegram",
426
+ Surface: "clawgram",
427
+ MessageSid: normalized.messageId,
428
+ MessageSidFull: normalized.messageId,
429
+ Timestamp: normalized.timestamp,
430
+ ReplyToId: normalized.replyToMessageId,
431
+ MessageThreadId: normalized.messageThreadId,
432
+ NativeChannelId: normalized.chatId,
433
+ OriginatingChannel: "clawgram",
434
+ OriginatingTo: conversationRouteTarget,
435
+ });
436
+ const groupReplyAddress = (0, group_reply_address_1.buildGroupReplyAddress)({
437
+ senderUsername: normalized.senderUsername,
438
+ senderDisplay: normalized.senderDisplay,
439
+ senderId,
440
+ });
441
+ (0, group_reply_address_1.rememberGroupReplyAddress)({
442
+ accountId: route.accountId ?? accountId,
443
+ chatId: normalized.chatId,
444
+ replyToId: normalized.messageId,
445
+ address: groupReplyAddress,
446
+ });
447
+ const messageThreadId = parseOptionalThreadId(normalized.messageThreadId);
448
+ const groupTypingTarget = normalized.chatId;
449
+ await gram.withTyping(groupTypingTarget, async () => {
450
+ log?.info?.("clawgram dispatching group reply", {
451
+ accountId,
452
+ chatId: normalized.chatId,
453
+ messageId: normalized.messageId,
454
+ routeSessionKey: route.sessionKey,
455
+ storePath,
456
+ });
457
+ await channelRuntime.session.recordInboundSession({
458
+ storePath,
459
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
460
+ ctx: ctxPayload,
461
+ updateLastRoute: {
462
+ sessionKey: route.sessionKey,
463
+ channel: constants_1.CHANNEL_ID,
464
+ to: conversationRouteTarget,
465
+ accountId: route.accountId ?? accountId,
466
+ },
467
+ onRecordError: (err) => {
468
+ log?.info?.("clawgram failed to update group last route", {
469
+ accountId,
470
+ chatId: normalized.chatId,
471
+ messageId: normalized.messageId,
472
+ error: String(err),
473
+ });
474
+ },
475
+ });
476
+ const dispatchBase = (0, inbound_reply_dispatch_1.buildInboundReplyDispatchBase)({
477
+ cfg,
478
+ channel: "clawgram",
479
+ accountId: route.accountId ?? accountId,
480
+ route,
481
+ storePath,
482
+ ctxPayload,
483
+ core: { channel: channelRuntime },
484
+ });
485
+ const { onModelSelected, ...replyPipeline } = (0, channel_reply_pipeline_1.createChannelReplyPipeline)({
486
+ cfg,
487
+ agentId: route.agentId,
488
+ channel: "clawgram",
489
+ accountId: route.accountId ?? accountId,
490
+ });
491
+ const dispatchResult = await dispatchBase.dispatchReplyWithBufferedBlockDispatcher({
492
+ ctx: ctxPayload,
493
+ cfg,
494
+ dispatcherOptions: {
495
+ ...replyPipeline,
496
+ deliver: async (payload) => {
497
+ const outboundText = typeof payload.text === "string" ? payload.text.trim() : "";
498
+ log?.info?.("clawgram deliver group payload", {
499
+ accountId,
500
+ chatId: normalized.chatId,
501
+ messageId: normalized.messageId,
502
+ payloadText: outboundText,
503
+ payloadReplyToId: payload.replyToId ?? null,
504
+ });
505
+ if (!outboundText) {
506
+ return;
507
+ }
508
+ // The agent may decline to answer by returning the shared
509
+ // silent token. Drop it before addressing: otherwise the
510
+ // reply-address prefix turns it into a visible message.
511
+ const visibleText = (0, helpers_1.stripSilentReplyToken)(outboundText);
512
+ if (!visibleText) {
513
+ log?.info?.("clawgram suppressing silent group reply", {
514
+ accountId,
515
+ chatId: normalized.chatId,
516
+ messageId: normalized.messageId,
517
+ });
518
+ return;
519
+ }
520
+ const replyToMessageId = payload.replyToId ? Number(payload.replyToId) : Number(normalized.messageId);
521
+ const rememberedAddress = (0, group_reply_address_1.consumeGroupReplyAddress)({
522
+ accountId: route.accountId ?? accountId,
523
+ chatId: normalized.chatId,
524
+ replyToId: payload.replyToId ?? normalized.messageId,
525
+ });
526
+ await sendTextToConversation({
527
+ text: (0, helpers_1.prefixReplyTextToAddress)(visibleText, rememberedAddress ?? groupReplyAddress),
528
+ replyToMessageId,
529
+ messageThreadId,
530
+ });
531
+ },
532
+ onError: (err, info) => {
533
+ log?.error?.("clawgram failed to dispatch group reply", {
534
+ accountId,
535
+ chatId: normalized.chatId,
536
+ messageId: normalized.messageId,
537
+ kind: info.kind,
538
+ error: String(err),
539
+ });
540
+ },
541
+ },
542
+ replyOptions: {
543
+ onModelSelected,
544
+ },
545
+ });
546
+ log?.info?.("clawgram group dispatch completed", {
547
+ accountId,
548
+ chatId: normalized.chatId,
549
+ messageId: normalized.messageId,
550
+ queuedFinal: dispatchResult?.queuedFinal ?? null,
551
+ counts: dispatchResult?.counts ?? null,
552
+ });
553
+ const dispatchCounts = dispatchResult?.counts ?? { tool: 0, block: 0, final: 0 };
554
+ const nothingDelivered = dispatchResult?.queuedFinal !== true &&
555
+ (dispatchCounts.tool ?? 0) === 0 &&
556
+ (dispatchCounts.block ?? 0) === 0 &&
557
+ (dispatchCounts.final ?? 0) === 0;
558
+ if (nothingDelivered) {
559
+ const fallbackText = (0, helpers_1.readLatestAssistantFallbackFromTranscript)(route.sessionKey, storePath);
560
+ // A suppressed silent reply legitimately delivers nothing, so
561
+ // this fallback fires right after it. Without the same check
562
+ // the token would be read back from the transcript and sent.
563
+ const visibleFallbackText = fallbackText ? (0, helpers_1.stripSilentReplyToken)(fallbackText) : "";
564
+ if (fallbackText && !visibleFallbackText) {
565
+ log?.info?.("clawgram skipping silent transcript fallback", {
566
+ accountId,
567
+ chatId: normalized.chatId,
568
+ messageId: normalized.messageId,
569
+ routeSessionKey: route.sessionKey,
570
+ });
571
+ }
572
+ else if (visibleFallbackText) {
573
+ log?.warn?.("clawgram using transcript fallback reply", {
574
+ accountId,
575
+ chatId: normalized.chatId,
576
+ messageId: normalized.messageId,
577
+ routeSessionKey: route.sessionKey,
578
+ fallbackText: visibleFallbackText,
579
+ });
580
+ await sendTextToConversation({
581
+ text: (0, helpers_1.prefixReplyTextToAddress)(visibleFallbackText, groupReplyAddress),
582
+ replyToMessageId: Number(normalized.messageId),
583
+ messageThreadId,
584
+ });
585
+ }
586
+ else {
587
+ log?.warn?.("clawgram transcript fallback unavailable", {
588
+ accountId,
589
+ chatId: normalized.chatId,
590
+ messageId: normalized.messageId,
591
+ routeSessionKey: route.sessionKey,
592
+ });
593
+ }
594
+ }
595
+ }, {
596
+ readMessageId: Number(normalized.messageId),
597
+ messageThreadId,
598
+ });
599
+ log?.info?.("clawgram group inbound handled", {
600
+ accountId,
601
+ chatId: normalized.chatId,
602
+ messageId: normalized.messageId,
603
+ senderId,
604
+ senderLabel,
605
+ wasMentioned: mentionDecision.effectiveWasMentioned,
606
+ wasReplyToSelf,
607
+ });
608
+ return;
609
+ }
610
+ if (!(0, helpers_1.isSenderAllowed)({
611
+ allowFrom: directAllowFrom,
612
+ senderId,
613
+ senderUsername: normalized.senderUsername,
614
+ })) {
615
+ log?.info?.("clawgram direct allowFrom mismatch", {
616
+ accountId,
617
+ senderId,
618
+ senderUsername: normalized.senderUsername,
619
+ allowFrom: directAllowFrom,
620
+ });
621
+ return;
622
+ }
623
+ const access = await (0, direct_dm_1.resolveInboundDirectDmAccessWithRuntime)({
624
+ cfg,
625
+ channel: "clawgram",
626
+ accountId,
627
+ dmPolicy,
628
+ allowFrom: directAllowFrom,
629
+ senderId,
630
+ rawBody: text,
631
+ runtime: channelRuntime.commands,
632
+ isSenderAllowed: (_candidateSenderId, allowEntries) => (0, helpers_1.isSenderAllowed)({
633
+ allowFrom: allowEntries,
634
+ senderId,
635
+ senderUsername,
636
+ }),
637
+ readStoreAllowFrom: pairing.readStoreForDmPolicy,
638
+ });
639
+ if (access.access.decision === "block") {
640
+ log?.info?.("clawgram blocking inbound direct message", {
641
+ accountId,
642
+ chatId: normalized.chatId,
643
+ messageId: normalized.messageId,
644
+ senderId,
645
+ reason: access.access.reason,
646
+ reasonCode: access.access.reasonCode,
647
+ });
648
+ return;
649
+ }
650
+ if (access.access.decision === "pairing") {
651
+ await pairing.issueChallenge({
652
+ senderId,
653
+ senderIdLine: `Your Telegram user id: ${senderId}`,
654
+ meta: {
655
+ username: normalized.senderUsername,
656
+ name: normalized.senderDisplay,
657
+ },
658
+ sendPairingReply: async (pairingText) => {
659
+ await sendTextToConversation({
660
+ text: pairingText,
661
+ });
662
+ },
663
+ onReplyError: (err) => {
664
+ log?.info?.("clawgram pairing reply failed", {
665
+ accountId,
666
+ chatId: normalized.chatId,
667
+ senderId,
668
+ error: String(err),
669
+ });
670
+ },
671
+ });
672
+ log?.info?.("clawgram pairing required for inbound direct message", {
673
+ accountId,
674
+ chatId: normalized.chatId,
675
+ messageId: normalized.messageId,
676
+ senderId,
677
+ });
678
+ return;
679
+ }
680
+ await gram.withTyping(conversationTarget, async () => {
681
+ await (0, direct_dm_1.dispatchInboundDirectDmWithRuntime)({
682
+ cfg,
683
+ runtime: { channel: channelRuntime },
684
+ channel: "clawgram",
685
+ channelLabel: "Telegram",
686
+ accountId,
687
+ peer: {
688
+ kind: "direct",
689
+ id: senderId,
690
+ },
691
+ senderId,
692
+ senderAddress: `telegram:${senderId}`,
693
+ recipientAddress: selfId ? `telegram:${selfId}` : `telegram:${accountId}`,
694
+ conversationLabel: senderLabel,
695
+ rawBody: text,
696
+ messageId: normalized.messageId,
697
+ timestamp: normalized.timestamp,
698
+ commandAuthorized: access.commandAuthorized,
699
+ provider: "telegram",
700
+ surface: "clawgram",
701
+ originatingChannel: "clawgram",
702
+ originatingTo: senderId,
703
+ extraContext: {
704
+ SenderUsername: normalized.senderUsername,
705
+ SenderName: normalized.senderDisplay,
706
+ ReplyToId: normalized.replyToMessageId,
707
+ NativeChannelId: normalized.chatId,
708
+ },
709
+ deliver: async (payload) => {
710
+ const outboundText = typeof payload.text === "string" ? payload.text.trim() : "";
711
+ if (!outboundText) {
712
+ return;
713
+ }
714
+ const visibleText = (0, helpers_1.stripSilentReplyToken)(outboundText);
715
+ if (!visibleText) {
716
+ log?.info?.("clawgram suppressing silent direct reply", {
717
+ accountId,
718
+ chatId: normalized.chatId,
719
+ messageId: normalized.messageId,
720
+ });
721
+ return;
722
+ }
723
+ await sendTextToConversation({
724
+ text: visibleText,
725
+ replyToMessageId: payload.replyToId ? Number(payload.replyToId) : undefined,
726
+ });
727
+ },
728
+ onRecordError: (err) => {
729
+ log?.info?.("clawgram failed to record inbound session", {
730
+ accountId,
731
+ chatId: normalized.chatId,
732
+ messageId: normalized.messageId,
733
+ error: String(err),
734
+ });
735
+ },
736
+ onDispatchError: (err, info) => {
737
+ log?.info?.("clawgram failed to dispatch reply", {
738
+ accountId,
739
+ chatId: normalized.chatId,
740
+ messageId: normalized.messageId,
741
+ kind: info.kind,
742
+ error: String(err),
743
+ });
744
+ },
745
+ });
746
+ }, {
747
+ readMessageId: Number(normalized.messageId),
748
+ });
749
+ log?.info?.("clawgram inbound handled", {
750
+ accountId,
751
+ chatId: normalized.chatId,
752
+ messageId: normalized.messageId,
753
+ senderId,
754
+ senderLabel,
755
+ });
756
+ }
757
+ catch (error) {
758
+ const rawMessage = event?.message;
759
+ log?.error?.("clawgram inbound handling failed", {
760
+ accountId,
761
+ chatId: String(rawMessage?.chatId ?? rawMessage?.peerId?.userId ?? rawMessage?.peerId?.chatId ?? rawMessage?.peerId?.channelId ?? ""),
762
+ messageId: String(rawMessage?.id ?? ""),
763
+ error: String(error),
764
+ });
765
+ log?.info?.("clawgram inbound preflight failed", {
766
+ accountId,
767
+ chatId: String(rawMessage?.chatId ?? rawMessage?.peerId?.userId ?? rawMessage?.peerId?.chatId ?? rawMessage?.peerId?.channelId ?? ""),
768
+ messageId: String(rawMessage?.id ?? ""),
769
+ error: String(error),
770
+ });
771
+ }
772
+ };
773
+ client.addEventHandler(eventHandler, eventBuilder);
774
+ // Being added to a chat arrives as a service message, which `NewMessage`
775
+ // drops — so joins are observed on the raw update stream instead. Only
776
+ // additions of this account are journalled; who else joins is not ours
777
+ // to record.
778
+ const joinsJournalPath = (0, joins_1.resolveJoinsJournalPath)(account, accountId);
779
+ const joinEventHandler = async (update) => {
780
+ try {
781
+ const join = (0, joins_1.parseJoinEvent)(update?.message, selfId);
782
+ if (!join) {
783
+ return;
784
+ }
785
+ (0, joins_1.appendJoinRecord)(joinsJournalPath, join);
786
+ // Ids of people stay out of the log; the chat and the fact are enough
787
+ // to debug, and the journal itself holds the detail.
788
+ log?.info?.("clawgram join observed", {
789
+ accountId,
790
+ chatId: join.chatId,
791
+ via: join.via,
792
+ hasInviter: join.inviterId !== undefined,
793
+ });
794
+ }
795
+ catch (error) {
796
+ log?.warn?.("clawgram join observation failed", {
797
+ accountId,
798
+ error: String(error),
799
+ });
800
+ }
801
+ };
802
+ const joinEventBuilder = new events_1.Raw({});
803
+ client.addEventHandler(joinEventHandler, joinEventBuilder);
804
+ await (0, channel_runtime_1.waitUntilAbort)(ctx.abortSignal, async () => {
805
+ client.removeEventHandler(eventHandler, eventBuilder);
806
+ client.removeEventHandler(joinEventHandler, joinEventBuilder);
807
+ const runtime = runtimes.get(accountId);
808
+ if (!runtime) {
809
+ return;
810
+ }
811
+ await runtime.stop();
812
+ runtimes.delete(accountId);
813
+ console.info("clawgram disconnected", {
814
+ accountId,
815
+ selfLabel,
816
+ });
817
+ });
818
+ },
819
+ },
820
+ messaging: {
821
+ targetPrefixes: [constants_1.CHANNEL_ID, "tguserbot", "telegram", "tg"],
822
+ normalizeTarget(raw) {
823
+ const normalized = (0, helpers_1.normalizeOutboundTarget)(raw);
824
+ return normalized || undefined;
825
+ },
826
+ inferTargetChatType(params) {
827
+ const kind = (0, helpers_1.inferOutboundTargetKind)(params.to);
828
+ if (kind === "group" || kind === "channel") {
829
+ return kind;
830
+ }
831
+ if (kind === "user") {
832
+ return "direct";
833
+ }
834
+ return undefined;
835
+ },
836
+ targetResolver: {
837
+ looksLikeId(raw, normalized) {
838
+ const candidate = (normalized?.trim() || (0, helpers_1.normalizeOutboundTarget)(raw)).trim();
839
+ if (!candidate) {
840
+ return false;
841
+ }
842
+ if (candidate === "me" || candidate === "self" || candidate === "saved") {
843
+ return true;
844
+ }
845
+ if (candidate.startsWith("@")) {
846
+ return true;
847
+ }
848
+ return /^-?\d+$/.test(candidate);
849
+ },
850
+ async resolveTarget(params) {
851
+ const target = params.normalized?.trim() || (0, helpers_1.normalizeOutboundTarget)(params.input);
852
+ if (!target) {
853
+ return null;
854
+ }
855
+ const inferredKind = (0, helpers_1.inferOutboundTargetKind)(params.input, params.preferredKind);
856
+ const accountId = resolveRuntimeAccountId(params.cfg, params.accountId);
857
+ const gram = accountId ? runtimes.get(accountId) : undefined;
858
+ const resolved = gram ? await gram.resolvePeer(target, { kind: inferredKind }).catch(() => undefined) : undefined;
859
+ const kind = resolved?.chatType === "group" || inferredKind === "group"
860
+ ? "group"
861
+ : resolved?.chatType === "channel" || inferredKind === "channel"
862
+ ? "channel"
863
+ : "user";
864
+ return {
865
+ to: resolved?.chatId ?? target,
866
+ kind,
867
+ source: "normalized",
868
+ };
869
+ },
870
+ },
871
+ async resolveOutboundSessionRoute(params) {
872
+ const rawTarget = params.resolvedTarget?.to ?? params.target;
873
+ const targetKind = (0, helpers_1.inferOutboundTargetKind)(rawTarget, params.resolvedTarget?.kind);
874
+ const target = (0, helpers_1.normalizeOutboundTarget)(rawTarget);
875
+ if (!target) {
876
+ return null;
877
+ }
878
+ const accountId = resolveRuntimeAccountId(params.cfg, params.accountId);
879
+ const gram = accountId ? runtimes.get(accountId) : undefined;
880
+ const resolved = gram ? await gram.resolvePeer(target, { kind: targetKind }).catch(() => undefined) : undefined;
881
+ const peerId = resolved?.chatId ?? target;
882
+ const chatType = resolved?.chatType === "group" || targetKind === "group"
883
+ ? "group"
884
+ : resolved?.chatType === "channel" || targetKind === "channel"
885
+ ? "channel"
886
+ : "direct";
887
+ const scopedPeerId = chatType === "group" || chatType === "channel"
888
+ ? (0, helpers_1.buildScopedGroupPeerId)(accountId, peerId)
889
+ : peerId;
890
+ return (0, core_1.buildChannelOutboundSessionRoute)({
891
+ cfg: params.cfg,
892
+ agentId: params.agentId,
893
+ channel: constants_1.CHANNEL_ID,
894
+ accountId,
895
+ peer: {
896
+ kind: (0, helpers_1.routeKindFromChatType)(chatType),
897
+ id: scopedPeerId,
898
+ },
899
+ chatType,
900
+ from: accountId ?? "default",
901
+ to: target,
902
+ threadId: params.threadId ?? undefined,
903
+ });
904
+ },
905
+ formatTargetDisplay(params) {
906
+ const display = params.display?.trim();
907
+ if (display) {
908
+ return display;
909
+ }
910
+ const target = (0, helpers_1.normalizeOutboundTarget)(params.target);
911
+ return target.startsWith("@") ? target : `telegram:${target}`;
912
+ },
913
+ },
914
+ actions: {
915
+ describeMessageTool: ({ cfg, accountId }) => {
916
+ const resolvedAccountId = resolveRuntimeAccountId(cfg, accountId);
917
+ if (!resolvedAccountId) {
918
+ return null;
919
+ }
920
+ return {
921
+ actions: ["send", "read", "participants", "joins"],
922
+ capabilities: [],
923
+ };
924
+ },
925
+ extractToolSend: ({ args }) => (0, tool_send_1.extractToolSend)(args, "sendMessage"),
926
+ handleAction: async ({ action, params, cfg, accountId, dryRun, toolContext }) => {
927
+ // `read` is what OpenClaw core dispatches (`openclaw message read`,
928
+ // MCP `messages_read`). `list` is accepted as a synonym so a caller that
929
+ // guessed the other obvious name is not silently refused.
930
+ if (action === "read" || action === "list") {
931
+ const listParams = (0, history_1.parseListMessagesParams)(params);
932
+ const listAccountId = resolveRuntimeAccountId(cfg, accountId);
933
+ if (!listAccountId) {
934
+ throw new Error("clawgram: no configured account found");
935
+ }
936
+ // Reading is not a side effect, so a dry run still answers — reporting
937
+ // an empty window would look like a quiet chat rather than a no-op.
938
+ if (!(0, history_1.isChatReadable)(listParams.target, resolveAccountReadChats(cfg, listAccountId))) {
939
+ actionLog.warn("clawgram list refused: chat outside read scope", {
940
+ accountId: listAccountId,
941
+ target: listParams.target,
942
+ });
943
+ throw new Error(`clawgram: not-allowed-chat ${listParams.target}`);
944
+ }
945
+ const listGram = runtimes.get(listAccountId);
946
+ if (!listGram) {
947
+ throw new Error(`clawgram: runtime not found for account ${listAccountId}`);
948
+ }
949
+ const history = await listGram.listMessages(listParams);
950
+ // Metadata only. Message text is the user's correspondence and has no
951
+ // business in a log that is read while debugging something else.
952
+ actionLog.info("clawgram handleAction list completed", {
953
+ accountId: listAccountId,
954
+ target: listParams.target,
955
+ limit: listParams.limit,
956
+ since: listParams.since ?? null,
957
+ until: listParams.until ?? null,
958
+ returned: history.messages.length,
959
+ truncated: history.truncated,
960
+ });
961
+ return (0, core_1.jsonResult)({
962
+ ok: true,
963
+ accountId: listAccountId,
964
+ chatId: history.chatId ?? listParams.target,
965
+ count: history.messages.length,
966
+ truncated: history.truncated,
967
+ messages: history.messages,
968
+ });
969
+ }
970
+ // Membership is a read, so the same `readChats` scope that gates history
971
+ // gates it too: this cannot become a way to enumerate chats the account
972
+ // was never allowed to read.
973
+ if (action === "participants" || action === "members") {
974
+ const participantsParams = (0, history_1.parseListParticipantsParams)(params);
975
+ const participantsAccountId = resolveRuntimeAccountId(cfg, accountId);
976
+ if (!participantsAccountId) {
977
+ throw new Error("clawgram: no configured account found");
978
+ }
979
+ if (!(0, history_1.isChatReadable)(participantsParams.target, resolveAccountReadChats(cfg, participantsAccountId))) {
980
+ actionLog.warn("clawgram participants refused: chat outside read scope", {
981
+ accountId: participantsAccountId,
982
+ target: participantsParams.target,
983
+ });
984
+ throw new Error(`clawgram: not-allowed-chat ${participantsParams.target}`);
985
+ }
986
+ const participantsGram = runtimes.get(participantsAccountId);
987
+ if (!participantsGram) {
988
+ throw new Error(`clawgram: runtime not found for account ${participantsAccountId}`);
989
+ }
990
+ const membership = await participantsGram.listParticipants(participantsParams);
991
+ // Counts only. Member ids are personal data and have no business in a
992
+ // log that is read while debugging something else.
993
+ actionLog.info("clawgram handleAction participants completed", {
994
+ accountId: participantsAccountId,
995
+ target: participantsParams.target,
996
+ limit: participantsParams.limit,
997
+ returned: membership.participants.length,
998
+ truncated: membership.truncated,
999
+ });
1000
+ return (0, core_1.jsonResult)({
1001
+ ok: true,
1002
+ accountId: participantsAccountId,
1003
+ chatId: membership.chatId ?? participantsParams.target,
1004
+ count: membership.participants.length,
1005
+ truncated: membership.truncated,
1006
+ participants: membership.participants,
1007
+ });
1008
+ }
1009
+ // Where this account was recently added, and by whom. Reading the journal
1010
+ // has no scope check of its own: it only ever contains chats this account
1011
+ // was put into, which is exactly what the caller is allowed to learn.
1012
+ if (action === "joins") {
1013
+ const joinsParams = (0, joins_1.parseJoinsParams)(params);
1014
+ const joinsAccountId = resolveRuntimeAccountId(cfg, accountId);
1015
+ if (!joinsAccountId) {
1016
+ throw new Error("clawgram: no configured account found");
1017
+ }
1018
+ const journalPath = (0, joins_1.resolveJoinsJournalPath)(cfg?.channels?.["clawgram"]?.accounts?.[joinsAccountId], joinsAccountId);
1019
+ const selected = (0, joins_1.selectJoinRecords)((0, joins_1.readJoinRecords)(journalPath), joinsParams);
1020
+ actionLog.info("clawgram handleAction joins completed", {
1021
+ accountId: joinsAccountId,
1022
+ since: joinsParams.since ?? null,
1023
+ limit: joinsParams.limit,
1024
+ returned: selected.length,
1025
+ });
1026
+ return (0, core_1.jsonResult)({
1027
+ ok: true,
1028
+ accountId: joinsAccountId,
1029
+ count: selected.length,
1030
+ joins: selected,
1031
+ });
1032
+ }
1033
+ if (action !== "send") {
1034
+ throw new Error(`clawgram: unsupported message action ${action}`);
1035
+ }
1036
+ const rawTo = (0, helpers_1.resolveActionTarget)(params, toolContext);
1037
+ const targetKind = (0, helpers_1.inferOutboundTargetKind)(rawTo);
1038
+ const to = (0, helpers_1.normalizeOutboundTarget)(rawTo);
1039
+ const replyToId = (0, param_readers_1.readStringOrNumberParam)(params, "replyToId") ?? (0, param_readers_1.readStringOrNumberParam)(params, "replyTo");
1040
+ const threadId = (0, param_readers_1.readStringOrNumberParam)(params, "threadId");
1041
+ const messageThreadId = parseOptionalThreadId(threadId);
1042
+ actionLog.info("clawgram handleAction send", {
1043
+ requestedAccountId: accountId,
1044
+ dryRun: dryRun === true,
1045
+ rawTo,
1046
+ to,
1047
+ targetKind,
1048
+ replyToId: replyToId ?? null,
1049
+ threadId: threadId ?? null,
1050
+ toolContextCurrentChannelId: toolContext?.currentChannelId ?? null,
1051
+ });
1052
+ const resolvedAccountId = resolveRuntimeAccountId(cfg, accountId);
1053
+ if (!resolvedAccountId) {
1054
+ throw new Error("clawgram: no configured account found");
1055
+ }
1056
+ const currentChannelId = toolContext?.currentChannelId?.trim() ?? "";
1057
+ const currentMessageId = toolContext?.currentMessageId;
1058
+ const currentChannelTarget = currentChannelId ? (0, helpers_1.normalizeOutboundTarget)(currentChannelId) : "";
1059
+ const sendingToCurrentGroup = Boolean(currentChannelTarget &&
1060
+ currentChannelTarget === to &&
1061
+ targetKind === "group");
1062
+ if (sendingToCurrentGroup &&
1063
+ !replyToId &&
1064
+ currentMessageId !== null &&
1065
+ currentMessageId !== undefined &&
1066
+ (0, group_visible_reply_guard_1.hasRecentVisibleGroupReply)({
1067
+ accountId: resolvedAccountId,
1068
+ chatId: to,
1069
+ currentMessageId,
1070
+ })) {
1071
+ actionLog.warn("clawgram suppressing duplicate visible group reply", {
1072
+ accountId: resolvedAccountId,
1073
+ to,
1074
+ currentMessageId: String(currentMessageId),
1075
+ toolContextCurrentChannelId: currentChannelId || null,
1076
+ });
1077
+ return (0, core_1.jsonResult)({
1078
+ ok: true,
1079
+ suppressedDuplicate: true,
1080
+ to,
1081
+ accountId: resolvedAccountId,
1082
+ });
1083
+ }
1084
+ const groupReplyAddress = (0, group_reply_address_1.consumeGroupReplyAddress)({
1085
+ accountId: resolvedAccountId,
1086
+ chatId: to,
1087
+ replyToId,
1088
+ });
1089
+ const text = (0, helpers_1.prefixReplyTextToAddress)((0, helpers_1.readMessageText)(params).replaceAll("\\n", "\n"), groupReplyAddress);
1090
+ if (!text) {
1091
+ throw new Error("clawgram: message text is required");
1092
+ }
1093
+ if (dryRun) {
1094
+ return (0, core_1.jsonResult)({
1095
+ ok: true,
1096
+ dryRun: true,
1097
+ to,
1098
+ accountId: resolvedAccountId,
1099
+ });
1100
+ }
1101
+ const gram = runtimes.get(resolvedAccountId);
1102
+ if (!gram) {
1103
+ throw new Error(`clawgram: runtime not found for account ${resolvedAccountId}`);
1104
+ }
1105
+ const sent = await gram.sendText({
1106
+ target: to,
1107
+ text,
1108
+ targetKind,
1109
+ replyToMessageId: (0, helpers_1.resolveReplyToMessageIdForTarget)(rawTo, replyToId),
1110
+ messageThreadId,
1111
+ });
1112
+ if (sendingToCurrentGroup &&
1113
+ !replyToId &&
1114
+ currentMessageId !== null &&
1115
+ currentMessageId !== undefined) {
1116
+ (0, group_visible_reply_guard_1.rememberVisibleGroupReply)({
1117
+ accountId: resolvedAccountId,
1118
+ chatId: to,
1119
+ currentMessageId,
1120
+ });
1121
+ }
1122
+ actionLog.info("clawgram handleAction send completed", {
1123
+ accountId: resolvedAccountId,
1124
+ to,
1125
+ replyToId: replyToId ?? null,
1126
+ sentMessageId: String(sent?.id ?? ""),
1127
+ });
1128
+ return (0, core_1.jsonResult)({
1129
+ ok: true,
1130
+ to,
1131
+ accountId: resolvedAccountId,
1132
+ messageId: String(sent?.id ?? ""),
1133
+ });
1134
+ },
1135
+ },
1136
+ outbound: {
1137
+ async resolveTarget(ctx) {
1138
+ actionLog.info("clawgram outbound resolveTarget", {
1139
+ accountId: ctx.accountId,
1140
+ rawTo: ctx.to,
1141
+ });
1142
+ const gram = runtimes.get(ctx.accountId);
1143
+ if (!gram) {
1144
+ throw new Error(`clawgram: runtime not found for account ${ctx.accountId}`);
1145
+ }
1146
+ const targetKind = (0, helpers_1.inferOutboundTargetKind)(ctx.to);
1147
+ const target = (0, helpers_1.normalizeOutboundTarget)(ctx.to);
1148
+ return {
1149
+ ok: true,
1150
+ to: (await gram.resolvePeer(target, { kind: targetKind })).chatId ?? target,
1151
+ };
1152
+ },
1153
+ async sendText(ctx) {
1154
+ actionLog.info("clawgram outbound sendText", {
1155
+ accountId: ctx.accountId,
1156
+ rawTo: ctx.to,
1157
+ replyToId: ctx.replyToId ?? null,
1158
+ threadId: ctx.threadId ?? null,
1159
+ text: ctx.text,
1160
+ });
1161
+ const gram = runtimes.get(ctx.accountId);
1162
+ if (!gram) {
1163
+ throw new Error(`clawgram: runtime not found for account ${ctx.accountId}`);
1164
+ }
1165
+ const groupReplyAddress = (0, group_reply_address_1.consumeGroupReplyAddress)({
1166
+ accountId: ctx.accountId,
1167
+ chatId: ctx.to,
1168
+ replyToId: ctx.replyToId,
1169
+ });
1170
+ const targetKind = (0, helpers_1.inferOutboundTargetKind)(ctx.to);
1171
+ const target = (0, helpers_1.normalizeOutboundTarget)(ctx.to);
1172
+ const messageThreadId = parseOptionalThreadId(ctx.threadId);
1173
+ const sent = await gram.sendText({
1174
+ target,
1175
+ text: (0, helpers_1.prefixReplyTextToAddress)(ctx.text, groupReplyAddress),
1176
+ targetKind,
1177
+ replyToMessageId: (0, helpers_1.resolveReplyToMessageIdForTarget)(ctx.to, ctx.replyToId),
1178
+ messageThreadId,
1179
+ });
1180
+ actionLog.info("clawgram outbound sendText completed", {
1181
+ accountId: ctx.accountId,
1182
+ to: target,
1183
+ targetKind,
1184
+ replyToId: ctx.replyToId ?? null,
1185
+ sentMessageId: String(sent?.id ?? ""),
1186
+ });
1187
+ return {
1188
+ ok: true,
1189
+ messageId: String(sent?.id ?? ""),
1190
+ };
1191
+ },
1192
+ async sendMedia(ctx) {
1193
+ const gram = runtimes.get(ctx.accountId);
1194
+ if (!gram) {
1195
+ throw new Error(`clawgram: runtime not found for account ${ctx.accountId}`);
1196
+ }
1197
+ actionLog.info("clawgram outbound sendMedia", {
1198
+ accountId: ctx.accountId,
1199
+ to: ctx.to,
1200
+ replyToId: ctx.replyToId ?? null,
1201
+ threadId: ctx.threadId ?? null,
1202
+ filePath: ctx.filePath ?? null,
1203
+ mediaUrl: ctx.mediaUrl ?? null,
1204
+ hasText: Boolean(ctx.text),
1205
+ hasCaption: Boolean(ctx.caption),
1206
+ });
1207
+ const file = ctx.filePath ?? ctx.mediaUrl;
1208
+ if (!file) {
1209
+ throw new Error("clawgram: sendMedia requires filePath or mediaUrl");
1210
+ }
1211
+ const messageThreadId = parseOptionalThreadId(ctx.threadId);
1212
+ const sent = await gram.sendMedia({
1213
+ target: ctx.to,
1214
+ file,
1215
+ caption: ctx.caption ?? ctx.text,
1216
+ replyToMessageId: (0, helpers_1.resolveReplyToMessageIdForTarget)(ctx.to, ctx.replyToId),
1217
+ messageThreadId,
1218
+ });
1219
+ actionLog.info("clawgram outbound sendMedia completed", {
1220
+ accountId: ctx.accountId,
1221
+ to: ctx.to,
1222
+ replyToId: ctx.replyToId ?? null,
1223
+ sentMessageId: String(sent?.id ?? ""),
1224
+ });
1225
+ return {
1226
+ ok: true,
1227
+ messageId: String(sent?.id ?? ""),
1228
+ };
1229
+ },
1230
+ },
1231
+ };
1232
+ };
1233
+ exports.createChannelPlugin = createChannelPlugin;