@link-assistant/hive-mind 1.40.2 → 1.42.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.42.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5aa82f5: Add /stop and /start commands for telegram bot to control task acceptance per chat (Issue #1081)
8
+
9
+ ## 1.41.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 2c9396d: feat: simplify Dockerfile — bump sandbox 1.5.0→1.6.0, remove Playwright setup, eliminate USER root, remove silent fallbacks (#1505)
14
+
3
15
  ## 1.40.2
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.40.2",
3
+ "version": "1.42.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -53,6 +53,7 @@ const { formatUsageMessage, getAllCachedLimits } = await import('./limits.lib.mj
53
53
  const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
54
54
  const { escapeMarkdown, escapeMarkdownV2, cleanNonPrintableChars, makeSpecialCharsVisible } = await import('./telegram-markdown.lib.mjs');
55
55
  const { getSolveQueue, createQueueExecuteCallback } = await import('./telegram-solve-queue.lib.mjs');
56
+ const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STOP_REASON } = await import('./telegram-start-stop-command.lib.mjs');
56
57
  const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
57
58
  // Import bot launcher with exponential backoff retry (issue #1240)
58
59
  const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
@@ -603,23 +604,17 @@ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args,
603
604
  }
604
605
 
605
606
  bot.command('help', async ctx => {
606
- if (VERBOSE) {
607
- console.log('[VERBOSE] /help command received');
608
- }
607
+ VERBOSE && console.log('[VERBOSE] /help command received');
609
608
 
610
609
  // Ignore messages sent before bot started
611
610
  if (isOldMessage(ctx)) {
612
- if (VERBOSE) {
613
- console.log('[VERBOSE] /help ignored: old message');
614
- }
611
+ VERBOSE && console.log('[VERBOSE] /help ignored: old message');
615
612
  return;
616
613
  }
617
614
 
618
615
  // Ignore forwarded or reply messages
619
616
  if (isForwardedOrReply(ctx)) {
620
- if (VERBOSE) {
621
- console.log('[VERBOSE] /help ignored: forwarded or reply');
622
- }
617
+ VERBOSE && console.log('[VERBOSE] /help ignored: forwarded or reply');
623
618
  return;
624
619
  }
625
620
 
@@ -628,6 +623,19 @@ bot.command('help', async ctx => {
628
623
  const chatTitle = ctx.chat.title || 'Private Chat';
629
624
 
630
625
  let message = '🤖 *SwarmMindBot Help*\n\n';
626
+
627
+ // Show stopped status if chat is stopped (issue #1081)
628
+ if (isChatStopped(chatId)) {
629
+ const stopInfo = getChatStopInfo(chatId);
630
+ const reason = stopInfo?.reason || DEFAULT_STOP_REASON;
631
+ message += '🛑 *Bot Status: STOPPED*\n';
632
+ message += `Reason: ${reason}\n`;
633
+ if (stopInfo?.stoppedAt) {
634
+ message += `Stopped: ${stopInfo.stoppedAt.toISOString()}\n`;
635
+ }
636
+ message += 'Use /start (chat owner only) to resume.\n\n';
637
+ }
638
+
631
639
  message += '📋 *Diagnostic Information:*\n';
632
640
  message += `• Chat ID: \`${chatId}\`\n`;
633
641
  message += `• Chat Type: ${chatType}\n`;
@@ -666,8 +674,10 @@ bot.command('help', async ctx => {
666
674
  message += '*/merge* - Merge queue (experimental)\n';
667
675
  message += 'Usage: `/merge <github-repo-url>`\n';
668
676
  message += "Merges all PRs with 'ready' label sequentially.\n";
669
- message += '*/help* - Show this help message\n\n';
670
- message += '⚠️ *Note:* /solve, /hive, /solve\\_queue, /limits, /version, /accept\\_invites and /merge commands only work in group chats.\n\n';
677
+ message += '*/help* - Show this help message\n';
678
+ message += '*/stop* - Stop accepting new tasks (owner only)\n';
679
+ message += '*/start* - Resume accepting tasks (owner only)\n\n';
680
+ message += '⚠️ *Note:* /solve, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats.\n\n';
671
681
  message += '🔧 *Common Options:*\n';
672
682
  message += `• \`--model <model>\` or \`-m\` - ${buildModelOptionDescription()}\n`;
673
683
  message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
@@ -693,36 +703,20 @@ bot.command('help', async ctx => {
693
703
  });
694
704
 
695
705
  bot.command('limits', async ctx => {
696
- if (VERBOSE) {
697
- console.log('[VERBOSE] /limits command received');
698
- }
706
+ VERBOSE && console.log('[VERBOSE] /limits command received');
699
707
 
700
708
  // Add breadcrumb for error tracking
701
- await addBreadcrumb({
702
- category: 'telegram.command',
703
- message: '/limits command received',
704
- level: 'info',
705
- data: {
706
- chatId: ctx.chat?.id,
707
- chatType: ctx.chat?.type,
708
- userId: ctx.from?.id,
709
- username: ctx.from?.username,
710
- },
711
- });
709
+ await addBreadcrumb({ category: 'telegram.command', message: '/limits command received', level: 'info', data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username } });
712
710
 
713
711
  // Ignore messages sent before bot started
714
712
  if (isOldMessage(ctx)) {
715
- if (VERBOSE) {
716
- console.log('[VERBOSE] /limits ignored: old message');
717
- }
713
+ VERBOSE && console.log('[VERBOSE] /limits ignored: old message');
718
714
  return;
719
715
  }
720
716
 
721
717
  // Ignore forwarded or reply messages
722
718
  if (isForwardedOrReply(ctx)) {
723
- if (VERBOSE) {
724
- console.log('[VERBOSE] /limits ignored: forwarded or reply');
725
- }
719
+ VERBOSE && console.log('[VERBOSE] /limits ignored: forwarded or reply');
726
720
  return;
727
721
  }
728
722
 
@@ -736,9 +730,7 @@ bot.command('limits', async ctx => {
736
730
 
737
731
  const chatId = ctx.chat.id;
738
732
  if (!isChatAuthorized(chatId)) {
739
- if (VERBOSE) {
740
- console.log('[VERBOSE] /limits ignored: chat not authorized');
741
- }
733
+ VERBOSE && console.log('[VERBOSE] /limits ignored: chat not authorized');
742
734
  await ctx.reply(`❌ This chat (ID: ${chatId}) is not authorized to use this bot. Please contact the bot administrator.`, { reply_to_message_id: ctx.message.message_id });
743
735
  return;
744
736
  }
@@ -807,6 +799,8 @@ registerMergeCommand(bot, {
807
799
  isGroupChat: _isGroupChat,
808
800
  isChatAuthorized,
809
801
  addBreadcrumb,
802
+ isChatStopped,
803
+ getStoppedChatRejectMessage,
810
804
  });
811
805
 
812
806
  // Register /solve_queue command from separate module (issue #1232)
@@ -886,10 +880,15 @@ async function handleSolveCommand(ctx) {
886
880
  return;
887
881
  }
888
882
 
889
- if (VERBOSE) {
890
- console.log('[VERBOSE] /solve passed all checks, executing...');
883
+ // Check if chat is stopped (issue #1081) - reject with same style as queue rejected mode
884
+ if (isChatStopped(chatId)) {
885
+ VERBOSE && console.log('[VERBOSE] /solve rejected: chat is stopped');
886
+ await safeReply(ctx, getStoppedChatRejectMessage(chatId, 'Solve'), { reply_to_message_id: ctx.message.message_id });
887
+ return;
891
888
  }
892
889
 
890
+ VERBOSE && console.log('[VERBOSE] /solve passed all checks, executing...');
891
+
893
892
  let userArgs = parseCommandArgs(ctx.message.text);
894
893
 
895
894
  // Check if this is a reply to a message and user didn't provide URL as first argument
@@ -1109,10 +1108,15 @@ async function handleHiveCommand(ctx) {
1109
1108
  return;
1110
1109
  }
1111
1110
 
1112
- if (VERBOSE) {
1113
- console.log('[VERBOSE] /hive passed all checks, executing...');
1111
+ // Check if chat is stopped (issue #1081) - reject with same style as queue rejected mode
1112
+ if (isChatStopped(chatId)) {
1113
+ VERBOSE && console.log('[VERBOSE] /hive rejected: chat is stopped');
1114
+ await safeReply(ctx, getStoppedChatRejectMessage(chatId, 'Hive'), { reply_to_message_id: ctx.message.message_id });
1115
+ return;
1114
1116
  }
1115
1117
 
1118
+ VERBOSE && console.log('[VERBOSE] /hive passed all checks, executing...');
1119
+
1116
1120
  const userArgs = parseCommandArgs(ctx.message.text);
1117
1121
 
1118
1122
  // Issue #1102: Allow issues_list/pulls_list URLs and normalize to repo URLs
@@ -1197,16 +1201,12 @@ async function handleHiveCommand(ctx) {
1197
1201
 
1198
1202
  bot.command(/^hive$/i, handleHiveCommand);
1199
1203
 
1200
- // Register /top command from separate module
1201
- // This keeps telegram-bot.mjs under the 1500 line limit
1204
+ // Register commands from separate modules (keeps telegram-bot.mjs under line limit)
1202
1205
  const { registerTopCommand } = await import('./telegram-top-command.lib.mjs');
1203
- registerTopCommand(bot, {
1204
- VERBOSE,
1205
- isOldMessage,
1206
- isForwardedOrReply,
1207
- isGroupChat: _isGroupChat,
1208
- isChatAuthorized,
1209
- });
1206
+ const { registerStartStopCommands } = await import('./telegram-start-stop-command.lib.mjs');
1207
+ const commandOptions = { VERBOSE, isOldMessage, isForwardedOrReply, isGroupChat: _isGroupChat, isChatAuthorized };
1208
+ registerTopCommand(bot, commandOptions);
1209
+ registerStartStopCommands(bot, commandOptions); // issue #1081
1210
1210
 
1211
1211
  // Add message listener for verbose debugging
1212
1212
  if (VERBOSE) {
@@ -133,7 +133,7 @@ function formatUserError(error, verbose) {
133
133
  * @param {Function} options.addBreadcrumb - Function to add breadcrumbs for monitoring
134
134
  */
135
135
  export function registerMergeCommand(bot, options) {
136
- const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, addBreadcrumb } = options;
136
+ const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, addBreadcrumb, isChatStopped, getStoppedChatRejectMessage } = options;
137
137
 
138
138
  bot.command(/^merge$/i, async ctx => {
139
139
  VERBOSE && console.log('[VERBOSE] /merge command received');
@@ -161,6 +161,13 @@ export function registerMergeCommand(bot, options) {
161
161
  });
162
162
  }
163
163
 
164
+ // Check if chat is stopped (issue #1081) - reject with same style as queue rejected mode
165
+ if (isChatStopped && isChatStopped(chatId)) {
166
+ VERBOSE && console.log('[VERBOSE] /merge rejected: chat is stopped');
167
+ const rejectMsg = getStoppedChatRejectMessage ? getStoppedChatRejectMessage(chatId, 'Merge') : '❌ Merge command rejected.';
168
+ return await ctx.reply(rejectMsg, { reply_to_message_id: ctx.message.message_id });
169
+ }
170
+
164
171
  // Parse arguments
165
172
  const args = parseCommandArgs(ctx.message.text);
166
173
 
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Telegram /start and /stop command implementation
3
+ *
4
+ * This module provides the /start and /stop command functionality for the Telegram bot,
5
+ * allowing chat owners to control whether the bot accepts new tasks in a specific chat.
6
+ *
7
+ * Features:
8
+ * - Per-chat stop state management
9
+ * - Owner-only access control (creator only, not admins)
10
+ * - Graceful stop: existing queue items continue to process
11
+ * - Read-only commands (/help, /limits, /version) remain available when stopped
12
+ * - Write commands (/solve, /hive) are rejected when stopped
13
+ *
14
+ * @see https://github.com/link-assistant/hive-mind/issues/1081
15
+ */
16
+
17
+ // Store stopped chats: Map<chatId, { stoppedAt: Date, stoppedBy: { id, username, firstName }, reason?: string }>
18
+ const stoppedChats = new Map();
19
+
20
+ /**
21
+ * Check if a chat is currently stopped
22
+ * @param {number} chatId - The chat ID to check
23
+ * @returns {boolean} True if the chat is stopped
24
+ */
25
+ export function isChatStopped(chatId) {
26
+ return stoppedChats.has(chatId);
27
+ }
28
+
29
+ /**
30
+ * Get stop information for a chat
31
+ * @param {number} chatId - The chat ID
32
+ * @returns {Object|null} Stop info or null if not stopped
33
+ */
34
+ export function getChatStopInfo(chatId) {
35
+ return stoppedChats.get(chatId) || null;
36
+ }
37
+
38
+ /**
39
+ * Set chat stopped state
40
+ * @param {number} chatId - The chat ID
41
+ * @param {boolean} stopped - Whether to stop or start the chat
42
+ * @param {Object} user - The user who issued the command (for stop)
43
+ * @param {string} [reason] - Optional reason for stopping (only used when stopped=true)
44
+ */
45
+ export function setChatStopped(chatId, stopped, user = null, reason = null) {
46
+ if (stopped) {
47
+ stoppedChats.set(chatId, {
48
+ stoppedAt: new Date(),
49
+ stoppedBy: user
50
+ ? {
51
+ id: user.id,
52
+ username: user.username,
53
+ firstName: user.first_name,
54
+ }
55
+ : null,
56
+ reason: reason || null,
57
+ });
58
+ } else {
59
+ stoppedChats.delete(chatId);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get all stopped chats (for debugging/admin purposes)
65
+ * @returns {Map} Map of stopped chats
66
+ */
67
+ export function getStoppedChats() {
68
+ return stoppedChats;
69
+ }
70
+
71
+ /**
72
+ * Default reason used when no custom reason is provided for /stop
73
+ */
74
+ export const DEFAULT_STOP_REASON = 'This bot is currently not accepting new tasks.';
75
+
76
+ /**
77
+ * Get rejection message for when a command is used on a stopped chat.
78
+ * Matches the style of queue `rejected` mode output for consistency.
79
+ * @param {number} chatId - The chat ID
80
+ * @param {string} commandName - The command that was rejected (e.g., 'Solve', 'Hive')
81
+ * @returns {string} Markdown-formatted rejection message
82
+ */
83
+ export function getStoppedChatRejectMessage(chatId, commandName = 'Command') {
84
+ const stopInfo = getChatStopInfo(chatId);
85
+ const reason = stopInfo?.reason || DEFAULT_STOP_REASON;
86
+ return `❌ ${commandName} command rejected.\n\n🚫 Reason: ${reason}\n\nUse /start to resume (chat owner only).`;
87
+ }
88
+
89
+ /**
90
+ * Registers the /start and /stop command handlers with the bot
91
+ * @param {Object} bot - The Telegraf bot instance
92
+ * @param {Object} options - Options object
93
+ * @param {boolean} options.VERBOSE - Whether to enable verbose logging
94
+ * @param {Function} options.isOldMessage - Function to check if message is old
95
+ * @param {Function} options.isForwardedOrReply - Function to check if message is forwarded/reply
96
+ * @param {Function} options.isGroupChat - Function to check if chat is a group
97
+ * @param {Function} options.isChatAuthorized - Function to check if chat is authorized
98
+ */
99
+ export function registerStartStopCommands(bot, options) {
100
+ const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized } = options;
101
+
102
+ /**
103
+ * Validate command context: checks old message, forwarded, group chat, authorized, and owner status.
104
+ * @param {Object} ctx - Telegraf context
105
+ * @param {string} cmdName - Command name for logging (e.g., '/stop', '/start')
106
+ * @param {Object} [opts] - Options
107
+ * @param {boolean} [opts.allowPrivate] - If true, skip group chat check (for /start welcome)
108
+ * @returns {Promise<{valid: boolean, chatId?: number, isPrivate?: boolean}>}
109
+ */
110
+ async function validateOwnerCommand(ctx, cmdName, opts = {}) {
111
+ VERBOSE && console.log(`[VERBOSE] ${cmdName} command received`);
112
+ if (isOldMessage(ctx)) {
113
+ VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: old message`);
114
+ return { valid: false };
115
+ }
116
+ if (isForwardedOrReply(ctx)) {
117
+ VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: forwarded or reply`);
118
+ return { valid: false };
119
+ }
120
+ if (!isGroupChat(ctx)) {
121
+ if (opts.allowPrivate) return { valid: false, isPrivate: true };
122
+ VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: not a group chat`);
123
+ await ctx.reply(`❌ The ${cmdName} command only works in group chats.`, { reply_to_message_id: ctx.message.message_id });
124
+ return { valid: false };
125
+ }
126
+ const chatId = ctx.chat.id;
127
+ if (!isChatAuthorized(chatId)) {
128
+ VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: chat not authorized`);
129
+ await ctx.reply(`❌ This chat (ID: ${chatId}) is not authorized to use this bot.`, { reply_to_message_id: ctx.message.message_id });
130
+ return { valid: false };
131
+ }
132
+ try {
133
+ const chatMember = await ctx.telegram.getChatMember(chatId, ctx.from.id);
134
+ if (chatMember.status !== 'creator') {
135
+ VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: user is not chat owner`);
136
+ await ctx.reply('❌ This command is only available to the chat owner.', { reply_to_message_id: ctx.message.message_id });
137
+ return { valid: false };
138
+ }
139
+ } catch (error) {
140
+ console.error('[ERROR] Failed to check chat member status:', error);
141
+ await ctx.reply('❌ Failed to verify permissions.', { reply_to_message_id: ctx.message.message_id });
142
+ return { valid: false };
143
+ }
144
+ VERBOSE && console.log(`[VERBOSE] ${cmdName} passed all checks`);
145
+ return { valid: true, chatId };
146
+ }
147
+
148
+ // /stop command - stop accepting new tasks in this chat
149
+ // Only accessible by chat owner (creator)
150
+ bot.command('stop', async ctx => {
151
+ const check = await validateOwnerCommand(ctx, '/stop');
152
+ if (!check.valid) return;
153
+ const chatId = check.chatId;
154
+
155
+ // Check if already stopped
156
+ if (isChatStopped(chatId)) {
157
+ const stopInfo = getChatStopInfo(chatId);
158
+ const stoppedAtStr = stopInfo?.stoppedAt ? stopInfo.stoppedAt.toISOString() : 'unknown';
159
+ let alreadyStoppedMsg = `ℹ️ Bot is already stopped in this chat.\n\nStopped at: ${stoppedAtStr}`;
160
+ if (stopInfo?.reason) {
161
+ alreadyStoppedMsg += `\nReason: ${stopInfo.reason}`;
162
+ }
163
+ alreadyStoppedMsg += '\n\nUse /start to resume accepting tasks.';
164
+ await ctx.reply(alreadyStoppedMsg, {
165
+ reply_to_message_id: ctx.message.message_id,
166
+ });
167
+ return;
168
+ }
169
+
170
+ // Parse optional reason from message text (anything after "/stop ")
171
+ // Supports: /stop reason, /stop "reason", /stop 'reason'
172
+ const messageText = ctx.message.text || '';
173
+ let reason = messageText.replace(/^\/stop(@\w+)?\s*/i, '').trim() || null;
174
+ // Strip surrounding quotes (single or double) from reason
175
+ if (reason && ((reason.startsWith('"') && reason.endsWith('"')) || (reason.startsWith("'") && reason.endsWith("'")))) {
176
+ reason = reason.slice(1, -1).trim() || null;
177
+ }
178
+
179
+ if (VERBOSE && reason) {
180
+ console.log(`[VERBOSE] /stop reason: ${reason}`);
181
+ }
182
+
183
+ // Set chat as stopped with optional reason
184
+ setChatStopped(chatId, true, ctx.from, reason);
185
+
186
+ if (VERBOSE) {
187
+ console.log(`[VERBOSE] Chat ${chatId} is now stopped`);
188
+ }
189
+
190
+ let stopMessage = '🛑 *Bot Stopped*\n\n' + 'This bot is now in read-only mode for this chat.\n\n';
191
+ if (reason) {
192
+ stopMessage += `*Reason:* ${reason}\n\n`;
193
+ }
194
+ stopMessage += '*Disabled commands:*\n' + '• /solve - No new issues will be accepted\n' + '• /hive - No new hive commands will be accepted\n' + '• /merge - No new merge operations will be accepted\n\n' + '*Still available:*\n' + '• /help - Show help\n' + '• /limits - Show usage limits\n' + '• /version - Show version info\n' + '• /start - Resume accepting tasks (owner only)\n\n' + '💡 Any tasks already in queue will continue to process.';
195
+
196
+ await ctx.reply(stopMessage, {
197
+ parse_mode: 'Markdown',
198
+ reply_to_message_id: ctx.message.message_id,
199
+ });
200
+ });
201
+
202
+ // /start command - resume accepting new tasks in this chat
203
+ // Only accessible by chat owner (creator)
204
+ // Note: This overrides Telegram's default /start behavior, but that's intentional
205
+ // as in group chats we want this to control the bot's task acceptance
206
+ bot.command('start', async ctx => {
207
+ const check = await validateOwnerCommand(ctx, '/start', { allowPrivate: true });
208
+ if (!check.valid) {
209
+ // In private chats, show a welcome message instead
210
+ if (check.isPrivate) {
211
+ VERBOSE && console.log('[VERBOSE] /start in private chat: showing welcome');
212
+ await ctx.reply('👋 *Welcome to SwarmMindBot!*\n\n' + 'This bot helps solve GitHub issues using AI.\n\n' + 'To use this bot:\n' + '1. Add me to a group chat\n' + '2. Make me an admin\n' + '3. Use /solve to solve GitHub issues\n\n' + 'Use /help in a group chat for more information.', { parse_mode: 'Markdown' });
213
+ }
214
+ return;
215
+ }
216
+ const chatId = check.chatId;
217
+
218
+ // Check if already running (not stopped)
219
+ if (!isChatStopped(chatId)) {
220
+ await ctx.reply('ℹ️ Bot is already accepting tasks in this chat.\n\nUse /help to see available commands.', {
221
+ reply_to_message_id: ctx.message.message_id,
222
+ });
223
+ return;
224
+ }
225
+
226
+ // Get stop info for the message
227
+ const stopInfo = getChatStopInfo(chatId);
228
+ const stoppedDuration = stopInfo?.stoppedAt ? Math.round((Date.now() - stopInfo.stoppedAt.getTime()) / 1000) : 0;
229
+
230
+ // Clear the stopped state
231
+ setChatStopped(chatId, false);
232
+
233
+ if (VERBOSE) {
234
+ console.log(`[VERBOSE] Chat ${chatId} is now started`);
235
+ }
236
+
237
+ let durationStr = '';
238
+ if (stoppedDuration > 0) {
239
+ if (stoppedDuration < 60) {
240
+ durationStr = `${stoppedDuration} seconds`;
241
+ } else if (stoppedDuration < 3600) {
242
+ durationStr = `${Math.round(stoppedDuration / 60)} minutes`;
243
+ } else {
244
+ durationStr = `${Math.round(stoppedDuration / 3600)} hours`;
245
+ }
246
+ }
247
+
248
+ await ctx.reply('✅ *Bot Started*\n\n' + 'This bot is now accepting tasks in this chat.\n\n' + (durationStr ? `Bot was stopped for ${durationStr}.\n\n` : '') + 'Use /help to see available commands.', {
249
+ parse_mode: 'Markdown',
250
+ reply_to_message_id: ctx.message.message_id,
251
+ });
252
+ });
253
+ }