@ebowwa/channel-telegram 1.12.5 → 1.13.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.
Files changed (49) hide show
  1. package/README.md +78 -44
  2. package/dist/commands/index.d.ts +1 -0
  3. package/dist/commands/index.d.ts.map +1 -1
  4. package/dist/commands/index.js +3 -0
  5. package/dist/commands/index.js.map +1 -1
  6. package/dist/commands/restart.d.ts +7 -0
  7. package/dist/commands/restart.d.ts.map +1 -0
  8. package/dist/commands/restart.js +29 -0
  9. package/dist/commands/restart.js.map +1 -0
  10. package/dist/commands/settings.d.ts +8 -0
  11. package/dist/commands/settings.d.ts.map +1 -0
  12. package/dist/commands/settings.js +16 -0
  13. package/dist/commands/settings.js.map +1 -0
  14. package/dist/index.d.ts +83 -29
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +350 -712
  17. package/dist/index.js.map +1 -1
  18. package/package.json +9 -13
  19. package/src/commands/index.ts +3 -0
  20. package/src/commands/restart.ts +41 -0
  21. package/src/commands/settings.ts +24 -0
  22. package/src/index.ts +415 -823
  23. package/dist/mcp/client.d.ts +0 -50
  24. package/dist/mcp/client.d.ts.map +0 -1
  25. package/dist/mcp/client.js +0 -150
  26. package/dist/mcp/client.js.map +0 -1
  27. package/dist/mcp/index.d.ts +0 -5
  28. package/dist/mcp/index.d.ts.map +0 -1
  29. package/dist/mcp/index.js +0 -5
  30. package/dist/mcp/index.js.map +0 -1
  31. package/src/api/fetch-retry.js +0 -96
  32. package/src/api/keys.js +0 -25
  33. package/src/commands/cancel.js +0 -120
  34. package/src/commands/clear.js +0 -59
  35. package/src/commands/doppler.js +0 -118
  36. package/src/commands/git.js +0 -126
  37. package/src/commands/help.js +0 -74
  38. package/src/commands/index.js +0 -65
  39. package/src/commands/logs.js +0 -81
  40. package/src/commands/pause.js +0 -133
  41. package/src/commands/resources.js +0 -87
  42. package/src/commands/resume.js +0 -95
  43. package/src/commands/start.js +0 -68
  44. package/src/commands/status.js +0 -62
  45. package/src/commands/tools.js +0 -67
  46. package/src/commands/toolsoutput.js +0 -85
  47. package/src/commands/types.js +0 -5
  48. package/src/mcp/client.ts +0 -188
  49. package/src/mcp/index.ts +0 -5
package/dist/index.js CHANGED
@@ -1,335 +1,181 @@
1
1
  /**
2
- * GLM Daemon Telegram Bot
2
+ * @ebowwa/channel-telegram
3
3
  *
4
- * Full-featured Telegram bot with GLM-4.7 AI, git status, Doppler integration
4
+ * Telegram channel adapter implementing ChannelConnector.
5
5
  *
6
- * Features:
7
- * - GLM-4.7 AI responses via Z.AI API
8
- * - 429 rate limit handling with exponential backoff
9
- * - /git command for git status + GitHub auth
10
- * - /doppler command for Doppler secrets status
11
- * - Rolling API key support
6
+ * This package handles:
7
+ * - Telegram protocol operations (polling, message delivery)
8
+ * - Built-in commands (/start, /help, /status, etc.)
9
+ * - Typing indicators, message chunking, access control
10
+ * - Message normalization to ChannelMessage format
11
+ *
12
+ * Intelligence (GLM, tools, memory) can be provided by:
13
+ * 1. This package (standalone mode with built-in tools)
14
+ * 2. External daemon/consumer (adapter mode via onMessage handler)
12
15
  */
13
- import TelegramBot from 'node-telegram-bot-api';
14
- import { execSync } from 'child_process';
15
- import { getStore, seedPrompts } from '@ebowwa/structured-prompts';
16
- import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
17
- import { ConversationMemory } from './conversation-memory';
18
- import { getZAIKey } from './api/keys';
19
- import { fetchWithRetry } from './api/fetch-retry';
20
- import { registerAllCommands, isQuiet } from './commands';
21
- import { MCPClient } from './mcp';
22
- const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || '';
23
- const TELEGRAM_TEST_CHAT_ID = process.env.TELEGRAM_TEST_CHAT_ID || '';
24
- // MCP Client instance (initialized on startup)
25
- let mcpClient = null;
26
- const TOOLS = [
27
- {
28
- name: 'read_file',
29
- description: 'Read a file from the filesystem. Use for checking configs, logs, code.',
30
- parameters: {
31
- type: 'object',
32
- properties: {
33
- path: { type: 'string', description: 'Absolute file path to read' }
34
- },
35
- required: ['path']
16
+ import TelegramBot from "node-telegram-bot-api";
17
+ import { createChannelId, } from "@ebowwa/channel-types";
18
+ import { ConversationMemory } from "./conversation-memory.js";
19
+ import { registerAllCommands } from "./commands/index.js";
20
+ // Re-export for external use
21
+ export { ConversationMemory } from "./conversation-memory.js";
22
+ export { registerAllCommands, isQuiet } from "./commands/index.js";
23
+ export function createTelegramConfigFromEnv() {
24
+ const allowedUsers = process.env.TELEGRAM_ALLOWED_USERS
25
+ ?.split(",")
26
+ .map((s) => parseInt(s.trim(), 10))
27
+ .filter((n) => !isNaN(n));
28
+ const allowedChats = process.env.TELEGRAM_ALLOWED_CHATS
29
+ ?.split(",")
30
+ .map((s) => parseInt(s.trim(), 10))
31
+ .filter((n) => !isNaN(n));
32
+ return {
33
+ botToken: process.env.TELEGRAM_BOT_TOKEN || "",
34
+ testChatId: process.env.TELEGRAM_TEST_CHAT_ID,
35
+ allowedUsers,
36
+ allowedChats,
37
+ conversationsFile: process.env.CONVERSATIONS_FILE,
38
+ };
39
+ }
40
+ // ============================================================
41
+ // TELEGRAM CHANNEL
42
+ // ============================================================
43
+ export class TelegramChannel {
44
+ id;
45
+ label = "Telegram";
46
+ capabilities = {
47
+ supports: {
48
+ text: true,
49
+ media: true,
50
+ replies: true,
51
+ threads: false,
52
+ reactions: true,
53
+ editing: true,
54
+ streaming: false,
36
55
  },
37
- handler: async (args) => {
38
- const path = args.path;
39
- try {
40
- if (!existsSync(path))
41
- return `File not found: ${path}`;
42
- const content = readFileSync(path, 'utf-8');
43
- return content.length > 4000 ? content.slice(0, 4000) + '\n...[truncated]' : content;
44
- }
45
- catch (e) {
46
- return `Error reading file: ${e.message}`;
47
- }
48
- }
49
- },
50
- {
51
- name: 'write_file',
52
- description: 'Write content to a file. Creates the file if it does not exist, overwrites if it does.',
53
- parameters: {
54
- type: 'object',
55
- properties: {
56
- path: { type: 'string', description: 'Absolute file path to write' },
57
- content: { type: 'string', description: 'Content to write to the file' }
58
- },
59
- required: ['path', 'content']
56
+ media: {
57
+ maxFileSize: 50 * 1024 * 1024, // 50MB
58
+ supportedMimeTypes: ["image/*", "video/*", "audio/*", "application/pdf"],
60
59
  },
61
- handler: async (args) => {
62
- const path = args.path;
63
- const content = args.content;
64
- try {
65
- writeFileSync(path, content, 'utf-8');
66
- return `Successfully wrote ${content.length} bytes to ${path}`;
67
- }
68
- catch (e) {
69
- return `Error writing file: ${e.message}`;
70
- }
71
- }
72
- },
73
- {
74
- name: 'edit_file',
75
- description: 'Edit a file by replacing a specific string with new content.',
76
- parameters: {
77
- type: 'object',
78
- properties: {
79
- path: { type: 'string', description: 'Absolute file path to edit' },
80
- old_string: { type: 'string', description: 'The exact text to find and replace' },
81
- new_string: { type: 'string', description: 'The text to replace it with' }
82
- },
83
- required: ['path', 'old_string', 'new_string']
60
+ rateLimits: {
61
+ messagesPerMinute: 30,
62
+ charactersPerMessage: 4096,
84
63
  },
85
- handler: async (args) => {
86
- const path = args.path;
87
- const oldStr = args.old_string;
88
- const newStr = args.new_string;
89
- try {
90
- if (!existsSync(path))
91
- return `File not found: ${path}`;
92
- const content = readFileSync(path, 'utf-8');
93
- if (!content.includes(oldStr)) {
94
- return `String not found in file. First 500 chars:\n${content.slice(0, 500)}`;
95
- }
96
- const newContent = content.replace(oldStr, newStr);
97
- writeFileSync(path, newContent, 'utf-8');
98
- return `Successfully edited ${path}`;
99
- }
100
- catch (e) {
101
- return `Error editing file: ${e.message}`;
102
- }
103
- }
104
- },
105
- {
106
- name: 'append_file',
107
- description: 'Append content to the end of a file. Creates the file if it does not exist.',
108
- parameters: {
109
- type: 'object',
110
- properties: {
111
- path: { type: 'string', description: 'Absolute file path to append to' },
112
- content: { type: 'string', description: 'Content to append to the file' }
113
- },
114
- required: ['path', 'content']
115
- },
116
- handler: async (args) => {
117
- const path = args.path;
118
- const content = args.content;
119
- try {
120
- appendFileSync(path, content, 'utf-8');
121
- return `Successfully appended ${content.length} bytes to ${path}`;
122
- }
123
- catch (e) {
124
- return `Error appending to file: ${e.message}`;
125
- }
126
- }
127
- },
128
- {
129
- name: 'list_dir',
130
- description: 'List contents of a directory. Shows files and subdirectories.',
131
- parameters: {
132
- type: 'object',
133
- properties: {
134
- path: { type: 'string', description: 'Directory path to list (default: current working directory)' }
135
- }
136
- },
137
- handler: async (args) => {
138
- const path = args.path || process.cwd();
139
- try {
140
- const items = execSync(`ls -la "${path}" 2>&1`).toString();
141
- return items;
142
- }
143
- catch (e) {
144
- return `Error listing directory: ${e.message}`;
145
- }
146
- }
147
- },
148
- {
149
- name: 'run_command',
150
- description: 'Execute a shell command. Use for git, system info, etc. BE CAREFUL with destructive commands.',
151
- parameters: {
152
- type: 'object',
153
- properties: {
154
- command: { type: 'string', description: 'Shell command to execute' },
155
- cwd: { type: 'string', description: 'Working directory (optional)' }
64
+ };
65
+ bot;
66
+ config;
67
+ memory;
68
+ tools;
69
+ messageHandler;
70
+ typingIntervals = new Map();
71
+ connected = false;
72
+ constructor(config) {
73
+ this.config = config;
74
+ this.id = createChannelId("telegram", "default");
75
+ this.bot = new TelegramBot(config.botToken, { polling: false });
76
+ this.memory = new ConversationMemory(config.conversationsFile);
77
+ this.tools = config.tools || [];
78
+ }
79
+ // ============================================================
80
+ // ChannelConnector Implementation
81
+ // ============================================================
82
+ async start() {
83
+ console.log("[TelegramChannel] Starting...");
84
+ // Start polling
85
+ this.bot = new TelegramBot(this.config.botToken, {
86
+ polling: {
87
+ interval: 300,
88
+ autoStart: true,
89
+ params: {
90
+ allowed_updates: ["message", "edited_message", "message_reaction", "callback_query"],
91
+ },
156
92
  },
157
- required: ['command']
158
- },
159
- handler: async (args) => {
160
- const cmd = args.command;
161
- const cwd = args.cwd || process.cwd();
162
- // Safety check - block dangerous commands
163
- const blocked = ['rm -rf', 'mkfs', 'dd if=', '> /dev/', 'chmod 777'];
164
- if (blocked.some(b => cmd.includes(b))) {
165
- return `Blocked: command contains dangerous pattern`;
166
- }
167
- try {
168
- const result = execSync(cmd, { timeout: 10000, cwd }).toString();
169
- return result || '(no output)';
170
- }
171
- catch (e) {
172
- return e.stdout?.toString() || e.message;
173
- }
174
- }
175
- },
176
- {
177
- name: 'git_status',
178
- description: 'Check git repository status',
179
- parameters: {
180
- type: 'object',
181
- properties: {
182
- cwd: { type: 'string', description: 'Repository path (optional)' }
183
- }
184
- },
185
- handler: async (args) => {
186
- const cwd = args.cwd || process.cwd();
187
- try {
188
- const status = execSync('git status 2>&1', { cwd }).toString();
189
- const branch = execSync('git branch --show-current 2>&1', { cwd }).toString();
190
- return `Branch: ${branch}\n\n${status}`;
191
- }
192
- catch (e) {
193
- return `Error: ${e.message}`;
194
- }
93
+ });
94
+ // Register built-in commands
95
+ registerAllCommands(this.bot, this.memory, this.tools);
96
+ // Setup message routing
97
+ this.setupMessageHandlers();
98
+ // Register Telegram commands menu
99
+ await this.registerCommands();
100
+ this.connected = true;
101
+ console.log("[TelegramChannel] Connected and polling");
102
+ // Send startup notification if configured
103
+ if (this.config.testChatId) {
104
+ await this.sendMessage(Number(this.config.testChatId), "Telegram channel adapter online");
195
105
  }
196
- },
197
- {
198
- name: 'get_prompt',
199
- description: 'Get a prompt template from the prompt store',
200
- parameters: {
201
- type: 'object',
202
- properties: {
203
- id: { type: 'string', description: 'Prompt ID to retrieve' }
204
- },
205
- required: ['id']
206
- },
207
- handler: async (args) => {
208
- const store = getStore(process.env.PROMPTS_FILE);
209
- const prompt = store.get(args.id);
210
- return prompt ? JSON.stringify(prompt, null, 2) : `Prompt not found: ${args.id}`;
106
+ }
107
+ async stop() {
108
+ console.log("[TelegramChannel] Stopping...");
109
+ await this.bot.stopPolling();
110
+ this.connected = false;
111
+ }
112
+ /**
113
+ * Set the message handler. The daemon/consumer provides intelligence.
114
+ */
115
+ onMessage(handler) {
116
+ this.messageHandler = handler;
117
+ }
118
+ /**
119
+ * Send a response back to Telegram.
120
+ */
121
+ async send(response) {
122
+ const chatId = this.extractChatId(response.replyTo);
123
+ if (!chatId) {
124
+ console.error("[TelegramChannel] Cannot send: no chat ID");
125
+ return;
211
126
  }
212
- },
213
- {
214
- name: 'list_prompts',
215
- description: 'List all available prompt templates',
216
- parameters: { type: 'object', properties: {} },
217
- handler: async () => {
218
- const store = getStore(process.env.PROMPTS_FILE);
219
- const prompts = store.list();
220
- return prompts.map(p => `- ${p.id}: ${p.name}`).join('\n') || 'No prompts found';
127
+ const options = {};
128
+ if (response.content.replyToOriginal) {
129
+ options.reply_to_message_id = parseInt(response.replyTo.messageId, 10);
221
130
  }
222
- },
223
- {
224
- name: 'system_info',
225
- description: 'Get system resource info (CPU, memory, disk, uptime)',
226
- parameters: { type: 'object', properties: {} },
227
- handler: async () => {
228
- try {
229
- const cpu = execSync('nproc 2>/dev/null || echo "unknown"').toString().trim();
230
- const mem = execSync('free -h 2>/dev/null | grep Mem || echo "unknown"').toString().trim();
231
- const disk = execSync('df -h / 2>/dev/null | tail -1 || echo "unknown"').toString().trim();
232
- const uptime = execSync('uptime -p 2>/dev/null || uptime 2>/dev/null || echo "unknown"').toString().trim();
233
- return `CPU cores: ${cpu}\nMemory: ${mem}\nDisk: ${disk}\nUptime: ${uptime}`;
234
- }
235
- catch (e) {
236
- return `Error: ${e.message}`;
237
- }
131
+ if (response.content.options?.parseMode) {
132
+ options.parse_mode = response.content.options.parseMode;
238
133
  }
134
+ await this.sendLongMessage(chatId, response.content.text, options);
239
135
  }
240
- ];
241
- // Get all tools (local + MCP) in GLM API format
242
- function getGLMTools() {
243
- const allTools = [...TOOLS.map(t => ({
244
- type: 'function',
245
- function: {
246
- name: t.name,
247
- description: t.description,
248
- parameters: t.parameters
249
- }
250
- }))];
251
- // Add MCP tools if connected
252
- if (mcpClient) {
253
- const mcpTools = mcpClient.getTools();
254
- for (const tool of mcpTools) {
255
- allTools.push({
256
- type: 'function',
257
- function: {
258
- name: tool.name,
259
- description: tool.description,
260
- parameters: tool.inputSchema
261
- }
262
- });
263
- }
136
+ isConnected() {
137
+ return this.connected;
264
138
  }
265
- return allTools;
266
- }
267
- // Convert tools to GLM API format (static fallback)
268
- const GLM_TOOLS = TOOLS.map(t => ({
269
- type: 'function',
270
- function: {
271
- name: t.name,
272
- description: t.description,
273
- parameters: t.parameters
139
+ // ============================================================
140
+ // Public Helpers
141
+ // ============================================================
142
+ /**
143
+ * Get the underlying TelegramBot instance for advanced operations.
144
+ */
145
+ getBot() {
146
+ return this.bot;
274
147
  }
275
- }));
276
- // Execute a tool by name (checks local tools first, then MCP)
277
- async function executeTool(name, args) {
278
- // Check local tools first
279
- const tool = TOOLS.find(t => t.name === name);
280
- if (tool) {
281
- return tool.handler(args);
148
+ /**
149
+ * Get the conversation memory.
150
+ */
151
+ getMemory() {
152
+ return this.memory;
282
153
  }
283
- // Check MCP tools
284
- if (mcpClient) {
285
- const mcpTools = mcpClient.getTools();
286
- const mcpTool = mcpTools.find(t => t.name === name);
287
- if (mcpTool) {
288
- try {
289
- return await mcpClient.callTool(name, args);
290
- }
291
- catch (error) {
292
- return `MCP tool error: ${error.message}`;
293
- }
294
- }
154
+ /**
155
+ * Send a simple text message.
156
+ */
157
+ async sendMessage(chatId, text, options) {
158
+ await this.bot.sendMessage(chatId, text, options);
295
159
  }
296
- return `Unknown tool: ${name}`;
297
- }
298
- const ZAI_API_ENDPOINT = 'https://api.z.ai/api/coding/paas/v4/chat/completions';
299
- export class TelegramGLMBot {
300
- bot;
301
- memory;
302
- typingIntervals = new Map();
303
- constructor(token) {
304
- // Enable polling with all required update types
305
- this.bot = new TelegramBot(token, {
306
- polling: {
307
- interval: 300,
308
- autoStart: true,
309
- params: {
310
- allowed_updates: ['message', 'edited_message', 'message_reaction', 'callback_query']
311
- }
312
- }
313
- });
314
- this.memory = new ConversationMemory(process.env.CONVERSATIONS_FILE);
160
+ /**
161
+ * Send typing indicator.
162
+ */
163
+ async sendTyping(chatId) {
164
+ await this.bot.sendChatAction(chatId, "typing");
315
165
  }
316
166
  /**
317
- * Start periodic typing indicator for a chat
318
- * Telegram typing indicator lasts ~5 seconds, so refresh every 3 seconds
167
+ * Start continuous typing indicator (until response is ready).
319
168
  */
320
169
  startTypingIndicator(chatId) {
321
- // Clear any existing interval
322
170
  this.stopTypingIndicator(chatId);
323
- // Send initial typing action
324
- this.bot.sendChatAction(chatId, 'typing').catch(() => { });
325
- // Set up periodic refresh every 3 seconds
171
+ this.bot.sendChatAction(chatId, "typing").catch(() => { });
326
172
  const interval = setInterval(() => {
327
- this.bot.sendChatAction(chatId, 'typing').catch(() => { });
173
+ this.bot.sendChatAction(chatId, "typing").catch(() => { });
328
174
  }, 3000);
329
175
  this.typingIntervals.set(chatId, interval);
330
176
  }
331
177
  /**
332
- * Stop periodic typing indicator for a chat
178
+ * Stop typing indicator.
333
179
  */
334
180
  stopTypingIndicator(chatId) {
335
181
  const interval = this.typingIntervals.get(chatId);
@@ -339,456 +185,248 @@ export class TelegramGLMBot {
339
185
  }
340
186
  }
341
187
  /**
342
- * Send message, splitting if too long for Telegram (4096 char limit)
188
+ * Check if user/chat is allowed.
343
189
  */
344
- async sendLongMessage(chatId, text) {
345
- const MAX_LENGTH = 4000; // Leave buffer for safety
346
- if (text.length <= MAX_LENGTH) {
347
- await this.bot.sendMessage(chatId, text);
348
- return;
190
+ isAllowed(userId, chatId) {
191
+ if (!this.config.allowedUsers?.length && !this.config.allowedChats?.length) {
192
+ return true;
349
193
  }
350
- // Split into chunks at word boundaries when possible
351
- const chunks = [];
352
- let remaining = text;
353
- while (remaining.length > 0) {
354
- if (remaining.length <= MAX_LENGTH) {
355
- chunks.push(remaining);
356
- break;
357
- }
358
- // Try to find a good break point (newline or space)
359
- let breakPoint = remaining.lastIndexOf('\n', MAX_LENGTH);
360
- if (breakPoint < 1000) {
361
- breakPoint = remaining.lastIndexOf(' ', MAX_LENGTH);
362
- }
363
- if (breakPoint < 1000) {
364
- breakPoint = MAX_LENGTH;
365
- }
366
- chunks.push(remaining.slice(0, breakPoint));
367
- remaining = remaining.slice(breakPoint).trim();
194
+ if (this.config.allowedUsers?.length && userId) {
195
+ if (this.config.allowedUsers.includes(userId))
196
+ return true;
368
197
  }
369
- // Send chunks with continuation indicator
370
- for (let i = 0; i < chunks.length; i++) {
371
- const prefix = chunks.length > 1 ? `[${i + 1}/${chunks.length}] ` : '';
372
- await this.bot.sendMessage(chatId, prefix + chunks[i]);
198
+ if (this.config.allowedChats?.length && chatId) {
199
+ if (this.config.allowedChats.includes(chatId))
200
+ return true;
373
201
  }
202
+ return false;
374
203
  }
375
- async start() {
376
- console.log('🤖 GLM Daemon Telegram Bot starting with GLM-4.7...');
377
- // Initialize MCP client for external tool access
378
- mcpClient = new MCPClient();
379
- try {
380
- await mcpClient.connect();
381
- }
382
- catch (error) {
383
- console.error('[MCP] Failed to initialize MCP client:', error);
384
- // Continue without MCP - local tools still work
385
- }
386
- // Register all commands from commands/ directory
387
- registerAllCommands(this.bot, this.memory, TOOLS);
388
- // Register commands with Telegram for autocomplete
389
- await this.bot.setMyCommands([
390
- { command: 'start', description: 'Start the bot' },
391
- { command: 'help', description: 'Show available commands' },
392
- { command: 'status', description: 'Check bot and API status' },
393
- { command: 'git', description: 'Check git status and GitHub auth' },
394
- { command: 'doppler', description: 'Check Doppler secrets status' },
395
- { command: 'logs', description: 'View recent bot logs' },
396
- { command: 'clear', description: 'Clear conversation history' },
397
- { command: 'tools', description: 'List available tools' },
398
- { command: 'cancel', description: 'Stop current task immediately' },
399
- { command: 'pause', description: 'Pause execution' },
400
- { command: 'resume', description: 'Resume after pause/cancel' },
401
- { command: 'quiet', description: 'Hide tool logging' },
402
- { command: 'verbose', description: 'Show tool logging' },
403
- ]);
404
- // Handle all text messages with GLM-4.7
405
- this.bot.on('message', async (msg) => {
406
- if (!msg.text)
204
+ // ============================================================
205
+ // Message Routing (Internal)
206
+ // ============================================================
207
+ setupMessageHandlers() {
208
+ // Text messages (skip commands - handled by onText handlers)
209
+ this.bot.on("message", async (msg) => {
210
+ if (!msg.text || msg.text.startsWith("/"))
407
211
  return;
408
- if (msg.text.startsWith('/'))
212
+ if (!this.isAllowed(msg.from?.id, msg.chat.id))
409
213
  return;
410
- const chatId = msg.chat.id;
411
- const userName = msg.from?.first_name || msg.from?.username || 'User';
412
- const messageId = msg.message_id;
413
- // Check if this is a reply to another message
414
- const replyContext = msg.reply_to_message ? {
415
- isReply: true,
416
- originalMessage: msg.reply_to_message.text || '[non-text message]',
417
- originalFrom: msg.reply_to_message.from?.first_name || 'User',
418
- originalMessageId: msg.reply_to_message.message_id
419
- } : null;
420
- if (replyContext) {
421
- console.log(`[Telegram] ${userName} (replying to ${replyContext.originalFrom}): ${msg.text}`);
422
- }
423
- else {
424
- console.log(`[Telegram] ${userName}: ${msg.text}`);
425
- }
426
- // Build message with reply context if present
427
- let messageWithContext = msg.text;
428
- if (replyContext) {
429
- messageWithContext = `[Replying to ${replyContext.originalFrom}'s message: "${replyContext.originalMessage.slice(0, 200)}${replyContext.originalMessage.length > 200 ? '...' : ''}"]\n\n${msg.text}`;
430
- }
431
- // Typing indicator is now handled by getGLMResponse with periodic refresh
432
- // Get response with conversation history
433
- const response = await this.getGLMResponse(chatId, messageWithContext, userName);
434
- // Save to memory with message ID and reply context
435
- this.memory.addWithReply(chatId, 'user', msg.text, {
436
- messageId,
437
- replyTo: replyContext ? {
438
- messageId: replyContext.originalMessageId,
439
- text: replyContext.originalMessage,
440
- from: replyContext.originalFrom
441
- } : undefined
442
- });
443
- this.memory.add(chatId, 'assistant', response);
444
- // Send response - if replying, use reply_to_message_id for threading
445
- if (replyContext) {
446
- await this.bot.sendMessage(chatId, response, {
447
- reply_to_message_id: messageId
448
- });
449
- }
450
- else {
451
- await this.sendLongMessage(chatId, response);
214
+ const message = this.createChannelMessage(msg);
215
+ console.log(`[TelegramChannel] Message from ${message.sender.displayName}: ${message.text.slice(0, 50)}...`);
216
+ // Route to handler if set
217
+ if (this.messageHandler) {
218
+ try {
219
+ const response = await this.messageHandler(message);
220
+ if (response) {
221
+ await this.send(response);
222
+ }
223
+ }
224
+ catch (error) {
225
+ console.error("[TelegramChannel] Handler error:", error);
226
+ await this.sendMessage(msg.chat.id, `Error: ${error.message}`);
227
+ }
452
228
  }
453
229
  });
454
- // Polling errors
455
- this.bot.on('polling_error', (error) => {
456
- console.error('[Telegram] Polling error:', error.message);
457
- });
458
- // ====================================================================
459
- // Message Edit Handler
460
- // ====================================================================
461
- this.bot.on('edited_message', async (msg) => {
230
+ // Edited messages
231
+ this.bot.on("edited_message", async (msg) => {
462
232
  if (!msg.text)
463
233
  return;
464
- const chatId = msg.chat.id;
465
- const userName = msg.from?.first_name || msg.from?.username || 'User';
466
- const messageId = msg.message_id;
467
- const editDate = msg.edit_date || Date.now();
468
- console.log(`[Telegram] EDIT ${userName}: ${msg.text.slice(0, 50)}...`);
469
- // Update the message in memory
470
- const updated = this.memory.updateByMessageId(chatId, messageId, msg.text, editDate);
471
- if (updated) {
472
- // Message was found and updated - optionally re-process with AI
473
- const response = await this.getGLMResponse(chatId, `[User edited their previous message to: "${msg.text}"]`, userName);
474
- this.memory.add(chatId, 'assistant', response);
475
- await this.sendLongMessage(chatId, `📝 *Edit noted:*\n\n${response}`);
476
- }
477
- else {
478
- // Message not in memory (too old or new) - just acknowledge
479
- console.log(`[Telegram] Edit for unknown message ${messageId}`);
234
+ if (!this.isAllowed(msg.from?.id, msg.chat.id))
235
+ return;
236
+ const message = this.createChannelMessage(msg);
237
+ // Mark as edited in text
238
+ message.text = `[Edited] ${msg.text}`;
239
+ if (this.messageHandler) {
240
+ try {
241
+ const response = await this.messageHandler(message);
242
+ if (response) {
243
+ await this.send(response);
244
+ }
245
+ }
246
+ catch (error) {
247
+ console.error("[TelegramChannel] Handler error:", error);
248
+ }
480
249
  }
481
250
  });
482
- // ====================================================================
483
- // Reaction Handler (Likes/Emojis)
484
- // ====================================================================
485
- // Note: message_reaction requires Telegram Bot API 5.0+
486
- // The type definitions may not include this yet, so we use any
487
- this.bot.on('message_reaction', async (reaction) => {
488
- const chatId = reaction.chat.id;
489
- const messageId = reaction.message_id;
490
- const userId = reaction.user.id;
491
- const userName = reaction.user.first_name || 'User';
492
- // Extract emojis from reactions
493
- const newEmojis = reaction.new_reaction
494
- .filter(r => r.type === 'emoji' && r.emoji)
495
- .map(r => r.emoji);
496
- const oldEmojis = reaction.old_reaction
497
- .filter(r => r.type === 'emoji' && r.emoji)
498
- .map(r => r.emoji);
499
- // Find added reactions
500
- const added = newEmojis.filter(e => !oldEmojis.includes(e));
501
- const removed = oldEmojis.filter(e => !newEmojis.includes(e));
502
- console.log(`[Telegram] REACTION by ${userName}: +${added.join(',')} -${removed.join(',')} on msg ${messageId}`);
503
- // Update memory for each added reaction
504
- for (const emoji of added) {
505
- this.memory.addReaction(chatId, messageId, emoji, userId);
506
- }
507
- // Update memory for each removed reaction
508
- for (const emoji of removed) {
509
- this.memory.removeReaction(chatId, messageId, emoji, userId);
510
- }
511
- // Optionally respond to specific reactions
512
- if (added.includes('👍') || added.includes('❤️')) {
513
- // Positive feedback - could track for learning
514
- console.log(`[Telegram] Positive feedback received on message ${messageId}`);
515
- }
516
- if (added.includes('👎')) {
517
- // Negative feedback - could ask for clarification
518
- await this.bot.sendMessage(chatId, `Thanks for the feedback, ${userName}. I'll try to do better!`);
251
+ // Reactions
252
+ this.bot.on("message_reaction", async (reaction) => {
253
+ const message = {
254
+ messageId: reaction.message_id?.toString(),
255
+ channelId: this.id,
256
+ timestamp: new Date(),
257
+ sender: {
258
+ id: reaction.user?.id?.toString(),
259
+ displayName: reaction.user?.first_name || "User",
260
+ },
261
+ text: "",
262
+ context: { isDM: reaction.chat?.type === "private" },
263
+ metadata: { type: "reaction", reaction: reaction.new_reaction },
264
+ };
265
+ if (this.messageHandler) {
266
+ await this.messageHandler(message);
519
267
  }
520
268
  });
521
- // ====================================================================
522
- // Callback Query Handler (Inline Button Reactions)
523
- // ====================================================================
524
- this.bot.on('callback_query', async (query) => {
269
+ // Callback queries (button clicks)
270
+ this.bot.on("callback_query", async (query) => {
525
271
  const chatId = query.message?.chat.id;
526
- const messageId = query.message?.message_id;
527
- const data = query.data;
528
- const userName = query.from.first_name || 'User';
529
- if (!chatId || !data) {
272
+ if (!chatId || !query.data) {
530
273
  await this.bot.answerCallbackQuery(query.id);
531
274
  return;
532
275
  }
533
- console.log(`[Telegram] CALLBACK ${userName}: ${data}`);
534
- // Parse callback data (format: "action:value")
535
- const [action, value] = data.split(':');
536
- switch (action) {
537
- case 'react': {
538
- // Toggle reaction via inline button
539
- const count = parseInt(value) || 0;
540
- const newCount = count + 1;
541
- await this.bot.editMessageReplyMarkup({
542
- inline_keyboard: [[
543
- { text: `👍 ${newCount}`, callback_data: `react:${newCount}` }
544
- ]]
545
- }, { chat_id: chatId, message_id: messageId });
546
- break;
547
- }
548
- case 'approve': {
549
- // Approval action (useful for workflows)
550
- await this.bot.sendMessage(chatId, `✅ Approved by ${userName}`);
551
- // Could trigger follow-up actions here
552
- break;
553
- }
554
- case 'reject': {
555
- // Rejection action
556
- await this.bot.sendMessage(chatId, `❌ Rejected by ${userName}`);
557
- break;
558
- }
559
- case 'cancel': {
560
- // Cancel current task
561
- await this.bot.sendMessage(chatId, `🛑 Task cancelled by ${userName}`);
562
- break;
276
+ const message = {
277
+ messageId: query.id,
278
+ channelId: this.id,
279
+ timestamp: new Date(),
280
+ sender: {
281
+ id: query.from?.id?.toString(),
282
+ displayName: query.from?.first_name || "User",
283
+ },
284
+ text: query.data,
285
+ context: { isDM: query.message?.chat.type === "private" },
286
+ metadata: { type: "callback" },
287
+ };
288
+ if (this.messageHandler) {
289
+ try {
290
+ const response = await this.messageHandler(message);
291
+ if (response) {
292
+ await this.send(response);
293
+ }
563
294
  }
564
- default: {
565
- // Unknown callback - let AI handle it
566
- const response = await this.getGLMResponse(chatId, `[User clicked button: "${data}". Respond appropriately.]`, userName);
567
- await this.bot.sendMessage(chatId, response);
295
+ catch (error) {
296
+ console.error("[TelegramChannel] Callback handler error:", error);
568
297
  }
569
298
  }
570
- // Always acknowledge the callback to remove loading state
571
299
  await this.bot.answerCallbackQuery(query.id);
572
300
  });
573
- console.log('✅ Telegram bot is running with GLM-4.7!');
574
- if (TELEGRAM_TEST_CHAT_ID) {
575
- await this.sendTestMessage(Number(TELEGRAM_TEST_CHAT_ID));
576
- // Check for crash and report
577
- await this.checkCrashAndReport(Number(TELEGRAM_TEST_CHAT_ID));
578
- }
579
- else {
580
- console.log('\n💡 Tip: Get your chat ID from @userinfobot and set TELEGRAM_TEST_CHAT_ID');
581
- }
301
+ // Polling errors
302
+ this.bot.on("polling_error", (error) => {
303
+ console.error("[TelegramChannel] Polling error:", error.message);
304
+ });
305
+ }
306
+ /**
307
+ * Register Telegram command menu
308
+ */
309
+ async registerCommands() {
310
+ await this.bot.setMyCommands([
311
+ { command: "start", description: "Start the bot" },
312
+ { command: "help", description: "Show available commands" },
313
+ { command: "status", description: "Check bot status" },
314
+ { command: "git", description: "Check git status" },
315
+ { command: "doppler", description: "Check Doppler secrets" },
316
+ { command: "logs", description: "View recent logs" },
317
+ { command: "clear", description: "Clear conversation history" },
318
+ { command: "tools", description: "List available tools" },
319
+ { command: "restart", description: "Restart the bot service" },
320
+ { command: "cancel", description: "Stop current task" },
321
+ { command: "quiet", description: "Hide tool logging" },
322
+ { command: "verbose", description: "Show tool logging" },
323
+ ]);
582
324
  }
583
- async getGLMResponse(chatId, userMessage, userName) {
584
- const apiKey = getZAIKey();
585
- if (!apiKey) {
586
- return '⚠️ Z_AI_API_KEY not configured in Doppler secrets.\n\nPlease set Z_AI_API_KEY to use GLM-4.7.';
325
+ /**
326
+ * Normalize Telegram message to ChannelMessage format.
327
+ */
328
+ createChannelMessage(msg) {
329
+ const sender = {
330
+ id: msg.from?.id?.toString() || msg.chat.id.toString(),
331
+ username: msg.from?.username,
332
+ displayName: msg.from?.first_name || msg.from?.username || "User",
333
+ isBot: msg.from?.is_bot || false,
334
+ };
335
+ const context = {
336
+ isDM: msg.chat.type === "private",
337
+ groupName: msg.chat.type !== "private" ? msg.chat.title : undefined,
338
+ };
339
+ // Build reply context
340
+ let replyText;
341
+ if (msg.reply_to_message?.text) {
342
+ const replyFrom = msg.reply_to_message.from?.first_name || "User";
343
+ replyText = `[Replying to ${replyFrom}: "${msg.reply_to_message.text.slice(0, 100)}"]`;
587
344
  }
588
- // Start typing indicator - will refresh every 3 seconds during processing
589
- this.startTypingIndicator(chatId);
590
- // Get system prompt from structured-prompts store
591
- const store = getStore(process.env.PROMPTS_FILE);
592
- const systemPrompt = store.get('glm-daemon-system');
593
- const systemContent = systemPrompt?.template ?? 'You are GLM Daemon, a helpful AI assistant.';
594
- const temperature = systemPrompt?.metadata?.temperature ?? 0.7;
595
- const maxTokens = systemPrompt?.metadata?.maxTokens ?? 2048;
596
- // Build messages with conversation history
597
- const history = this.memory.get(chatId);
598
- const messages = [
599
- { role: 'system', content: systemContent },
600
- ...history,
601
- { role: 'user', content: userMessage }
602
- ];
603
- try {
604
- console.log(`🔄 Calling GLM-4.7 API for: ${userMessage} (${history.length} history)`);
605
- // Loop to handle tool calls
606
- let iterations = 0;
607
- const maxIterations = 50; // Allow more tool calls for complex tasks
608
- while (iterations < maxIterations) {
609
- iterations++;
610
- const response = await fetchWithRetry(ZAI_API_ENDPOINT, {
611
- method: 'POST',
612
- headers: {
613
- 'Content-Type': 'application/json',
614
- 'Authorization': `Bearer ${apiKey}`
615
- },
616
- body: JSON.stringify({
617
- model: 'glm-4.7',
618
- messages,
619
- tools: getGLMTools(),
620
- temperature,
621
- max_tokens: maxTokens
622
- })
623
- }, 3, 1000);
624
- const data = await response.json();
625
- const message = data.choices?.[0]?.message;
626
- if (!message) {
627
- console.error('❌ Unexpected API response format:', data);
628
- return '❌ Unexpected response from AI API.';
629
- }
630
- // Check if LLM wants to call tools
631
- if (message.tool_calls && message.tool_calls.length > 0) {
632
- console.log(`🔧 LLM calling ${message.tool_calls.length} tool(s)`);
633
- // Add assistant message with tool calls to history
634
- messages.push({
635
- role: 'assistant',
636
- tool_calls: message.tool_calls
637
- });
638
- // Check quiet mode for Telegram messages
639
- const quiet = isQuiet();
640
- // Execute each tool and add results
641
- for (const toolCall of message.tool_calls) {
642
- const toolName = toolCall.function.name;
643
- const args = JSON.parse(toolCall.function.arguments);
644
- console.log(` → ${toolName}(${JSON.stringify(args)})`);
645
- // Send tool call notification to Telegram (skip if quiet)
646
- if (!quiet) {
647
- // Sanitize to prevent entity parsing errors - strip Markdown special chars
648
- const sanitizeForTelegram = (str) => {
649
- return str
650
- .replace(/[_*[\]()~`>#+=|{}.!-]/g, '') // Remove Markdown special chars
651
- .replace(/\\/g, '') // Remove backslashes
652
- .slice(0, 100); // Truncate to prevent long messages
653
- };
654
- const argsPreview = Object.keys(args).length > 0
655
- ? sanitizeForTelegram(JSON.stringify(args))
656
- : '';
657
- await this.bot.sendMessage(chatId, `🔧 ${toolName}${argsPreview ? ' ' + argsPreview : ''}`);
658
- }
659
- // Execute tool locally
660
- const result = await executeTool(toolName, args);
661
- console.log(` ← ${result.slice(0, 100)}...`);
662
- // Send result to Telegram (skip if quiet)
663
- if (!quiet) {
664
- const sanitizedResult = result
665
- .replace(/[_*[\]()~`>#+=|{}.!-]/g, '')
666
- .replace(/\\/g, '');
667
- const resultPreview = sanitizedResult.length > 500 ? sanitizedResult.slice(0, 500) + ' [truncated]' : sanitizedResult;
668
- await this.bot.sendMessage(chatId, `📥 ${resultPreview}`);
669
- }
670
- // Add tool result to messages
671
- messages.push({
672
- role: 'tool',
673
- name: toolName,
674
- content: result
675
- });
676
- }
677
- // Continue loop to get final response
678
- continue;
679
- }
680
- // No tool calls - we have the final response
681
- if (message.content) {
682
- console.log(`✅ GLM-4.7 response: ${message.content.slice(0, 100)}...`);
683
- return message.content;
345
+ return {
346
+ messageId: msg.message_id.toString(),
347
+ channelId: this.id,
348
+ timestamp: new Date(msg.date * 1000),
349
+ sender,
350
+ text: replyText ? `${replyText}\n\n${msg.text || ""}` : (msg.text || ""),
351
+ context,
352
+ replyTo: msg.reply_to_message
353
+ ? {
354
+ messageId: msg.reply_to_message.message_id.toString(),
355
+ channelId: this.id,
684
356
  }
685
- // No content and no tool calls - something weird
686
- return '❌ Empty response from AI.';
687
- }
688
- return '⚠️ Reached tool limit (50). The AI was being very thorough! Try a more specific request.';
689
- }
690
- catch (error) {
691
- console.error('❌ Error calling GLM-4.7:', error.message);
692
- if (error.message.includes('429')) {
693
- return '⚠️ Rate limited by API. Please try again in a moment.';
694
- }
695
- return `❌ Error: ${error.message}`;
696
- }
697
- finally {
698
- // Always stop typing indicator when done
699
- this.stopTypingIndicator(chatId);
700
- }
357
+ : undefined,
358
+ };
701
359
  }
702
- async sendTestMessage(chatId) {
703
- const apiKey = getZAIKey();
704
- await this.bot.sendMessage(chatId, '✅ GLM Daemon Telegram Bot is NOW ONLINE!\n\n' +
705
- '🎉 Connected from seed-node-prod!\n\n' +
706
- `🧠 AI: ${apiKey ? 'GLM-4.7 via Z.AI' : 'Not configured'}\n\n` +
707
- 'Commands:\n' +
708
- '/start - Show welcome message\n' +
709
- '/status - Check API status\n' +
710
- '/git - Git & GitHub status\n' +
711
- '/doppler - Doppler config\n' +
712
- '/help - Show all commands\n' +
713
- 'Any message - Chat with GLM-4.7 AI');
714
- console.log(`✅ Test message sent to chat ${chatId}`);
715
- }
716
- async stop() {
717
- console.log('🛑 Stopping Telegram bot...');
718
- this.bot.stopPolling();
360
+ extractChatId(replyTo) {
361
+ const accountId = replyTo.channelId.accountId;
362
+ const parsed = parseInt(accountId, 10);
363
+ if (!isNaN(parsed))
364
+ return parsed;
365
+ return null;
719
366
  }
720
367
  /**
721
- * Check for crash recovery - analyze previous session and report
722
- * Always sends a startup message (crash or normal)
368
+ * Send long message by splitting into chunks.
723
369
  */
724
- async checkCrashAndReport(chatId) {
725
- try {
726
- // Get previous boot/session logs to check for crash
727
- const crashLog = execSync('journalctl -u telegram-bot -b -1 -n 50 --no-pager 2>/dev/null || journalctl -u telegram-bot --since "5 minutes ago" -n 50 --no-pager 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
728
- // Look for crash indicators
729
- const crashIndicators = ['Error:', 'error', 'crashed', 'killed', 'ETELEGRAM', '400 Bad Request', 'exception'];
730
- const hasCrash = crashLog && crashIndicators.some(ind => crashLog.toLowerCase().includes(ind.toLowerCase()));
731
- if (hasCrash) {
732
- // Crash detected - analyze and report
733
- console.log('🔍 Crash detected, analyzing...');
734
- await this.bot.sendMessage(chatId, '🔄 I detected a crash in my previous session. Analyzing...');
735
- // Extract error lines
736
- const errorLines = crashLog.split('\n')
737
- .filter(line => crashIndicators.some(ind => line.toLowerCase().includes(ind.toLowerCase())))
738
- .slice(-10)
739
- .join('\n');
740
- if (errorLines) {
741
- // Ask LLM to diagnose
742
- const diagnosis = await this.getGLMResponse(chatId, `Analyze this crash log and tell me what went wrong in one short paragraph, then suggest a fix:
743
-
744
- \`\`\`
745
- ${errorLines.slice(0, 1000)}
746
- \`\`\`
747
-
748
- Format: "Issue: [what happened]. Fix: [how to fix it]"`, 'CrashAnalyzer');
749
- await this.sendLongMessage(chatId, `🏥 Crash Diagnosis:\n\n${diagnosis}`);
750
- }
751
- }
752
- else {
753
- // Normal start - no crash detected
754
- await this.bot.sendMessage(chatId, '✅ Bot started normally.\n\n' +
755
- 'Previous session ended cleanly. Ready to assist!');
370
+ async sendLongMessage(chatId, text, options) {
371
+ const MAX_LENGTH = 4000;
372
+ if (text.length <= MAX_LENGTH) {
373
+ await this.bot.sendMessage(chatId, text, options);
374
+ return;
375
+ }
376
+ const chunks = [];
377
+ let remaining = text;
378
+ while (remaining.length > 0) {
379
+ if (remaining.length <= MAX_LENGTH) {
380
+ chunks.push(remaining);
381
+ break;
756
382
  }
383
+ let breakPoint = remaining.lastIndexOf("\n", MAX_LENGTH);
384
+ if (breakPoint < 1000)
385
+ breakPoint = remaining.lastIndexOf(" ", MAX_LENGTH);
386
+ if (breakPoint < 1000)
387
+ breakPoint = MAX_LENGTH;
388
+ chunks.push(remaining.slice(0, breakPoint));
389
+ remaining = remaining.slice(breakPoint).trim();
757
390
  }
758
- catch (e) {
759
- // If we can't check logs, still notify of normal start
760
- console.log('Crash check skipped:', e.message);
761
- await this.bot.sendMessage(chatId, '✅ Bot started fresh.\n\n' +
762
- 'No previous session data available. Ready to assist!');
391
+ for (let i = 0; i < chunks.length; i++) {
392
+ const prefix = chunks.length > 1 ? `[${i + 1}/${chunks.length}] ` : "";
393
+ await this.bot.sendMessage(chatId, prefix + chunks[i], options);
763
394
  }
764
395
  }
765
396
  }
766
- // Main entry point
397
+ // ============================================================
398
+ // FACTORY
399
+ // ============================================================
400
+ export function createTelegramChannel(config) {
401
+ return new TelegramChannel(config);
402
+ }
403
+ // ============================================================
404
+ // MAIN ENTRY POINT (for standalone testing)
405
+ // ============================================================
767
406
  async function main() {
768
- if (!TELEGRAM_BOT_TOKEN) {
769
- console.error('❌ TELEGRAM_BOT_TOKEN not found in Doppler secrets');
770
- console.error('Make sure to set in your seed/prd Doppler project');
407
+ const config = createTelegramConfigFromEnv();
408
+ if (!config.botToken) {
409
+ console.error("TELEGRAM_BOT_TOKEN not found in environment");
771
410
  process.exit(1);
772
411
  }
773
- // Ensure prompts are seeded
774
- const promptsFile = process.env.PROMPTS_FILE;
775
- const store = getStore(promptsFile);
776
- if (!store.get('glm-daemon-system')) {
777
- console.log('📝 Seeding prompts...');
778
- seedPrompts(promptsFile);
779
- }
780
- console.log(`📱 Bot token loaded: ${TELEGRAM_BOT_TOKEN.substring(0, 10)}...`);
781
- console.log(`🤖 Bot name: @SimulationapiBot`);
782
- const bot = new TelegramGLMBot(TELEGRAM_BOT_TOKEN);
783
- await bot.start();
784
- process.on('SIGINT', async () => {
785
- await bot.stop();
412
+ const channel = createTelegramChannel(config);
413
+ // Example: Echo handler for testing
414
+ channel.onMessage(async (msg) => {
415
+ console.log(`Received: ${msg.text}`);
416
+ return {
417
+ content: { text: `Echo: ${msg.text}` },
418
+ replyTo: { messageId: msg.messageId, channelId: msg.channelId },
419
+ };
420
+ });
421
+ process.on("SIGINT", async () => {
422
+ await channel.stop();
786
423
  process.exit(0);
787
424
  });
788
- process.on('SIGTERM', async () => {
789
- await bot.stop();
425
+ process.on("SIGTERM", async () => {
426
+ await channel.stop();
790
427
  process.exit(0);
791
428
  });
429
+ await channel.start();
792
430
  }
793
431
  main().catch(console.error);
794
432
  //# sourceMappingURL=index.js.map