this repo has no description
0
fork

Configure Feed

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

Merge pull request #9 from HenrickTheBull/announce-fix

Rewrote announcement management commands and enhance message formatting

authored by

Henrick and committed by
GitHub
2c6b8805 2cc3e74b

+496 -124
+2
README.md
··· 163 163 - `/setcount [number]` - Set number of images per post interval 164 164 - `/clear` - Clear the entire queue 165 165 - `/cleancache` - Clean expired items from media cache 166 + - `/announce` - Create a new announcement 167 + - `/announcements` - Manage existing announcements 166 168 167 169 ### Adding Images to Queue 168 170
+446 -117
bot/telegramBot.js
··· 45 45 /setcount [number] - Set number of images per scheduled post (default: 1) 46 46 /clear - Clear the queue 47 47 /cleancache - Clean expired items from media cache 48 + /announce - Create a new announcement 49 + /announcements - Manage existing announcements 48 50 /update - Update bot from GitHub repository (owner only) 49 51 50 52 Send any link to a supported site to add it to the queue. ··· 293 295 }); 294 296 295 297 // Command to add a text announcement 296 - this.bot.onText(/\/announce(?:\s+(.+))?/, async (msg, match) => { 298 + this.bot.onText(/^\/announce(?!\S)/, async (msg) => { 297 299 const chatId = msg.chat.id; 298 300 299 301 if (!this.isAuthorized(msg.from.id)) { ··· 301 303 return; 302 304 } 303 305 304 - // Check if there's text after the command 305 - const text = match && match[1] ? match[1].trim() : ''; 306 - 307 - if (!text) { 308 - // No parameters - show usage help 309 - this.bot.sendMessage( 310 - chatId, 311 - 'Usage: /announce [message text]\n\nThis will initiate the announcement creation process. After sending the message text, you\'ll be prompted to set a name, schedule, and an optional button link for the announcement.' 312 - ); 313 - return; 314 - } 315 - 316 - // Store the message text in session and ask for a name 306 + // Initialize the interactive announcement creation process 317 307 this.pendingAnnouncements = this.pendingAnnouncements || {}; 318 - this.pendingAnnouncements[msg.from.id] = { message: text }; 308 + this.pendingAnnouncements[msg.from.id] = {}; 319 309 310 + // Display introduction message with formatting options 320 311 this.bot.sendMessage( 321 312 chatId, 322 - 'Please enter a name for this announcement (or type "skip" for auto-generated name):', 323 - { reply_markup: { force_reply: true } } 313 + '📣 *Create New Announcement*\n\n' + 314 + 'I\'ll guide you through creating an announcement step by step:\n' + 315 + '1️⃣ Name your announcement\n' + 316 + '2️⃣ Write the message content\n' + 317 + '3️⃣ Set a schedule\n' + 318 + '4️⃣ Add an optional button (if desired)\n\n' + 319 + 'You can use these formatting options in your message:\n' + 320 + '- *text* for italic\n' + 321 + '- **text** for bold\n' + 322 + '- __text__ for underlined\n' + 323 + '- ~~text~~ for strikethrough\n\n' + 324 + 'Let\'s start! First, what would you like to name this announcement?', 325 + { 326 + parse_mode: 'Markdown', 327 + reply_markup: { force_reply: true } 328 + } 324 329 ).then(namePrompt => { 325 330 // Set up a one-time listener for the name response 326 331 this.bot.onReplyToMessage(chatId, namePrompt.message_id, async (nameMsg) => { 327 332 const announcementName = nameMsg.text === 'skip' ? '' : nameMsg.text; 328 333 this.pendingAnnouncements[msg.from.id].name = announcementName; 329 334 330 - // Now ask for a schedule 335 + // Now ask for the announcement message text 331 336 this.bot.sendMessage( 332 337 chatId, 333 - 'Please enter a cron schedule for when this announcement should run (use https://crontab.guru/ for help):', 334 - { reply_markup: { force_reply: true } } 335 - ).then(schedulePrompt => { 336 - // Set up a one-time listener for the schedule response 337 - this.bot.onReplyToMessage(chatId, schedulePrompt.message_id, async (scheduleMsg) => { 338 - const cronSchedule = scheduleMsg.text; 339 - 340 - // Ask if they want to add a button 338 + 'Great! Now enter the announcement message content.\n\n' + 339 + 'Your message can contain multiple lines and formatting:\n' + 340 + '- *text* for italic\n' + 341 + '- **text** for bold\n' + 342 + '- __text__ for underlined\n' + 343 + '- ~~text~~ for strikethrough\n\n' + 344 + 'Type your message now:', 345 + { 346 + parse_mode: 'Markdown', 347 + reply_markup: { force_reply: true } 348 + } 349 + ).then(messagePrompt => { 350 + // Set up a one-time listener for the message text response 351 + this.bot.onReplyToMessage(chatId, messagePrompt.message_id, async (messageTextMsg) => { 352 + this.pendingAnnouncements[msg.from.id].message = messageTextMsg.text; 353 + 354 + try { 355 + // Show a preview of the formatted message 356 + const previewText = this.announcements.formatMessageText(this.pendingAnnouncements[msg.from.id].message); 357 + 358 + // Send a preview message to show how it will look 359 + await this.bot.sendMessage( 360 + chatId, 361 + "Here's a preview of your announcement with formatting:", 362 + { parse_mode: 'Markdown' } 363 + ); 364 + 365 + // Send the actual preview 366 + await this.bot.sendMessage( 367 + chatId, 368 + previewText, 369 + { parse_mode: 'HTML' } 370 + ); 371 + } catch (error) { 372 + console.error("Error showing announcement preview:", error); 373 + await this.bot.sendMessage( 374 + chatId, 375 + "Note: There might be issues with your formatting. Please ensure all formatting tags are properly closed." 376 + ); 377 + } 378 + // Now ask for a schedule 341 379 this.bot.sendMessage( 342 380 chatId, 343 - 'Would you like to add a button with a link to this announcement?', 344 - { 345 - reply_markup: { 346 - inline_keyboard: [ 347 - [ 348 - { text: 'Yes', callback_data: 'add_button' }, 349 - { text: 'No', callback_data: 'skip_button' } 350 - ] 351 - ] 352 - } 381 + 'Now, let\'s set the schedule for this announcement.\n\n' + 382 + 'Enter a cron schedule expression. Examples:\n' + 383 + '- `0 9 * * *` = Every day at 9:00 AM\n' + 384 + '- `0 18 * * 5` = Every Friday at 6:00 PM\n' + 385 + '- `0 12 1 * *` = First day of each month at noon\n\n' + 386 + 'For more options, visit https://crontab.guru/', 387 + { 388 + parse_mode: 'Markdown', 389 + reply_markup: { force_reply: true } 353 390 } 354 - ).then(buttonPrompt => { 355 - // Callback handler for yes/no button selection 356 - this.bot.once('callback_query', async (query) => { 357 - await this.bot.answerCallbackQuery(query.id); 358 - 359 - // Delete the yes/no prompt 360 - await this.bot.deleteMessage(chatId, buttonPrompt.message_id); 391 + ).then(schedulePrompt => { 392 + // Set up a one-time listener for the schedule response 393 + this.bot.onReplyToMessage(chatId, schedulePrompt.message_id, async (scheduleMsg) => { 394 + const cronSchedule = scheduleMsg.text; 361 395 362 - if (query.data === 'add_button') { 363 - // User wants to add a button 396 + // Validate the cron schedule 397 + if (!this.announcements.isValidCronExpression(cronSchedule)) { 364 398 this.bot.sendMessage( 365 399 chatId, 366 - 'Please enter the button text:', 367 - { reply_markup: { force_reply: true } } 368 - ).then(buttonTextPrompt => { 369 - this.bot.onReplyToMessage(chatId, buttonTextPrompt.message_id, async (buttonTextMsg) => { 370 - const buttonText = buttonTextMsg.text; 371 - 372 - // Now ask for the button URL 400 + '⚠️ That doesn\'t appear to be a valid cron schedule. Please try again using the format shown in the examples.', 401 + { parse_mode: 'Markdown' } 402 + ).then(() => { 403 + // Ask again for a valid schedule 404 + this.bot.sendMessage( 405 + chatId, 406 + 'Please enter a valid cron schedule. Examples:\n' + 407 + '- `0 9 * * *` = Every day at 9:00 AM\n' + 408 + '- `0 18 * * 5` = Every Friday at 6:00 PM\n' + 409 + '- `0 12 1 * *` = First day of each month at noon', 410 + { 411 + parse_mode: 'Markdown', 412 + reply_markup: { force_reply: true } 413 + } 414 + ).then((newSchedulePrompt) => { 415 + // Handle the new schedule response 416 + this.bot.onReplyToMessage(chatId, newSchedulePrompt.message_id, (newScheduleMsg) => { 417 + // Replace the schedule with the new one 418 + const validCronSchedule = newScheduleMsg.text; 419 + 420 + if (!this.announcements.isValidCronExpression(validCronSchedule)) { 421 + this.bot.sendMessage( 422 + chatId, 423 + '⚠️ Still not a valid cron schedule. Using "0 12 * * *" (daily at noon) as a default. You can edit this later.' 424 + ); 425 + this.pendingAnnouncements[msg.from.id].cronSchedule = "0 12 * * *"; 426 + 427 + // Continue to button step 428 + this.askAboutButton(chatId, msg.from.id); 429 + } else { 430 + this.pendingAnnouncements[msg.from.id].cronSchedule = validCronSchedule; 431 + 432 + // Continue to button step 433 + this.askAboutButton(chatId, msg.from.id); 434 + } 435 + }); 436 + }); 437 + }); 438 + return; 439 + } 440 + 441 + // Store the schedule 442 + this.pendingAnnouncements[msg.from.id].cronSchedule = cronSchedule; 443 + 444 + // Ask if they want to add a button 445 + this.bot.sendMessage( 446 + chatId, 447 + 'Would you like to add a button with a link to this announcement?', 448 + { 449 + reply_markup: { 450 + inline_keyboard: [ 451 + [ 452 + { text: 'Yes', callback_data: 'add_button' }, 453 + { text: 'No', callback_data: 'skip_button' } 454 + ] 455 + ] 456 + } 457 + } 458 + ).then(buttonPrompt => { 459 + // Callback handler for yes/no button selection 460 + this.bot.once('callback_query', async (query) => { 461 + await this.bot.answerCallbackQuery(query.id); 462 + 463 + // Delete the yes/no prompt 464 + await this.bot.deleteMessage(chatId, buttonPrompt.message_id); 465 + 466 + if (query.data === 'add_button') { 467 + // User wants to add a button 373 468 this.bot.sendMessage( 374 469 chatId, 375 - 'Please enter the button URL:', 470 + 'Please enter the button text:', 376 471 { reply_markup: { force_reply: true } } 377 - ).then(buttonUrlPrompt => { 378 - this.bot.onReplyToMessage(chatId, buttonUrlPrompt.message_id, async (buttonUrlMsg) => { 379 - const buttonUrl = buttonUrlMsg.text; 380 - 381 - // Create the button object 382 - const button = { 383 - text: buttonText, 384 - url: buttonUrl 385 - }; 472 + ).then(buttonTextPrompt => { 473 + this.bot.onReplyToMessage(chatId, buttonTextPrompt.message_id, async (buttonTextMsg) => { 474 + const buttonText = buttonTextMsg.text; 386 475 387 - try { 388 - const announcement = await this.announcements.addAnnouncement( 389 - this.pendingAnnouncements[msg.from.id].message, 390 - cronSchedule, 391 - this.pendingAnnouncements[msg.from.id].name, 392 - button 393 - ); 394 - 395 - delete this.pendingAnnouncements[msg.from.id]; 396 - 397 - this.bot.sendMessage( 398 - chatId, 399 - `✅ Announcement "${announcement.name}" created!\n\n`+ 400 - `Scheduled for: ${announcement.cronSchedule}\n\n`+ 401 - `Button: "${button.text}" → ${button.url}\n\n`+ 402 - `You can manage all announcements with /announcements` 403 - ); 404 - } catch (error) { 405 - this.bot.sendMessage( 406 - chatId, 407 - `Error creating announcement: ${error.message}\n\nPlease try again.` 408 - ); 409 - } 476 + // Now ask for the button URL 477 + this.bot.sendMessage( 478 + chatId, 479 + 'Please enter the button URL:', 480 + { reply_markup: { force_reply: true } } 481 + ).then(buttonUrlPrompt => { 482 + this.bot.onReplyToMessage(chatId, buttonUrlPrompt.message_id, async (buttonUrlMsg) => { 483 + const buttonUrl = buttonUrlMsg.text; 484 + 485 + // Store the button object 486 + const button = { 487 + text: buttonText, 488 + url: buttonUrl 489 + }; 490 + 491 + // Show confirmation with preview 492 + await this.showAnnouncementConfirmation( 493 + chatId, 494 + msg.from.id, 495 + this.pendingAnnouncements[msg.from.id].name, 496 + this.pendingAnnouncements[msg.from.id].message, 497 + this.pendingAnnouncements[msg.from.id].cronSchedule, 498 + button 499 + ); 500 + }); 501 + }); 410 502 }); 411 503 }); 412 - }); 504 + } else { 505 + // User doesn't want to add a button 506 + // Show confirmation with preview 507 + await this.showAnnouncementConfirmation( 508 + chatId, 509 + msg.from.id, 510 + this.pendingAnnouncements[msg.from.id].name, 511 + this.pendingAnnouncements[msg.from.id].message, 512 + this.pendingAnnouncements[msg.from.id].cronSchedule 513 + ); 514 + } 413 515 }); 414 - } else { 415 - // User doesn't want to add a button 416 - try { 417 - const announcement = await this.announcements.addAnnouncement( 418 - this.pendingAnnouncements[msg.from.id].message, 419 - cronSchedule, 420 - this.pendingAnnouncements[msg.from.id].name 421 - ); 422 - 423 - delete this.pendingAnnouncements[msg.from.id]; 424 - 425 - this.bot.sendMessage( 426 - chatId, 427 - `✅ Announcement "${announcement.name}" created!\n\nScheduled for: ${announcement.cronSchedule}\n\nYou can manage all announcements with /announcements` 428 - ); 429 - } catch (error) { 430 - this.bot.sendMessage( 431 - chatId, 432 - `Error creating announcement: ${error.message}\n\nPlease try again with a valid cron expression.` 433 - ); 434 - } 435 - } 516 + }); 436 517 }); 437 518 }); 438 519 }); ··· 442 523 }); 443 524 444 525 // Command to list and manage all announcements 445 - this.bot.onText(/\/announcements/, async (msg) => { 526 + this.bot.onText(/^\/announcements(?!\S)/, async (msg) => { 446 527 const chatId = msg.chat.id; 447 528 448 529 if (!this.isAuthorized(msg.from.id)) { ··· 478 559 message += `Button: "${announcement.button.text}" → ${announcement.button.url}\n`; 479 560 } 480 561 481 - message += `Message: "${announcement.message.substring(0, 50)}${announcement.message.length > 50 ? '...' : ''}"\n\n`; 562 + // Format the message preview, replacing line breaks with special character 563 + const previewMessage = announcement.message 564 + .replace(/\n/g, '↵') // Replace line breaks with a visible symbol 565 + .substring(0, 50); 566 + message += `Message: "${previewMessage}${announcement.message.length > 50 ? '...' : ''}"\n\n`; 482 567 483 568 // Add buttons for this announcement 484 569 inlineKeyboard.push([ ··· 698 783 699 784 // Refresh announcements list 700 785 await this.bot.deleteMessage(chatId, query.message.message_id); 701 - await this.bot.onText.handlers.find(h => h.regexp.toString().includes('/announcements'))?._callback({ chat: { id: chatId } }); 786 + await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } }); 702 787 } 703 788 break; 704 789 } ··· 751 836 await this.bot.deleteMessage(chatId, query.message.message_id); 752 837 753 838 // Refresh announcements list 754 - await this.bot.onText.handlers.find(h => h.regexp.toString().includes('/announcements'))?._callback({ chat: { id: chatId } }); 839 + await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } }); 755 840 } catch (error) { 756 841 await this.bot.answerCallbackQuery(query.id, { text: `Error: ${error.message}` }); 757 842 } ··· 839 924 let promptText = ''; 840 925 switch (field) { 841 926 case 'message': 842 - promptText = `Please enter the new message text for the announcement "${announcement.name}":\n\nCurrent message:\n${announcement.message}`; 927 + promptText = `Please enter the new message text for the announcement "${announcement.name}":\n\nCurrent message:\n${announcement.message}\n\nYou can use line breaks and formatting in your announcement.`; 843 928 break; 844 929 case 'name': 845 930 promptText = `Please enter the new name for the announcement "${announcement.name}":`; ··· 909 994 return; 910 995 } 911 996 } 997 + 998 + // For message, name, and schedule we'll send a prompt and handle the reply 999 + if (field === 'message' || field === 'name' || field === 'schedule') { 1000 + // Send the prompt with force_reply 1001 + const promptMsg = await this.bot.sendMessage( 1002 + chatId, 1003 + promptText, 1004 + { reply_markup: { force_reply: true } } 1005 + ); 1006 + 1007 + // Set up one-time handler for the response 1008 + this.bot.onReplyToMessage(chatId, promptMsg.message_id, async (responseMsg) => { 1009 + try { 1010 + // Get the response text - preserve line breaks and formatting exactly as received 1011 + const responseText = responseMsg.text; 1012 + 1013 + // Prepare the update object 1014 + const updates = {}; 1015 + updates[field] = responseText; // Raw text will preserve line breaks 1016 + 1017 + // Update the announcement 1018 + await this.announcements.updateAnnouncement(announcementId, updates); 1019 + 1020 + // Notify user of success 1021 + let successMsg = `✅ Announcement ${field} updated successfully!`; 1022 + if (field === 'message') { 1023 + successMsg += '\n\nYour message with all line breaks and formatting has been saved.'; 1024 + } 1025 + await this.bot.sendMessage(chatId, successMsg); 1026 + 1027 + // Refresh the announcements list 1028 + await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } }); 1029 + } catch (error) { 1030 + await this.bot.sendMessage( 1031 + chatId, 1032 + `❌ Error updating announcement: ${error.message}` 1033 + ); 1034 + } 1035 + }); 1036 + 1037 + // Skip the rest of the code for button 1038 + return; 1039 + } 1040 + 1041 + // For button editing, we'll first ask if they want to add, edit, or remove a button 1042 + const hasButton = announcement.button && announcement.button.text && announcement.button.url; 1043 + 1044 + if (hasButton) { 1045 + // Show options to edit or remove existing button 1046 + await this.bot.sendMessage( 1047 + chatId, 1048 + `Current button: "${announcement.button.text}" → ${announcement.button.url}\n\nWhat would you like to do?`, 1049 + { 1050 + reply_markup: { 1051 + inline_keyboard: [ 1052 + [ 1053 + { 1054 + text: '✏️ Edit Button', 1055 + callback_data: `edit_announcement_button_edit_${announcementId}` 1056 + } 1057 + ], 1058 + [ 1059 + { 1060 + text: '❌ Remove Button', 1061 + callback_data: `edit_announcement_button_remove_${announcementId}` 1062 + } 1063 + ], 1064 + [ 1065 + { 1066 + text: '↩️ Cancel', 1067 + callback_data: 'cancel_edit_announcement_button' 1068 + } 1069 + ] 1070 + ] 1071 + } 1072 + } 1073 + ); 1074 + return; 1075 + } else { 1076 + // No existing button, ask if they want to add one 1077 + await this.bot.sendMessage( 1078 + chatId, 1079 + `This announcement doesn't have a button. Would you like to add one?`, 1080 + { 1081 + reply_markup: { 1082 + inline_keyboard: [ 1083 + [ 1084 + { 1085 + text: '➕ Add Button', 1086 + callback_data: `edit_announcement_button_add_${announcementId}` 1087 + } 1088 + ], 1089 + [ 1090 + { 1091 + text: '↩️ Cancel', 1092 + callback_data: 'cancel_edit_announcement_button' 1093 + } 1094 + ] 1095 + ] 1096 + } 1097 + } 1098 + ); 1099 + return; 1100 + } 912 1101 } 913 1102 break; 914 1103 } ··· 935 1124 await this.bot.deleteMessage(chatId, query.message.message_id); 936 1125 937 1126 // Refresh announcements list 938 - await this.bot.onText.handlers.find(h => h.regexp.toString().includes('/announcements'))?._callback({ chat: { id: chatId } }); 1127 + await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } }); 939 1128 } 940 1129 break; 941 1130 } ··· 1007 1196 delete this.editingAnnouncementButton[query.from.id]; 1008 1197 1009 1198 // Refresh announcements list 1010 - await this.bot.onText.handlers.find(h => h.regexp.toString().includes('/announcements'))?._callback({ chat: { id: chatId } }); 1199 + await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } }); 1011 1200 } catch (error) { 1012 1201 await this.bot.sendMessage( 1013 1202 chatId, ··· 1034 1223 await this.bot.deleteMessage(chatId, query.message.message_id); 1035 1224 1036 1225 // Refresh announcements list 1037 - await this.bot.onText.handlers.find(h => h.regexp.toString().includes('/announcements'))?._callback({ chat: { id: chatId } }); 1226 + await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } }); 1038 1227 } catch (error) { 1039 1228 await this.bot.answerCallbackQuery(query.id, { text: `Error: ${error.message}` }); 1040 1229 } ··· 1049 1238 await this.bot.deleteMessage(chatId, query.message.message_id); 1050 1239 1051 1240 // Refresh announcements list 1052 - await this.bot.onText.handlers.find(h => h.regexp.toString().includes('/announcements'))?._callback({ chat: { id: chatId } }); 1241 + await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } }); 1053 1242 } 1054 1243 break; 1055 1244 } ··· 1386 1575 console.error('Error posting media:', error); 1387 1576 return false; 1388 1577 } 1578 + } 1579 + 1580 + /** 1581 + * Shutdown the bot gracefully 1582 + * @returns {Promise<void>} 1583 + */ 1584 + /** 1585 + * Helper method to ask about adding a button to an announcement 1586 + * @param {number} chatId - The chat ID where to send the message 1587 + * @param {number} userId - The user ID for tracking state 1588 + */ 1589 + askAboutButton(chatId, userId) { 1590 + this.bot.sendMessage( 1591 + chatId, 1592 + 'Would you like to add a button with a link to this announcement?', 1593 + { 1594 + reply_markup: { 1595 + inline_keyboard: [ 1596 + [ 1597 + { text: 'Yes', callback_data: 'add_button' }, 1598 + { text: 'No', callback_data: 'skip_button' } 1599 + ] 1600 + ] 1601 + } 1602 + } 1603 + ); 1604 + } 1605 + 1606 + /** 1607 + * Helper method to show announcement confirmation 1608 + * @param {number} chatId - The chat ID where to send the message 1609 + * @param {number} userId - The user ID for tracking state 1610 + * @param {string} name - Announcement name 1611 + * @param {string} message - Announcement message 1612 + * @param {string} cronSchedule - Cron schedule 1613 + * @param {Object} button - Button object (optional) 1614 + */ 1615 + async showAnnouncementConfirmation(chatId, userId, name, message, cronSchedule, button = null) { 1616 + // Store all the data for the confirmation callback 1617 + this.confirmAnnouncement = this.confirmAnnouncement || {}; 1618 + this.confirmAnnouncement[userId] = { 1619 + name, 1620 + message, 1621 + cronSchedule, 1622 + button 1623 + }; 1624 + 1625 + // Create confirmation message with all details 1626 + let confirmationMessage = "📣 *Announcement Preview*\n\n"; 1627 + confirmationMessage += `*Name*: ${name || "(Auto-generated)"}\n`; 1628 + confirmationMessage += `*Schedule*: \`${cronSchedule}\`\n`; 1629 + 1630 + if (button) { 1631 + confirmationMessage += `*Button*: "${button.text}" → ${button.url}\n`; 1632 + } else { 1633 + confirmationMessage += "*Button*: None\n"; 1634 + } 1635 + 1636 + confirmationMessage += "\n*Message Preview*:\n------------------\n"; 1637 + 1638 + // Send confirmation message 1639 + await this.bot.sendMessage( 1640 + chatId, 1641 + confirmationMessage, 1642 + { parse_mode: 'Markdown' } 1643 + ); 1644 + 1645 + // Send formatted message preview 1646 + const formattedMessage = this.announcements.formatMessageText(message); 1647 + await this.bot.sendMessage( 1648 + chatId, 1649 + formattedMessage, 1650 + { parse_mode: 'HTML' } 1651 + ); 1652 + 1653 + // Ask for confirmation 1654 + await this.bot.sendMessage( 1655 + chatId, 1656 + "Does everything look correct? Ready to create this announcement?", 1657 + { 1658 + reply_markup: { 1659 + inline_keyboard: [ 1660 + [ 1661 + { text: '✅ Create Announcement', callback_data: 'confirm_announcement' }, 1662 + { text: '❌ Cancel', callback_data: 'cancel_announcement' } 1663 + ] 1664 + ] 1665 + } 1666 + } 1667 + ); 1668 + 1669 + // Set up a one-time listener for the confirmation response 1670 + this.bot.once('callback_query', async (query) => { 1671 + if (query.from.id !== userId) return; // Make sure it's the same user 1672 + 1673 + await this.bot.answerCallbackQuery(query.id); 1674 + 1675 + if (query.data === 'confirm_announcement') { 1676 + try { 1677 + // Create the announcement 1678 + const announcement = await this.announcements.addAnnouncement( 1679 + message, 1680 + cronSchedule, 1681 + name, 1682 + button 1683 + ); 1684 + 1685 + // Send success message 1686 + let successMessage = `✅ Announcement "${announcement.name}" created!\n\n`; 1687 + successMessage += `Scheduled for: ${announcement.cronSchedule}\n\n`; 1688 + 1689 + if (button) { 1690 + successMessage += `Button: "${button.text}" → ${button.url}\n\n`; 1691 + } 1692 + 1693 + successMessage += "You can manage all announcements with /announcements"; 1694 + 1695 + await this.bot.sendMessage(chatId, successMessage); 1696 + 1697 + // Clean up 1698 + delete this.pendingAnnouncements[userId]; 1699 + delete this.confirmAnnouncement[userId]; 1700 + } catch (error) { 1701 + this.bot.sendMessage( 1702 + chatId, 1703 + `Error creating announcement: ${error.message}\n\nPlease try again.` 1704 + ); 1705 + } 1706 + } else { 1707 + // User canceled 1708 + await this.bot.sendMessage( 1709 + chatId, 1710 + "Announcement creation canceled. You can start over with /announce" 1711 + ); 1712 + 1713 + // Clean up 1714 + delete this.pendingAnnouncements[userId]; 1715 + delete this.confirmAnnouncement[userId]; 1716 + } 1717 + }); 1389 1718 } 1390 1719 1391 1720 /**
+48 -7
utils/announcementManager.js
··· 94 94 if (button && (!button.text || !button.url)) { 95 95 throw new Error('Button must have both text and url properties'); 96 96 } 97 + 98 + // Store the raw message - ensure we preserve line breaks 99 + // No additional processing, just use the message as-is 100 + const rawMessage = message; 97 101 98 102 // Create new announcement 99 103 const id = Date.now().toString(); 100 104 const announcement = { 101 105 id, 102 - message, 106 + message: rawMessage, // Use the raw message with preserved line breaks 103 107 cronSchedule, 104 108 name: name || `Announcement ${id.substring(id.length - 4)}`, 105 109 createdAt: new Date().toISOString(), ··· 163 167 164 168 // Update fields 165 169 if (updates.message !== undefined) { 170 + // Store the raw message exactly as received, preserving all line breaks 171 + // Do not trim() as it can remove important whitespace 166 172 announcement.message = updates.message; 167 173 } 168 174 ··· 222 228 try { 223 229 console.log(`Running scheduled announcement: ${announcement.name}`); 224 230 225 - // Create message options with markdown support 226 - const messageOptions = { parse_mode: 'Markdown' }; 231 + // Create message options with HTML support which handles line breaks better 232 + const messageOptions = { parse_mode: 'HTML' }; 233 + 234 + // Process the message to handle line breaks properly in HTML mode 235 + // Convert line breaks to HTML breaks and escape any HTML entities 236 + let processedMessage = this.formatMessageText(announcement.message); 227 237 228 238 // Add inline keyboard if a button is defined 229 239 if (announcement.button && announcement.button.text && announcement.button.url) { ··· 242 252 // Send the announcement message to the channel 243 253 const result = await this.telegramBot.bot.sendMessage( 244 254 this.telegramBot.channelId, 245 - announcement.message, 255 + processedMessage, 246 256 messageOptions 247 257 ); 248 258 ··· 286 296 isValidCronExpression(cronExpression) { 287 297 return cron.validate(cronExpression); 288 298 } 299 + 300 + /** 301 + * Format message text to properly handle line breaks and special formatting 302 + * @param {string} text - The original message text 303 + * @returns {string} - Formatted message text for Telegram 304 + */ 305 + formatMessageText(text) { 306 + if (!text) return ''; 307 + 308 + // Escape HTML special characters 309 + let formattedText = text 310 + .replace(/&/g, '&amp;') 311 + .replace(/</g, '&lt;') 312 + .replace(/>/g, '&gt;'); 313 + 314 + // Line breaks are preserved automatically in Telegram's HTML mode 315 + // Do NOT replace newlines with <br/> tags as Telegram doesn't support them 316 + 317 + // Handle common markdown-style formatting with multiline support 318 + // Process text in a way that handles multiline content properly 319 + // Uses non-greedy quantifiers (.*?) and the 's' flag to match across multiple lines 320 + formattedText = formattedText.replace(/\*\*([\s\S]*?)\*\*/g, '<b>$1</b>'); 321 + formattedText = formattedText.replace(/\*([\s\S]*?)\*/g, '<i>$1</i>'); 322 + formattedText = formattedText.replace(/__([\s\S]*?)__/g, '<u>$1</u>'); 323 + formattedText = formattedText.replace(/~~([\s\S]*?)~~/g, '<s>$1</s>'); 324 + 325 + return formattedText; 326 + } 289 327 290 328 /** 291 329 * Send an announcement immediately (one-time) ··· 300 338 } 301 339 302 340 try { 303 - // Create message options with markdown support 304 - const messageOptions = { parse_mode: 'Markdown' }; 341 + // Create message options with HTML support 342 + const messageOptions = { parse_mode: 'HTML' }; 343 + 344 + // Process the message to handle line breaks properly in HTML mode 345 + let processedMessage = this.formatMessageText(announcement.message); 305 346 306 347 // Add inline keyboard if a button is defined 307 348 if (announcement.button && announcement.button.text && announcement.button.url) { ··· 319 360 320 361 await this.telegramBot.bot.sendMessage( 321 362 this.telegramBot.channelId, 322 - announcement.message, 363 + processedMessage, 323 364 messageOptions 324 365 ); 325 366