···163163- `/setcount [number]` - Set number of images per post interval
164164- `/clear` - Clear the entire queue
165165- `/cleancache` - Clean expired items from media cache
166166+- `/announce` - Create a new announcement
167167+- `/announcements` - Manage existing announcements
166168167169### Adding Images to Queue
168170
+446-117
bot/telegramBot.js
···4545/setcount [number] - Set number of images per scheduled post (default: 1)
4646/clear - Clear the queue
4747/cleancache - Clean expired items from media cache
4848+/announce - Create a new announcement
4949+/announcements - Manage existing announcements
4850/update - Update bot from GitHub repository (owner only)
49515052Send any link to a supported site to add it to the queue.
···293295 });
294296295297 // Command to add a text announcement
296296- this.bot.onText(/\/announce(?:\s+(.+))?/, async (msg, match) => {
298298+ this.bot.onText(/^\/announce(?!\S)/, async (msg) => {
297299 const chatId = msg.chat.id;
298300299301 if (!this.isAuthorized(msg.from.id)) {
···301303 return;
302304 }
303305304304- // Check if there's text after the command
305305- const text = match && match[1] ? match[1].trim() : '';
306306-307307- if (!text) {
308308- // No parameters - show usage help
309309- this.bot.sendMessage(
310310- chatId,
311311- '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.'
312312- );
313313- return;
314314- }
315315-316316- // Store the message text in session and ask for a name
306306+ // Initialize the interactive announcement creation process
317307 this.pendingAnnouncements = this.pendingAnnouncements || {};
318318- this.pendingAnnouncements[msg.from.id] = { message: text };
308308+ this.pendingAnnouncements[msg.from.id] = {};
319309310310+ // Display introduction message with formatting options
320311 this.bot.sendMessage(
321312 chatId,
322322- 'Please enter a name for this announcement (or type "skip" for auto-generated name):',
323323- { reply_markup: { force_reply: true } }
313313+ '📣 *Create New Announcement*\n\n' +
314314+ 'I\'ll guide you through creating an announcement step by step:\n' +
315315+ '1️⃣ Name your announcement\n' +
316316+ '2️⃣ Write the message content\n' +
317317+ '3️⃣ Set a schedule\n' +
318318+ '4️⃣ Add an optional button (if desired)\n\n' +
319319+ 'You can use these formatting options in your message:\n' +
320320+ '- *text* for italic\n' +
321321+ '- **text** for bold\n' +
322322+ '- __text__ for underlined\n' +
323323+ '- ~~text~~ for strikethrough\n\n' +
324324+ 'Let\'s start! First, what would you like to name this announcement?',
325325+ {
326326+ parse_mode: 'Markdown',
327327+ reply_markup: { force_reply: true }
328328+ }
324329 ).then(namePrompt => {
325330 // Set up a one-time listener for the name response
326331 this.bot.onReplyToMessage(chatId, namePrompt.message_id, async (nameMsg) => {
327332 const announcementName = nameMsg.text === 'skip' ? '' : nameMsg.text;
328333 this.pendingAnnouncements[msg.from.id].name = announcementName;
329334330330- // Now ask for a schedule
335335+ // Now ask for the announcement message text
331336 this.bot.sendMessage(
332337 chatId,
333333- 'Please enter a cron schedule for when this announcement should run (use https://crontab.guru/ for help):',
334334- { reply_markup: { force_reply: true } }
335335- ).then(schedulePrompt => {
336336- // Set up a one-time listener for the schedule response
337337- this.bot.onReplyToMessage(chatId, schedulePrompt.message_id, async (scheduleMsg) => {
338338- const cronSchedule = scheduleMsg.text;
339339-340340- // Ask if they want to add a button
338338+ 'Great! Now enter the announcement message content.\n\n' +
339339+ 'Your message can contain multiple lines and formatting:\n' +
340340+ '- *text* for italic\n' +
341341+ '- **text** for bold\n' +
342342+ '- __text__ for underlined\n' +
343343+ '- ~~text~~ for strikethrough\n\n' +
344344+ 'Type your message now:',
345345+ {
346346+ parse_mode: 'Markdown',
347347+ reply_markup: { force_reply: true }
348348+ }
349349+ ).then(messagePrompt => {
350350+ // Set up a one-time listener for the message text response
351351+ this.bot.onReplyToMessage(chatId, messagePrompt.message_id, async (messageTextMsg) => {
352352+ this.pendingAnnouncements[msg.from.id].message = messageTextMsg.text;
353353+354354+ try {
355355+ // Show a preview of the formatted message
356356+ const previewText = this.announcements.formatMessageText(this.pendingAnnouncements[msg.from.id].message);
357357+358358+ // Send a preview message to show how it will look
359359+ await this.bot.sendMessage(
360360+ chatId,
361361+ "Here's a preview of your announcement with formatting:",
362362+ { parse_mode: 'Markdown' }
363363+ );
364364+365365+ // Send the actual preview
366366+ await this.bot.sendMessage(
367367+ chatId,
368368+ previewText,
369369+ { parse_mode: 'HTML' }
370370+ );
371371+ } catch (error) {
372372+ console.error("Error showing announcement preview:", error);
373373+ await this.bot.sendMessage(
374374+ chatId,
375375+ "Note: There might be issues with your formatting. Please ensure all formatting tags are properly closed."
376376+ );
377377+ }
378378+ // Now ask for a schedule
341379 this.bot.sendMessage(
342380 chatId,
343343- 'Would you like to add a button with a link to this announcement?',
344344- {
345345- reply_markup: {
346346- inline_keyboard: [
347347- [
348348- { text: 'Yes', callback_data: 'add_button' },
349349- { text: 'No', callback_data: 'skip_button' }
350350- ]
351351- ]
352352- }
381381+ 'Now, let\'s set the schedule for this announcement.\n\n' +
382382+ 'Enter a cron schedule expression. Examples:\n' +
383383+ '- `0 9 * * *` = Every day at 9:00 AM\n' +
384384+ '- `0 18 * * 5` = Every Friday at 6:00 PM\n' +
385385+ '- `0 12 1 * *` = First day of each month at noon\n\n' +
386386+ 'For more options, visit https://crontab.guru/',
387387+ {
388388+ parse_mode: 'Markdown',
389389+ reply_markup: { force_reply: true }
353390 }
354354- ).then(buttonPrompt => {
355355- // Callback handler for yes/no button selection
356356- this.bot.once('callback_query', async (query) => {
357357- await this.bot.answerCallbackQuery(query.id);
358358-359359- // Delete the yes/no prompt
360360- await this.bot.deleteMessage(chatId, buttonPrompt.message_id);
391391+ ).then(schedulePrompt => {
392392+ // Set up a one-time listener for the schedule response
393393+ this.bot.onReplyToMessage(chatId, schedulePrompt.message_id, async (scheduleMsg) => {
394394+ const cronSchedule = scheduleMsg.text;
361395362362- if (query.data === 'add_button') {
363363- // User wants to add a button
396396+ // Validate the cron schedule
397397+ if (!this.announcements.isValidCronExpression(cronSchedule)) {
364398 this.bot.sendMessage(
365399 chatId,
366366- 'Please enter the button text:',
367367- { reply_markup: { force_reply: true } }
368368- ).then(buttonTextPrompt => {
369369- this.bot.onReplyToMessage(chatId, buttonTextPrompt.message_id, async (buttonTextMsg) => {
370370- const buttonText = buttonTextMsg.text;
371371-372372- // Now ask for the button URL
400400+ '⚠️ That doesn\'t appear to be a valid cron schedule. Please try again using the format shown in the examples.',
401401+ { parse_mode: 'Markdown' }
402402+ ).then(() => {
403403+ // Ask again for a valid schedule
404404+ this.bot.sendMessage(
405405+ chatId,
406406+ 'Please enter a valid cron schedule. Examples:\n' +
407407+ '- `0 9 * * *` = Every day at 9:00 AM\n' +
408408+ '- `0 18 * * 5` = Every Friday at 6:00 PM\n' +
409409+ '- `0 12 1 * *` = First day of each month at noon',
410410+ {
411411+ parse_mode: 'Markdown',
412412+ reply_markup: { force_reply: true }
413413+ }
414414+ ).then((newSchedulePrompt) => {
415415+ // Handle the new schedule response
416416+ this.bot.onReplyToMessage(chatId, newSchedulePrompt.message_id, (newScheduleMsg) => {
417417+ // Replace the schedule with the new one
418418+ const validCronSchedule = newScheduleMsg.text;
419419+420420+ if (!this.announcements.isValidCronExpression(validCronSchedule)) {
421421+ this.bot.sendMessage(
422422+ chatId,
423423+ '⚠️ Still not a valid cron schedule. Using "0 12 * * *" (daily at noon) as a default. You can edit this later.'
424424+ );
425425+ this.pendingAnnouncements[msg.from.id].cronSchedule = "0 12 * * *";
426426+427427+ // Continue to button step
428428+ this.askAboutButton(chatId, msg.from.id);
429429+ } else {
430430+ this.pendingAnnouncements[msg.from.id].cronSchedule = validCronSchedule;
431431+432432+ // Continue to button step
433433+ this.askAboutButton(chatId, msg.from.id);
434434+ }
435435+ });
436436+ });
437437+ });
438438+ return;
439439+ }
440440+441441+ // Store the schedule
442442+ this.pendingAnnouncements[msg.from.id].cronSchedule = cronSchedule;
443443+444444+ // Ask if they want to add a button
445445+ this.bot.sendMessage(
446446+ chatId,
447447+ 'Would you like to add a button with a link to this announcement?',
448448+ {
449449+ reply_markup: {
450450+ inline_keyboard: [
451451+ [
452452+ { text: 'Yes', callback_data: 'add_button' },
453453+ { text: 'No', callback_data: 'skip_button' }
454454+ ]
455455+ ]
456456+ }
457457+ }
458458+ ).then(buttonPrompt => {
459459+ // Callback handler for yes/no button selection
460460+ this.bot.once('callback_query', async (query) => {
461461+ await this.bot.answerCallbackQuery(query.id);
462462+463463+ // Delete the yes/no prompt
464464+ await this.bot.deleteMessage(chatId, buttonPrompt.message_id);
465465+466466+ if (query.data === 'add_button') {
467467+ // User wants to add a button
373468 this.bot.sendMessage(
374469 chatId,
375375- 'Please enter the button URL:',
470470+ 'Please enter the button text:',
376471 { reply_markup: { force_reply: true } }
377377- ).then(buttonUrlPrompt => {
378378- this.bot.onReplyToMessage(chatId, buttonUrlPrompt.message_id, async (buttonUrlMsg) => {
379379- const buttonUrl = buttonUrlMsg.text;
380380-381381- // Create the button object
382382- const button = {
383383- text: buttonText,
384384- url: buttonUrl
385385- };
472472+ ).then(buttonTextPrompt => {
473473+ this.bot.onReplyToMessage(chatId, buttonTextPrompt.message_id, async (buttonTextMsg) => {
474474+ const buttonText = buttonTextMsg.text;
386475387387- try {
388388- const announcement = await this.announcements.addAnnouncement(
389389- this.pendingAnnouncements[msg.from.id].message,
390390- cronSchedule,
391391- this.pendingAnnouncements[msg.from.id].name,
392392- button
393393- );
394394-395395- delete this.pendingAnnouncements[msg.from.id];
396396-397397- this.bot.sendMessage(
398398- chatId,
399399- `✅ Announcement "${announcement.name}" created!\n\n`+
400400- `Scheduled for: ${announcement.cronSchedule}\n\n`+
401401- `Button: "${button.text}" → ${button.url}\n\n`+
402402- `You can manage all announcements with /announcements`
403403- );
404404- } catch (error) {
405405- this.bot.sendMessage(
406406- chatId,
407407- `Error creating announcement: ${error.message}\n\nPlease try again.`
408408- );
409409- }
476476+ // Now ask for the button URL
477477+ this.bot.sendMessage(
478478+ chatId,
479479+ 'Please enter the button URL:',
480480+ { reply_markup: { force_reply: true } }
481481+ ).then(buttonUrlPrompt => {
482482+ this.bot.onReplyToMessage(chatId, buttonUrlPrompt.message_id, async (buttonUrlMsg) => {
483483+ const buttonUrl = buttonUrlMsg.text;
484484+485485+ // Store the button object
486486+ const button = {
487487+ text: buttonText,
488488+ url: buttonUrl
489489+ };
490490+491491+ // Show confirmation with preview
492492+ await this.showAnnouncementConfirmation(
493493+ chatId,
494494+ msg.from.id,
495495+ this.pendingAnnouncements[msg.from.id].name,
496496+ this.pendingAnnouncements[msg.from.id].message,
497497+ this.pendingAnnouncements[msg.from.id].cronSchedule,
498498+ button
499499+ );
500500+ });
501501+ });
410502 });
411503 });
412412- });
504504+ } else {
505505+ // User doesn't want to add a button
506506+ // Show confirmation with preview
507507+ await this.showAnnouncementConfirmation(
508508+ chatId,
509509+ msg.from.id,
510510+ this.pendingAnnouncements[msg.from.id].name,
511511+ this.pendingAnnouncements[msg.from.id].message,
512512+ this.pendingAnnouncements[msg.from.id].cronSchedule
513513+ );
514514+ }
413515 });
414414- } else {
415415- // User doesn't want to add a button
416416- try {
417417- const announcement = await this.announcements.addAnnouncement(
418418- this.pendingAnnouncements[msg.from.id].message,
419419- cronSchedule,
420420- this.pendingAnnouncements[msg.from.id].name
421421- );
422422-423423- delete this.pendingAnnouncements[msg.from.id];
424424-425425- this.bot.sendMessage(
426426- chatId,
427427- `✅ Announcement "${announcement.name}" created!\n\nScheduled for: ${announcement.cronSchedule}\n\nYou can manage all announcements with /announcements`
428428- );
429429- } catch (error) {
430430- this.bot.sendMessage(
431431- chatId,
432432- `Error creating announcement: ${error.message}\n\nPlease try again with a valid cron expression.`
433433- );
434434- }
435435- }
516516+ });
436517 });
437518 });
438519 });
···442523 });
443524444525 // Command to list and manage all announcements
445445- this.bot.onText(/\/announcements/, async (msg) => {
526526+ this.bot.onText(/^\/announcements(?!\S)/, async (msg) => {
446527 const chatId = msg.chat.id;
447528448529 if (!this.isAuthorized(msg.from.id)) {
···478559 message += `Button: "${announcement.button.text}" → ${announcement.button.url}\n`;
479560 }
480561481481- message += `Message: "${announcement.message.substring(0, 50)}${announcement.message.length > 50 ? '...' : ''}"\n\n`;
562562+ // Format the message preview, replacing line breaks with special character
563563+ const previewMessage = announcement.message
564564+ .replace(/\n/g, '↵') // Replace line breaks with a visible symbol
565565+ .substring(0, 50);
566566+ message += `Message: "${previewMessage}${announcement.message.length > 50 ? '...' : ''}"\n\n`;
482567483568 // Add buttons for this announcement
484569 inlineKeyboard.push([
···698783699784 // Refresh announcements list
700785 await this.bot.deleteMessage(chatId, query.message.message_id);
701701- await this.bot.onText.handlers.find(h => h.regexp.toString().includes('/announcements'))?._callback({ chat: { id: chatId } });
786786+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
702787 }
703788 break;
704789 }
···751836 await this.bot.deleteMessage(chatId, query.message.message_id);
752837753838 // Refresh announcements list
754754- await this.bot.onText.handlers.find(h => h.regexp.toString().includes('/announcements'))?._callback({ chat: { id: chatId } });
839839+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
755840 } catch (error) {
756841 await this.bot.answerCallbackQuery(query.id, { text: `Error: ${error.message}` });
757842 }
···839924 let promptText = '';
840925 switch (field) {
841926 case 'message':
842842- promptText = `Please enter the new message text for the announcement "${announcement.name}":\n\nCurrent message:\n${announcement.message}`;
927927+ 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.`;
843928 break;
844929 case 'name':
845930 promptText = `Please enter the new name for the announcement "${announcement.name}":`;
···909994 return;
910995 }
911996 }
997997+998998+ // For message, name, and schedule we'll send a prompt and handle the reply
999999+ if (field === 'message' || field === 'name' || field === 'schedule') {
10001000+ // Send the prompt with force_reply
10011001+ const promptMsg = await this.bot.sendMessage(
10021002+ chatId,
10031003+ promptText,
10041004+ { reply_markup: { force_reply: true } }
10051005+ );
10061006+10071007+ // Set up one-time handler for the response
10081008+ this.bot.onReplyToMessage(chatId, promptMsg.message_id, async (responseMsg) => {
10091009+ try {
10101010+ // Get the response text - preserve line breaks and formatting exactly as received
10111011+ const responseText = responseMsg.text;
10121012+10131013+ // Prepare the update object
10141014+ const updates = {};
10151015+ updates[field] = responseText; // Raw text will preserve line breaks
10161016+10171017+ // Update the announcement
10181018+ await this.announcements.updateAnnouncement(announcementId, updates);
10191019+10201020+ // Notify user of success
10211021+ let successMsg = `✅ Announcement ${field} updated successfully!`;
10221022+ if (field === 'message') {
10231023+ successMsg += '\n\nYour message with all line breaks and formatting has been saved.';
10241024+ }
10251025+ await this.bot.sendMessage(chatId, successMsg);
10261026+10271027+ // Refresh the announcements list
10281028+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
10291029+ } catch (error) {
10301030+ await this.bot.sendMessage(
10311031+ chatId,
10321032+ `❌ Error updating announcement: ${error.message}`
10331033+ );
10341034+ }
10351035+ });
10361036+10371037+ // Skip the rest of the code for button
10381038+ return;
10391039+ }
10401040+10411041+ // For button editing, we'll first ask if they want to add, edit, or remove a button
10421042+ const hasButton = announcement.button && announcement.button.text && announcement.button.url;
10431043+10441044+ if (hasButton) {
10451045+ // Show options to edit or remove existing button
10461046+ await this.bot.sendMessage(
10471047+ chatId,
10481048+ `Current button: "${announcement.button.text}" → ${announcement.button.url}\n\nWhat would you like to do?`,
10491049+ {
10501050+ reply_markup: {
10511051+ inline_keyboard: [
10521052+ [
10531053+ {
10541054+ text: '✏️ Edit Button',
10551055+ callback_data: `edit_announcement_button_edit_${announcementId}`
10561056+ }
10571057+ ],
10581058+ [
10591059+ {
10601060+ text: '❌ Remove Button',
10611061+ callback_data: `edit_announcement_button_remove_${announcementId}`
10621062+ }
10631063+ ],
10641064+ [
10651065+ {
10661066+ text: '↩️ Cancel',
10671067+ callback_data: 'cancel_edit_announcement_button'
10681068+ }
10691069+ ]
10701070+ ]
10711071+ }
10721072+ }
10731073+ );
10741074+ return;
10751075+ } else {
10761076+ // No existing button, ask if they want to add one
10771077+ await this.bot.sendMessage(
10781078+ chatId,
10791079+ `This announcement doesn't have a button. Would you like to add one?`,
10801080+ {
10811081+ reply_markup: {
10821082+ inline_keyboard: [
10831083+ [
10841084+ {
10851085+ text: '➕ Add Button',
10861086+ callback_data: `edit_announcement_button_add_${announcementId}`
10871087+ }
10881088+ ],
10891089+ [
10901090+ {
10911091+ text: '↩️ Cancel',
10921092+ callback_data: 'cancel_edit_announcement_button'
10931093+ }
10941094+ ]
10951095+ ]
10961096+ }
10971097+ }
10981098+ );
10991099+ return;
11001100+ }
9121101 }
9131102 break;
9141103 }
···9351124 await this.bot.deleteMessage(chatId, query.message.message_id);
93611259371126 // Refresh announcements list
938938- await this.bot.onText.handlers.find(h => h.regexp.toString().includes('/announcements'))?._callback({ chat: { id: chatId } });
11271127+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
9391128 }
9401129 break;
9411130 }
···10071196 delete this.editingAnnouncementButton[query.from.id];
1008119710091198 // Refresh announcements list
10101010- await this.bot.onText.handlers.find(h => h.regexp.toString().includes('/announcements'))?._callback({ chat: { id: chatId } });
11991199+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
10111200 } catch (error) {
10121201 await this.bot.sendMessage(
10131202 chatId,
···10341223 await this.bot.deleteMessage(chatId, query.message.message_id);
1035122410361225 // Refresh announcements list
10371037- await this.bot.onText.handlers.find(h => h.regexp.toString().includes('/announcements'))?._callback({ chat: { id: chatId } });
12261226+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
10381227 } catch (error) {
10391228 await this.bot.answerCallbackQuery(query.id, { text: `Error: ${error.message}` });
10401229 }
···10491238 await this.bot.deleteMessage(chatId, query.message.message_id);
1050123910511240 // Refresh announcements list
10521052- await this.bot.onText.handlers.find(h => h.regexp.toString().includes('/announcements'))?._callback({ chat: { id: chatId } });
12411241+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
10531242 }
10541243 break;
10551244 }
···13861575 console.error('Error posting media:', error);
13871576 return false;
13881577 }
15781578+ }
15791579+15801580+ /**
15811581+ * Shutdown the bot gracefully
15821582+ * @returns {Promise<void>}
15831583+ */
15841584+ /**
15851585+ * Helper method to ask about adding a button to an announcement
15861586+ * @param {number} chatId - The chat ID where to send the message
15871587+ * @param {number} userId - The user ID for tracking state
15881588+ */
15891589+ askAboutButton(chatId, userId) {
15901590+ this.bot.sendMessage(
15911591+ chatId,
15921592+ 'Would you like to add a button with a link to this announcement?',
15931593+ {
15941594+ reply_markup: {
15951595+ inline_keyboard: [
15961596+ [
15971597+ { text: 'Yes', callback_data: 'add_button' },
15981598+ { text: 'No', callback_data: 'skip_button' }
15991599+ ]
16001600+ ]
16011601+ }
16021602+ }
16031603+ );
16041604+ }
16051605+16061606+ /**
16071607+ * Helper method to show announcement confirmation
16081608+ * @param {number} chatId - The chat ID where to send the message
16091609+ * @param {number} userId - The user ID for tracking state
16101610+ * @param {string} name - Announcement name
16111611+ * @param {string} message - Announcement message
16121612+ * @param {string} cronSchedule - Cron schedule
16131613+ * @param {Object} button - Button object (optional)
16141614+ */
16151615+ async showAnnouncementConfirmation(chatId, userId, name, message, cronSchedule, button = null) {
16161616+ // Store all the data for the confirmation callback
16171617+ this.confirmAnnouncement = this.confirmAnnouncement || {};
16181618+ this.confirmAnnouncement[userId] = {
16191619+ name,
16201620+ message,
16211621+ cronSchedule,
16221622+ button
16231623+ };
16241624+16251625+ // Create confirmation message with all details
16261626+ let confirmationMessage = "📣 *Announcement Preview*\n\n";
16271627+ confirmationMessage += `*Name*: ${name || "(Auto-generated)"}\n`;
16281628+ confirmationMessage += `*Schedule*: \`${cronSchedule}\`\n`;
16291629+16301630+ if (button) {
16311631+ confirmationMessage += `*Button*: "${button.text}" → ${button.url}\n`;
16321632+ } else {
16331633+ confirmationMessage += "*Button*: None\n";
16341634+ }
16351635+16361636+ confirmationMessage += "\n*Message Preview*:\n------------------\n";
16371637+16381638+ // Send confirmation message
16391639+ await this.bot.sendMessage(
16401640+ chatId,
16411641+ confirmationMessage,
16421642+ { parse_mode: 'Markdown' }
16431643+ );
16441644+16451645+ // Send formatted message preview
16461646+ const formattedMessage = this.announcements.formatMessageText(message);
16471647+ await this.bot.sendMessage(
16481648+ chatId,
16491649+ formattedMessage,
16501650+ { parse_mode: 'HTML' }
16511651+ );
16521652+16531653+ // Ask for confirmation
16541654+ await this.bot.sendMessage(
16551655+ chatId,
16561656+ "Does everything look correct? Ready to create this announcement?",
16571657+ {
16581658+ reply_markup: {
16591659+ inline_keyboard: [
16601660+ [
16611661+ { text: '✅ Create Announcement', callback_data: 'confirm_announcement' },
16621662+ { text: '❌ Cancel', callback_data: 'cancel_announcement' }
16631663+ ]
16641664+ ]
16651665+ }
16661666+ }
16671667+ );
16681668+16691669+ // Set up a one-time listener for the confirmation response
16701670+ this.bot.once('callback_query', async (query) => {
16711671+ if (query.from.id !== userId) return; // Make sure it's the same user
16721672+16731673+ await this.bot.answerCallbackQuery(query.id);
16741674+16751675+ if (query.data === 'confirm_announcement') {
16761676+ try {
16771677+ // Create the announcement
16781678+ const announcement = await this.announcements.addAnnouncement(
16791679+ message,
16801680+ cronSchedule,
16811681+ name,
16821682+ button
16831683+ );
16841684+16851685+ // Send success message
16861686+ let successMessage = `✅ Announcement "${announcement.name}" created!\n\n`;
16871687+ successMessage += `Scheduled for: ${announcement.cronSchedule}\n\n`;
16881688+16891689+ if (button) {
16901690+ successMessage += `Button: "${button.text}" → ${button.url}\n\n`;
16911691+ }
16921692+16931693+ successMessage += "You can manage all announcements with /announcements";
16941694+16951695+ await this.bot.sendMessage(chatId, successMessage);
16961696+16971697+ // Clean up
16981698+ delete this.pendingAnnouncements[userId];
16991699+ delete this.confirmAnnouncement[userId];
17001700+ } catch (error) {
17011701+ this.bot.sendMessage(
17021702+ chatId,
17031703+ `Error creating announcement: ${error.message}\n\nPlease try again.`
17041704+ );
17051705+ }
17061706+ } else {
17071707+ // User canceled
17081708+ await this.bot.sendMessage(
17091709+ chatId,
17101710+ "Announcement creation canceled. You can start over with /announce"
17111711+ );
17121712+17131713+ // Clean up
17141714+ delete this.pendingAnnouncements[userId];
17151715+ delete this.confirmAnnouncement[userId];
17161716+ }
17171717+ });
13891718 }
1390171913911720 /**
+48-7
utils/announcementManager.js
···9494 if (button && (!button.text || !button.url)) {
9595 throw new Error('Button must have both text and url properties');
9696 }
9797+9898+ // Store the raw message - ensure we preserve line breaks
9999+ // No additional processing, just use the message as-is
100100+ const rawMessage = message;
9710198102 // Create new announcement
99103 const id = Date.now().toString();
100104 const announcement = {
101105 id,
102102- message,
106106+ message: rawMessage, // Use the raw message with preserved line breaks
103107 cronSchedule,
104108 name: name || `Announcement ${id.substring(id.length - 4)}`,
105109 createdAt: new Date().toISOString(),
···163167164168 // Update fields
165169 if (updates.message !== undefined) {
170170+ // Store the raw message exactly as received, preserving all line breaks
171171+ // Do not trim() as it can remove important whitespace
166172 announcement.message = updates.message;
167173 }
168174···222228 try {
223229 console.log(`Running scheduled announcement: ${announcement.name}`);
224230225225- // Create message options with markdown support
226226- const messageOptions = { parse_mode: 'Markdown' };
231231+ // Create message options with HTML support which handles line breaks better
232232+ const messageOptions = { parse_mode: 'HTML' };
233233+234234+ // Process the message to handle line breaks properly in HTML mode
235235+ // Convert line breaks to HTML breaks and escape any HTML entities
236236+ let processedMessage = this.formatMessageText(announcement.message);
227237228238 // Add inline keyboard if a button is defined
229239 if (announcement.button && announcement.button.text && announcement.button.url) {
···242252 // Send the announcement message to the channel
243253 const result = await this.telegramBot.bot.sendMessage(
244254 this.telegramBot.channelId,
245245- announcement.message,
255255+ processedMessage,
246256 messageOptions
247257 );
248258···286296 isValidCronExpression(cronExpression) {
287297 return cron.validate(cronExpression);
288298 }
299299+300300+ /**
301301+ * Format message text to properly handle line breaks and special formatting
302302+ * @param {string} text - The original message text
303303+ * @returns {string} - Formatted message text for Telegram
304304+ */
305305+ formatMessageText(text) {
306306+ if (!text) return '';
307307+308308+ // Escape HTML special characters
309309+ let formattedText = text
310310+ .replace(/&/g, '&')
311311+ .replace(/</g, '<')
312312+ .replace(/>/g, '>');
313313+314314+ // Line breaks are preserved automatically in Telegram's HTML mode
315315+ // Do NOT replace newlines with <br/> tags as Telegram doesn't support them
316316+317317+ // Handle common markdown-style formatting with multiline support
318318+ // Process text in a way that handles multiline content properly
319319+ // Uses non-greedy quantifiers (.*?) and the 's' flag to match across multiple lines
320320+ formattedText = formattedText.replace(/\*\*([\s\S]*?)\*\*/g, '<b>$1</b>');
321321+ formattedText = formattedText.replace(/\*([\s\S]*?)\*/g, '<i>$1</i>');
322322+ formattedText = formattedText.replace(/__([\s\S]*?)__/g, '<u>$1</u>');
323323+ formattedText = formattedText.replace(/~~([\s\S]*?)~~/g, '<s>$1</s>');
324324+325325+ return formattedText;
326326+ }
289327290328 /**
291329 * Send an announcement immediately (one-time)
···300338 }
301339302340 try {
303303- // Create message options with markdown support
304304- const messageOptions = { parse_mode: 'Markdown' };
341341+ // Create message options with HTML support
342342+ const messageOptions = { parse_mode: 'HTML' };
343343+344344+ // Process the message to handle line breaks properly in HTML mode
345345+ let processedMessage = this.formatMessageText(announcement.message);
305346306347 // Add inline keyboard if a button is defined
307348 if (announcement.button && announcement.button.text && announcement.button.url) {
···319360320361 await this.telegramBot.bot.sendMessage(
321362 this.telegramBot.channelId,
322322- announcement.message,
363363+ processedMessage,
323364 messageOptions
324365 );
325366