@nextclaw/channel-extension-telegram 0.1.1-beta.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.
- package/LICENSE +21 -0
- package/dist/controllers/telegram-stream-preview.controller.js +198 -0
- package/dist/controllers/telegram-stream-preview.controller.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -0
- package/dist/providers/groq-transcription.provider.js +29 -0
- package/dist/providers/groq-transcription.provider.js.map +1 -0
- package/dist/services/telegram-channel.service.js +344 -0
- package/dist/services/telegram-channel.service.js.map +1 -0
- package/dist/utils/telegram-message.utils.js +138 -0
- package/dist/utils/telegram-message.utils.js.map +1 -0
- package/nextclaw.extension.json +42 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NextClaw contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { TELEGRAM_TEXT_LIMIT, markdownToTelegramHtml, readReplyToMessageId } from "../utils/telegram-message.utils.js";
|
|
2
|
+
//#region src/controllers/telegram-stream-preview.controller.ts
|
|
3
|
+
const STREAM_PREVIEW_MIN_CHARS = 30;
|
|
4
|
+
const STREAM_PREVIEW_PARTIAL_MIN_INTERVAL_MS = 700;
|
|
5
|
+
const STREAM_PREVIEW_BLOCK_MIN_INTERVAL_MS = 1200;
|
|
6
|
+
const STREAM_PREVIEW_BLOCK_MIN_GROWTH = 120;
|
|
7
|
+
var TelegramStreamPreviewController = class {
|
|
8
|
+
states = /* @__PURE__ */ new Map();
|
|
9
|
+
constructor(params) {
|
|
10
|
+
this.params = params;
|
|
11
|
+
}
|
|
12
|
+
handleReset = async (msg) => {
|
|
13
|
+
const chatId = String(msg.chatId);
|
|
14
|
+
this.dispose(chatId);
|
|
15
|
+
if (this.params.resolveMode() === "off") return;
|
|
16
|
+
const replyToMessageId = readReplyToMessageId(msg.metadata);
|
|
17
|
+
this.states.set(chatId, {
|
|
18
|
+
chatId,
|
|
19
|
+
rawText: "",
|
|
20
|
+
lastRenderedText: "",
|
|
21
|
+
messageId: void 0,
|
|
22
|
+
replyToMessageId,
|
|
23
|
+
silent: msg.metadata?.silent === true,
|
|
24
|
+
lastSentAt: 0,
|
|
25
|
+
lastEmittedChars: 0,
|
|
26
|
+
inFlight: false,
|
|
27
|
+
pending: false,
|
|
28
|
+
timer: null
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
handleDelta = async (msg, delta) => {
|
|
32
|
+
if (!delta || this.params.resolveMode() === "off") return;
|
|
33
|
+
const chatId = String(msg.chatId);
|
|
34
|
+
const state = this.ensureState(chatId);
|
|
35
|
+
state.rawText += delta;
|
|
36
|
+
this.scheduleFlush(state);
|
|
37
|
+
};
|
|
38
|
+
finalizeWithFinalMessage = async (msg) => {
|
|
39
|
+
if (this.params.resolveMode() === "off") return false;
|
|
40
|
+
const chatId = String(msg.chatId);
|
|
41
|
+
const state = this.states.get(chatId);
|
|
42
|
+
if (!state) return false;
|
|
43
|
+
state.rawText = msg.content ?? "";
|
|
44
|
+
state.silent = msg.metadata?.silent === true || state.silent;
|
|
45
|
+
if (!state.rawText.trim()) {
|
|
46
|
+
this.dispose(chatId);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const handled = await this.flushNow(state, {
|
|
50
|
+
force: true,
|
|
51
|
+
allowInitialBelowThreshold: true,
|
|
52
|
+
replyToMessageId: msg.replyTo ? Number(msg.replyTo) : state.replyToMessageId,
|
|
53
|
+
silent: state.silent
|
|
54
|
+
});
|
|
55
|
+
this.dispose(chatId);
|
|
56
|
+
return handled;
|
|
57
|
+
};
|
|
58
|
+
stopAll = () => {
|
|
59
|
+
for (const chatId of this.states.keys()) this.dispose(chatId);
|
|
60
|
+
};
|
|
61
|
+
ensureState = (chatId) => {
|
|
62
|
+
const existing = this.states.get(chatId);
|
|
63
|
+
if (existing) return existing;
|
|
64
|
+
const created = createStreamState(chatId);
|
|
65
|
+
this.states.set(chatId, created);
|
|
66
|
+
return created;
|
|
67
|
+
};
|
|
68
|
+
scheduleFlush = (state) => {
|
|
69
|
+
if (state.timer) return;
|
|
70
|
+
const minInterval = this.params.resolveMode() === "block" ? STREAM_PREVIEW_BLOCK_MIN_INTERVAL_MS : STREAM_PREVIEW_PARTIAL_MIN_INTERVAL_MS;
|
|
71
|
+
const delay = Math.max(0, minInterval - (Date.now() - state.lastSentAt));
|
|
72
|
+
state.timer = setTimeout(() => {
|
|
73
|
+
state.timer = null;
|
|
74
|
+
this.flushScheduled(state);
|
|
75
|
+
}, delay);
|
|
76
|
+
};
|
|
77
|
+
flushScheduled = async (state) => {
|
|
78
|
+
if (this.states.get(state.chatId) !== state) return;
|
|
79
|
+
if (state.inFlight) {
|
|
80
|
+
state.pending = true;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
state.inFlight = true;
|
|
84
|
+
try {
|
|
85
|
+
await this.flushNow(state, {
|
|
86
|
+
force: false,
|
|
87
|
+
allowInitialBelowThreshold: false,
|
|
88
|
+
replyToMessageId: state.replyToMessageId,
|
|
89
|
+
silent: state.silent
|
|
90
|
+
});
|
|
91
|
+
} finally {
|
|
92
|
+
this.finishScheduledFlush(state);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
finishScheduledFlush = (state) => {
|
|
96
|
+
state.inFlight = false;
|
|
97
|
+
if (state.pending) {
|
|
98
|
+
state.pending = false;
|
|
99
|
+
this.scheduleFlush(state);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
flushNow = async (state, opts) => {
|
|
103
|
+
const bot = this.params.getBot();
|
|
104
|
+
const payload = this.resolvePayload(state, opts);
|
|
105
|
+
if (!bot || !payload) return false;
|
|
106
|
+
if (typeof state.messageId === "number") return this.editExistingPreview(bot, state, payload);
|
|
107
|
+
return this.sendNewPreview(bot, state, payload, opts);
|
|
108
|
+
};
|
|
109
|
+
resolvePayload = (state, opts) => {
|
|
110
|
+
const plainText = state.rawText.trimEnd();
|
|
111
|
+
if (!this.shouldEmit(state, plainText, opts)) return null;
|
|
112
|
+
const renderedText = markdownToTelegramHtml(plainText).trimEnd().slice(0, TELEGRAM_TEXT_LIMIT);
|
|
113
|
+
if (!renderedText || renderedText === state.lastRenderedText) return null;
|
|
114
|
+
return {
|
|
115
|
+
plainText,
|
|
116
|
+
renderedText
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
shouldEmit = (state, plainText, opts) => {
|
|
120
|
+
if (!plainText) return false;
|
|
121
|
+
if (this.params.resolveMode() === "block" && !opts.force && plainText.length - state.lastEmittedChars < STREAM_PREVIEW_BLOCK_MIN_GROWTH) return false;
|
|
122
|
+
if (typeof state.messageId === "number" || opts.allowInitialBelowThreshold) return true;
|
|
123
|
+
return plainText.length >= STREAM_PREVIEW_MIN_CHARS;
|
|
124
|
+
};
|
|
125
|
+
editExistingPreview = async (bot, state, payload) => {
|
|
126
|
+
try {
|
|
127
|
+
await bot.editMessageText(payload.renderedText, {
|
|
128
|
+
chat_id: Number(state.chatId),
|
|
129
|
+
message_id: state.messageId,
|
|
130
|
+
parse_mode: "HTML"
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
try {
|
|
134
|
+
await bot.editMessageText(payload.plainText.slice(0, TELEGRAM_TEXT_LIMIT), {
|
|
135
|
+
chat_id: Number(state.chatId),
|
|
136
|
+
message_id: state.messageId
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
this.recordPayload(state, payload);
|
|
143
|
+
return true;
|
|
144
|
+
};
|
|
145
|
+
sendNewPreview = async (bot, state, payload, opts) => {
|
|
146
|
+
const sent = await this.sendPreviewMessage(bot, state.chatId, payload, opts);
|
|
147
|
+
if (typeof sent.message_id === "number") state.messageId = sent.message_id;
|
|
148
|
+
this.recordPayload(state, payload);
|
|
149
|
+
return true;
|
|
150
|
+
};
|
|
151
|
+
sendPreviewMessage = async (bot, chatId, payload, opts) => {
|
|
152
|
+
try {
|
|
153
|
+
return await bot.sendMessage(Number(chatId), payload.renderedText, {
|
|
154
|
+
parse_mode: "HTML",
|
|
155
|
+
...opts.replyToMessageId ? { reply_to_message_id: opts.replyToMessageId } : {},
|
|
156
|
+
...opts.silent ? { disable_notification: true } : {}
|
|
157
|
+
});
|
|
158
|
+
} catch {
|
|
159
|
+
return bot.sendMessage(Number(chatId), payload.plainText.slice(0, TELEGRAM_TEXT_LIMIT), {
|
|
160
|
+
...opts.replyToMessageId ? { reply_to_message_id: opts.replyToMessageId } : {},
|
|
161
|
+
...opts.silent ? { disable_notification: true } : {}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
recordPayload = (state, payload) => {
|
|
166
|
+
state.lastRenderedText = payload.renderedText;
|
|
167
|
+
state.lastSentAt = Date.now();
|
|
168
|
+
state.lastEmittedChars = payload.plainText.length;
|
|
169
|
+
};
|
|
170
|
+
dispose = (chatId) => {
|
|
171
|
+
const state = this.states.get(chatId);
|
|
172
|
+
if (!state) return;
|
|
173
|
+
if (state.timer) {
|
|
174
|
+
clearTimeout(state.timer);
|
|
175
|
+
state.timer = null;
|
|
176
|
+
}
|
|
177
|
+
this.states.delete(chatId);
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
function createStreamState(chatId) {
|
|
181
|
+
return {
|
|
182
|
+
chatId,
|
|
183
|
+
rawText: "",
|
|
184
|
+
lastRenderedText: "",
|
|
185
|
+
messageId: void 0,
|
|
186
|
+
replyToMessageId: void 0,
|
|
187
|
+
silent: false,
|
|
188
|
+
lastSentAt: 0,
|
|
189
|
+
lastEmittedChars: 0,
|
|
190
|
+
inFlight: false,
|
|
191
|
+
pending: false,
|
|
192
|
+
timer: null
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
export { TelegramStreamPreviewController };
|
|
197
|
+
|
|
198
|
+
//# sourceMappingURL=telegram-stream-preview.controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram-stream-preview.controller.js","names":[],"sources":["../../src/controllers/telegram-stream-preview.controller.ts"],"sourcesContent":["import type TelegramBot from \"node-telegram-bot-api\";\nimport type { OutboundMessage } from \"@nextclaw/core\";\nimport {\n TELEGRAM_TEXT_LIMIT,\n markdownToTelegramHtml,\n readReplyToMessageId,\n type TelegramStreamingMode\n} from \"../utils/telegram-message.utils.js\";\n\nconst STREAM_PREVIEW_MIN_CHARS = 30;\nconst STREAM_PREVIEW_PARTIAL_MIN_INTERVAL_MS = 700;\nconst STREAM_PREVIEW_BLOCK_MIN_INTERVAL_MS = 1200;\nconst STREAM_PREVIEW_BLOCK_MIN_GROWTH = 120;\n\ntype TelegramStreamState = {\n chatId: string;\n rawText: string;\n lastRenderedText: string;\n messageId?: number;\n replyToMessageId?: number;\n silent: boolean;\n lastSentAt: number;\n lastEmittedChars: number;\n inFlight: boolean;\n pending: boolean;\n timer: ReturnType<typeof setTimeout> | null;\n};\n\ntype PreviewPayload = {\n plainText: string;\n renderedText: string;\n};\n\nexport class TelegramStreamPreviewController {\n private readonly states = new Map<string, TelegramStreamState>();\n\n constructor(private readonly params: {\n resolveMode: () => TelegramStreamingMode;\n getBot: () => TelegramBot | null;\n }) { }\n\n handleReset = async (msg: OutboundMessage): Promise<void> => {\n const chatId = String(msg.chatId);\n this.dispose(chatId);\n if (this.params.resolveMode() === \"off\") {\n return;\n }\n const replyToMessageId = readReplyToMessageId(msg.metadata);\n this.states.set(chatId, {\n chatId,\n rawText: \"\",\n lastRenderedText: \"\",\n messageId: undefined,\n replyToMessageId,\n silent: msg.metadata?.silent === true,\n lastSentAt: 0,\n lastEmittedChars: 0,\n inFlight: false,\n pending: false,\n timer: null\n });\n };\n\n handleDelta = async (msg: OutboundMessage, delta: string): Promise<void> => {\n if (!delta || this.params.resolveMode() === \"off\") {\n return;\n }\n const chatId = String(msg.chatId);\n const state = this.ensureState(chatId);\n state.rawText += delta;\n this.scheduleFlush(state);\n };\n\n finalizeWithFinalMessage = async (msg: OutboundMessage): Promise<boolean> => {\n if (this.params.resolveMode() === \"off\") {\n return false;\n }\n const chatId = String(msg.chatId);\n const state = this.states.get(chatId);\n if (!state) {\n return false;\n }\n state.rawText = msg.content ?? \"\";\n state.silent = msg.metadata?.silent === true || state.silent;\n if (!state.rawText.trim()) {\n this.dispose(chatId);\n return false;\n }\n const handled = await this.flushNow(state, {\n force: true,\n allowInitialBelowThreshold: true,\n replyToMessageId: msg.replyTo ? Number(msg.replyTo) : state.replyToMessageId,\n silent: state.silent\n });\n this.dispose(chatId);\n return handled;\n };\n\n stopAll = (): void => {\n for (const chatId of this.states.keys()) {\n this.dispose(chatId);\n }\n };\n\n private ensureState = (chatId: string): TelegramStreamState => {\n const existing = this.states.get(chatId);\n if (existing) {\n return existing;\n }\n const created = createStreamState(chatId);\n this.states.set(chatId, created);\n return created;\n };\n\n private scheduleFlush = (state: TelegramStreamState): void => {\n if (state.timer) {\n return;\n }\n const minInterval = this.params.resolveMode() === \"block\"\n ? STREAM_PREVIEW_BLOCK_MIN_INTERVAL_MS\n : STREAM_PREVIEW_PARTIAL_MIN_INTERVAL_MS;\n const delay = Math.max(0, minInterval - (Date.now() - state.lastSentAt));\n state.timer = setTimeout(() => {\n state.timer = null;\n void this.flushScheduled(state);\n }, delay);\n };\n\n private flushScheduled = async (state: TelegramStreamState): Promise<void> => {\n const current = this.states.get(state.chatId);\n if (current !== state) {\n return;\n }\n if (state.inFlight) {\n state.pending = true;\n return;\n }\n state.inFlight = true;\n try {\n await this.flushNow(state, {\n force: false,\n allowInitialBelowThreshold: false,\n replyToMessageId: state.replyToMessageId,\n silent: state.silent\n });\n }\n finally {\n this.finishScheduledFlush(state);\n }\n };\n\n private finishScheduledFlush = (state: TelegramStreamState): void => {\n state.inFlight = false;\n if (state.pending) {\n state.pending = false;\n this.scheduleFlush(state);\n }\n };\n\n private flushNow = async (state: TelegramStreamState, opts: {\n force: boolean;\n allowInitialBelowThreshold: boolean;\n replyToMessageId?: number;\n silent: boolean;\n }): Promise<boolean> => {\n const bot = this.params.getBot();\n const payload = this.resolvePayload(state, opts);\n if (!bot || !payload) {\n return false;\n }\n if (typeof state.messageId === \"number\") {\n return this.editExistingPreview(bot, state, payload);\n }\n return this.sendNewPreview(bot, state, payload, opts);\n };\n\n private resolvePayload = (state: TelegramStreamState, opts: {\n force: boolean;\n allowInitialBelowThreshold: boolean;\n }): PreviewPayload | null => {\n const plainText = state.rawText.trimEnd();\n if (!this.shouldEmit(state, plainText, opts)) {\n return null;\n }\n const renderedText = markdownToTelegramHtml(plainText).trimEnd().slice(0, TELEGRAM_TEXT_LIMIT);\n if (!renderedText || renderedText === state.lastRenderedText) {\n return null;\n }\n return { plainText, renderedText };\n };\n\n private shouldEmit = (state: TelegramStreamState, plainText: string, opts: {\n force: boolean;\n allowInitialBelowThreshold: boolean;\n }): boolean => {\n if (!plainText) {\n return false;\n }\n const isBlockGrowthTooSmall = this.params.resolveMode() === \"block\" &&\n !opts.force &&\n plainText.length - state.lastEmittedChars < STREAM_PREVIEW_BLOCK_MIN_GROWTH;\n if (isBlockGrowthTooSmall) {\n return false;\n }\n if (typeof state.messageId === \"number\" || opts.allowInitialBelowThreshold) {\n return true;\n }\n return plainText.length >= STREAM_PREVIEW_MIN_CHARS;\n };\n\n private editExistingPreview = async (\n bot: TelegramBot,\n state: TelegramStreamState,\n payload: PreviewPayload\n ): Promise<boolean> => {\n try {\n await bot.editMessageText(payload.renderedText, {\n chat_id: Number(state.chatId),\n message_id: state.messageId,\n parse_mode: \"HTML\"\n });\n }\n catch {\n try {\n await bot.editMessageText(payload.plainText.slice(0, TELEGRAM_TEXT_LIMIT), {\n chat_id: Number(state.chatId),\n message_id: state.messageId\n });\n }\n catch {\n return false;\n }\n }\n this.recordPayload(state, payload);\n return true;\n };\n\n private sendNewPreview = async (\n bot: TelegramBot,\n state: TelegramStreamState,\n payload: PreviewPayload,\n opts: {\n replyToMessageId?: number;\n silent: boolean;\n }\n ): Promise<boolean> => {\n const sent = await this.sendPreviewMessage(bot, state.chatId, payload, opts);\n if (typeof sent.message_id === \"number\") {\n state.messageId = sent.message_id;\n }\n this.recordPayload(state, payload);\n return true;\n };\n\n private sendPreviewMessage = async (\n bot: TelegramBot,\n chatId: string,\n payload: PreviewPayload,\n opts: {\n replyToMessageId?: number;\n silent: boolean;\n }\n ): Promise<MessageWithId> => {\n try {\n return await bot.sendMessage(Number(chatId), payload.renderedText, {\n parse_mode: \"HTML\",\n ...(opts.replyToMessageId ? { reply_to_message_id: opts.replyToMessageId } : {}),\n ...(opts.silent ? { disable_notification: true } : {})\n });\n }\n catch {\n return bot.sendMessage(Number(chatId), payload.plainText.slice(0, TELEGRAM_TEXT_LIMIT), {\n ...(opts.replyToMessageId ? { reply_to_message_id: opts.replyToMessageId } : {}),\n ...(opts.silent ? { disable_notification: true } : {})\n });\n }\n };\n\n private recordPayload = (state: TelegramStreamState, payload: PreviewPayload): void => {\n state.lastRenderedText = payload.renderedText;\n state.lastSentAt = Date.now();\n state.lastEmittedChars = payload.plainText.length;\n };\n\n private dispose = (chatId: string): void => {\n const state = this.states.get(chatId);\n if (!state) {\n return;\n }\n if (state.timer) {\n clearTimeout(state.timer);\n state.timer = null;\n }\n this.states.delete(chatId);\n };\n}\n\ntype MessageWithId = {\n message_id?: number;\n};\n\nfunction createStreamState(chatId: string): TelegramStreamState {\n return {\n chatId,\n rawText: \"\",\n lastRenderedText: \"\",\n messageId: undefined,\n replyToMessageId: undefined,\n silent: false,\n lastSentAt: 0,\n lastEmittedChars: 0,\n inFlight: false,\n pending: false,\n timer: null\n };\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TelegramChannel } from "./services/telegram-channel.service.js";
|
|
2
|
+
import { startBusChannelExtension, warnNcpEventError } from "@nextclaw/extension-sdk";
|
|
3
|
+
//#region src/main.ts
|
|
4
|
+
await startBusChannelExtension({
|
|
5
|
+
channelId: "telegram",
|
|
6
|
+
createChannel: ({ config, bus, channel }) => new TelegramChannel(config, bus, channel.commands),
|
|
7
|
+
onChannelStartError: warnNcpEventError("telegram")
|
|
8
|
+
});
|
|
9
|
+
//#endregion
|
|
10
|
+
export {};
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","names":[],"sources":["../src/main.ts"],"sourcesContent":["import type { Config, MessageBus } from \"@nextclaw/core\";\nimport { startBusChannelExtension, warnNcpEventError } from \"@nextclaw/extension-sdk\";\nimport { TelegramChannel } from \"./services/telegram-channel.service.js\";\n\nawait startBusChannelExtension<Config[\"channels\"][\"telegram\"], MessageBus>({\n channelId: \"telegram\",\n createChannel: ({ config, bus, channel }) => new TelegramChannel(config, bus, channel.commands),\n onChannelStartError: warnNcpEventError(\"telegram\"),\n});\n"],"mappings":";;;AAIA,MAAM,yBAAqE;CACzE,WAAW;CACX,gBAAgB,EAAE,QAAQ,KAAK,cAAc,IAAI,gBAAgB,QAAQ,KAAK,QAAQ,SAAS;CAC/F,qBAAqB,kBAAkB,WAAW;CACnD,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createReadStream, existsSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { FormData, fetch } from "undici";
|
|
4
|
+
//#region src/providers/groq-transcription.provider.ts
|
|
5
|
+
var GroqTranscriptionProvider = class {
|
|
6
|
+
apiKey;
|
|
7
|
+
apiUrl = "https://api.groq.com/openai/v1/audio/transcriptions";
|
|
8
|
+
constructor(apiKey) {
|
|
9
|
+
this.apiKey = apiKey ?? process.env.GROQ_API_KEY ?? null;
|
|
10
|
+
}
|
|
11
|
+
transcribe = async (filePath) => {
|
|
12
|
+
if (!this.apiKey) return "";
|
|
13
|
+
if (!existsSync(filePath)) return "";
|
|
14
|
+
const form = new FormData();
|
|
15
|
+
form.append("file", createReadStream(filePath), basename(filePath));
|
|
16
|
+
form.append("model", "whisper-large-v3");
|
|
17
|
+
const response = await fetch(this.apiUrl, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
20
|
+
body: form
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) return "";
|
|
23
|
+
return (await response.json()).text ?? "";
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { GroqTranscriptionProvider };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=groq-transcription.provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"groq-transcription.provider.js","names":[],"sources":["../../src/providers/groq-transcription.provider.ts"],"sourcesContent":["import { createReadStream, existsSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport { FormData, fetch } from \"undici\";\nexport class GroqTranscriptionProvider {\n private apiKey?: string | null;\n private apiUrl = \"https://api.groq.com/openai/v1/audio/transcriptions\";\n constructor(apiKey?: string | null) {\n this.apiKey = apiKey ?? process.env.GROQ_API_KEY ?? null;\n }\n transcribe = async (filePath: string): Promise<string> => {\n if (!this.apiKey) {\n return \"\";\n }\n if (!existsSync(filePath)) {\n return \"\";\n }\n const form = new FormData();\n form.append(\"file\", createReadStream(filePath), basename(filePath));\n form.append(\"model\", \"whisper-large-v3\");\n const response = await fetch(this.apiUrl, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`\n },\n body: form\n });\n if (!response.ok) {\n return \"\";\n }\n const data = (await response.json()) as {\n text?: string;\n };\n return data.text ?? \"\";\n };\n}\n\n"],"mappings":";;;;AAGA,IAAa,4BAAb,MAAuC;CACnC;CACA,SAAiB;CACjB,YAAY,QAAwB;AAChC,OAAK,SAAS,UAAU,QAAQ,IAAI,gBAAgB;;CAExD,aAAa,OAAO,aAAsC;AACtD,MAAI,CAAC,KAAK,OACN,QAAO;AAEX,MAAI,CAAC,WAAW,SAAS,CACrB,QAAO;EAEX,MAAM,OAAO,IAAI,UAAU;AAC3B,OAAK,OAAO,QAAQ,iBAAiB,SAAS,EAAE,SAAS,SAAS,CAAC;AACnE,OAAK,OAAO,SAAS,mBAAmB;EACxC,MAAM,WAAW,MAAM,MAAM,KAAK,QAAQ;GACtC,QAAQ;GACR,SAAS,EACL,eAAe,UAAU,KAAK,UACjC;GACD,MAAM;GACT,CAAC;AACF,MAAI,CAAC,SAAS,GACV,QAAO;AAKX,UAHc,MAAM,SAAS,MAAM,EAGvB,QAAQ"}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { GroqTranscriptionProvider } from "../providers/groq-transcription.provider.js";
|
|
2
|
+
import { getExtension, inferMediaMimeType, isLocalTelegramCommand, markdownToTelegramHtml, resolveMedia, resolveSender, resolveTelegramStreamingMode, shouldSendAckReaction, toTelegramReaction } from "../utils/telegram-message.utils.js";
|
|
3
|
+
import { TelegramStreamPreviewController } from "../controllers/telegram-stream-preview.controller.js";
|
|
4
|
+
import { ChannelTypingController } from "@nextclaw/extension-sdk";
|
|
5
|
+
import TelegramBot from "node-telegram-bot-api";
|
|
6
|
+
import { APP_NAME, BaseChannel, getDataPath, isAssistantStreamResetControlMessage, isTypingStopControlMessage, readAssistantStreamDelta } from "@nextclaw/core";
|
|
7
|
+
import { mkdirSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
//#region src/services/telegram-channel.service.ts
|
|
10
|
+
const TYPING_HEARTBEAT_MS = 6e3;
|
|
11
|
+
const TYPING_AUTO_STOP_MS = 12e4;
|
|
12
|
+
const BOT_COMMANDS = [
|
|
13
|
+
{
|
|
14
|
+
command: "start",
|
|
15
|
+
description: "Start the bot"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
command: "reset",
|
|
19
|
+
description: "Reset conversation history"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
command: "help",
|
|
23
|
+
description: "Show available commands"
|
|
24
|
+
}
|
|
25
|
+
];
|
|
26
|
+
var TelegramChannel = class extends BaseChannel {
|
|
27
|
+
name = "telegram";
|
|
28
|
+
bot = null;
|
|
29
|
+
botUserId = null;
|
|
30
|
+
botUsername = null;
|
|
31
|
+
typingController;
|
|
32
|
+
streamPreview;
|
|
33
|
+
transcriber;
|
|
34
|
+
constructor(config, bus, commands, groqApiKey) {
|
|
35
|
+
super(config, bus);
|
|
36
|
+
this.commands = commands;
|
|
37
|
+
this.transcriber = new GroqTranscriptionProvider(groqApiKey ?? null);
|
|
38
|
+
this.typingController = new ChannelTypingController({
|
|
39
|
+
heartbeatMs: TYPING_HEARTBEAT_MS,
|
|
40
|
+
autoStopMs: TYPING_AUTO_STOP_MS,
|
|
41
|
+
sendTyping: async (chatId) => {
|
|
42
|
+
await this.bot?.sendChatAction(Number(chatId), "typing");
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
this.streamPreview = new TelegramStreamPreviewController({
|
|
46
|
+
resolveMode: () => resolveTelegramStreamingMode(this.config),
|
|
47
|
+
getBot: () => this.bot
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
start = async () => {
|
|
51
|
+
if (!this.config.token) throw new Error("Telegram bot token not configured");
|
|
52
|
+
this.running = true;
|
|
53
|
+
const options = { polling: true };
|
|
54
|
+
if (this.config.proxy) options.request = { proxy: this.config.proxy };
|
|
55
|
+
this.bot = new TelegramBot(this.config.token, options);
|
|
56
|
+
try {
|
|
57
|
+
const me = await this.bot.getMe();
|
|
58
|
+
this.botUserId = me.id;
|
|
59
|
+
this.botUsername = me.username ?? null;
|
|
60
|
+
} catch {
|
|
61
|
+
this.botUserId = null;
|
|
62
|
+
this.botUsername = null;
|
|
63
|
+
}
|
|
64
|
+
this.bot.onText(/^\/start$/, async (msg) => {
|
|
65
|
+
await this.bot?.sendMessage(msg.chat.id, `👋 Hi ${msg.from?.first_name ?? ""}! I'm ${APP_NAME}.\n\nSend me a message and I'll respond!\nType /help to see available commands.`);
|
|
66
|
+
});
|
|
67
|
+
this.bot.onText(/^\/help$/, async (msg) => {
|
|
68
|
+
const helpText = `🤖 <b>${APP_NAME} commands</b>\n\n/start — Start the bot
|
|
69
|
+
/reset — Reset conversation history
|
|
70
|
+
/help — Show this help message
|
|
71
|
+
|
|
72
|
+
Just send me a text message to chat!`;
|
|
73
|
+
await this.bot?.sendMessage(msg.chat.id, helpText, { parse_mode: "HTML" });
|
|
74
|
+
});
|
|
75
|
+
this.bot.on("message", async (msg) => {
|
|
76
|
+
if (!msg.text && !msg.caption && !msg.photo && !msg.voice && !msg.audio && !msg.document) return;
|
|
77
|
+
if (msg.text?.startsWith("/")) {
|
|
78
|
+
if (!isLocalTelegramCommand(msg.text)) await this.handleTextCommand(msg);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await this.handleIncoming(msg);
|
|
82
|
+
});
|
|
83
|
+
this.bot.on("channel_post", async (msg) => {
|
|
84
|
+
if (!msg.text && !msg.caption && !msg.photo && !msg.voice && !msg.audio && !msg.document) return;
|
|
85
|
+
if (msg.text?.startsWith("/")) {
|
|
86
|
+
if (!isLocalTelegramCommand(msg.text)) await this.handleTextCommand(msg);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await this.handleIncoming(msg);
|
|
90
|
+
});
|
|
91
|
+
await this.bot.setMyCommands(BOT_COMMANDS);
|
|
92
|
+
};
|
|
93
|
+
stop = async () => {
|
|
94
|
+
this.running = false;
|
|
95
|
+
this.typingController.stopAll();
|
|
96
|
+
this.streamPreview.stopAll();
|
|
97
|
+
if (this.bot) {
|
|
98
|
+
await this.bot.stopPolling();
|
|
99
|
+
this.bot = null;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
handleControlMessage = async (msg) => {
|
|
103
|
+
if (isTypingStopControlMessage(msg)) {
|
|
104
|
+
this.stopTyping(msg.chatId);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (isAssistantStreamResetControlMessage(msg)) {
|
|
108
|
+
await this.streamPreview.handleReset(msg);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
const delta = readAssistantStreamDelta(msg);
|
|
112
|
+
if (delta !== null) {
|
|
113
|
+
await this.streamPreview.handleDelta(msg, delta);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
};
|
|
118
|
+
send = async (msg) => {
|
|
119
|
+
if (isTypingStopControlMessage(msg)) {
|
|
120
|
+
this.stopTyping(msg.chatId);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!this.bot) return;
|
|
124
|
+
this.stopTyping(msg.chatId);
|
|
125
|
+
if (await this.streamPreview.finalizeWithFinalMessage(msg)) return;
|
|
126
|
+
const htmlContent = markdownToTelegramHtml(msg.content ?? "");
|
|
127
|
+
const silent = msg.metadata?.silent === true;
|
|
128
|
+
const replyTo = msg.replyTo ? Number(msg.replyTo) : void 0;
|
|
129
|
+
const options = {
|
|
130
|
+
parse_mode: "HTML",
|
|
131
|
+
...replyTo ? { reply_to_message_id: replyTo } : {},
|
|
132
|
+
...silent ? { disable_notification: true } : {}
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
await this.bot.sendMessage(Number(msg.chatId), htmlContent, options);
|
|
136
|
+
} catch {
|
|
137
|
+
await this.bot.sendMessage(Number(msg.chatId), msg.content ?? "", {
|
|
138
|
+
...replyTo ? { reply_to_message_id: replyTo } : {},
|
|
139
|
+
...silent ? { disable_notification: true } : {}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
handleIncoming = async (message) => {
|
|
144
|
+
if (!this.bot) return;
|
|
145
|
+
const context = this.resolveIncomingContext(message);
|
|
146
|
+
if (!context) return;
|
|
147
|
+
const { sender, senderId, chatId, isGroup, mentionState } = context;
|
|
148
|
+
const { content, attachments } = await this.buildIncomingPayload(message);
|
|
149
|
+
await this.maybeAddAckReaction({
|
|
150
|
+
message,
|
|
151
|
+
chatId,
|
|
152
|
+
isGroup,
|
|
153
|
+
mentionState
|
|
154
|
+
});
|
|
155
|
+
this.startTyping(chatId);
|
|
156
|
+
try {
|
|
157
|
+
await this.dispatchToBus(senderId, chatId, content, attachments, {
|
|
158
|
+
message_id: message.message_id,
|
|
159
|
+
user_id: sender.id,
|
|
160
|
+
username: sender.username,
|
|
161
|
+
first_name: sender.firstName,
|
|
162
|
+
sender_type: sender.type,
|
|
163
|
+
is_bot: sender.isBot,
|
|
164
|
+
is_group: isGroup,
|
|
165
|
+
account_id: this.resolveAccountId(),
|
|
166
|
+
accountId: this.resolveAccountId(),
|
|
167
|
+
peer_kind: isGroup ? "group" : "direct",
|
|
168
|
+
peer_id: isGroup ? chatId : String(sender.id),
|
|
169
|
+
was_mentioned: mentionState.wasMentioned,
|
|
170
|
+
require_mention: mentionState.requireMention
|
|
171
|
+
});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
this.stopTyping(chatId);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
resolveIncomingContext = (message) => {
|
|
178
|
+
const sender = resolveSender(message);
|
|
179
|
+
if (!sender) return null;
|
|
180
|
+
const chatId = String(message.chat.id);
|
|
181
|
+
const isGroup = message.chat.type !== "private";
|
|
182
|
+
if (!this.isAllowedByPolicy({
|
|
183
|
+
senderId: String(sender.id),
|
|
184
|
+
chatId,
|
|
185
|
+
isGroup
|
|
186
|
+
})) return null;
|
|
187
|
+
const mentionState = this.resolveMentionState({
|
|
188
|
+
message,
|
|
189
|
+
chatId,
|
|
190
|
+
isGroup
|
|
191
|
+
});
|
|
192
|
+
if (mentionState.requireMention && !mentionState.wasMentioned) return null;
|
|
193
|
+
return {
|
|
194
|
+
sender,
|
|
195
|
+
senderId: sender.username ? `${sender.id}|${sender.username}` : String(sender.id),
|
|
196
|
+
chatId,
|
|
197
|
+
isGroup,
|
|
198
|
+
mentionState
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
buildIncomingPayload = async (message) => {
|
|
202
|
+
const contentParts = [message.text, message.caption].filter((part) => Boolean(part));
|
|
203
|
+
const attachments = [];
|
|
204
|
+
const mediaPart = await this.resolveIncomingMediaPart(message);
|
|
205
|
+
if (mediaPart) {
|
|
206
|
+
contentParts.push(mediaPart.content);
|
|
207
|
+
attachments.push(mediaPart.attachment);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
content: contentParts.length ? contentParts.join("\n") : "[empty message]",
|
|
211
|
+
attachments
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
resolveIncomingMediaPart = async (message) => {
|
|
215
|
+
if (!this.bot) return null;
|
|
216
|
+
const { fileId, mediaType, mimeType } = resolveMedia(message);
|
|
217
|
+
if (!fileId || !mediaType) return null;
|
|
218
|
+
const mediaDir = join(getDataPath(), "media");
|
|
219
|
+
mkdirSync(mediaDir, { recursive: true });
|
|
220
|
+
const extension = getExtension(mediaType, mimeType);
|
|
221
|
+
const downloaded = await this.bot.downloadFile(fileId, mediaDir);
|
|
222
|
+
const finalPath = extension && !downloaded.endsWith(extension) ? `${downloaded}${extension}` : downloaded;
|
|
223
|
+
return {
|
|
224
|
+
content: await this.renderMediaContent(mediaType, finalPath),
|
|
225
|
+
attachment: {
|
|
226
|
+
id: fileId,
|
|
227
|
+
name: finalPath.split("/").pop(),
|
|
228
|
+
path: finalPath,
|
|
229
|
+
mimeType: mimeType ?? inferMediaMimeType(mediaType),
|
|
230
|
+
source: "telegram",
|
|
231
|
+
status: "ready"
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
renderMediaContent = async (mediaType, finalPath) => {
|
|
236
|
+
if (mediaType !== "voice" && mediaType !== "audio") return `[${mediaType}: ${finalPath}]`;
|
|
237
|
+
const transcription = await this.transcriber.transcribe(finalPath);
|
|
238
|
+
return transcription ? `[transcription: ${transcription}]` : `[${mediaType}: ${finalPath}]`;
|
|
239
|
+
};
|
|
240
|
+
handleTextCommand = async (message) => {
|
|
241
|
+
if (!message.text || !this.commands) return;
|
|
242
|
+
const sender = resolveSender(message);
|
|
243
|
+
if (!sender) return;
|
|
244
|
+
const result = await this.commands.executeText({
|
|
245
|
+
rawText: message.text,
|
|
246
|
+
conversationId: String(message.chat.id),
|
|
247
|
+
senderId: String(sender.id),
|
|
248
|
+
metadata: {
|
|
249
|
+
message_id: message.message_id,
|
|
250
|
+
user_id: sender.id,
|
|
251
|
+
username: sender.username,
|
|
252
|
+
account_id: this.resolveAccountId(),
|
|
253
|
+
accountId: this.resolveAccountId(),
|
|
254
|
+
is_group: message.chat.type !== "private"
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
if (result?.content) await this.bot?.sendMessage(message.chat.id, result.content);
|
|
258
|
+
};
|
|
259
|
+
dispatchToBus = async (senderId, chatId, content, attachments, metadata) => {
|
|
260
|
+
await this.handleMessage({
|
|
261
|
+
senderId,
|
|
262
|
+
chatId,
|
|
263
|
+
content,
|
|
264
|
+
attachments,
|
|
265
|
+
metadata
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
startTyping = (chatId) => {
|
|
269
|
+
this.typingController.start(chatId);
|
|
270
|
+
};
|
|
271
|
+
stopTyping = (chatId) => {
|
|
272
|
+
this.typingController.stop(chatId);
|
|
273
|
+
};
|
|
274
|
+
resolveAccountId = () => {
|
|
275
|
+
return this.config.accountId?.trim() || "default";
|
|
276
|
+
};
|
|
277
|
+
maybeAddAckReaction = async (params) => {
|
|
278
|
+
const { message, chatId, isGroup, mentionState } = params;
|
|
279
|
+
if (!this.bot) return;
|
|
280
|
+
if (typeof message.message_id !== "number") return;
|
|
281
|
+
const emoji = (this.config.ackReaction ?? "👀").trim();
|
|
282
|
+
if (!emoji) return;
|
|
283
|
+
if (!shouldSendAckReaction({
|
|
284
|
+
scope: this.config.ackReactionScope,
|
|
285
|
+
isDirect: !isGroup,
|
|
286
|
+
isGroup,
|
|
287
|
+
requireMention: mentionState.requireMention,
|
|
288
|
+
wasMentioned: mentionState.wasMentioned
|
|
289
|
+
})) return;
|
|
290
|
+
const reaction = toTelegramReaction(emoji);
|
|
291
|
+
try {
|
|
292
|
+
await this.bot.setMessageReaction(Number(chatId), message.message_id, { reaction });
|
|
293
|
+
} catch {}
|
|
294
|
+
};
|
|
295
|
+
isAllowedByPolicy = (params) => {
|
|
296
|
+
const { senderId, chatId, isGroup } = params;
|
|
297
|
+
if (!isGroup) {
|
|
298
|
+
if (this.config.dmPolicy === "disabled") return false;
|
|
299
|
+
const allowFrom = this.config.allowFrom ?? [];
|
|
300
|
+
if (this.config.dmPolicy === "allowlist" || this.config.dmPolicy === "pairing") return this.isAllowed(senderId);
|
|
301
|
+
if (allowFrom.includes("*")) return true;
|
|
302
|
+
return allowFrom.length === 0 ? true : this.isAllowed(senderId);
|
|
303
|
+
}
|
|
304
|
+
if (this.config.groupPolicy === "disabled") return false;
|
|
305
|
+
if (this.config.groupPolicy === "allowlist") {
|
|
306
|
+
const allowFrom = this.config.groupAllowFrom ?? [];
|
|
307
|
+
return allowFrom.includes("*") || allowFrom.includes(chatId);
|
|
308
|
+
}
|
|
309
|
+
return true;
|
|
310
|
+
};
|
|
311
|
+
resolveMentionState = (params) => {
|
|
312
|
+
const { message, chatId, isGroup } = params;
|
|
313
|
+
if (!isGroup) return {
|
|
314
|
+
wasMentioned: false,
|
|
315
|
+
requireMention: false
|
|
316
|
+
};
|
|
317
|
+
const groups = this.config.groups ?? {};
|
|
318
|
+
const groupRule = groups[chatId] ?? groups["*"];
|
|
319
|
+
const requireMention = groupRule?.requireMention ?? this.config.requireMention ?? false;
|
|
320
|
+
if (!requireMention) return {
|
|
321
|
+
wasMentioned: false,
|
|
322
|
+
requireMention: false
|
|
323
|
+
};
|
|
324
|
+
const content = `${message.text ?? ""}\n${message.caption ?? ""}`.trim();
|
|
325
|
+
const patterns = [...this.config.mentionPatterns ?? [], ...groupRule?.mentionPatterns ?? []].map((pattern) => pattern.trim()).filter(Boolean);
|
|
326
|
+
const usernameMentioned = this.botUsername ? content.includes(`@${this.botUsername}`) : false;
|
|
327
|
+
const replyToBot = Boolean(this.botUserId) && Boolean(message.reply_to_message?.from) && message.reply_to_message?.from?.id === this.botUserId;
|
|
328
|
+
const patternMentioned = patterns.some((pattern) => {
|
|
329
|
+
try {
|
|
330
|
+
return new RegExp(pattern, "i").test(content);
|
|
331
|
+
} catch {
|
|
332
|
+
return content.toLowerCase().includes(pattern.toLowerCase());
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
wasMentioned: usernameMentioned || replyToBot || patternMentioned,
|
|
337
|
+
requireMention
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
};
|
|
341
|
+
//#endregion
|
|
342
|
+
export { TelegramChannel };
|
|
343
|
+
|
|
344
|
+
//# sourceMappingURL=telegram-channel.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram-channel.service.js","names":[],"sources":["../../src/services/telegram-channel.service.ts"],"sourcesContent":["import TelegramBot, { type Message, type BotCommand } from \"node-telegram-bot-api\";\nimport { APP_NAME, BaseChannel, getDataPath, isAssistantStreamResetControlMessage, isTypingStopControlMessage, readAssistantStreamDelta, type Config, type InboundAttachment, type MessageBus, type OutboundMessage, } from \"@nextclaw/core\";\nimport { ChannelTypingController, type ExtensionChannelCommands } from \"@nextclaw/extension-sdk\";\nimport { GroqTranscriptionProvider } from \"../providers/groq-transcription.provider.js\";\nimport { TelegramStreamPreviewController } from \"../controllers/telegram-stream-preview.controller.js\";\nimport {\n getExtension,\n inferMediaMimeType,\n isLocalTelegramCommand,\n markdownToTelegramHtml,\n resolveMedia,\n resolveSender,\n resolveTelegramStreamingMode,\n shouldSendAckReaction,\n toTelegramReaction,\n type TelegramMentionState\n} from \"../utils/telegram-message.utils.js\";\nimport { join } from \"node:path\";\nimport { mkdirSync } from \"node:fs\";\nconst TYPING_HEARTBEAT_MS = 6000;\nconst TYPING_AUTO_STOP_MS = 120000;\nconst BOT_COMMANDS: BotCommand[] = [\n { command: \"start\", description: \"Start the bot\" },\n { command: \"reset\", description: \"Reset conversation history\" },\n { command: \"help\", description: \"Show available commands\" }\n];\nexport class TelegramChannel extends BaseChannel<Config[\"channels\"][\"telegram\"]> {\n name = \"telegram\";\n private bot: TelegramBot | null = null;\n private botUserId: number | null = null;\n private botUsername: string | null = null;\n private readonly typingController: ChannelTypingController;\n private readonly streamPreview: TelegramStreamPreviewController;\n private transcriber: GroqTranscriptionProvider;\n constructor(config: Config[\"channels\"][\"telegram\"], bus: MessageBus, private readonly commands?: ExtensionChannelCommands, groqApiKey?: string | null) {\n super(config, bus);\n this.transcriber = new GroqTranscriptionProvider(groqApiKey ?? null);\n this.typingController = new ChannelTypingController({\n heartbeatMs: TYPING_HEARTBEAT_MS,\n autoStopMs: TYPING_AUTO_STOP_MS,\n sendTyping: async (chatId) => {\n await this.bot?.sendChatAction(Number(chatId), \"typing\");\n }\n });\n this.streamPreview = new TelegramStreamPreviewController({\n resolveMode: () => resolveTelegramStreamingMode(this.config),\n getBot: () => this.bot\n });\n }\n start = async (): Promise<void> => {\n if (!this.config.token) {\n throw new Error(\"Telegram bot token not configured\");\n }\n this.running = true;\n const options: TelegramBot.ConstructorOptions = { polling: true };\n if (this.config.proxy) {\n options.request = { proxy: this.config.proxy } as TelegramBot.ConstructorOptions[\"request\"];\n }\n this.bot = new TelegramBot(this.config.token, options);\n try {\n const me = await this.bot.getMe();\n this.botUserId = me.id;\n this.botUsername = me.username ?? null;\n }\n catch {\n this.botUserId = null;\n this.botUsername = null;\n }\n this.bot.onText(/^\\/start$/, async (msg: Message) => {\n await this.bot?.sendMessage(msg.chat.id, `👋 Hi ${msg.from?.first_name ?? \"\"}! I'm ${APP_NAME}.\\n\\nSend me a message and I'll respond!\\nType /help to see available commands.`);\n });\n this.bot.onText(/^\\/help$/, async (msg: Message) => {\n const helpText = `🤖 <b>${APP_NAME} commands</b>\\n\\n` +\n \"/start — Start the bot\\n\" +\n \"/reset — Reset conversation history\\n\" +\n \"/help — Show this help message\\n\\n\" +\n \"Just send me a text message to chat!\";\n await this.bot?.sendMessage(msg.chat.id, helpText, { parse_mode: \"HTML\" });\n });\n this.bot.on(\"message\", async (msg: Message) => {\n if (!msg.text && !msg.caption && !msg.photo && !msg.voice && !msg.audio && !msg.document) {\n return;\n }\n if (msg.text?.startsWith(\"/\")) {\n if (!isLocalTelegramCommand(msg.text)) {\n await this.handleTextCommand(msg);\n }\n return;\n }\n await this.handleIncoming(msg);\n });\n this.bot.on(\"channel_post\", async (msg: Message) => {\n if (!msg.text && !msg.caption && !msg.photo && !msg.voice && !msg.audio && !msg.document) {\n return;\n }\n if (msg.text?.startsWith(\"/\")) {\n if (!isLocalTelegramCommand(msg.text)) {\n await this.handleTextCommand(msg);\n }\n return;\n }\n await this.handleIncoming(msg);\n });\n await this.bot.setMyCommands(BOT_COMMANDS);\n };\n stop = async (): Promise<void> => {\n this.running = false;\n this.typingController.stopAll();\n this.streamPreview.stopAll();\n if (this.bot) {\n await this.bot.stopPolling();\n this.bot = null;\n }\n };\n handleControlMessage = async (msg: OutboundMessage): Promise<boolean> => {\n if (isTypingStopControlMessage(msg)) {\n this.stopTyping(msg.chatId);\n return true;\n }\n if (isAssistantStreamResetControlMessage(msg)) {\n await this.streamPreview.handleReset(msg);\n return true;\n }\n const delta = readAssistantStreamDelta(msg);\n if (delta !== null) {\n await this.streamPreview.handleDelta(msg, delta);\n return true;\n }\n return false;\n };\n send = async (msg: OutboundMessage): Promise<void> => {\n if (isTypingStopControlMessage(msg)) {\n this.stopTyping(msg.chatId);\n return;\n }\n if (!this.bot) {\n return;\n }\n this.stopTyping(msg.chatId);\n if (await this.streamPreview.finalizeWithFinalMessage(msg)) {\n return;\n }\n const htmlContent = markdownToTelegramHtml(msg.content ?? \"\");\n const silent = msg.metadata?.silent === true;\n const replyTo = msg.replyTo ? Number(msg.replyTo) : undefined;\n const options = {\n parse_mode: \"HTML\" as const,\n ...(replyTo ? { reply_to_message_id: replyTo } : {}),\n ...(silent ? { disable_notification: true } : {})\n };\n try {\n await this.bot.sendMessage(Number(msg.chatId), htmlContent, options);\n }\n catch {\n await this.bot.sendMessage(Number(msg.chatId), msg.content ?? \"\", {\n ...(replyTo ? { reply_to_message_id: replyTo } : {}),\n ...(silent ? { disable_notification: true } : {})\n });\n }\n };\n private handleIncoming = async (message: Message): Promise<void> => {\n if (!this.bot) {\n return;\n }\n const context = this.resolveIncomingContext(message);\n if (!context) {\n return;\n }\n const { sender, senderId, chatId, isGroup, mentionState } = context;\n const { content, attachments } = await this.buildIncomingPayload(message);\n await this.maybeAddAckReaction({\n message,\n chatId,\n isGroup,\n mentionState\n });\n this.startTyping(chatId);\n try {\n await this.dispatchToBus(senderId, chatId, content, attachments, {\n message_id: message.message_id,\n user_id: sender.id,\n username: sender.username,\n first_name: sender.firstName,\n sender_type: sender.type,\n is_bot: sender.isBot,\n is_group: isGroup,\n account_id: this.resolveAccountId(),\n accountId: this.resolveAccountId(),\n peer_kind: isGroup ? \"group\" : \"direct\",\n peer_id: isGroup ? chatId : String(sender.id),\n was_mentioned: mentionState.wasMentioned,\n require_mention: mentionState.requireMention\n });\n }\n catch (error) {\n this.stopTyping(chatId);\n throw error;\n }\n };\n private resolveIncomingContext = (message: Message): {\n sender: NonNullable<ReturnType<typeof resolveSender>>;\n senderId: string;\n chatId: string;\n isGroup: boolean;\n mentionState: TelegramMentionState;\n } | null => {\n const sender = resolveSender(message);\n if (!sender) {\n return null;\n }\n const chatId = String(message.chat.id);\n const isGroup = message.chat.type !== \"private\";\n if (!this.isAllowedByPolicy({ senderId: String(sender.id), chatId, isGroup })) {\n return null;\n }\n const mentionState = this.resolveMentionState({ message, chatId, isGroup });\n if (mentionState.requireMention && !mentionState.wasMentioned) {\n return null;\n }\n const senderId = sender.username ? `${sender.id}|${sender.username}` : String(sender.id);\n return { sender, senderId, chatId, isGroup, mentionState };\n };\n private buildIncomingPayload = async (message: Message): Promise<{\n content: string;\n attachments: InboundAttachment[];\n }> => {\n const contentParts = [message.text, message.caption].filter((part): part is string => Boolean(part));\n const attachments: InboundAttachment[] = [];\n const mediaPart = await this.resolveIncomingMediaPart(message);\n if (mediaPart) {\n contentParts.push(mediaPart.content);\n attachments.push(mediaPart.attachment);\n }\n return {\n content: contentParts.length ? contentParts.join(\"\\n\") : \"[empty message]\",\n attachments\n };\n };\n private resolveIncomingMediaPart = async (message: Message): Promise<{\n content: string;\n attachment: InboundAttachment;\n } | null> => {\n if (!this.bot) {\n return null;\n }\n const { fileId, mediaType, mimeType } = resolveMedia(message);\n if (!fileId || !mediaType) {\n return null;\n }\n const mediaDir = join(getDataPath(), \"media\");\n mkdirSync(mediaDir, { recursive: true });\n const extension = getExtension(mediaType, mimeType);\n const downloaded = await this.bot.downloadFile(fileId, mediaDir);\n const finalPath = extension && !downloaded.endsWith(extension) ? `${downloaded}${extension}` : downloaded;\n return {\n content: await this.renderMediaContent(mediaType, finalPath),\n attachment: {\n id: fileId,\n name: finalPath.split(\"/\").pop(),\n path: finalPath,\n mimeType: mimeType ?? inferMediaMimeType(mediaType),\n source: \"telegram\",\n status: \"ready\"\n }\n };\n };\n private renderMediaContent = async (mediaType: string, finalPath: string): Promise<string> => {\n if (mediaType !== \"voice\" && mediaType !== \"audio\") {\n return `[${mediaType}: ${finalPath}]`;\n }\n const transcription = await this.transcriber.transcribe(finalPath);\n return transcription ? `[transcription: ${transcription}]` : `[${mediaType}: ${finalPath}]`;\n };\n private handleTextCommand = async (message: Message): Promise<void> => {\n if (!message.text || !this.commands) {\n return;\n }\n const sender = resolveSender(message);\n if (!sender) {\n return;\n }\n const result = await this.commands.executeText({\n rawText: message.text,\n conversationId: String(message.chat.id),\n senderId: String(sender.id),\n metadata: {\n message_id: message.message_id,\n user_id: sender.id,\n username: sender.username,\n account_id: this.resolveAccountId(),\n accountId: this.resolveAccountId(),\n is_group: message.chat.type !== \"private\",\n },\n });\n if (result?.content) {\n await this.bot?.sendMessage(message.chat.id, result.content);\n }\n };\n private dispatchToBus = async (senderId: string, chatId: string, content: string, attachments: InboundAttachment[], metadata: Record<string, unknown>): Promise<void> => {\n await this.handleMessage({ senderId, chatId, content, attachments, metadata });\n };\n private startTyping = (chatId: string): void => {\n this.typingController.start(chatId);\n };\n private stopTyping = (chatId: string): void => {\n this.typingController.stop(chatId);\n };\n private resolveAccountId = (): string => {\n const accountId = this.config.accountId?.trim();\n return accountId || \"default\";\n };\n private maybeAddAckReaction = async (params: {\n message: Message;\n chatId: string;\n isGroup: boolean;\n mentionState: TelegramMentionState;\n }): Promise<void> => {\n const { message, chatId, isGroup, mentionState } = params;\n if (!this.bot) {\n return;\n }\n if (typeof message.message_id !== \"number\") {\n return;\n }\n const emoji = (this.config.ackReaction ?? \"👀\").trim();\n if (!emoji) {\n return;\n }\n const shouldAck = shouldSendAckReaction({\n scope: this.config.ackReactionScope,\n isDirect: !isGroup,\n isGroup,\n requireMention: mentionState.requireMention,\n wasMentioned: mentionState.wasMentioned\n });\n if (!shouldAck) {\n return;\n }\n const reaction = toTelegramReaction(emoji);\n try {\n await this.bot.setMessageReaction(Number(chatId), message.message_id, {\n reaction\n });\n }\n catch {\n // ignore reaction errors\n }\n };\n private isAllowedByPolicy = (params: {\n senderId: string;\n chatId: string;\n isGroup: boolean;\n }): boolean => {\n const { senderId, chatId, isGroup } = params;\n if (!isGroup) {\n if (this.config.dmPolicy === \"disabled\") {\n return false;\n }\n const allowFrom = this.config.allowFrom ?? [];\n if (this.config.dmPolicy === \"allowlist\" || this.config.dmPolicy === \"pairing\") {\n return this.isAllowed(senderId);\n }\n if (allowFrom.includes(\"*\")) {\n return true;\n }\n return allowFrom.length === 0 ? true : this.isAllowed(senderId);\n }\n if (this.config.groupPolicy === \"disabled\") {\n return false;\n }\n if (this.config.groupPolicy === \"allowlist\") {\n const allowFrom = this.config.groupAllowFrom ?? [];\n return allowFrom.includes(\"*\") || allowFrom.includes(chatId);\n }\n return true;\n };\n private resolveMentionState = (params: {\n message: Message;\n chatId: string;\n isGroup: boolean;\n }): TelegramMentionState => {\n const { message, chatId, isGroup } = params;\n if (!isGroup) {\n return { wasMentioned: false, requireMention: false };\n }\n const groups = this.config.groups ?? {};\n const groupRule = groups[chatId] ?? groups[\"*\"];\n const requireMention = groupRule?.requireMention ?? this.config.requireMention ?? false;\n if (!requireMention) {\n return { wasMentioned: false, requireMention: false };\n }\n const content = `${message.text ?? \"\"}\\n${message.caption ?? \"\"}`.trim();\n const patterns = [\n ...(this.config.mentionPatterns ?? []),\n ...(groupRule?.mentionPatterns ?? [])\n ]\n .map((pattern) => pattern.trim())\n .filter(Boolean);\n const usernameMentioned = this.botUsername ? content.includes(`@${this.botUsername}`) : false;\n const replyToBot = Boolean(this.botUserId) &&\n Boolean(message.reply_to_message?.from) &&\n message.reply_to_message?.from?.id === this.botUserId;\n const patternMentioned = patterns.some((pattern) => {\n try {\n return new RegExp(pattern, \"i\").test(content);\n }\n catch {\n return content.toLowerCase().includes(pattern.toLowerCase());\n }\n });\n return {\n wasMentioned: usernameMentioned || replyToBot || patternMentioned,\n requireMention\n };\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
//#region src/utils/telegram-message.utils.ts
|
|
2
|
+
const TELEGRAM_TEXT_LIMIT = 4096;
|
|
3
|
+
function resolveSender(message) {
|
|
4
|
+
if (message.from) return {
|
|
5
|
+
id: message.from.id,
|
|
6
|
+
username: message.from.username,
|
|
7
|
+
firstName: message.from.first_name,
|
|
8
|
+
isBot: Boolean(message.from.is_bot),
|
|
9
|
+
type: "user"
|
|
10
|
+
};
|
|
11
|
+
if (message.sender_chat) return {
|
|
12
|
+
id: message.sender_chat.id,
|
|
13
|
+
username: message.sender_chat.username,
|
|
14
|
+
firstName: message.sender_chat.title,
|
|
15
|
+
isBot: true,
|
|
16
|
+
type: "sender_chat"
|
|
17
|
+
};
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function isLocalTelegramCommand(text) {
|
|
21
|
+
return /^\/(?:start|help)(?:@\w+)?(?:\s|$)/i.test(text.trim());
|
|
22
|
+
}
|
|
23
|
+
function resolveMedia(message) {
|
|
24
|
+
if (message.photo?.length) return {
|
|
25
|
+
fileId: message.photo[message.photo.length - 1].file_id,
|
|
26
|
+
mediaType: "image",
|
|
27
|
+
mimeType: "image/jpeg"
|
|
28
|
+
};
|
|
29
|
+
if (message.voice) return {
|
|
30
|
+
fileId: message.voice.file_id,
|
|
31
|
+
mediaType: "voice",
|
|
32
|
+
mimeType: message.voice.mime_type
|
|
33
|
+
};
|
|
34
|
+
if (message.audio) return {
|
|
35
|
+
fileId: message.audio.file_id,
|
|
36
|
+
mediaType: "audio",
|
|
37
|
+
mimeType: message.audio.mime_type
|
|
38
|
+
};
|
|
39
|
+
if (message.document) return {
|
|
40
|
+
fileId: message.document.file_id,
|
|
41
|
+
mediaType: "file",
|
|
42
|
+
mimeType: message.document.mime_type
|
|
43
|
+
};
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
function getExtension(mediaType, mimeType) {
|
|
47
|
+
const map = {
|
|
48
|
+
"image/jpeg": ".jpg",
|
|
49
|
+
"image/png": ".png",
|
|
50
|
+
"image/gif": ".gif",
|
|
51
|
+
"audio/ogg": ".ogg",
|
|
52
|
+
"audio/mpeg": ".mp3",
|
|
53
|
+
"audio/mp4": ".m4a"
|
|
54
|
+
};
|
|
55
|
+
if (mimeType && map[mimeType]) return map[mimeType];
|
|
56
|
+
return {
|
|
57
|
+
image: ".jpg",
|
|
58
|
+
voice: ".ogg",
|
|
59
|
+
audio: ".mp3",
|
|
60
|
+
file: ""
|
|
61
|
+
}[mediaType] ?? "";
|
|
62
|
+
}
|
|
63
|
+
function inferMediaMimeType(mediaType) {
|
|
64
|
+
if (!mediaType) return;
|
|
65
|
+
if (mediaType === "image") return "image/jpeg";
|
|
66
|
+
if (mediaType === "voice") return "audio/ogg";
|
|
67
|
+
if (mediaType === "audio") return "audio/mpeg";
|
|
68
|
+
}
|
|
69
|
+
function shouldSendAckReaction(params) {
|
|
70
|
+
const { isDirect, isGroup, requireMention, wasMentioned } = params;
|
|
71
|
+
const scope = params.scope ?? "all";
|
|
72
|
+
if (scope === "off") return false;
|
|
73
|
+
if (scope === "all") return true;
|
|
74
|
+
if (scope === "direct") return isDirect;
|
|
75
|
+
if (scope === "group-all") return isGroup;
|
|
76
|
+
if (scope === "group-mentions") return isGroup && requireMention && wasMentioned;
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
function readReplyToMessageId(metadata) {
|
|
80
|
+
const raw = metadata.message_id;
|
|
81
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return Math.trunc(raw);
|
|
82
|
+
if (typeof raw === "string") {
|
|
83
|
+
const parsed = Number(raw);
|
|
84
|
+
if (Number.isFinite(parsed)) return Math.trunc(parsed);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function resolveTelegramStreamingMode(config) {
|
|
88
|
+
const raw = config.streaming;
|
|
89
|
+
if (raw === true) return "partial";
|
|
90
|
+
if (raw === false || raw === void 0 || raw === null) return "off";
|
|
91
|
+
if (raw === "progress") return "partial";
|
|
92
|
+
if (raw === "partial" || raw === "block" || raw === "off") return raw;
|
|
93
|
+
return "off";
|
|
94
|
+
}
|
|
95
|
+
function markdownToTelegramHtml(text) {
|
|
96
|
+
if (!text) return "";
|
|
97
|
+
const codeBlocks = [];
|
|
98
|
+
let rendered = text.replace(/```[\w]*\n?([\s\S]*?)```/g, (_m, code) => {
|
|
99
|
+
codeBlocks.push(code);
|
|
100
|
+
return `\x00CB${codeBlocks.length - 1}\x00`;
|
|
101
|
+
});
|
|
102
|
+
const inlineCodes = [];
|
|
103
|
+
rendered = rendered.replace(/`([^`]+)`/g, (_m, code) => {
|
|
104
|
+
inlineCodes.push(code);
|
|
105
|
+
return `\x00IC${inlineCodes.length - 1}\x00`;
|
|
106
|
+
});
|
|
107
|
+
rendered = rendered.replace(/^#{1,6}\s+(.+)$/gm, "$1");
|
|
108
|
+
rendered = rendered.replace(/^>\s*(.*)$/gm, "$1");
|
|
109
|
+
rendered = rendered.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
110
|
+
rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<a href=\"$2\">$1</a>");
|
|
111
|
+
rendered = rendered.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
|
|
112
|
+
rendered = rendered.replace(/__(.+?)__/g, "<b>$1</b>");
|
|
113
|
+
rendered = rendered.replace(/(?<![a-zA-Z0-9])_([^_]+)_(?![a-zA-Z0-9])/g, "<i>$1</i>");
|
|
114
|
+
rendered = rendered.replace(/~~(.+?)~~/g, "<s>$1</s>");
|
|
115
|
+
rendered = rendered.replace(/^[-*]\s+/gm, "• ");
|
|
116
|
+
inlineCodes.forEach((code, i) => {
|
|
117
|
+
const escaped = escapeTelegramHtml(code);
|
|
118
|
+
rendered = rendered.replace(`\x00IC${i}\x00`, `<code>${escaped}</code>`);
|
|
119
|
+
});
|
|
120
|
+
codeBlocks.forEach((code, i) => {
|
|
121
|
+
const escaped = escapeTelegramHtml(code);
|
|
122
|
+
rendered = rendered.replace(`\x00CB${i}\x00`, `<pre><code>${escaped}</code></pre>`);
|
|
123
|
+
});
|
|
124
|
+
return rendered;
|
|
125
|
+
}
|
|
126
|
+
function toTelegramReaction(emoji) {
|
|
127
|
+
return [{
|
|
128
|
+
type: "emoji",
|
|
129
|
+
emoji
|
|
130
|
+
}];
|
|
131
|
+
}
|
|
132
|
+
function escapeTelegramHtml(text) {
|
|
133
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
export { TELEGRAM_TEXT_LIMIT, getExtension, inferMediaMimeType, isLocalTelegramCommand, markdownToTelegramHtml, readReplyToMessageId, resolveMedia, resolveSender, resolveTelegramStreamingMode, shouldSendAckReaction, toTelegramReaction };
|
|
137
|
+
|
|
138
|
+
//# sourceMappingURL=telegram-message.utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram-message.utils.js","names":[],"sources":["../../src/utils/telegram-message.utils.ts"],"sourcesContent":["import type TelegramBot from \"node-telegram-bot-api\";\nimport type { Message } from \"node-telegram-bot-api\";\nimport type { Config } from \"@nextclaw/core\";\n\nexport const TELEGRAM_TEXT_LIMIT = 4096;\n\nexport type TelegramMentionState = {\n wasMentioned: boolean;\n requireMention: boolean;\n};\n\nexport type TelegramAckReactionScope = Config[\"channels\"][\"telegram\"][\"ackReactionScope\"];\n\nexport type TelegramStreamingMode = \"off\" | \"partial\" | \"block\";\n\nexport function resolveSender(message: Message): {\n id: number;\n username?: string;\n firstName?: string;\n isBot: boolean;\n type: \"user\" | \"sender_chat\";\n} | null {\n if (message.from) {\n return {\n id: message.from.id,\n username: message.from.username,\n firstName: message.from.first_name,\n isBot: Boolean(message.from.is_bot),\n type: \"user\"\n };\n }\n if (message.sender_chat) {\n return {\n id: message.sender_chat.id,\n username: message.sender_chat.username,\n firstName: message.sender_chat.title,\n isBot: true,\n type: \"sender_chat\"\n };\n }\n return null;\n}\n\nexport function isLocalTelegramCommand(text: string): boolean {\n return /^\\/(?:start|help)(?:@\\w+)?(?:\\s|$)/i.test(text.trim());\n}\n\nexport function resolveMedia(message: Message): {\n fileId?: string;\n mediaType?: string;\n mimeType?: string;\n} {\n if (message.photo?.length) {\n const photo = message.photo[message.photo.length - 1];\n return { fileId: photo.file_id, mediaType: \"image\", mimeType: \"image/jpeg\" };\n }\n if (message.voice) {\n return { fileId: message.voice.file_id, mediaType: \"voice\", mimeType: message.voice.mime_type };\n }\n if (message.audio) {\n return { fileId: message.audio.file_id, mediaType: \"audio\", mimeType: message.audio.mime_type };\n }\n if (message.document) {\n return { fileId: message.document.file_id, mediaType: \"file\", mimeType: message.document.mime_type };\n }\n return {};\n}\n\nexport function getExtension(mediaType: string, mimeType?: string | null): string {\n const map: Record<string, string> = {\n \"image/jpeg\": \".jpg\",\n \"image/png\": \".png\",\n \"image/gif\": \".gif\",\n \"audio/ogg\": \".ogg\",\n \"audio/mpeg\": \".mp3\",\n \"audio/mp4\": \".m4a\"\n };\n if (mimeType && map[mimeType]) {\n return map[mimeType];\n }\n const fallback: Record<string, string> = {\n image: \".jpg\",\n voice: \".ogg\",\n audio: \".mp3\",\n file: \"\"\n };\n return fallback[mediaType] ?? \"\";\n}\n\nexport function inferMediaMimeType(mediaType?: string): string | undefined {\n if (!mediaType) {\n return undefined;\n }\n if (mediaType === \"image\") {\n return \"image/jpeg\";\n }\n if (mediaType === \"voice\") {\n return \"audio/ogg\";\n }\n if (mediaType === \"audio\") {\n return \"audio/mpeg\";\n }\n return undefined;\n}\n\nexport function shouldSendAckReaction(params: {\n scope?: TelegramAckReactionScope;\n isDirect: boolean;\n isGroup: boolean;\n requireMention: boolean;\n wasMentioned: boolean;\n}): boolean {\n const { isDirect, isGroup, requireMention, wasMentioned } = params;\n const scope = params.scope ?? \"all\";\n if (scope === \"off\") {\n return false;\n }\n if (scope === \"all\") {\n return true;\n }\n if (scope === \"direct\") {\n return isDirect;\n }\n if (scope === \"group-all\") {\n return isGroup;\n }\n if (scope === \"group-mentions\") {\n return isGroup && requireMention && wasMentioned;\n }\n return false;\n}\n\nexport function readReplyToMessageId(metadata: Record<string, unknown>): number | undefined {\n const raw = metadata.message_id;\n if (typeof raw === \"number\" && Number.isFinite(raw)) {\n return Math.trunc(raw);\n }\n if (typeof raw === \"string\") {\n const parsed = Number(raw);\n if (Number.isFinite(parsed)) {\n return Math.trunc(parsed);\n }\n }\n return undefined;\n}\n\nexport function resolveTelegramStreamingMode(config: Config[\"channels\"][\"telegram\"]): TelegramStreamingMode {\n const raw = config.streaming;\n if (raw === true) {\n return \"partial\";\n }\n if (raw === false || raw === undefined || raw === null) {\n return \"off\";\n }\n if (raw === \"progress\") {\n return \"partial\";\n }\n if (raw === \"partial\" || raw === \"block\" || raw === \"off\") {\n return raw;\n }\n return \"off\";\n}\n\nexport function markdownToTelegramHtml(text: string): string {\n if (!text) {\n return \"\";\n }\n const codeBlocks: string[] = [];\n let rendered = text.replace(/```[\\w]*\\n?([\\s\\S]*?)```/g, (_m, code) => {\n codeBlocks.push(code);\n return `\\x00CB${codeBlocks.length - 1}\\x00`;\n });\n const inlineCodes: string[] = [];\n rendered = rendered.replace(/`([^`]+)`/g, (_m, code) => {\n inlineCodes.push(code);\n return `\\x00IC${inlineCodes.length - 1}\\x00`;\n });\n rendered = rendered.replace(/^#{1,6}\\s+(.+)$/gm, \"$1\");\n rendered = rendered.replace(/^>\\s*(.*)$/gm, \"$1\");\n rendered = rendered.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n rendered = rendered.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\">$1</a>');\n rendered = rendered.replace(/\\*\\*(.+?)\\*\\*/g, \"<b>$1</b>\");\n rendered = rendered.replace(/__(.+?)__/g, \"<b>$1</b>\");\n rendered = rendered.replace(/(?<![a-zA-Z0-9])_([^_]+)_(?![a-zA-Z0-9])/g, \"<i>$1</i>\");\n rendered = rendered.replace(/~~(.+?)~~/g, \"<s>$1</s>\");\n rendered = rendered.replace(/^[-*]\\s+/gm, \"• \");\n inlineCodes.forEach((code, i) => {\n const escaped = escapeTelegramHtml(code);\n rendered = rendered.replace(`\\x00IC${i}\\x00`, `<code>${escaped}</code>`);\n });\n codeBlocks.forEach((code, i) => {\n const escaped = escapeTelegramHtml(code);\n rendered = rendered.replace(`\\x00CB${i}\\x00`, `<pre><code>${escaped}</code></pre>`);\n });\n return rendered;\n}\n\nexport function toTelegramReaction(emoji: string): TelegramBot.ReactionType[] {\n return [{ type: \"emoji\", emoji } as TelegramBot.ReactionType];\n}\n\nfunction escapeTelegramHtml(text: string): string {\n return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n"],"mappings":";AAIA,MAAa,sBAAsB;AAWnC,SAAgB,cAAc,SAMrB;AACL,KAAI,QAAQ,KACR,QAAO;EACH,IAAI,QAAQ,KAAK;EACjB,UAAU,QAAQ,KAAK;EACvB,WAAW,QAAQ,KAAK;EACxB,OAAO,QAAQ,QAAQ,KAAK,OAAO;EACnC,MAAM;EACT;AAEL,KAAI,QAAQ,YACR,QAAO;EACH,IAAI,QAAQ,YAAY;EACxB,UAAU,QAAQ,YAAY;EAC9B,WAAW,QAAQ,YAAY;EAC/B,OAAO;EACP,MAAM;EACT;AAEL,QAAO;;AAGX,SAAgB,uBAAuB,MAAuB;AAC1D,QAAO,sCAAsC,KAAK,KAAK,MAAM,CAAC;;AAGlE,SAAgB,aAAa,SAI3B;AACE,KAAI,QAAQ,OAAO,OAEf,QAAO;EAAE,QADK,QAAQ,MAAM,QAAQ,MAAM,SAAS,GAC5B;EAAS,WAAW;EAAS,UAAU;EAAc;AAEhF,KAAI,QAAQ,MACR,QAAO;EAAE,QAAQ,QAAQ,MAAM;EAAS,WAAW;EAAS,UAAU,QAAQ,MAAM;EAAW;AAEnG,KAAI,QAAQ,MACR,QAAO;EAAE,QAAQ,QAAQ,MAAM;EAAS,WAAW;EAAS,UAAU,QAAQ,MAAM;EAAW;AAEnG,KAAI,QAAQ,SACR,QAAO;EAAE,QAAQ,QAAQ,SAAS;EAAS,WAAW;EAAQ,UAAU,QAAQ,SAAS;EAAW;AAExG,QAAO,EAAE;;AAGb,SAAgB,aAAa,WAAmB,UAAkC;CAC9E,MAAM,MAA8B;EAChC,cAAc;EACd,aAAa;EACb,aAAa;EACb,aAAa;EACb,cAAc;EACd,aAAa;EAChB;AACD,KAAI,YAAY,IAAI,UAChB,QAAO,IAAI;AAQf,QANyC;EACrC,OAAO;EACP,OAAO;EACP,OAAO;EACP,MAAM;EACT,CACe,cAAc;;AAGlC,SAAgB,mBAAmB,WAAwC;AACvE,KAAI,CAAC,UACD;AAEJ,KAAI,cAAc,QACd,QAAO;AAEX,KAAI,cAAc,QACd,QAAO;AAEX,KAAI,cAAc,QACd,QAAO;;AAKf,SAAgB,sBAAsB,QAM1B;CACR,MAAM,EAAE,UAAU,SAAS,gBAAgB,iBAAiB;CAC5D,MAAM,QAAQ,OAAO,SAAS;AAC9B,KAAI,UAAU,MACV,QAAO;AAEX,KAAI,UAAU,MACV,QAAO;AAEX,KAAI,UAAU,SACV,QAAO;AAEX,KAAI,UAAU,YACV,QAAO;AAEX,KAAI,UAAU,iBACV,QAAO,WAAW,kBAAkB;AAExC,QAAO;;AAGX,SAAgB,qBAAqB,UAAuD;CACxF,MAAM,MAAM,SAAS;AACrB,KAAI,OAAO,QAAQ,YAAY,OAAO,SAAS,IAAI,CAC/C,QAAO,KAAK,MAAM,IAAI;AAE1B,KAAI,OAAO,QAAQ,UAAU;EACzB,MAAM,SAAS,OAAO,IAAI;AAC1B,MAAI,OAAO,SAAS,OAAO,CACvB,QAAO,KAAK,MAAM,OAAO;;;AAMrC,SAAgB,6BAA6B,QAA+D;CACxG,MAAM,MAAM,OAAO;AACnB,KAAI,QAAQ,KACR,QAAO;AAEX,KAAI,QAAQ,SAAS,QAAQ,KAAA,KAAa,QAAQ,KAC9C,QAAO;AAEX,KAAI,QAAQ,WACR,QAAO;AAEX,KAAI,QAAQ,aAAa,QAAQ,WAAW,QAAQ,MAChD,QAAO;AAEX,QAAO;;AAGX,SAAgB,uBAAuB,MAAsB;AACzD,KAAI,CAAC,KACD,QAAO;CAEX,MAAM,aAAuB,EAAE;CAC/B,IAAI,WAAW,KAAK,QAAQ,8BAA8B,IAAI,SAAS;AACnE,aAAW,KAAK,KAAK;AACrB,SAAO,SAAS,WAAW,SAAS,EAAE;GACxC;CACF,MAAM,cAAwB,EAAE;AAChC,YAAW,SAAS,QAAQ,eAAe,IAAI,SAAS;AACpD,cAAY,KAAK,KAAK;AACtB,SAAO,SAAS,YAAY,SAAS,EAAE;GACzC;AACF,YAAW,SAAS,QAAQ,qBAAqB,KAAK;AACtD,YAAW,SAAS,QAAQ,gBAAgB,KAAK;AACjD,YAAW,SAAS,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,OAAO;AACtF,YAAW,SAAS,QAAQ,4BAA4B,wBAAsB;AAC9E,YAAW,SAAS,QAAQ,kBAAkB,YAAY;AAC1D,YAAW,SAAS,QAAQ,cAAc,YAAY;AACtD,YAAW,SAAS,QAAQ,6CAA6C,YAAY;AACrF,YAAW,SAAS,QAAQ,cAAc,YAAY;AACtD,YAAW,SAAS,QAAQ,cAAc,KAAK;AAC/C,aAAY,SAAS,MAAM,MAAM;EAC7B,MAAM,UAAU,mBAAmB,KAAK;AACxC,aAAW,SAAS,QAAQ,SAAS,EAAE,OAAO,SAAS,QAAQ,SAAS;GAC1E;AACF,YAAW,SAAS,MAAM,MAAM;EAC5B,MAAM,UAAU,mBAAmB,KAAK;AACxC,aAAW,SAAS,QAAQ,SAAS,EAAE,OAAO,cAAc,QAAQ,eAAe;GACrF;AACF,QAAO;;AAGX,SAAgB,mBAAmB,OAA2C;AAC1E,QAAO,CAAC;EAAE,MAAM;EAAS;EAAO,CAA6B;;AAGjE,SAAS,mBAAmB,MAAsB;AAC9C,QAAO,KAAK,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,OAAO"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nextclaw-channel-extension-telegram",
|
|
3
|
+
"name": "NextClaw Telegram Channel Extension",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"server": {
|
|
6
|
+
"type": "stdio",
|
|
7
|
+
"command": "node",
|
|
8
|
+
"args": ["dist/main.js"]
|
|
9
|
+
},
|
|
10
|
+
"contributes": {
|
|
11
|
+
"channels": [
|
|
12
|
+
{
|
|
13
|
+
"id": "telegram",
|
|
14
|
+
"name": "Telegram",
|
|
15
|
+
"description": "Telegram bot channel",
|
|
16
|
+
"configSchema": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": true,
|
|
19
|
+
"properties": {
|
|
20
|
+
"enabled": { "type": "boolean" },
|
|
21
|
+
"token": { "type": "string" },
|
|
22
|
+
"proxy": { "type": ["string", "null"] },
|
|
23
|
+
"ackReaction": { "type": "string" },
|
|
24
|
+
"accountId": { "type": "string" },
|
|
25
|
+
"dmPolicy": { "type": "string" },
|
|
26
|
+
"groupPolicy": { "type": "string" },
|
|
27
|
+
"streaming": {},
|
|
28
|
+
"requireMention": { "type": "boolean" },
|
|
29
|
+
"allowFrom": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"items": { "type": "string" }
|
|
32
|
+
},
|
|
33
|
+
"groupAllowFrom": {
|
|
34
|
+
"type": "array",
|
|
35
|
+
"items": { "type": "string" }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nextclaw/channel-extension-telegram",
|
|
3
|
+
"version": "0.1.1-beta.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "NextClaw Telegram channel extension process.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"development": "./src/index.ts",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"nextclaw.extension.json"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"node-telegram-bot-api": "^0.66.0",
|
|
21
|
+
"undici": "^6.21.0",
|
|
22
|
+
"@nextclaw/extension-sdk": "0.1.12-beta.0",
|
|
23
|
+
"@nextclaw/core": "0.12.25-beta.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^20.17.6",
|
|
27
|
+
"@types/node-telegram-bot-api": "^0.64.8",
|
|
28
|
+
"tsx": "^4.19.2",
|
|
29
|
+
"typescript": "^5.6.3"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsdown src/index.ts src/main.ts --dts.sourcemap --clean --target es2022 --no-fixedExtension --unbundle",
|
|
33
|
+
"lint": "eslint src --max-warnings=0",
|
|
34
|
+
"tsc": "tsc -p tsconfig.json"
|
|
35
|
+
}
|
|
36
|
+
}
|