this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Enhance announcement feature to support multiple buttons

- Updated the announcement creation workflow to allow users to add multiple buttons (up to 6) instead of a single button.
- Refactored the button handling logic in AnnouncementCreationHelper and AnnouncementManager to accommodate the new buttons array format.
- Migrated existing announcements with a single button to the new format during the loading process.
- Improved the user interface for managing buttons, including options to add and remove buttons.
- Updated confirmation messages and success messages to reflect the changes in button handling.
- Added a syntax check script to package.json for ensuring code quality.

+667 -396
+3
.gitignore
··· 2 2 .env 3 3 announcements.json 4 4 5 + # AI Assistant instructions (local only) 6 + CLAUDE.md 7 + 5 8 # Queue files 6 9 queue/queue.json 7 10 queue/queue.json.backup
+1 -311
bot/telegrambot/commands/announce.js
··· 1 + // filepath: /home/hstafford/stagehand/bot/telegrambot/commands/announce.js 1 2 const AnnouncementCreationHelper = require('../helpers/announcementCreationHelper'); 2 3 3 4 /** ··· 22 23 23 24 // Use the creation helper to start the workflow 24 25 await this.creationHelper.startCreationWorkflow(chatId, msg.from.id, 'command'); 25 - }); 26 - } 27 - 28 - handleNameStep(chatId, userId, messageId) { 29 - this.bot.onReplyToMessage(chatId, messageId, async (nameMsg) => { 30 - const announcementName = nameMsg.text === 'skip' ? '' : nameMsg.text; 31 - this.pendingAnnouncements[userId].name = announcementName; 32 - 33 - // Now ask for the announcement message text 34 - this.bot.sendMessage( 35 - chatId, 36 - 'Great! Now enter the announcement message content.\n\n' + 37 - 'Your message can contain multiple lines and formatting:\n' + 38 - '- *text* for italic\n' + 39 - '- **text** for bold\n' + 40 - '- __text__ for underlined\n' + 41 - '- ~~text~~ for strikethrough\n\n' + 42 - 'Type your message now:', 43 - { 44 - parse_mode: 'Markdown', 45 - reply_markup: { force_reply: true } 46 - } 47 - ).then(messagePrompt => { 48 - this.handleMessageStep(chatId, userId, messagePrompt.message_id); 49 - }); 50 - }); 51 - } 52 - 53 - handleMessageStep(chatId, userId, messageId) { 54 - this.bot.onReplyToMessage(chatId, messageId, async (messageTextMsg) => { 55 - this.pendingAnnouncements[userId].message = messageTextMsg.text; 56 - 57 - try { 58 - // Show a preview of the formatted message 59 - const previewText = this.announcements.formatMessageText(this.pendingAnnouncements[userId].message); 60 - 61 - // Send a preview message to show how it will look 62 - await this.bot.sendMessage( 63 - chatId, 64 - "Here's a preview of your announcement with formatting:", 65 - { parse_mode: 'Markdown' } 66 - ); 67 - 68 - // Send the actual preview 69 - await this.bot.sendMessage( 70 - chatId, 71 - previewText, 72 - { parse_mode: 'HTML' } 73 - ); 74 - } catch (error) { 75 - console.error("Error showing announcement preview:", error); 76 - await this.bot.sendMessage( 77 - chatId, 78 - "Note: There might be issues with your formatting. Please ensure all formatting tags are properly closed." 79 - ); 80 - } 81 - 82 - // Now ask for a schedule 83 - this.bot.sendMessage( 84 - chatId, 85 - 'Now, let\'s set the schedule for this announcement.\n\n' + 86 - 'Enter a cron schedule expression. Examples:\n' + 87 - '- `0 9 * * *` = Every day at 9:00 AM\n' + 88 - '- `0 18 * * 5` = Every Friday at 6:00 PM\n' + 89 - '- `0 12 1 * *` = First day of each month at noon\n\n' + 90 - 'For more options, visit https://crontab.guru/', 91 - { 92 - parse_mode: 'Markdown', 93 - reply_markup: { force_reply: true } 94 - } 95 - ).then(schedulePrompt => { 96 - this.handleScheduleStep(chatId, userId, schedulePrompt.message_id); 97 - }); 98 - }); 99 - } 100 - 101 - handleScheduleStep(chatId, userId, messageId) { 102 - this.bot.onReplyToMessage(chatId, messageId, async (scheduleMsg) => { 103 - const cronSchedule = scheduleMsg.text; 104 - 105 - // Validate the cron schedule 106 - if (!this.announcements.isValidCronExpression(cronSchedule)) { 107 - this.bot.sendMessage( 108 - chatId, 109 - '⚠️ That doesn\'t appear to be a valid cron schedule. Please try again using the format shown in the examples.', 110 - { parse_mode: 'Markdown' } 111 - ).then(() => { 112 - // Ask again for a valid schedule 113 - this.bot.sendMessage( 114 - chatId, 115 - 'Please enter a valid cron schedule. Examples:\n' + 116 - '- `0 9 * * *` = Every day at 9:00 AM\n' + 117 - '- `0 18 * * 5` = Every Friday at 6:00 PM\n' + 118 - '- `0 12 1 * *` = First day of each month at noon', 119 - { 120 - parse_mode: 'Markdown', 121 - reply_markup: { force_reply: true } 122 - } 123 - ).then((newSchedulePrompt) => { 124 - // Handle the new schedule response 125 - this.bot.onReplyToMessage(chatId, newSchedulePrompt.message_id, (newScheduleMsg) => { 126 - // Replace the schedule with the new one 127 - const validCronSchedule = newScheduleMsg.text; 128 - 129 - if (!this.announcements.isValidCronExpression(validCronSchedule)) { 130 - this.bot.sendMessage( 131 - chatId, 132 - '⚠️ Still not a valid cron schedule. Using "0 12 * * *" (daily at noon) as a default. You can edit this later.' 133 - ); 134 - this.pendingAnnouncements[userId].cronSchedule = "0 12 * * *"; 135 - 136 - // Continue to button step 137 - this.askAboutButton(chatId, userId); 138 - } else { 139 - this.pendingAnnouncements[userId].cronSchedule = validCronSchedule; 140 - 141 - // Continue to button step 142 - this.askAboutButton(chatId, userId); 143 - } 144 - }); 145 - }); 146 - }); 147 - return; 148 - } 149 - 150 - // Store the schedule 151 - this.pendingAnnouncements[userId].cronSchedule = cronSchedule; 152 - 153 - // Ask if they want to add a button 154 - this.askAboutButton(chatId, userId); 155 - }); 156 - } 157 - 158 - askAboutButton(chatId, userId) { 159 - this.bot.sendMessage( 160 - chatId, 161 - 'Would you like to add a button with a link to this announcement?', 162 - { 163 - reply_markup: { 164 - inline_keyboard: [ 165 - [ 166 - { text: 'Yes', callback_data: 'add_button' }, 167 - { text: 'No', callback_data: 'skip_button' } 168 - ] 169 - ] 170 - } 171 - } 172 - ).then(buttonPrompt => { 173 - // Callback handler for yes/no button selection 174 - this.bot.once('callback_query', async (query) => { 175 - await this.bot.answerCallbackQuery(query.id); 176 - 177 - // Delete the yes/no prompt 178 - await this.bot.deleteMessage(chatId, buttonPrompt.message_id); 179 - 180 - if (query.data === 'add_button') { 181 - this.handleButtonCreation(chatId, userId); 182 - } else { 183 - // User doesn't want to add a button 184 - await this.showAnnouncementConfirmation( 185 - chatId, 186 - userId, 187 - this.pendingAnnouncements[userId].name, 188 - this.pendingAnnouncements[userId].message, 189 - this.pendingAnnouncements[userId].cronSchedule 190 - ); 191 - } 192 - }); 193 - }); 194 - } 195 - 196 - handleButtonCreation(chatId, userId) { 197 - this.bot.sendMessage( 198 - chatId, 199 - 'Please enter the button text:', 200 - { reply_markup: { force_reply: true } } 201 - ).then(buttonTextPrompt => { 202 - this.bot.onReplyToMessage(chatId, buttonTextPrompt.message_id, async (buttonTextMsg) => { 203 - const buttonText = buttonTextMsg.text; 204 - 205 - // Now ask for the button URL 206 - this.bot.sendMessage( 207 - chatId, 208 - 'Please enter the button URL:', 209 - { reply_markup: { force_reply: true } } 210 - ).then(buttonUrlPrompt => { 211 - this.bot.onReplyToMessage(chatId, buttonUrlPrompt.message_id, async (buttonUrlMsg) => { 212 - const buttonUrl = buttonUrlMsg.text; 213 - 214 - // Store the button object 215 - const button = { 216 - text: buttonText, 217 - url: buttonUrl 218 - }; 219 - 220 - // Show confirmation with preview 221 - await this.showAnnouncementConfirmation( 222 - chatId, 223 - userId, 224 - this.pendingAnnouncements[userId].name, 225 - this.pendingAnnouncements[userId].message, 226 - this.pendingAnnouncements[userId].cronSchedule, 227 - button 228 - ); 229 - }); 230 - }); 231 - }); 232 - }); 233 - } 234 - 235 - async showAnnouncementConfirmation(chatId, userId, name, message, cronSchedule, button = null) { 236 - // Store all the data for the confirmation callback 237 - this.confirmAnnouncement[userId] = { 238 - name, 239 - message, 240 - cronSchedule, 241 - button 242 - }; 243 - 244 - // Create confirmation message with all details 245 - let confirmationMessage = "📣 *Announcement Preview*\n\n"; 246 - confirmationMessage += `*Name*: ${name || "(Auto-generated)"}\n`; 247 - confirmationMessage += `*Schedule*: \`${cronSchedule}\`\n`; 248 - 249 - if (button) { 250 - confirmationMessage += `*Button*: "${button.text}" → ${button.url}\n`; 251 - } else { 252 - confirmationMessage += "*Button*: None\n"; 253 - } 254 - 255 - confirmationMessage += "\n*Message Preview*:\n------------------\n"; 256 - 257 - // Send confirmation message 258 - await this.bot.sendMessage( 259 - chatId, 260 - confirmationMessage, 261 - { parse_mode: 'Markdown' } 262 - ); 263 - 264 - // Send formatted message preview 265 - const formattedMessage = this.announcements.formatMessageText(message); 266 - await this.bot.sendMessage( 267 - chatId, 268 - formattedMessage, 269 - { parse_mode: 'HTML' } 270 - ); 271 - 272 - // Ask for confirmation 273 - await this.bot.sendMessage( 274 - chatId, 275 - "Does everything look correct? Ready to create this announcement?", 276 - { 277 - reply_markup: { 278 - inline_keyboard: [ 279 - [ 280 - { text: '✅ Create Announcement', callback_data: 'confirm_announcement' }, 281 - { text: '❌ Cancel', callback_data: 'cancel_announcement' } 282 - ] 283 - ] 284 - } 285 - } 286 - ); 287 - 288 - // Set up a one-time listener for the confirmation response 289 - this.bot.once('callback_query', async (query) => { 290 - if (query.from.id !== userId) return; // Make sure it's the same user 291 - 292 - await this.bot.answerCallbackQuery(query.id); 293 - 294 - if (query.data === 'confirm_announcement') { 295 - try { 296 - // Create the announcement 297 - const announcement = await this.announcements.addAnnouncement( 298 - message, 299 - cronSchedule, 300 - name, 301 - button 302 - ); 303 - 304 - // Send success message 305 - let successMessage = `✅ Announcement "${announcement.name}" created!\n\n`; 306 - successMessage += `Scheduled for: ${announcement.cronSchedule}\n\n`; 307 - 308 - if (button) { 309 - successMessage += `Button: "${button.text}" → ${button.url}\n\n`; 310 - } 311 - 312 - successMessage += "You can manage all announcements with /announcements"; 313 - 314 - await this.bot.sendMessage(chatId, successMessage); 315 - 316 - // Clean up 317 - delete this.pendingAnnouncements[userId]; 318 - delete this.confirmAnnouncement[userId]; 319 - } catch (error) { 320 - this.bot.sendMessage( 321 - chatId, 322 - `Error creating announcement: ${error.message}\n\nPlease try again.` 323 - ); 324 - } 325 - } else { 326 - // User canceled 327 - await this.bot.sendMessage( 328 - chatId, 329 - "Announcement creation canceled. You can start over with /announce" 330 - ); 331 - 332 - // Clean up 333 - delete this.pendingAnnouncements[userId]; 334 - delete this.confirmAnnouncement[userId]; 335 - } 336 26 }); 337 27 } 338 28 }
+341
bot/telegrambot/commands/announce.js.backup
··· 1 + const AnnouncementCreationHelper = require('../helpers/announcementCreationHelper'); 2 + 3 + /** 4 + * /announce command handler - uses AnnouncementCreationHelper for workflow 5 + */ 6 + class AnnounceCommand { 7 + constructor(bot, authHelper, announcements) { 8 + this.bot = bot; 9 + this.authHelper = authHelper; 10 + this.announcements = announcements; 11 + this.creationHelper = new AnnouncementCreationHelper(bot, announcements); 12 + } 13 + 14 + register() { 15 + this.bot.onText(/^\/announce(?!\S)/, async (msg) => { 16 + const chatId = msg.chat.id; 17 + 18 + if (!this.authHelper.isAuthorized(msg.from.id)) { 19 + this.bot.sendMessage(chatId, 'You are not authorized to use this command.'); 20 + return; 21 + } 22 + 23 + // Use the creation helper to start the workflow 24 + await this.creationHelper.startCreationWorkflow(chatId, msg.from.id, 'command'); 25 + }); 26 + } 27 + } 28 + 29 + handleNameStep(chatId, userId, messageId) { 30 + this.bot.onReplyToMessage(chatId, messageId, async (nameMsg) => { 31 + const announcementName = nameMsg.text === 'skip' ? '' : nameMsg.text; 32 + this.pendingAnnouncements[userId].name = announcementName; 33 + 34 + // Now ask for the announcement message text 35 + this.bot.sendMessage( 36 + chatId, 37 + 'Great! Now enter the announcement message content.\n\n' + 38 + 'Your message can contain multiple lines and formatting:\n' + 39 + '- *text* for italic\n' + 40 + '- **text** for bold\n' + 41 + '- __text__ for underlined\n' + 42 + '- ~~text~~ for strikethrough\n\n' + 43 + 'Type your message now:', 44 + { 45 + parse_mode: 'Markdown', 46 + reply_markup: { force_reply: true } 47 + } 48 + ).then(messagePrompt => { 49 + this.handleMessageStep(chatId, userId, messagePrompt.message_id); 50 + }); 51 + }); 52 + } 53 + 54 + handleMessageStep(chatId, userId, messageId) { 55 + this.bot.onReplyToMessage(chatId, messageId, async (messageTextMsg) => { 56 + this.pendingAnnouncements[userId].message = messageTextMsg.text; 57 + 58 + try { 59 + // Show a preview of the formatted message 60 + const previewText = this.announcements.formatMessageText(this.pendingAnnouncements[userId].message); 61 + 62 + // Send a preview message to show how it will look 63 + await this.bot.sendMessage( 64 + chatId, 65 + "Here's a preview of your announcement with formatting:", 66 + { parse_mode: 'Markdown' } 67 + ); 68 + 69 + // Send the actual preview 70 + await this.bot.sendMessage( 71 + chatId, 72 + previewText, 73 + { parse_mode: 'HTML' } 74 + ); 75 + } catch (error) { 76 + console.error("Error showing announcement preview:", error); 77 + await this.bot.sendMessage( 78 + chatId, 79 + "Note: There might be issues with your formatting. Please ensure all formatting tags are properly closed." 80 + ); 81 + } 82 + 83 + // Now ask for a schedule 84 + this.bot.sendMessage( 85 + chatId, 86 + 'Now, let\'s set the schedule for this announcement.\n\n' + 87 + 'Enter a cron schedule expression. Examples:\n' + 88 + '- `0 9 * * *` = Every day at 9:00 AM\n' + 89 + '- `0 18 * * 5` = Every Friday at 6:00 PM\n' + 90 + '- `0 12 1 * *` = First day of each month at noon\n\n' + 91 + 'For more options, visit https://crontab.guru/', 92 + { 93 + parse_mode: 'Markdown', 94 + reply_markup: { force_reply: true } 95 + } 96 + ).then(schedulePrompt => { 97 + this.handleScheduleStep(chatId, userId, schedulePrompt.message_id); 98 + }); 99 + }); 100 + } 101 + 102 + handleScheduleStep(chatId, userId, messageId) { 103 + this.bot.onReplyToMessage(chatId, messageId, async (scheduleMsg) => { 104 + const cronSchedule = scheduleMsg.text; 105 + 106 + // Validate the cron schedule 107 + if (!this.announcements.isValidCronExpression(cronSchedule)) { 108 + this.bot.sendMessage( 109 + chatId, 110 + '⚠️ That doesn\'t appear to be a valid cron schedule. Please try again using the format shown in the examples.', 111 + { parse_mode: 'Markdown' } 112 + ).then(() => { 113 + // Ask again for a valid schedule 114 + this.bot.sendMessage( 115 + chatId, 116 + 'Please enter a valid cron schedule. Examples:\n' + 117 + '- `0 9 * * *` = Every day at 9:00 AM\n' + 118 + '- `0 18 * * 5` = Every Friday at 6:00 PM\n' + 119 + '- `0 12 1 * *` = First day of each month at noon', 120 + { 121 + parse_mode: 'Markdown', 122 + reply_markup: { force_reply: true } 123 + } 124 + ).then((newSchedulePrompt) => { 125 + // Handle the new schedule response 126 + this.bot.onReplyToMessage(chatId, newSchedulePrompt.message_id, (newScheduleMsg) => { 127 + // Replace the schedule with the new one 128 + const validCronSchedule = newScheduleMsg.text; 129 + 130 + if (!this.announcements.isValidCronExpression(validCronSchedule)) { 131 + this.bot.sendMessage( 132 + chatId, 133 + '⚠️ Still not a valid cron schedule. Using "0 12 * * *" (daily at noon) as a default. You can edit this later.' 134 + ); 135 + this.pendingAnnouncements[userId].cronSchedule = "0 12 * * *"; 136 + 137 + // Continue to button step 138 + this.askAboutButton(chatId, userId); 139 + } else { 140 + this.pendingAnnouncements[userId].cronSchedule = validCronSchedule; 141 + 142 + // Continue to button step 143 + this.askAboutButton(chatId, userId); 144 + } 145 + }); 146 + }); 147 + }); 148 + return; 149 + } 150 + 151 + // Store the schedule 152 + this.pendingAnnouncements[userId].cronSchedule = cronSchedule; 153 + 154 + // Ask if they want to add a button 155 + this.askAboutButton(chatId, userId); 156 + }); 157 + } 158 + 159 + askAboutButton(chatId, userId) { 160 + this.bot.sendMessage( 161 + chatId, 162 + 'Would you like to add a button with a link to this announcement?', 163 + { 164 + reply_markup: { 165 + inline_keyboard: [ 166 + [ 167 + { text: 'Yes', callback_data: 'add_button' }, 168 + { text: 'No', callback_data: 'skip_button' } 169 + ] 170 + ] 171 + } 172 + } 173 + ).then(buttonPrompt => { 174 + // Callback handler for yes/no button selection 175 + this.bot.once('callback_query', async (query) => { 176 + await this.bot.answerCallbackQuery(query.id); 177 + 178 + // Delete the yes/no prompt 179 + await this.bot.deleteMessage(chatId, buttonPrompt.message_id); 180 + 181 + if (query.data === 'add_button') { 182 + this.handleButtonCreation(chatId, userId); 183 + } else { 184 + // User doesn't want to add a button 185 + await this.showAnnouncementConfirmation( 186 + chatId, 187 + userId, 188 + this.pendingAnnouncements[userId].name, 189 + this.pendingAnnouncements[userId].message, 190 + this.pendingAnnouncements[userId].cronSchedule 191 + ); 192 + } 193 + }); 194 + }); 195 + } 196 + 197 + handleButtonCreation(chatId, userId) { 198 + this.bot.sendMessage( 199 + chatId, 200 + 'Please enter the button text:', 201 + { reply_markup: { force_reply: true } } 202 + ).then(buttonTextPrompt => { 203 + this.bot.onReplyToMessage(chatId, buttonTextPrompt.message_id, async (buttonTextMsg) => { 204 + const buttonText = buttonTextMsg.text; 205 + 206 + // Now ask for the button URL 207 + this.bot.sendMessage( 208 + chatId, 209 + 'Please enter the button URL:', 210 + { reply_markup: { force_reply: true } } 211 + ).then(buttonUrlPrompt => { 212 + this.bot.onReplyToMessage(chatId, buttonUrlPrompt.message_id, async (buttonUrlMsg) => { 213 + const buttonUrl = buttonUrlMsg.text; 214 + 215 + // Store the button object 216 + const button = { 217 + text: buttonText, 218 + url: buttonUrl 219 + }; 220 + 221 + // Show confirmation with preview 222 + await this.showAnnouncementConfirmation( 223 + chatId, 224 + userId, 225 + this.pendingAnnouncements[userId].name, 226 + this.pendingAnnouncements[userId].message, 227 + this.pendingAnnouncements[userId].cronSchedule, 228 + button 229 + ); 230 + }); 231 + }); 232 + }); 233 + }); 234 + } 235 + 236 + async showAnnouncementConfirmation(chatId, userId, name, message, cronSchedule, button = null) { 237 + // Store all the data for the confirmation callback 238 + this.confirmAnnouncement[userId] = { 239 + name, 240 + message, 241 + cronSchedule, 242 + button 243 + }; 244 + 245 + // Create confirmation message with all details 246 + let confirmationMessage = "📣 *Announcement Preview*\n\n"; 247 + confirmationMessage += `*Name*: ${name || "(Auto-generated)"}\n`; 248 + confirmationMessage += `*Schedule*: \`${cronSchedule}\`\n`; 249 + 250 + if (button) { 251 + confirmationMessage += `*Button*: "${button.text}" → ${button.url}\n`; 252 + } else { 253 + confirmationMessage += "*Button*: None\n"; 254 + } 255 + 256 + confirmationMessage += "\n*Message Preview*:\n------------------\n"; 257 + 258 + // Send confirmation message 259 + await this.bot.sendMessage( 260 + chatId, 261 + confirmationMessage, 262 + { parse_mode: 'Markdown' } 263 + ); 264 + 265 + // Send formatted message preview 266 + const formattedMessage = this.announcements.formatMessageText(message); 267 + await this.bot.sendMessage( 268 + chatId, 269 + formattedMessage, 270 + { parse_mode: 'HTML' } 271 + ); 272 + 273 + // Ask for confirmation 274 + await this.bot.sendMessage( 275 + chatId, 276 + "Does everything look correct? Ready to create this announcement?", 277 + { 278 + reply_markup: { 279 + inline_keyboard: [ 280 + [ 281 + { text: '✅ Create Announcement', callback_data: 'confirm_announcement' }, 282 + { text: '❌ Cancel', callback_data: 'cancel_announcement' } 283 + ] 284 + ] 285 + } 286 + } 287 + ); 288 + 289 + // Set up a one-time listener for the confirmation response 290 + this.bot.once('callback_query', async (query) => { 291 + if (query.from.id !== userId) return; // Make sure it's the same user 292 + 293 + await this.bot.answerCallbackQuery(query.id); 294 + 295 + if (query.data === 'confirm_announcement') { 296 + try { 297 + // Create the announcement 298 + const announcement = await this.announcements.addAnnouncement( 299 + message, 300 + cronSchedule, 301 + name, 302 + button 303 + ); 304 + 305 + // Send success message 306 + let successMessage = `✅ Announcement "${announcement.name}" created!\n\n`; 307 + successMessage += `Scheduled for: ${announcement.cronSchedule}\n\n`; 308 + 309 + if (button) { 310 + successMessage += `Button: "${button.text}" → ${button.url}\n\n`; 311 + } 312 + 313 + successMessage += "You can manage all announcements with /announcements"; 314 + 315 + await this.bot.sendMessage(chatId, successMessage); 316 + 317 + // Clean up 318 + delete this.pendingAnnouncements[userId]; 319 + delete this.confirmAnnouncement[userId]; 320 + } catch (error) { 321 + this.bot.sendMessage( 322 + chatId, 323 + `Error creating announcement: ${error.message}\n\nPlease try again.` 324 + ); 325 + } 326 + } else { 327 + // User canceled 328 + await this.bot.sendMessage( 329 + chatId, 330 + "Announcement creation canceled. You can start over with /announce" 331 + ); 332 + 333 + // Clean up 334 + delete this.pendingAnnouncements[userId]; 335 + delete this.confirmAnnouncement[userId]; 336 + } 337 + }); 338 + } 339 + } 340 + 341 + module.exports = AnnounceCommand;
+11 -2
bot/telegrambot/commands/announcements.js
··· 41 41 message += `Last run: ${announcement.lastRun ? new Date(announcement.lastRun).toLocaleString() : 'Never'}\n`; 42 42 43 43 // Show button info if present 44 - if (announcement.button && announcement.button.text && announcement.button.url) { 45 - message += `Button: "${announcement.button.text}" → ${announcement.button.url}\n`; 44 + // Handle both old single button format and new buttons array format 45 + const buttons = announcement.buttons || (announcement.button ? [announcement.button] : []); 46 + if (buttons.length > 0) { 47 + if (buttons.length === 1) { 48 + message += `Button: "${buttons[0].text}" → ${buttons[0].url}\n`; 49 + } else { 50 + message += `Buttons (${buttons.length}):\n`; 51 + buttons.forEach((button, index) => { 52 + message += ` ${index + 1}. "${button.text}" → ${button.url}\n`; 53 + }); 54 + } 46 55 } 47 56 48 57 // Format the message preview, replacing line breaks with special character
+74 -29
bot/telegrambot/helpers/announcementCreationHelper.js
··· 181 181 } 182 182 183 183 /** 184 - * Ask if user wants to add a button 184 + * Ask if user wants to add buttons 185 185 */ 186 186 askAboutButton(chatId, userId) { 187 + // Initialize buttons array if not exists 188 + if (!this.pendingAnnouncements[userId].buttons) { 189 + this.pendingAnnouncements[userId].buttons = []; 190 + } 191 + 192 + const currentButtonCount = this.pendingAnnouncements[userId].buttons.length; 193 + const maxButtons = 6; 194 + 195 + if (currentButtonCount >= maxButtons) { 196 + // Maximum buttons reached, proceed to confirmation 197 + return this.showAnnouncementConfirmation( 198 + chatId, 199 + userId, 200 + this.pendingAnnouncements[userId].name, 201 + this.pendingAnnouncements[userId].message, 202 + this.pendingAnnouncements[userId].cronSchedule, 203 + this.pendingAnnouncements[userId].buttons 204 + ); 205 + } 206 + 207 + let promptMessage = currentButtonCount === 0 208 + ? 'Would you like to add buttons with links to this announcement?\n\n' 209 + : `You currently have ${currentButtonCount} button${currentButtonCount > 1 ? 's' : ''}. Would you like to add another?\n\n`; 210 + 211 + promptMessage += `You can add up to ${maxButtons} buttons total.`; 212 + 213 + const buttons = [ 214 + [ 215 + { text: 'Add Button', callback_data: 'add_button' }, 216 + { text: currentButtonCount === 0 ? 'No Buttons' : 'Finish', callback_data: 'skip_button' } 217 + ] 218 + ]; 219 + 187 220 this.bot.sendMessage( 188 221 chatId, 189 - 'Would you like to add a button with a link to this announcement?', 222 + promptMessage, 190 223 { 191 224 reply_markup: { 192 - inline_keyboard: [ 193 - [ 194 - { text: 'Yes', callback_data: 'add_button' }, 195 - { text: 'No', callback_data: 'skip_button' } 196 - ] 197 - ] 225 + inline_keyboard: buttons 198 226 } 199 227 } 200 228 ).then(buttonPrompt => { 201 - // Callback handler for yes/no button selection 229 + // Callback handler for add/skip button selection 202 230 this.bot.once('callback_query', async (query) => { 203 231 await this.bot.answerCallbackQuery(query.id); 204 232 205 - // Delete the yes/no prompt 233 + // Delete the prompt 206 234 await this.bot.deleteMessage(chatId, buttonPrompt.message_id); 207 235 208 236 if (query.data === 'add_button') { 209 237 this.handleButtonCreation(chatId, userId); 210 238 } else { 211 - // User doesn't want to add a button 239 + // User doesn't want to add more buttons 212 240 await this.showAnnouncementConfirmation( 213 241 chatId, 214 242 userId, 215 243 this.pendingAnnouncements[userId].name, 216 244 this.pendingAnnouncements[userId].message, 217 - this.pendingAnnouncements[userId].cronSchedule 245 + this.pendingAnnouncements[userId].cronSchedule, 246 + this.pendingAnnouncements[userId].buttons 218 247 ); 219 248 } 220 249 }); ··· 225 254 * Handle button creation workflow 226 255 */ 227 256 handleButtonCreation(chatId, userId) { 257 + const currentButtonCount = this.pendingAnnouncements[userId].buttons?.length || 0; 258 + 228 259 this.bot.sendMessage( 229 260 chatId, 230 - 'Great! Let\'s create a button for your announcement.\n\n' + 261 + `Great! Let's create button #${currentButtonCount + 1} for your announcement.\n\n` + 231 262 'First, what text should appear on the button?', 232 263 { 233 264 reply_markup: { force_reply: true } ··· 258 289 259 290 const button = { text: buttonText, url: buttonUrl }; 260 291 261 - // Show final confirmation with button 262 - await this.showAnnouncementConfirmation( 263 - chatId, 264 - userId, 265 - this.pendingAnnouncements[userId].name, 266 - this.pendingAnnouncements[userId].message, 267 - this.pendingAnnouncements[userId].cronSchedule, 268 - button 292 + // Initialize buttons array if not exists 293 + if (!this.pendingAnnouncements[userId].buttons) { 294 + this.pendingAnnouncements[userId].buttons = []; 295 + } 296 + 297 + // Add the button to the array 298 + this.pendingAnnouncements[userId].buttons.push(button); 299 + 300 + await this.bot.sendMessage( 301 + chatId, 302 + `✅ Button "${buttonText}" added successfully!\n\n` + 303 + `You now have ${this.pendingAnnouncements[userId].buttons.length} button(s).` 269 304 ); 305 + 306 + // Ask if they want to add another button 307 + this.askAboutButton(chatId, userId); 270 308 }); 271 309 }); 272 310 }); ··· 276 314 /** 277 315 * Show the final confirmation screen 278 316 */ 279 - async showAnnouncementConfirmation(chatId, userId, name, message, cronSchedule, button = null) { 317 + async showAnnouncementConfirmation(chatId, userId, name, message, cronSchedule, buttons = []) { 280 318 // Store all the data for the confirmation callback 281 319 if (!this.confirmAnnouncement) { 282 320 this.confirmAnnouncement = {}; ··· 285 323 name, 286 324 message, 287 325 cronSchedule, 288 - button 326 + buttons 289 327 }; 290 328 291 329 // Create confirmation message with all details ··· 293 331 confirmationMessage += `*Name*: ${name || "(Auto-generated)"}\n`; 294 332 confirmationMessage += `*Schedule*: \`${cronSchedule}\`\n`; 295 333 296 - if (button) { 297 - confirmationMessage += `*Button*: "${button.text}" → ${button.url}\n`; 334 + if (buttons && buttons.length > 0) { 335 + confirmationMessage += `*Buttons (${buttons.length})*:\n`; 336 + buttons.forEach((button, index) => { 337 + confirmationMessage += ` ${index + 1}. "${button.text}" → ${button.url}\n`; 338 + }); 298 339 } else { 299 - confirmationMessage += "*Button*: None\n"; 340 + confirmationMessage += "*Buttons*: None\n"; 300 341 } 301 342 302 343 confirmationMessage += "\n*Message Preview*:\n------------------\n"; ··· 345 386 message, 346 387 cronSchedule, 347 388 name, 348 - button 389 + buttons 349 390 ); 350 391 351 392 // Send success message 352 393 let successMessage = `✅ Announcement "${announcement.name}" created!\n\n`; 353 394 successMessage += `Scheduled for: ${announcement.cronSchedule}\n\n`; 354 395 355 - if (button) { 356 - successMessage += `Button: "${button.text}" → ${button.url}\n\n`; 396 + if (buttons && buttons.length > 0) { 397 + successMessage += `Buttons (${buttons.length}):\n`; 398 + buttons.forEach((button, index) => { 399 + successMessage += ` ${index + 1}. "${button.text}" → ${button.url}\n`; 400 + }); 401 + successMessage += '\n'; 357 402 } 358 403 359 404 successMessage += "You can manage all announcements with /announcements";
+149 -24
bot/telegrambot/helpers/callbackHandler.js
··· 95 95 // These will be handled by the AnnouncementCreationHelper's listener 96 96 return; 97 97 } 98 + // Handle confirm_remove_button callback 99 + if (query.data.startsWith('confirm_remove_button_')) { 100 + await this.handleConfirmRemoveButton(query, data, chatId); 101 + return; 102 + } 98 103 break; 99 104 } 100 105 } catch (error) { ··· 418 423 }); 419 424 break; 420 425 case 'button': 426 + // Show current buttons and options for multiple button management 427 + let buttonMessage = `🔗 *Managing buttons for "${announcement.name}"*\n\n`; 428 + 429 + // Display current buttons 430 + const currentButtons = announcement.buttons || []; 431 + if (currentButtons.length > 0) { 432 + buttonMessage += `*Current buttons (${currentButtons.length}/6):*\n`; 433 + currentButtons.forEach((button, index) => { 434 + buttonMessage += `${index + 1}. "${button.text}" → ${button.url}\n`; 435 + }); 436 + buttonMessage += '\n'; 437 + } else { 438 + buttonMessage += '*Current buttons:* None\n\n'; 439 + } 440 + 441 + buttonMessage += 'What would you like to do?'; 442 + 443 + // Create button options 444 + const buttonOptions = []; 445 + 446 + // Add button option (if under limit) 447 + if (currentButtons.length < 6) { 448 + buttonOptions.push([ 449 + { text: '➕ Add Button', callback_data: `edit_button_add_${announcement.id}` } 450 + ]); 451 + } 452 + 453 + // Remove button options (if buttons exist) 454 + if (currentButtons.length > 0) { 455 + buttonOptions.push([ 456 + { text: '🗑️ Remove Button', callback_data: `edit_button_remove_${announcement.id}` } 457 + ]); 458 + } 459 + 460 + // Cancel option 461 + buttonOptions.push([ 462 + { text: '❌ Cancel', callback_data: 'cancel_edit_announcement' } 463 + ]); 464 + 421 465 await this.bot.sendMessage( 422 466 chatId, 423 - `🔗 *Editing button for "${announcement.name}"*\n\n` + 424 - `Current button: ${announcement.button ? `"${announcement.button.text}" -> ${announcement.button.url}` : 'None'}\n\n` + 425 - `Would you like to add/edit a button or remove the existing one?`, 467 + buttonMessage, 426 468 { 427 469 parse_mode: 'Markdown', 428 470 reply_markup: { 429 - inline_keyboard: [ 430 - [ 431 - { text: '✏️ Add/Edit Button', callback_data: `edit_button_add_${announcement.id}` }, 432 - { text: '🗑️ Remove Button', callback_data: `edit_button_remove_${announcement.id}` } 433 - ], 434 - [ 435 - { text: '❌ Cancel', callback_data: 'cancel_edit_announcement' } 436 - ] 437 - ] 471 + inline_keyboard: buttonOptions 438 472 } 439 473 } 440 474 ); ··· 627 661 await this.bot.answerCallbackQuery(query.id); 628 662 629 663 if (action === 'remove') { 630 - // Remove the button 631 - try { 632 - await this.announcements.updateAnnouncement(announcement.id, { button: null }); 664 + // Show buttons to remove 665 + const currentButtons = announcement.buttons || []; 666 + if (currentButtons.length === 0) { 633 667 await this.bot.sendMessage( 634 668 chatId, 635 - `✅ *Button removed successfully!*\n\nThe announcement "${announcement.name}" no longer has a button.`, 669 + '❌ No buttons to remove.', 636 670 { parse_mode: 'Markdown' } 637 671 ); 638 - } catch (error) { 639 - console.error('Error removing button:', error); 640 - await this.bot.sendMessage(chatId, `❌ Error removing button: ${error.message}`); 672 + return; 641 673 } 674 + 675 + let removeMessage = `🗑️ *Select button to remove from "${announcement.name}":*\n\n`; 676 + currentButtons.forEach((button, index) => { 677 + removeMessage += `${index + 1}. "${button.text}" → ${button.url}\n`; 678 + }); 679 + 680 + // Create inline keyboard with button options 681 + const removeOptions = []; 682 + currentButtons.forEach((button, index) => { 683 + removeOptions.push([ 684 + { 685 + text: `❌ Remove "${button.text}"`, 686 + callback_data: `confirm_remove_button_${index}_${announcement.id}` 687 + } 688 + ]); 689 + }); 690 + 691 + removeOptions.push([ 692 + { text: '🔙 Back', callback_data: `edit_announcement_button_${announcement.id}` } 693 + ]); 694 + 695 + await this.bot.sendMessage( 696 + chatId, 697 + removeMessage, 698 + { 699 + parse_mode: 'Markdown', 700 + reply_markup: { 701 + inline_keyboard: removeOptions 702 + } 703 + } 704 + ); 705 + 642 706 } else if (action === 'add') { 707 + // Check if under limit 708 + const currentButtons = announcement.buttons || []; 709 + if (currentButtons.length >= 6) { 710 + await this.bot.sendMessage( 711 + chatId, 712 + '❌ Maximum of 6 buttons allowed per announcement.', 713 + { parse_mode: 'Markdown' } 714 + ); 715 + return; 716 + } 717 + 643 718 // Start the button creation process 644 719 const userId = chatId; // Using chatId as userId since this is a private chat 645 720 this.editingAnnouncementButton[userId] = { ··· 650 725 651 726 await this.bot.sendMessage( 652 727 chatId, 653 - `🔗 *Adding/Editing button for "${announcement.name}"*\n\n` + 728 + `🔗 *Adding button to "${announcement.name}"*\n\n` + 729 + `Current buttons: ${currentButtons.length}/6\n\n` + 654 730 `Please enter the button text (what users will see on the button):`, 655 731 { 656 732 parse_mode: 'Markdown', ··· 700 776 return; 701 777 } 702 778 703 - // Update the announcement with the new button 704 - const button = { text: buttonText, url: buttonUrl }; 705 - await this.announcements.updateAnnouncement(announcement.id, { button: button }); 779 + // Update the announcement by adding the new button to the buttons array 780 + const currentButtons = announcement.buttons || []; 781 + const newButtons = [...currentButtons, { text: buttonText, url: buttonUrl }]; 782 + await this.announcements.updateAnnouncement(announcement.id, { buttons: newButtons }); 706 783 707 784 await this.bot.sendMessage( 708 785 chatId, 709 - `✅ *Button updated successfully!*\n\nButton: "${buttonText}" -> ${buttonUrl}`, 786 + `✅ *Button added successfully!*\n\nButton "${buttonText}" → ${buttonUrl}\n\nTotal buttons: ${newButtons.length}/6`, 710 787 { parse_mode: 'Markdown' } 711 788 ); 712 789 ··· 718 795 await this.bot.sendMessage(chatId, `❌ Error updating button: ${error.message}`); 719 796 } 720 797 }); 798 + } 799 + 800 + async handleConfirmRemoveButton(query, data, chatId) { 801 + // Parse callback data: confirm_remove_button_${buttonIndex}_${announcementId} 802 + const parts = query.data.split('_'); 803 + const buttonIndex = parseInt(parts[3]); 804 + let announcementId = parts[4]; 805 + 806 + // Handle URL decoding in case Telegram encodes the callback data 807 + try { 808 + announcementId = decodeURIComponent(announcementId); 809 + } catch (e) { 810 + // If decoding fails, use the original value 811 + } 812 + 813 + const announcement = this.announcements.getAnnouncementById(announcementId); 814 + if (!announcement) { 815 + await this.bot.answerCallbackQuery(query.id, { text: 'Announcement not found.' }); 816 + return; 817 + } 818 + 819 + await this.bot.answerCallbackQuery(query.id); 820 + 821 + try { 822 + const currentButtons = announcement.buttons || []; 823 + 824 + if (buttonIndex >= 0 && buttonIndex < currentButtons.length) { 825 + const removedButton = currentButtons[buttonIndex]; 826 + const newButtons = currentButtons.filter((_, index) => index !== buttonIndex); 827 + 828 + await this.announcements.updateAnnouncement(announcement.id, { buttons: newButtons }); 829 + 830 + await this.bot.sendMessage( 831 + chatId, 832 + `✅ *Button removed successfully!*\n\nRemoved: "${removedButton.text}"\n\nRemaining buttons: ${newButtons.length}/6`, 833 + { parse_mode: 'Markdown' } 834 + ); 835 + } else { 836 + await this.bot.sendMessage( 837 + chatId, 838 + '❌ Button not found or invalid index.', 839 + { parse_mode: 'Markdown' } 840 + ); 841 + } 842 + } catch (error) { 843 + console.error('Error removing button:', error); 844 + await this.bot.sendMessage(chatId, `❌ Error removing button: ${error.message}`); 845 + } 721 846 } 722 847 723 848 // Allow access to editingAnnouncementButton for coordination with other modules
+2 -1
package.json
··· 12 12 "save": "pm2 save", 13 13 "dev": "nodemon index.js", 14 14 "start:node": "node index.js", 15 - "test": "echo \"Error: no test specified\" && exit 1" 15 + "test": "echo \"Error: no test specified\" && exit 1", 16 + "syntax-check": "node -e \"const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); function checkSyntax(dir) { const items = fs.readdirSync(dir, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(dir, item.name); if (item.isDirectory() && !['node_modules', '.git', 'cache'].includes(item.name)) { checkSyntax(fullPath); } else if (item.isFile() && item.name.endsWith('.js')) { try { execSync(\\`node -c \\\"\\${fullPath}\\\"\\`, { stdio: 'pipe' }); console.log(\\`✓ \\${fullPath}\\`); } catch (error) { console.error(\\`✗ \\${fullPath}: \\${error.message}\\`); process.exit(1); } } } } console.log('Checking JavaScript syntax...'); checkSyntax('.'); console.log('✓ All JavaScript files have valid syntax!');\"" 16 17 }, 17 18 "keywords": [ 18 19 "telegram",
+1 -1
queue/alert-state.json
··· 3 3 "emptyQueueAlertSent": false, 4 4 "lastLowQueueAlertTime": 1748136883217, 5 5 "lastEmptyQueueAlertTime": 1748136540595, 6 - "lastSaved": 1748139423444 6 + "lastSaved": 1749076222424 7 7 }
+85 -28
utils/announcementManager.js
··· 54 54 // Read and parse the file 55 55 const data = await readFileAsync(this.filePath, 'utf8'); 56 56 this.announcements = JSON.parse(data); 57 + 58 + // Migrate old button format to new buttons array format 59 + let migrationNeeded = false; 60 + for (const announcement of this.announcements) { 61 + if (announcement.button && !announcement.buttons) { 62 + // Convert old single button to buttons array 63 + announcement.buttons = [announcement.button]; 64 + delete announcement.button; 65 + migrationNeeded = true; 66 + } else if (!announcement.buttons) { 67 + // Ensure all announcements have a buttons array 68 + announcement.buttons = []; 69 + migrationNeeded = true; 70 + } 71 + } 72 + 73 + // Save migrated data if needed 74 + if (migrationNeeded) { 75 + await this.saveAnnouncementsToDisk(); 76 + console.log('Migrated announcement button format to support multiple buttons'); 77 + } 78 + 57 79 console.log(`Loaded ${this.announcements.length} announcements from disk`); 58 80 } catch (error) { 59 81 console.error('Error loading announcements from disk:', error); ··· 81 103 * @param {string} message - The text message to post 82 104 * @param {string} cronSchedule - Cron expression for schedule 83 105 * @param {string} name - Optional name/label for the announcement 84 - * @param {Object} button - Optional button configuration {text: string, url: string} 106 + * @param {Array} buttons - Optional array of button configurations [{text: string, url: string}, ...] 85 107 * @returns {Promise<Object>} - The added announcement object 86 108 */ 87 - async addAnnouncement(message, cronSchedule, name = '', button = null) { 109 + async addAnnouncement(message, cronSchedule, name = '', buttons = []) { 88 110 // Validate cron expression 89 111 if (!this.isValidCronExpression(cronSchedule)) { 90 112 throw new Error('Invalid cron expression'); 91 113 } 92 114 93 - // Validate button if provided 94 - if (button && (!button.text || !button.url)) { 95 - throw new Error('Button must have both text and url properties'); 115 + // Validate buttons if provided 116 + if (buttons && Array.isArray(buttons)) { 117 + if (buttons.length > 6) { 118 + throw new Error('Maximum of 6 buttons allowed'); 119 + } 120 + 121 + for (const button of buttons) { 122 + if (!button.text || !button.url) { 123 + throw new Error('Each button must have both text and url properties'); 124 + } 125 + } 96 126 } 97 127 98 128 // Store the raw message - ensure we preserve line breaks ··· 108 138 name: name || `Announcement ${id.substring(id.length - 4)}`, 109 139 createdAt: new Date().toISOString(), 110 140 lastRun: null, 111 - button: button 141 + buttons: buttons || [] 112 142 }; 113 143 114 144 // Add to our list ··· 177 207 } 178 208 179 209 if (updates.button !== undefined) { 180 - // Validate button if provided (null is allowed for removal) 210 + // Handle legacy single button format for backward compatibility 181 211 if (updates.button !== null && (!updates.button.text || !updates.button.url)) { 182 212 throw new Error('Button must have both text and url properties'); 183 213 } 184 - announcement.button = updates.button; 214 + // Convert single button to buttons array format 215 + announcement.buttons = updates.button ? [updates.button] : []; 216 + // Remove old button field if it exists 217 + delete announcement.button; 218 + } 219 + 220 + if (updates.buttons !== undefined) { 221 + // Handle new buttons array format 222 + if (updates.buttons && Array.isArray(updates.buttons)) { 223 + if (updates.buttons.length > 6) { 224 + throw new Error('Maximum of 6 buttons allowed'); 225 + } 226 + 227 + for (const button of updates.buttons) { 228 + if (!button.text || !button.url) { 229 + throw new Error('Each button must have both text and url properties'); 230 + } 231 + } 232 + } 233 + announcement.buttons = updates.buttons || []; 234 + // Remove old button field if it exists 235 + delete announcement.button; 185 236 } 186 237 187 238 if (updates.cronSchedule !== undefined) { ··· 243 294 // Convert line breaks to HTML breaks and escape any HTML entities 244 295 let processedMessage = this.formatMessageText(announcement.message); 245 296 246 - // Add inline keyboard if a button is defined 247 - if (announcement.button && announcement.button.text && announcement.button.url) { 297 + // Add inline keyboard if buttons are defined 298 + if (announcement.buttons && announcement.buttons.length > 0) { 299 + // Telegram supports up to 8 buttons per row, but we'll use 2 per row for better UX 300 + const buttonRows = []; 301 + for (let i = 0; i < announcement.buttons.length; i += 2) { 302 + const row = announcement.buttons.slice(i, i + 2).map(button => ({ 303 + text: button.text, 304 + url: button.url 305 + })); 306 + buttonRows.push(row); 307 + } 308 + 248 309 messageOptions.reply_markup = { 249 - inline_keyboard: [ 250 - [ 251 - { 252 - text: announcement.button.text, 253 - url: announcement.button.url 254 - } 255 - ] 256 - ] 310 + inline_keyboard: buttonRows 257 311 }; 258 312 } 259 313 ··· 352 406 // Process the message to handle line breaks properly in HTML mode 353 407 let processedMessage = this.formatMessageText(announcement.message); 354 408 355 - // Add inline keyboard if a button is defined 356 - if (announcement.button && announcement.button.text && announcement.button.url) { 409 + // Add inline keyboard if buttons are defined 410 + if (announcement.buttons && announcement.buttons.length > 0) { 411 + // Telegram supports up to 8 buttons per row, but we'll use 2 per row for better UX 412 + const buttonRows = []; 413 + for (let i = 0; i < announcement.buttons.length; i += 2) { 414 + const row = announcement.buttons.slice(i, i + 2).map(button => ({ 415 + text: button.text, 416 + url: button.url 417 + })); 418 + buttonRows.push(row); 419 + } 420 + 357 421 messageOptions.reply_markup = { 358 - inline_keyboard: [ 359 - [ 360 - { 361 - text: announcement.button.text, 362 - url: announcement.button.url 363 - } 364 - ] 365 - ] 422 + inline_keyboard: buttonRows 366 423 }; 367 424 } 368 425