···3030# Set to true to disable auto-updates and enable development features
3131DEV_MODE=false
32323333+# Queue Alert Configuration
3434+# Threshold below which to alert users (default: 10)
3535+QUEUE_LOW_THRESHOLD=10
3636+# Threshold at which to send empty queue alerts (default: 0)
3737+QUEUE_EMPTY_THRESHOLD=0
3838+# Enable queue monitoring alerts (default: true)
3939+QUEUE_ALERTS_ENABLED=true
4040+# Hours between repeated alerts (default: 24)
4141+QUEUE_ALERT_COOLDOWN_HOURS=24
4242+3343# API Keys
3444# Weasyl API Key for accessing their API
3545WEASYL_API_KEY=your_weasyl_api_key_here
···82828383This system ensures that all media is properly optimized before being sent to Telegram, providing reliable playback across all devices while managing bandwidth and storage efficiently.
84848585+## Bot Architecture
8686+8787+Stagehand uses a **modular bot architecture** for better maintainability and extensibility:
8888+8989+### Modular Design
9090+- **Commands**: Each bot command (`/start`, `/help`, `/queue`, etc.) is implemented as a separate module
9191+- **Helpers**: Shared functionality (auth, media posting, queue management) is organized into helper classes
9292+- **Registry System**: A central command registry coordinates all modules and their dependencies
9393+9494+### Migration Status
9595+- ✅ **Current Version**: Modular architecture (active)
9696+- 📁 **Legacy Backup**: Original monolithic version saved as `telegramBot.js.backup`
9797+- 🔄 **Migration Script**: Use `./migrate-bot.sh` to switch between versions if needed
9898+9999+### Benefits
100100+- **Easier Maintenance**: Changes to one command don't affect others
101101+- **Better Testing**: Individual modules can be tested in isolation
102102+- **Extensibility**: New commands and features can be added easily
103103+- **Code Organization**: Clear separation of concerns
104104+105105+For detailed architecture documentation, see [`docs/bot-architecture.md`](docs/bot-architecture.md).
106106+85107## Installation
86108871091. Clone this repository
···158180- `/start` - Start the bot
159181- `/help` - Show help information
160182- `/queue` - Show current queue status with interactive management
183183+- `/status` - Show detailed queue status and alert information
161184- `/send` - Post the next image in the queue immediately
162185- `/schedule [cron]` - Set posting schedule using cron syntax
163186- `/setcount [number]` - Set number of images per post interval
187187+- `/shuffle` - Toggle shuffle mode (randomizes queue after each post)
164188- `/clear` - Clear the entire queue
165189- `/cleancache` - Clean expired items from media cache
166190- `/announce` - Create a new announcement
···179203- **Item Removal**: Remove specific items from the queue with a single click
180204- **Reordering**: Move any item to the top of the queue to be posted next
181205- **Pagination**: Easily navigate through pages of queued items
206206+- **Shuffle Mode**: Automatically randomize the queue after each post with `/shuffle` command
182207183208The interface shows important information about each queued item including:
184209- Item position in queue
···186211- Title and source website
187212- Controls for managing each item
188213189189-## Adding New Website Scrapers
214214+## Queue Monitoring and Alerts
215215+216216+Stagehand includes an intelligent queue monitoring system that automatically alerts authorized users when the queue needs attention:
217217+218218+### Features
219219+- **Real-time monitoring**: Checks queue levels every 30 seconds
220220+- **Smart notifications**: Configurable thresholds for low and empty queue alerts
221221+- **24-hour cooldown**: Prevents alert spam with time-based rate limiting
222222+- **Multi-user support**: All authorized users receive alerts simultaneously
223223+- **Admin controls**: Test and manage alerts via the `/status` command
190224191191-To add support for a new website:
225225+### Configuration
226226+Add these variables to your `.env` file to customize alert behavior:
192227193193-1. Create a new scraper in the `scrapers` directory extending `BaseScraper`
194194-2. Implement the `canHandle` and `extract` methods
195195-3. Register the scraper in `utils/scraperManager.js`
228228+```bash
229229+# Queue Alert Configuration
230230+QUEUE_LOW_THRESHOLD=10 # Alert when queue ≤ this number
231231+QUEUE_EMPTY_THRESHOLD=0 # Alert when queue is critically low/empty
232232+QUEUE_ALERTS_ENABLED=true # Enable/disable monitoring
233233+QUEUE_ALERT_COOLDOWN_HOURS=24 # Hours between repeated alerts
234234+```
235235+236236+### Commands
237237+- `/status` - View detailed queue status and alert configuration
238238+- Use admin controls in `/status` to test alerts and reset cooldowns
239239+240240+For detailed configuration options, see [Queue Monitoring Configuration](docs/queue-monitoring-configuration.md).
241241+242242+## Documentation
243243+244244+This project includes comprehensive documentation in the `docs/` directory:
245245+246246+- **[Queue Monitoring Implementation](docs/queue-monitoring-implementation.md)** - Complete implementation details for the queue monitoring and alert system
247247+- **[Queue Monitoring Configuration](docs/queue-monitoring-configuration.md)** - Configuration guide for queue alerts and thresholds
248248+- **[Bot Architecture](docs/bot-architecture.md)** - Detailed architecture documentation for the modular bot system
249249+- **[Bot Architecture Migration](docs/bot-architecture-migration.md)** - Migration guide from monolithic to modular architecture
196250197251## License
198252···206260- [x] SoFurry Scraper
207261- [x] Weasyl Scraper
208262- [x] Interactive Graphical Queue Manager
263263+- [x] Add shuffle mode for queue
209264- [ ] Add perceptual hashing
210265- [ ] Redo Queue Manager
211211-- [ ] Redo Bluesky Module
212212-- [ ] Redo Telegram Module
266266+- [x] Redo Bluesky Module
267267+- [x] Redo Telegram Module
213268- [ ] Redo Discord Module
+30-1676
bot/telegramBot.js
···11const TelegramBot = require('node-telegram-bot-api');
22-const axios = require('axios');
33-const fs = require('fs');
44-const { exec } = require('child_process');
55-const { promisify } = require('util');
66-const execAsync = promisify(exec);
72const config = require('../config');
83const queueManager = require('../queue/queueManager');
99-const scraperManager = require('../utils/scraperManager');
1010-const mediaCache = require('../utils/mediaCache');
114const AnnouncementManager = require('../utils/announcementManager');
1212-const discordWebhook = require('./discordWebhook');
55+const QueueMonitor = require('../utils/queueMonitor');
66+const CommandRegistry = require('./telegrambot/commandRegistry');
137148class StagehandBot {
159 constructor() {
···1711 this.serviceName = 'telegram';
1812 this.channelId = config.channelId;
1913 this.announcements = new AnnouncementManager(this);
1414+ this.queueMonitor = new QueueMonitor(this);
1515+ this.commandRegistry = new CommandRegistry(this.bot, this.announcements, this.queueMonitor);
2016 this.init();
2117 }
22182319 async init() {
2420 await this.announcements.init();
2121+ await this.queueMonitor.init(queueManager);
2522 this.registerCommands();
2626- this.registerCallbacks();
2723 console.log('Telegram bot started...');
2824 }
29253026 registerCommands() {
3131- // Command to start the bot
3232- this.bot.onText(/\/start/, (msg) => {
3333- const chatId = msg.chat.id;
3434- this.bot.sendMessage(chatId, 'Stagehand bot is active. Send me links to queue images for posting!');
3535- });
3636-3737- // Command to show help
3838- this.bot.onText(/\/help/, (msg) => {
3939- const chatId = msg.chat.id;
4040- const helpText = `
4141-Stagehand Bot Commands:
4242-/queue - Show current queue status with interactive management
4343-/send - Post the next image in the queue
4444-/schedule [cron] - Set posting schedule (cron syntax, use https://crontab.guru/ for help)
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
5050-/update - Update bot from GitHub repository (owner only)
5151-5252-Send any link to a supported site to add it to the queue.
5353-Supported sites: e621, FurAffinity, SoFurry, Weasyl, Bluesky
5454- `;
5555- this.bot.sendMessage(chatId, helpText);
5656- });
5757-5858- // Command to show queue status with visual management
5959- this.bot.onText(/\/queue(?:\s+(\d+))?/, async (msg, match) => {
6060- const chatId = msg.chat.id;
6161-6262- if (!this.isAuthorized(msg.from.id)) {
6363- this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
6464- return;
6565- }
6666-6767- // Get page number from the command (defaults to 1)
6868- const page = parseInt(match[1]) || 1;
6969- const pageSize = 5;
7070-7171- await this.displayQueuePage(chatId, page, pageSize);
7272- });
7373-7474- // Command to post the next image
7575- this.bot.onText(/\/send/, async (msg) => {
7676- const chatId = msg.chat.id;
7777-7878- if (!this.isAuthorized(msg.from.id)) {
7979- this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
8080- return;
8181- }
8282-8383- const nextItem = await queueManager.getNextFromQueue();
8484-8585- if (!nextItem) {
8686- this.bot.sendMessage(chatId, 'Queue is empty, nothing to post.');
8787- return;
8888- }
8989-9090- // Status tracking variables
9191- let telegramSuccess = false;
9292- let discordSuccess = false;
9393- let telegramStatus = 'not attempted';
9494- let discordStatus = 'not attempted';
9595-9696- // Post to Telegram if it hasn't been posted yet
9797- if (!queueManager.hasBeenPostedByService(0, 'telegram')) {
9898- telegramStatus = 'attempting';
9999- const telegramResult = await this.postMedia(nextItem);
100100-101101- if (telegramResult) {
102102- await queueManager.markPostedByService(0, 'telegram');
103103- telegramSuccess = true;
104104- telegramStatus = 'posted';
105105- } else {
106106- telegramStatus = 'failed';
107107- }
108108- } else {
109109- telegramStatus = 'already posted';
110110- telegramSuccess = true;
111111- }
112112-113113- // Post to Discord if it's configured and hasn't been posted yet
114114- if (discordWebhook.isEnabled() && !queueManager.hasBeenPostedByService(0, 'discord')) {
115115- discordStatus = 'attempting';
116116- try {
117117- const discordResult = await discordWebhook.postMedia(nextItem);
118118-119119- if (discordResult) {
120120- await queueManager.markPostedByService(0, 'discord');
121121- discordSuccess = true;
122122- discordStatus = 'posted';
123123- } else {
124124- discordStatus = 'failed';
125125- }
126126- } catch (error) {
127127- console.error('Error posting to Discord:', error);
128128- discordStatus = 'error: ' + error.message;
129129- }
130130- } else if (discordWebhook.isEnabled()) {
131131- discordStatus = 'already posted';
132132- discordSuccess = true;
133133- } else {
134134- discordStatus = 'disabled';
135135- }
136136-137137- // Construct detailed response message
138138- const itemType = nextItem.isVideo ? 'Video' : 'Image';
139139- let responseMessage = `${itemType}: "${nextItem.title}"\n\n`;
140140- responseMessage += `Telegram: ${telegramStatus}\n`;
141141-142142- if (discordWebhook.isEnabled()) {
143143- responseMessage += `Discord: ${discordStatus}\n`;
144144- }
145145-146146- // If at least one service was successful, consider it a partial success
147147- if (telegramSuccess || discordSuccess) {
148148- this.bot.sendMessage(chatId, responseMessage);
149149- } else {
150150- this.bot.sendMessage(chatId, `Failed to post ${itemType} to any service.\n${responseMessage}`);
151151- }
152152- });
153153-154154- // Command to clean cache
155155- this.bot.onText(/\/cleancache/, async (msg) => {
156156- const chatId = msg.chat.id;
157157-158158- if (!this.isAuthorized(msg.from.id)) {
159159- this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
160160- return;
161161- }
162162-163163- this.bot.sendMessage(chatId, 'Cleaning media cache...');
164164-165165- try {
166166- await mediaCache.cleanupCache();
167167- this.bot.sendMessage(chatId, 'Media cache cleaned successfully.');
168168- } catch (error) {
169169- this.bot.sendMessage(chatId, `Error cleaning cache: ${error.message}`);
170170- }
171171- });
172172-173173- // Command to set posting schedule
174174- this.bot.onText(/\/schedule\s*(.*)/, async (msg, match) => {
175175- const chatId = msg.chat.id;
176176-177177- if (!this.isAuthorized(msg.from.id)) {
178178- this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
179179- return;
180180- }
181181-182182- const cronExpression = match[1].trim();
183183-184184- if (!cronExpression) {
185185- this.bot.sendMessage(chatId, `Current schedule: ${queueManager.cronSchedule}`);
186186- return;
187187- }
188188-189189- const success = queueManager.setCronSchedule(cronExpression);
190190-191191- if (success) {
192192- this.bot.sendMessage(chatId, `Schedule updated to: ${cronExpression}`);
193193- } else {
194194- this.bot.sendMessage(chatId, 'Invalid cron expression. Please use valid cron syntax.');
195195- }
196196- });
197197-198198- // Command to set number of images per scheduled post
199199- this.bot.onText(/\/setcount\s*(.*)/, (msg, match) => {
200200- const chatId = msg.chat.id;
201201-202202- if (!this.isAuthorized(msg.from.id)) {
203203- this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
204204- return;
205205- }
206206-207207- const count = parseInt(match[1].trim());
208208-209209- if (isNaN(count) || count < 1) {
210210- this.bot.sendMessage(chatId, `Current images per interval: ${queueManager.imagesPerInterval}`);
211211- return;
212212- }
213213-214214- const success = queueManager.setImagesPerInterval(count);
215215-216216- if (success) {
217217- this.bot.sendMessage(chatId, `Images per interval updated to: ${count}`);
218218- } else {
219219- this.bot.sendMessage(chatId, 'Invalid count. Please use a positive integer.');
220220- }
221221- });
222222-223223- // Command to clear the queue
224224- this.bot.onText(/\/clear/, async (msg) => {
225225- const chatId = msg.chat.id;
226226-227227- if (!this.isAuthorized(msg.from.id)) {
228228- this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
229229- return;
230230- }
231231-232232- const queueLength = await queueManager.getQueueLength();
233233-234234- if (queueLength === 0) {
235235- this.bot.sendMessage(chatId, 'Queue is already empty.');
236236- return;
237237- }
238238-239239- // Clear the queue by removing all items
240240- for (let i = 0; i < queueLength; i++) {
241241- await queueManager.removeFromQueue(0);
242242- }
243243-244244- this.bot.sendMessage(chatId, `Queue cleared (${queueLength} items removed).`);
245245- });
246246-247247- // Command to manually trigger an update from GitHub
248248- this.bot.onText(/\/update/, async (msg) => {
249249- const chatId = msg.chat.id;
250250-251251- // Only the bot owner can run updates
252252- if (!this.isOwner(msg.from.id)) {
253253- this.bot.sendMessage(chatId, 'Only the bot owner can trigger updates.');
254254- return;
255255- }
256256-257257- this.bot.sendMessage(chatId, 'Checking for updates...');
258258-259259- try {
260260- const updater = require('../utils/updater');
261261- const isUpdateAvailable = await updater.isUpdateAvailable();
262262-263263- if (!isUpdateAvailable) {
264264- this.bot.sendMessage(chatId, 'No updates available. Bot is already running the latest version.');
265265- return;
266266- }
267267-268268- const statusMessage = await this.bot.sendMessage(chatId, 'Updates found! Downloading and applying updates...');
269269-270270- const updateResult = await updater.manualUpdate();
271271-272272- if (updateResult) {
273273- await this.bot.editMessageText('Update successful! Bot will restart to apply changes.', {
274274- chat_id: chatId,
275275- message_id: statusMessage.message_id
276276- });
277277-278278- // Give a moment for the message to be delivered before restarting
279279- setTimeout(async () => {
280280- try {
281281- // Restart the bot using PM2
282282- await execAsync('pm2 restart --update-env stagehand');
283283- } catch (restartError) {
284284- console.error('Error restarting bot:', restartError);
285285- this.bot.sendMessage(chatId, `Error during restart: ${restartError.message}`);
286286- }
287287- }, 2000);
288288- } else {
289289- this.bot.sendMessage(chatId, 'Update process completed, but no changes were applied.');
290290- }
291291- } catch (error) {
292292- console.error('Error during manual update:', error);
293293- this.bot.sendMessage(chatId, `Error during update: ${error.message}`);
294294- }
295295- });
296296-297297- // Command to add a text announcement
298298- this.bot.onText(/^\/announce(?!\S)/, async (msg) => {
299299- const chatId = msg.chat.id;
300300-301301- if (!this.isAuthorized(msg.from.id)) {
302302- this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
303303- return;
304304- }
305305-306306- // Initialize the interactive announcement creation process
307307- this.pendingAnnouncements = this.pendingAnnouncements || {};
308308- this.pendingAnnouncements[msg.from.id] = {};
309309-310310- // Display introduction message with formatting options
311311- this.bot.sendMessage(
312312- chatId,
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- }
329329- ).then(namePrompt => {
330330- // Set up a one-time listener for the name response
331331- this.bot.onReplyToMessage(chatId, namePrompt.message_id, async (nameMsg) => {
332332- const announcementName = nameMsg.text === 'skip' ? '' : nameMsg.text;
333333- this.pendingAnnouncements[msg.from.id].name = announcementName;
334334-335335- // Now ask for the announcement message text
336336- this.bot.sendMessage(
337337- chatId,
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
379379- this.bot.sendMessage(
380380- chatId,
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 }
390390- }
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;
395395-396396- // Validate the cron schedule
397397- if (!this.announcements.isValidCronExpression(cronSchedule)) {
398398- this.bot.sendMessage(
399399- chatId,
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
468468- this.bot.sendMessage(
469469- chatId,
470470- 'Please enter the button text:',
471471- { reply_markup: { force_reply: true } }
472472- ).then(buttonTextPrompt => {
473473- this.bot.onReplyToMessage(chatId, buttonTextPrompt.message_id, async (buttonTextMsg) => {
474474- const buttonText = buttonTextMsg.text;
475475-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- });
502502- });
503503- });
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- }
515515- });
516516- });
517517- });
518518- });
519519- });
520520- });
521521- });
522522- });
523523- });
524524-525525- // Command to list and manage all announcements
526526- this.bot.onText(/^\/announcements(?!\S)/, async (msg) => {
527527- const chatId = msg.chat.id;
528528-529529- if (!this.isAuthorized(msg.from.id)) {
530530- this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
531531- return;
532532- }
533533-534534- const announcements = this.announcements.getAnnouncements();
535535-536536- if (announcements.length === 0) {
537537- this.bot.sendMessage(
538538- chatId,
539539- 'No announcements configured. Use /announce to create a new announcement.'
540540- );
541541- return;
542542- }
543543-544544- // Format the list of announcements with inline buttons
545545- let message = '📣 *Text Announcements*\n\n';
546546-547547- const inlineKeyboard = [];
548548-549549- for (let i = 0; i < announcements.length; i++) {
550550- const announcement = announcements[i];
551551-552552- // Add announcement details to message
553553- message += `*${i+1}. ${announcement.name}*\n`;
554554- message += `Schedule: \`${announcement.cronSchedule}\`\n`;
555555- message += `Last run: ${announcement.lastRun ? new Date(announcement.lastRun).toLocaleString() : 'Never'}\n`;
556556-557557- // Show button info if present
558558- if (announcement.button && announcement.button.text && announcement.button.url) {
559559- message += `Button: "${announcement.button.text}" → ${announcement.button.url}\n`;
560560- }
561561-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`;
567567-568568- // Add buttons for this announcement
569569- inlineKeyboard.push([
570570- {
571571- text: `▶️ Run #${i+1}`,
572572- callback_data: `run_announcement_${announcement.id}`
573573- },
574574- {
575575- text: `✏️ Edit #${i+1}`,
576576- callback_data: `edit_announcement_${announcement.id}`
577577- },
578578- {
579579- text: `❌ Delete #${i+1}`,
580580- callback_data: `delete_announcement_${announcement.id}`
581581- }
582582- ]);
583583- }
584584-585585- // Add a button to create a new announcement
586586- inlineKeyboard.push([
587587- {
588588- text: '➕ Add New Announcement',
589589- callback_data: 'new_announcement'
590590- }
591591- ]);
592592-593593- await this.bot.sendMessage(chatId, message, {
594594- parse_mode: 'Markdown',
595595- reply_markup: {
596596- inline_keyboard: inlineKeyboard
597597- }
598598- });
599599- });
600600-601601- // Handle URL links
602602- this.bot.on('message', async (msg) => {
603603- if (msg.text && msg.text.startsWith('http')) {
604604- const chatId = msg.chat.id;
605605-606606- // Skip processing if this is part of an announcement setup
607607- const isInAnnouncementFlow = this.pendingAnnouncements && this.pendingAnnouncements[msg.from.id];
608608- const isInButtonEditFlow = this.editingAnnouncementButton && this.editingAnnouncementButton[msg.from.id];
609609-610610- if (isInAnnouncementFlow || isInButtonEditFlow) {
611611- // This URL is part of an announcement setup, so we should not process it as a link
612612- return;
613613- }
614614-615615- if (!this.isAuthorized(msg.from.id)) {
616616- this.bot.sendMessage(chatId, 'You are not authorized to use this bot.');
617617- return;
618618- }
619619-620620- try {
621621- const url = msg.text.trim();
622622-623623- this.bot.sendMessage(chatId, 'Processing link...', { reply_to_message_id: msg.message_id });
624624-625625- const mediaData = await scraperManager.extractFromUrl(url);
626626-627627- // Check if the scraper returned an error (for temporarily disabled scrapers)
628628- if (mediaData.error) {
629629- this.bot.sendMessage(
630630- chatId,
631631- mediaData.error,
632632- { reply_to_message_id: msg.message_id }
633633- );
634634- return;
635635- }
636636-637637- await queueManager.addToQueue(mediaData);
638638-639639- const queueLength = await queueManager.getQueueLength();
640640- const mediaType = mediaData.isVideo ? 'Video' : 'Image';
641641-642642- this.bot.sendMessage(
643643- chatId,
644644- `Added to queue: ${mediaType} - ${mediaData.title}\nCurrent queue length: ${queueLength}`,
645645- { reply_to_message_id: msg.message_id }
646646- );
647647- } catch (error) {
648648- this.bot.sendMessage(
649649- chatId,
650650- `Error processing link: ${error.message}`,
651651- { reply_to_message_id: msg.message_id }
652652- );
653653- }
654654- }
655655- });
2727+ // Use the command registry to register all commands and handlers
2828+ this.commandRegistry.registerAll();
65629 }
6573065831 /**
659659- * Register callback query handlers for interactive buttons
3232+ * Post media (image or video) to the Telegram channel
3333+ * This method is used by the scheduler and external services
3434+ * @param {Object} mediaData - The media data to post
3535+ * @returns {Promise<boolean>} - Whether posting was successful
66036 */
661661- registerCallbacks() {
662662- this.bot.on('callback_query', async (query) => {
663663- try {
664664- const chatId = query.message.chat.id;
665665- if (!this.isAuthorized(query.from.id)) {
666666- await this.bot.answerCallbackQuery(query.id, { text: 'You are not authorized to use these controls.' });
667667- return;
668668- }
669669-670670- const data = query.data.split('_');
671671- const action = data[0];
672672-673673- switch (action) {
674674- case 'page': {
675675- // Handle page navigation
676676- const page = parseInt(data[1]);
677677- await this.bot.deleteMessage(chatId, query.message.message_id);
678678- await this.displayQueuePage(chatId, page, 5);
679679- await this.bot.answerCallbackQuery(query.id, { text: `Showing page ${page}` });
680680- break;
681681- }
682682-683683- case 'remove': {
684684- // Handle item removal
685685- const index = parseInt(data[1]);
686686- const removed = await queueManager.removeFromQueue(index);
687687- if (removed) {
688688- const itemType = removed.isVideo ? 'Video' : 'Image';
689689- await this.bot.answerCallbackQuery(query.id, { text: `Removed ${itemType}: ${removed.title}` });
690690-691691- // Update the queue display
692692- await this.bot.deleteMessage(chatId, query.message.message_id);
693693- const page = parseInt(data[2]) || 1;
694694- await this.displayQueuePage(chatId, page, 5);
695695- } else {
696696- await this.bot.answerCallbackQuery(query.id, { text: 'Failed to remove item' });
697697- }
698698- break;
699699- }
700700-701701- case 'top': {
702702- // Handle move to top (next to post)
703703- const index = parseInt(data[1]);
704704- const queue = await queueManager.getQueue();
705705-706706- if (index > 0 && index < queue.length) {
707707- // Remove the item from its current position
708708- const item = queue[index];
709709- queueManager.queueData.queue.splice(index, 1);
710710-711711- // Add it to the beginning
712712- queueManager.queueData.queue.unshift(item);
713713-714714- // Save changes
715715- await queueManager.saveQueueToDisk();
716716-717717- await this.bot.answerCallbackQuery(query.id, { text: `Moved "${item.title}" to top of queue` });
718718-719719- // Update the queue display
720720- await this.bot.deleteMessage(chatId, query.message.message_id);
721721- const page = parseInt(data[2]) || 1;
722722- await this.displayQueuePage(chatId, page, 5);
723723- } else {
724724- await this.bot.answerCallbackQuery(query.id, { text: 'Failed to move item' });
725725- }
726726- break;
727727- }
728728-729729- case 'preview': {
730730- // Handle preview item (send a preview of the queued item)
731731- const index = parseInt(data[1]);
732732- const queue = await queueManager.getQueue();
733733-734734- if (index >= 0 && index < queue.length) {
735735- const item = queue[index];
736736- await this.bot.answerCallbackQuery(query.id, { text: 'Sending preview...' });
737737-738738- // Send a temporary message
739739- const loadingMsg = await this.bot.sendMessage(chatId, 'Preparing preview...');
740740-741741- try {
742742- // Generate a preview for the item
743743- if (item.imageUrl && fs.existsSync(item.imageUrl)) {
744744- // Send the image as a preview
745745- const caption = `Preview of: ${item.title}\nFrom: ${item.siteName}\nPosition in queue: ${index + 1}`;
746746- await this.bot.sendPhoto(chatId, item.imageUrl, { caption });
747747- } else if (item.imageUrls && Array.isArray(item.imageUrls) && item.imageUrls.length > 0) {
748748- // Use the first image from multiple images
749749- const firstImage = item.imageUrls[0];
750750- if (fs.existsSync(firstImage)) {
751751- const caption = `Preview of: ${item.title}\nFrom: ${item.siteName}\nPosition in queue: ${index + 1}\n(${item.imageUrls.length} images total)`;
752752- await this.bot.sendPhoto(chatId, firstImage, { caption });
753753- }
754754- }
755755- } catch (error) {
756756- console.error('Error sending preview:', error);
757757- } finally {
758758- // Delete the loading message
759759- await this.bot.deleteMessage(chatId, loadingMsg.message_id);
760760- }
761761- } else {
762762- await this.bot.answerCallbackQuery(query.id, { text: 'Item not found' });
763763- }
764764- break;
765765- }
766766-767767- // New announcement management callback handlers
768768- case 'run': {
769769- if (data[1] === 'announcement') {
770770- const announcementId = data[2];
771771- await this.bot.answerCallbackQuery(query.id, { text: 'Sending announcement...' });
772772-773773- try {
774774- const result = await this.announcements.sendAnnouncementNow(announcementId);
775775- if (result) {
776776- await this.bot.sendMessage(chatId, `✅ Announcement sent successfully!`);
777777- } else {
778778- await this.bot.sendMessage(chatId, `❌ Failed to send announcement.`);
779779- }
780780- } catch (error) {
781781- await this.bot.sendMessage(chatId, `❌ Error: ${error.message}`);
782782- }
783783-784784- // Refresh announcements list
785785- await this.bot.deleteMessage(chatId, query.message.message_id);
786786- await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
787787- }
788788- break;
789789- }
790790-791791- case 'delete': {
792792- if (data[1] === 'announcement') {
793793- const announcementId = data[2];
794794-795795- // Get the announcement to show its name
796796- const announcement = this.announcements.getAnnouncementById(announcementId);
797797- if (!announcement) {
798798- await this.bot.answerCallbackQuery(query.id, { text: 'Announcement not found.' });
799799- return;
800800- }
801801-802802- // Show confirmation dialog
803803- await this.bot.answerCallbackQuery(query.id);
804804-805805- const confirmMessage = await this.bot.sendMessage(
806806- chatId,
807807- `Are you sure you want to delete the announcement "${announcement.name}"?`,
808808- {
809809- reply_markup: {
810810- inline_keyboard: [
811811- [
812812- { text: '✅ Yes, delete it', callback_data: `confirm_delete_announcement_${announcementId}` },
813813- { text: '❌ No, cancel', callback_data: 'cancel_delete_announcement' }
814814- ]
815815- ]
816816- }
817817- }
818818- );
819819- }
820820- break;
821821- }
822822-823823- case 'confirm': {
824824- if (data[1] === 'delete' && data[2] === 'announcement') {
825825- const announcementId = data[3];
826826-827827- try {
828828- const result = await this.announcements.removeAnnouncement(announcementId);
829829- if (result) {
830830- await this.bot.answerCallbackQuery(query.id, { text: 'Announcement deleted successfully.' });
831831- } else {
832832- await this.bot.answerCallbackQuery(query.id, { text: 'Failed to delete announcement.' });
833833- }
834834-835835- // Delete confirmation message
836836- await this.bot.deleteMessage(chatId, query.message.message_id);
837837-838838- // Refresh announcements list
839839- await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
840840- } catch (error) {
841841- await this.bot.answerCallbackQuery(query.id, { text: `Error: ${error.message}` });
842842- }
843843- }
844844- break;
845845- }
846846-847847- case 'cancel': {
848848- if (data[1] === 'delete' && data[2] === 'announcement') {
849849- await this.bot.answerCallbackQuery(query.id, { text: 'Delete cancelled.' });
850850- await this.bot.deleteMessage(chatId, query.message.message_id);
851851- }
852852- break;
853853- }
854854-855855- case 'edit': {
856856- if (data[1] === 'announcement') {
857857- const announcementId = data[2];
858858- const announcement = this.announcements.getAnnouncementById(announcementId);
859859-860860- if (!announcement) {
861861- await this.bot.answerCallbackQuery(query.id, { text: 'Announcement not found.' });
862862- return;
863863- }
864864-865865- await this.bot.answerCallbackQuery(query.id);
866866-867867- // Show edit options
868868- const editMessage = await this.bot.sendMessage(
869869- chatId,
870870- `Editing announcement: *${announcement.name}*\n\nWhat would you like to edit?`,
871871- {
872872- parse_mode: 'Markdown',
873873- reply_markup: {
874874- inline_keyboard: [
875875- [
876876- {
877877- text: '📝 Edit Message',
878878- callback_data: `edit_announcement_message_${announcementId}`
879879- }
880880- ],
881881- [
882882- {
883883- text: '⏰ Edit Schedule',
884884- callback_data: `edit_announcement_schedule_${announcementId}`
885885- }
886886- ],
887887- [
888888- {
889889- text: '🏷️ Edit Name',
890890- callback_data: `edit_announcement_name_${announcementId}`
891891- }
892892- ],
893893- [
894894- {
895895- text: '🔗 Edit Button',
896896- callback_data: `edit_announcement_button_${announcementId}`
897897- }
898898- ],
899899- [
900900- {
901901- text: '❌ Cancel',
902902- callback_data: 'cancel_edit_announcement'
903903- }
904904- ]
905905- ]
906906- }
907907- }
908908- );
909909- } else if (data[1] === 'announcement' && (data[2] === 'message' || data[2] === 'name' || data[2] === 'schedule' || data[2] === 'button')) {
910910- const field = data[2];
911911- const announcementId = data[3];
912912- const announcement = this.announcements.getAnnouncementById(announcementId);
913913-914914- if (!announcement) {
915915- await this.bot.answerCallbackQuery(query.id, { text: 'Announcement not found.' });
916916- return;
917917- }
918918-919919- await this.bot.answerCallbackQuery(query.id);
920920-921921- // Delete the edit options message
922922- await this.bot.deleteMessage(chatId, query.message.message_id);
923923-924924- let promptText = '';
925925- switch (field) {
926926- case '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.`;
928928- break;
929929- case 'name':
930930- promptText = `Please enter the new name for the announcement "${announcement.name}":`;
931931- break;
932932- case 'schedule':
933933- promptText = `Please enter the new cron schedule for the announcement "${announcement.name}" (use https://crontab.guru/ for help):\n\nCurrent schedule: ${announcement.cronSchedule}`;
934934- break;
935935- case 'button':
936936- // For button editing, we'll first ask if they want to add, edit, or remove a button
937937- const hasButton = announcement.button && announcement.button.text && announcement.button.url;
938938-939939- if (hasButton) {
940940- // Show options to edit or remove existing button
941941- await this.bot.sendMessage(
942942- chatId,
943943- `Current button: "${announcement.button.text}" → ${announcement.button.url}\n\nWhat would you like to do?`,
944944- {
945945- reply_markup: {
946946- inline_keyboard: [
947947- [
948948- {
949949- text: '✏️ Edit Button',
950950- callback_data: `edit_announcement_button_edit_${announcementId}`
951951- }
952952- ],
953953- [
954954- {
955955- text: '❌ Remove Button',
956956- callback_data: `edit_announcement_button_remove_${announcementId}`
957957- }
958958- ],
959959- [
960960- {
961961- text: '↩️ Cancel',
962962- callback_data: 'cancel_edit_announcement_button'
963963- }
964964- ]
965965- ]
966966- }
967967- }
968968- );
969969- return;
970970- } else {
971971- // No existing button, ask if they want to add one
972972- await this.bot.sendMessage(
973973- chatId,
974974- `This announcement doesn't have a button. Would you like to add one?`,
975975- {
976976- reply_markup: {
977977- inline_keyboard: [
978978- [
979979- {
980980- text: '➕ Add Button',
981981- callback_data: `edit_announcement_button_add_${announcementId}`
982982- }
983983- ],
984984- [
985985- {
986986- text: '↩️ Cancel',
987987- callback_data: 'cancel_edit_announcement_button'
988988- }
989989- ]
990990- ]
991991- }
992992- }
993993- );
994994- return;
995995- }
996996- }
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- }
11011101- }
11021102- break;
11031103- }
11041104-11051105- case 'new': {
11061106- if (data[1] === 'announcement') {
11071107- await this.bot.answerCallbackQuery(query.id);
11081108-11091109- // Delete the announcements list message
11101110- await this.bot.deleteMessage(chatId, query.message.message_id);
11111111-11121112- // Trigger the /announce command
11131113- await this.bot.sendMessage(
11141114- chatId,
11151115- 'Please use the /announce command followed by your announcement text to create a new announcement.'
11161116- );
11171117- }
11181118- break;
11191119- }
11201120-11211121- case 'cancel': {
11221122- if (data[1] === 'edit' && data[2] === 'announcement') {
11231123- await this.bot.answerCallbackQuery(query.id, { text: 'Edit cancelled.' });
11241124- await this.bot.deleteMessage(chatId, query.message.message_id);
11251125-11261126- // Refresh announcements list
11271127- await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
11281128- }
11291129- break;
11301130- }
11311131-11321132- case 'edit': {
11331133- if (data[1] === 'announcement' && data[2] === 'button') {
11341134- if (data[3] === 'add' || data[3] === 'edit') {
11351135- // Add or edit a button
11361136- const announcementId = data[4];
11371137- const announcement = this.announcements.getAnnouncementById(announcementId);
11381138-11391139- if (!announcement) {
11401140- await this.bot.answerCallbackQuery(query.id, { text: 'Announcement not found.' });
11411141- return;
11421142- }
11431143-11441144- await this.bot.answerCallbackQuery(query.id);
11451145-11461146- // Delete the options message
11471147- await this.bot.deleteMessage(chatId, query.message.message_id);
11481148-11491149- // Store the context for updating
11501150- this.editingAnnouncementButton = this.editingAnnouncementButton || {};
11511151- this.editingAnnouncementButton[query.from.id] = { id: announcementId };
11521152-11531153- // First ask for button text
11541154- const buttonTextPrompt = await this.bot.sendMessage(
11551155- chatId,
11561156- 'Please enter the button text:',
11571157- { reply_markup: { force_reply: true } }
11581158- );
11591159-11601160- this.bot.onReplyToMessage(chatId, buttonTextPrompt.message_id, async (buttonTextMsg) => {
11611161- const buttonText = buttonTextMsg.text;
11621162-11631163- // Now ask for the button URL
11641164- const buttonUrlPrompt = await this.bot.sendMessage(
11651165- chatId,
11661166- 'Please enter the button URL:',
11671167- { reply_markup: { force_reply: true } }
11681168- );
11691169-11701170- this.bot.onReplyToMessage(chatId, buttonUrlPrompt.message_id, async (buttonUrlMsg) => {
11711171- const buttonUrl = buttonUrlMsg.text;
11721172-11731173- // Create the button object
11741174- const button = {
11751175- text: buttonText,
11761176- url: buttonUrl
11771177- };
11781178-11791179- try {
11801180- // Update the announcement with the new button
11811181- const updated = await this.announcements.updateAnnouncement(announcementId, { button });
11821182-11831183- if (updated) {
11841184- await this.bot.sendMessage(
11851185- chatId,
11861186- `✅ Button ${data[3] === 'add' ? 'added' : 'updated'} successfully!`
11871187- );
11881188- } else {
11891189- await this.bot.sendMessage(
11901190- chatId,
11911191- `❌ Failed to ${data[3] === 'add' ? 'add' : 'update'} button.`
11921192- );
11931193- }
11941194-11951195- // Clean up
11961196- delete this.editingAnnouncementButton[query.from.id];
11971197-11981198- // Refresh announcements list
11991199- await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
12001200- } catch (error) {
12011201- await this.bot.sendMessage(
12021202- chatId,
12031203- `❌ Error updating button: ${error.message}`
12041204- );
12051205- }
12061206- });
12071207- });
12081208- } else if (data[3] === 'remove') {
12091209- // Remove a button
12101210- const announcementId = data[4];
12111211-12121212- try {
12131213- // Remove the button by setting it to null
12141214- const updated = await this.announcements.updateAnnouncement(announcementId, { button: null });
12151215-12161216- if (updated) {
12171217- await this.bot.answerCallbackQuery(query.id, { text: 'Button removed successfully.' });
12181218- } else {
12191219- await this.bot.answerCallbackQuery(query.id, { text: 'Failed to remove button.' });
12201220- }
12211221-12221222- // Delete the options message
12231223- await this.bot.deleteMessage(chatId, query.message.message_id);
12241224-12251225- // Refresh announcements list
12261226- await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
12271227- } catch (error) {
12281228- await this.bot.answerCallbackQuery(query.id, { text: `Error: ${error.message}` });
12291229- }
12301230- }
12311231- }
12321232- break;
12331233- }
12341234-12351235- case 'cancel': {
12361236- if (data[1] === 'edit' && data[2] === 'announcement' && data[3] === 'button') {
12371237- await this.bot.answerCallbackQuery(query.id, { text: 'Button edit cancelled.' });
12381238- await this.bot.deleteMessage(chatId, query.message.message_id);
12391239-12401240- // Refresh announcements list
12411241- await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
12421242- }
12431243- break;
12441244- }
12451245- }
12461246- } catch (error) {
12471247- console.error('Error handling callback query:', error);
12481248- await this.bot.answerCallbackQuery(query.id, { text: 'An error occurred' });
12491249- }
12501250- });
3737+ async postMedia(mediaData) {
3838+ return await this.commandRegistry.getMediaHelper().postMedia(mediaData);
125139 }
125240125341 /**
12541254- * Display a page of the queue with interactive buttons
12551255- * @param {number} chatId - Telegram chat ID
12561256- * @param {number} page - Page number to display (1-based)
12571257- * @param {number} pageSize - Number of items per page
4242+ * Check if a user is authorized
4343+ * @param {number} userId - The user ID to check
4444+ * @returns {boolean} - Whether the user is authorized
125845 */
12591259- async displayQueuePage(chatId, page, pageSize) {
12601260- const queue = await queueManager.getQueue();
12611261- const queueLength = queue.length;
12621262-12631263- if (queueLength === 0) {
12641264- this.bot.sendMessage(chatId, 'Queue is empty.');
12651265- return;
12661266- }
12671267-12681268- // Calculate total pages
12691269- const totalPages = Math.ceil(queueLength / pageSize);
12701270-12711271- // Ensure page is within bounds
12721272- const currentPage = Math.max(1, Math.min(page, totalPages));
12731273-12741274- // Calculate start and end indices for this page
12751275- const startIdx = (currentPage - 1) * pageSize;
12761276- const endIdx = Math.min(startIdx + pageSize, queueLength);
12771277-12781278- // Build message with queue items
12791279- let message = `📋 *Queue Management* (${queueLength} items total)\n`;
12801280- message += `Showing items ${startIdx + 1}-${endIdx} of ${queueLength}\n\n`;
12811281-12821282- // Add each queue item
12831283- for (let i = startIdx; i < endIdx; i++) {
12841284- const item = queue[i];
12851285- const itemType = item.isVideo ? '🎬' : '🖼️';
12861286- const itemIndex = i + 1;
12871287-12881288- // Show posting status for each service
12891289- let statusIcons = '';
12901290- if (item.postedTo) {
12911291- if (item.postedTo.telegram) statusIcons += '✅TG ';
12921292- else statusIcons += '❌TG ';
12931293-12941294- if (queueManager.postServices.includes('discord')) {
12951295- if (item.postedTo.discord) statusIcons += '✅DS';
12961296- else statusIcons += '❌DS';
12971297- }
12981298- }
12991299-13001300- message += `${itemIndex}. ${itemType} *${item.title}*\n From: ${item.siteName} ${statusIcons}\n`;
13011301- }
13021302-13031303- // Create navigation buttons and item action buttons
13041304- const inline_keyboard = [];
13051305-13061306- // Item action buttons
13071307- for (let i = startIdx; i < endIdx; i++) {
13081308- const row = [];
13091309-13101310- // Add "Preview" button
13111311- row.push({
13121312- text: `👁️ #${i+1}`,
13131313- callback_data: `preview_${i}_${currentPage}`
13141314- });
13151315-13161316- // Add "Remove" button
13171317- row.push({
13181318- text: `❌ #${i+1}`,
13191319- callback_data: `remove_${i}_${currentPage}`
13201320- });
13211321-13221322- // Only add "Move to top" if not already at top
13231323- if (i > 0) {
13241324- row.push({
13251325- text: `⬆️ #${i+1}`,
13261326- callback_data: `top_${i}_${currentPage}`
13271327- });
13281328- } else {
13291329- row.push({
13301330- text: `🔼 Next`,
13311331- callback_data: `preview_0_${currentPage}`
13321332- });
13331333- }
13341334-13351335- inline_keyboard.push(row);
13361336- }
13371337-13381338- // Navigation row for paging
13391339- const navRow = [];
13401340-13411341- // Previous page button
13421342- if (currentPage > 1) {
13431343- navRow.push({
13441344- text: '◀️ Previous',
13451345- callback_data: `page_${currentPage - 1}`
13461346- });
13471347- }
13481348-13491349- // Page indicator
13501350- navRow.push({
13511351- text: `Page ${currentPage}/${totalPages}`,
13521352- callback_data: `page_${currentPage}`
13531353- });
13541354-13551355- // Next page button
13561356- if (currentPage < totalPages) {
13571357- navRow.push({
13581358- text: 'Next ▶️',
13591359- callback_data: `page_${currentPage + 1}`
13601360- });
13611361- }
13621362-13631363- if (navRow.length > 0) {
13641364- inline_keyboard.push(navRow);
13651365- }
13661366-13671367- // Send the message with inline keyboard
13681368- await this.bot.sendMessage(chatId, message, {
13691369- parse_mode: 'Markdown',
13701370- reply_markup: {
13711371- inline_keyboard
13721372- }
13731373- });
13741374- }
13751375-137646 isAuthorized(userId) {
13771377- // If no authorized users are specified, anyone can use the bot
13781378- if (config.authorizedUsers.length === 0) {
13791379- return true;
13801380- }
13811381-13821382- return config.authorizedUsers.includes(userId.toString());
4747+ return this.commandRegistry.getAuthHelper().isAuthorized(userId);
138348 }
138449138550 /**
···138853 * @returns {boolean} - Whether the user is the owner
138954 */
139055 isOwner(userId) {
13911391- return config.ownerId && userId.toString() === config.ownerId.toString();
13921392- }
13931393-13941394- /**
13951395- * Post media (image or video) to the Telegram channel
13961396- * @param {Object} mediaData - The media data to post
13971397- * @returns {Promise<boolean>} - Whether posting was successful
13981398- */
13991399- async postMedia(mediaData) {
14001400- try {
14011401- // Create inline keyboard with link to source
14021402- let buttonText = `View on ${mediaData.siteName}`;
14031403-14041404- // Special butterfly emojis for Bluesky
14051405- if (mediaData.siteName === 'Bluesky') {
14061406- buttonText = `🦋 ${buttonText} 🦋`;
14071407- }
14081408-14091409- const inlineKeyboard = {
14101410- inline_keyboard: [
14111411- [
14121412- {
14131413- text: buttonText,
14141414- url: mediaData.sourceUrl
14151415- }
14161416- ]
14171417- ]
14181418- };
14191419-14201420- // Special caption for FurAffinity posts
14211421- let caption = '';
14221422- if (mediaData.siteName === 'FurAffinity' && mediaData.title && mediaData.name) {
14231423- caption = `🖼️: ${mediaData.title}\n🎨: ${mediaData.name}`;
14241424- }
14251425-14261426- // Check if we're dealing with multiple images (imageUrls array with more than one item)
14271427- if (mediaData.imageUrls && Array.isArray(mediaData.imageUrls) && mediaData.imageUrls.length > 1) {
14281428- console.log(`Posting multiple images: ${mediaData.imageUrls.length} images`);
14291429-14301430- // Since media groups don't support inline buttons, we'll include the link in the caption
14311431- const groupCaption = caption ?
14321432- `${caption}\n\nOriginal: ${mediaData.sourceUrl}` :
14331433- `${mediaData.title}\n\nOriginal: ${mediaData.sourceUrl}`;
14341434-14351435- // Prepare media group format for Telegram
14361436- const mediaGroup = [];
14371437-14381438- // Process each image in the array
14391439- for (let i = 0; i < mediaData.imageUrls.length; i++) {
14401440- const imagePath = mediaData.imageUrls[i];
14411441-14421442- if (fs.existsSync(imagePath)) {
14431443- // Add as InputMediaPhoto for the media group - use correct format
14441444- mediaGroup.push({
14451445- type: 'photo',
14461446- media: fs.createReadStream(imagePath),
14471447- // Only add caption to the first image
14481448- ...(i === 0 ? { caption: groupCaption } : {})
14491449- });
14501450- } else {
14511451- console.warn(`Image file not found: ${imagePath}`);
14521452- }
14531453- }
14541454-14551455- if (mediaGroup.length > 0) {
14561456- try {
14571457- console.log(`Sending media group with ${mediaGroup.length} images`);
14581458- // Send as a media group (album)
14591459- await this.bot.sendMediaGroup(config.channelId, mediaGroup);
14601460- return true;
14611461- } catch (mediaGroupError) {
14621462- console.error('Error posting media group:', mediaGroupError);
14631463- // If posting as a group fails, fall back to posting the first image
14641464- console.log('Falling back to posting single image');
14651465- }
14661466- }
14671467- }
14681468-14691469- // Check if we're dealing with a video
14701470- if (mediaData.isVideo && mediaData.videoUrl) {
14711471- console.log(`Posting video: ${mediaData.videoUrl}`);
14721472-14731473- // For videos from local cache, we need to use the file path
14741474- if (fs.existsSync(mediaData.videoUrl)) {
14751475- const response = await this.bot.sendVideo(
14761476- config.channelId,
14771477- mediaData.videoUrl,
14781478- {
14791479- caption: caption, // Add the caption here
14801480- reply_markup: inlineKeyboard
14811481- }
14821482- );
14831483- return true;
14841484- } else {
14851485- // Try to post from URL if not in cache
14861486- try {
14871487- const response = await this.bot.sendVideo(
14881488- config.channelId,
14891489- mediaData.videoUrl,
14901490- {
14911491- caption: caption, // Add the caption here
14921492- reply_markup: inlineKeyboard
14931493- }
14941494- );
14951495- return true;
14961496- } catch (videoError) {
14971497- console.error('Error posting video directly:', videoError);
14981498-14991499- // Fallback to sending image/thumbnail if video fails
15001500- if (mediaData.imageUrl && mediaData.imageUrl !== mediaData.videoUrl) {
15011501- const fallbackCaption = caption ?
15021502- `${caption}\n(Video post - see original)` :
15031503- "(Video post - see original)";
15041504-15051505- const response = await this.bot.sendPhoto(
15061506- config.channelId,
15071507- mediaData.imageUrl,
15081508- {
15091509- caption: fallbackCaption,
15101510- reply_markup: inlineKeyboard
15111511- }
15121512- );
15131513- return true;
15141514- }
15151515-15161516- throw videoError;
15171517- }
15181518- }
15191519- }
15201520-15211521- // Handle image posting (including video thumbnails as fallback)
15221522- console.log(`Posting image: ${mediaData.imageUrl}`);
15231523-15241524- // For images from local cache, we need to use the file path
15251525- if (fs.existsSync(mediaData.imageUrl)) {
15261526- const response = await this.bot.sendPhoto(
15271527- config.channelId,
15281528- mediaData.imageUrl,
15291529- {
15301530- caption: caption, // Add the caption here
15311531- reply_markup: inlineKeyboard
15321532- }
15331533- );
15341534- return true;
15351535- } else {
15361536- // Try to post from URL if not in cache
15371537- try {
15381538- const response = await this.bot.sendPhoto(
15391539- config.channelId,
15401540- mediaData.imageUrl,
15411541- {
15421542- caption: caption, // Add the caption here
15431543- reply_markup: inlineKeyboard
15441544- }
15451545- );
15461546- return true;
15471547- } catch (imageError) {
15481548- console.error('Error posting image:', imageError);
15491549-15501550- // Attempt to download and reupload if direct linking fails
15511551- try {
15521552- const imageResponse = await axios({
15531553- method: 'GET',
15541554- url: mediaData.imageUrl,
15551555- responseType: 'stream'
15561556- });
15571557-15581558- const response = await this.bot.sendPhoto(
15591559- config.channelId,
15601560- imageResponse.data,
15611561- {
15621562- caption: caption, // Add the caption here
15631563- reply_markup: inlineKeyboard
15641564- }
15651565- );
15661566-15671567- return true;
15681568- } catch (secondError) {
15691569- console.error('Error uploading image after download:', secondError);
15701570- return false;
15711571- }
15721572- }
15731573- }
15741574- } catch (error) {
15751575- console.error('Error posting media:', error);
15761576- return false;
15771577- }
5656+ return this.commandRegistry.getAuthHelper().isOwner(userId);
157857 }
157958158059 /**
15811581- * Shutdown the bot gracefully
15821582- * @returns {Promise<void>}
6060+ * Display a page of the queue with interactive buttons
6161+ * @param {number} chatId - Telegram chat ID
6262+ * @param {number} page - Page number to display (1-based)
6363+ * @param {number} pageSize - Number of items per page
158364 */
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- });
6565+ async displayQueuePage(chatId, page, pageSize) {
6666+ return await this.commandRegistry.getQueueHelper().displayQueuePage(chatId, page, pageSize);
171867 }
171968172069 /**
···172776 await this.bot.stopPolling();
172877 console.log('Telegram bot polling stopped');
1729787979+ // Shutdown queue monitor
8080+ if (this.queueMonitor) {
8181+ await this.queueMonitor.shutdown();
8282+ }
8383+173084 return true;
173185 } catch (error) {
173286 console.error('Error shutting down bot:', error);
···173589 }
173690}
17379117381738-module.exports = StagehandBot;9292+module.exports = StagehandBot;
+1765
bot/telegramBot.js.backup
···11+// NOTE: This is the original monolithic telegram bot file.
22+// For new development, use the modular version at:
33+// ./telegrambot/telegramBot.js
44+// This version is going to go completely unmaintained and will get no new features.
55+//
66+// The modular version breaks down commands and functionality into
77+// separate files for better maintainability and testing.
88+// See ./telegrambot/README.md for more information.
99+1010+const TelegramBot = require('node-telegram-bot-api');
1111+const axios = require('axios');
1212+const fs = require('fs');
1313+const { exec } = require('child_process');
1414+const { promisify } = require('util');
1515+const execAsync = promisify(exec);
1616+const queueManager = require('../queue/queueManager');
1717+const scraperManager = require('../utils/scraperManager');
1818+const mediaCache = require('../utils/mediaCache');
1919+const AnnouncementManager = require('../utils/announcementManager');
2020+const discordWebhook = require('./discordWebhook');
2121+2222+class StagehandBot {
2323+ constructor() {
2424+ this.bot = new TelegramBot(config.botToken, { polling: true });
2525+ this.serviceName = 'telegram';
2626+ this.channelId = config.channelId;
2727+ this.announcements = new AnnouncementManager(this);
2828+ this.init();
2929+ }
3030+3131+ async init() {
3232+ await this.announcements.init();
3333+ this.registerCommands();
3434+ this.registerCallbacks();
3535+ console.log('Telegram bot started...');
3636+ }
3737+3838+ registerCommands() {
3939+ // Command to start the bot
4040+ this.bot.onText(/\/start/, (msg) => {
4141+ const chatId = msg.chat.id;
4242+ this.bot.sendMessage(chatId, 'Stagehand bot is active. Send me links to queue images for posting!');
4343+ });
4444+4545+ // Command to show help
4646+ this.bot.onText(/\/help/, (msg) => {
4747+ const chatId = msg.chat.id;
4848+ const helpText = `
4949+Stagehand Bot Commands:
5050+/queue - Show current queue status with interactive management
5151+/send - Post the next image in the queue
5252+/schedule [cron] - Set posting schedule (cron syntax, use https://crontab.guru/ for help)
5353+/setcount [number] - Set number of images per scheduled post (default: 1)
5454+/clear - Clear the queue
5555+/cleancache - Clean expired items from media cache
5656+/announce - Create a new announcement
5757+/announcements - Manage existing announcements
5858+/shuffle - Toggle shuffle mode (shuffles queue after each post)
5959+/update - Update bot from GitHub repository (owner only)
6060+6161+Send any link to a supported site to add it to the queue.
6262+Supported sites: e621, FurAffinity, SoFurry, Weasyl, Bluesky
6363+ `;
6464+ this.bot.sendMessage(chatId, helpText);
6565+ });
6666+6767+ // Command to show queue status with visual management
6868+ this.bot.onText(/\/queue(?:\s+(\d+))?/, async (msg, match) => {
6969+ const chatId = msg.chat.id;
7070+7171+ if (!this.isAuthorized(msg.from.id)) {
7272+ this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
7373+ return;
7474+ }
7575+7676+ // Get page number from the command (defaults to 1)
7777+ const page = parseInt(match[1]) || 1;
7878+ const pageSize = 5;
7979+8080+ await this.displayQueuePage(chatId, page, pageSize);
8181+ });
8282+8383+ // Command to post the next image
8484+ this.bot.onText(/\/send/, async (msg) => {
8585+ const chatId = msg.chat.id;
8686+8787+ if (!this.isAuthorized(msg.from.id)) {
8888+ this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
8989+ return;
9090+ }
9191+9292+ const nextItem = await queueManager.getNextFromQueue();
9393+9494+ if (!nextItem) {
9595+ this.bot.sendMessage(chatId, 'Queue is empty, nothing to post.');
9696+ return;
9797+ }
9898+9999+ // Status tracking variables
100100+ let telegramSuccess = false;
101101+ let discordSuccess = false;
102102+ let telegramStatus = 'not attempted';
103103+ let discordStatus = 'not attempted';
104104+105105+ // Post to Telegram if it hasn't been posted yet
106106+ if (!queueManager.hasBeenPostedByService(0, 'telegram')) {
107107+ telegramStatus = 'attempting';
108108+ const telegramResult = await this.postMedia(nextItem);
109109+110110+ if (telegramResult) {
111111+ await queueManager.markPostedByService(0, 'telegram');
112112+ telegramSuccess = true;
113113+ telegramStatus = 'posted';
114114+ } else {
115115+ telegramStatus = 'failed';
116116+ }
117117+ } else {
118118+ telegramStatus = 'already posted';
119119+ telegramSuccess = true;
120120+ }
121121+122122+ // Post to Discord if it's configured and hasn't been posted yet
123123+ if (discordWebhook.isEnabled() && !queueManager.hasBeenPostedByService(0, 'discord')) {
124124+ discordStatus = 'attempting';
125125+ try {
126126+ const discordResult = await discordWebhook.postMedia(nextItem);
127127+128128+ if (discordResult) {
129129+ await queueManager.markPostedByService(0, 'discord');
130130+ discordSuccess = true;
131131+ discordStatus = 'posted';
132132+ } else {
133133+ discordStatus = 'failed';
134134+ }
135135+ } catch (error) {
136136+ console.error('Error posting to Discord:', error);
137137+ discordStatus = 'error: ' + error.message;
138138+ }
139139+ } else if (discordWebhook.isEnabled()) {
140140+ discordStatus = 'already posted';
141141+ discordSuccess = true;
142142+ } else {
143143+ discordStatus = 'disabled';
144144+ }
145145+146146+ // Construct detailed response message
147147+ const itemType = nextItem.isVideo ? 'Video' : 'Image';
148148+ let responseMessage = `${itemType}: "${nextItem.title}"\n\n`;
149149+ responseMessage += `Telegram: ${telegramStatus}\n`;
150150+151151+ if (discordWebhook.isEnabled()) {
152152+ responseMessage += `Discord: ${discordStatus}\n`;
153153+ }
154154+155155+ // If at least one service was successful, consider it a partial success
156156+ if (telegramSuccess || discordSuccess) {
157157+ this.bot.sendMessage(chatId, responseMessage);
158158+ } else {
159159+ this.bot.sendMessage(chatId, `Failed to post ${itemType} to any service.\n${responseMessage}`);
160160+ }
161161+ });
162162+163163+ // Command to clean cache
164164+ this.bot.onText(/\/cleancache/, async (msg) => {
165165+ const chatId = msg.chat.id;
166166+167167+ if (!this.isAuthorized(msg.from.id)) {
168168+ this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
169169+ return;
170170+ }
171171+172172+ this.bot.sendMessage(chatId, 'Cleaning media cache...');
173173+174174+ try {
175175+ await mediaCache.cleanupCache();
176176+ this.bot.sendMessage(chatId, 'Media cache cleaned successfully.');
177177+ } catch (error) {
178178+ this.bot.sendMessage(chatId, `Error cleaning cache: ${error.message}`);
179179+ }
180180+ });
181181+182182+ // Command to set posting schedule
183183+ this.bot.onText(/\/schedule\s*(.*)/, async (msg, match) => {
184184+ const chatId = msg.chat.id;
185185+186186+ if (!this.isAuthorized(msg.from.id)) {
187187+ this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
188188+ return;
189189+ }
190190+191191+ const cronExpression = match[1].trim();
192192+193193+ if (!cronExpression) {
194194+ this.bot.sendMessage(chatId, `Current schedule: ${queueManager.cronSchedule}`);
195195+ return;
196196+ }
197197+198198+ const success = queueManager.setCronSchedule(cronExpression);
199199+200200+ if (success) {
201201+ this.bot.sendMessage(chatId, `Schedule updated to: ${cronExpression}`);
202202+ } else {
203203+ this.bot.sendMessage(chatId, 'Invalid cron expression. Please use valid cron syntax.');
204204+ }
205205+ });
206206+207207+ // Command to set number of images per scheduled post
208208+ this.bot.onText(/\/setcount\s*(.*)/, (msg, match) => {
209209+ const chatId = msg.chat.id;
210210+211211+ if (!this.isAuthorized(msg.from.id)) {
212212+ this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
213213+ return;
214214+ }
215215+216216+ const count = parseInt(match[1].trim());
217217+218218+ if (isNaN(count) || count < 1) {
219219+ this.bot.sendMessage(chatId, `Current images per interval: ${queueManager.imagesPerInterval}`);
220220+ return;
221221+ }
222222+223223+ const success = queueManager.setImagesPerInterval(count);
224224+225225+ if (success) {
226226+ this.bot.sendMessage(chatId, `Images per interval updated to: ${count}`);
227227+ } else {
228228+ this.bot.sendMessage(chatId, 'Invalid count. Please use a positive integer.');
229229+ }
230230+ });
231231+232232+ // Command to clear the queue
233233+ this.bot.onText(/\/clear/, async (msg) => {
234234+ const chatId = msg.chat.id;
235235+236236+ if (!this.isAuthorized(msg.from.id)) {
237237+ this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
238238+ return;
239239+ }
240240+241241+ const queueLength = await queueManager.getQueueLength();
242242+243243+ if (queueLength === 0) {
244244+ this.bot.sendMessage(chatId, 'Queue is already empty.');
245245+ return;
246246+ }
247247+248248+ // Clear the queue by removing all items
249249+ for (let i = 0; i < queueLength; i++) {
250250+ await queueManager.removeFromQueue(0);
251251+ }
252252+253253+ this.bot.sendMessage(chatId, `Queue cleared (${queueLength} items removed).`);
254254+ });
255255+256256+ // Command to toggle shuffle mode
257257+ this.bot.onText(/\/shuffle/, async (msg) => {
258258+ const chatId = msg.chat.id;
259259+260260+ if (!this.isAuthorized(msg.from.id)) {
261261+ this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
262262+ return;
263263+ }
264264+265265+ const isEnabled = queueManager.toggleShuffleMode();
266266+267267+ if (isEnabled) {
268268+ this.bot.sendMessage(chatId, '🔀 Shuffle mode enabled! Queue will be randomized after each post.');
269269+ } else {
270270+ this.bot.sendMessage(chatId, '📋 Shuffle mode disabled. Queue will maintain its order.');
271271+ }
272272+ });
273273+274274+ // Command to manually trigger an update from GitHub
275275+ this.bot.onText(/\/update/, async (msg) => {
276276+ const chatId = msg.chat.id;
277277+278278+ // Only the bot owner can run updates
279279+ if (!this.isOwner(msg.from.id)) {
280280+ this.bot.sendMessage(chatId, 'Only the bot owner can trigger updates.');
281281+ return;
282282+ }
283283+284284+ this.bot.sendMessage(chatId, 'Checking for updates...');
285285+286286+ try {
287287+ const updater = require('../utils/updater');
288288+ const isUpdateAvailable = await updater.isUpdateAvailable();
289289+290290+ if (!isUpdateAvailable) {
291291+ this.bot.sendMessage(chatId, 'No updates available. Bot is already running the latest version.');
292292+ return;
293293+ }
294294+295295+ const statusMessage = await this.bot.sendMessage(chatId, 'Updates found! Downloading and applying updates...');
296296+297297+ const updateResult = await updater.manualUpdate();
298298+299299+ if (updateResult) {
300300+ await this.bot.editMessageText('Update successful! Bot will restart to apply changes.', {
301301+ chat_id: chatId,
302302+ message_id: statusMessage.message_id
303303+ });
304304+305305+ // Give a moment for the message to be delivered before restarting
306306+ setTimeout(async () => {
307307+ try {
308308+ // Restart the bot using PM2
309309+ await execAsync('pm2 restart --update-env stagehand');
310310+ } catch (restartError) {
311311+ console.error('Error restarting bot:', restartError);
312312+ this.bot.sendMessage(chatId, `Error during restart: ${restartError.message}`);
313313+ }
314314+ }, 2000);
315315+ } else {
316316+ this.bot.sendMessage(chatId, 'Update process completed, but no changes were applied.');
317317+ }
318318+ } catch (error) {
319319+ console.error('Error during manual update:', error);
320320+ this.bot.sendMessage(chatId, `Error during update: ${error.message}`);
321321+ }
322322+ });
323323+324324+ // Command to add a text announcement
325325+ this.bot.onText(/^\/announce(?!\S)/, async (msg) => {
326326+ const chatId = msg.chat.id;
327327+328328+ if (!this.isAuthorized(msg.from.id)) {
329329+ this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
330330+ return;
331331+ }
332332+333333+ // Initialize the interactive announcement creation process
334334+ this.pendingAnnouncements = this.pendingAnnouncements || {};
335335+ this.pendingAnnouncements[msg.from.id] = {};
336336+337337+ // Display introduction message with formatting options
338338+ this.bot.sendMessage(
339339+ chatId,
340340+ '📣 *Create New Announcement*\n\n' +
341341+ 'I\'ll guide you through creating an announcement step by step:\n' +
342342+ '1️⃣ Name your announcement\n' +
343343+ '2️⃣ Write the message content\n' +
344344+ '3️⃣ Set a schedule\n' +
345345+ '4️⃣ Add an optional button (if desired)\n\n' +
346346+ 'You can use these formatting options in your message:\n' +
347347+ '- *text* for italic\n' +
348348+ '- **text** for bold\n' +
349349+ '- __text__ for underlined\n' +
350350+ '- ~~text~~ for strikethrough\n\n' +
351351+ 'Let\'s start! First, what would you like to name this announcement?',
352352+ {
353353+ parse_mode: 'Markdown',
354354+ reply_markup: { force_reply: true }
355355+ }
356356+ ).then(namePrompt => {
357357+ // Set up a one-time listener for the name response
358358+ this.bot.onReplyToMessage(chatId, namePrompt.message_id, async (nameMsg) => {
359359+ const announcementName = nameMsg.text === 'skip' ? '' : nameMsg.text;
360360+ this.pendingAnnouncements[msg.from.id].name = announcementName;
361361+362362+ // Now ask for the announcement message text
363363+ this.bot.sendMessage(
364364+ chatId,
365365+ 'Great! Now enter the announcement message content.\n\n' +
366366+ 'Your message can contain multiple lines and formatting:\n' +
367367+ '- *text* for italic\n' +
368368+ '- **text** for bold\n' +
369369+ '- __text__ for underlined\n' +
370370+ '- ~~text~~ for strikethrough\n\n' +
371371+ 'Type your message now:',
372372+ {
373373+ parse_mode: 'Markdown',
374374+ reply_markup: { force_reply: true }
375375+ }
376376+ ).then(messagePrompt => {
377377+ // Set up a one-time listener for the message text response
378378+ this.bot.onReplyToMessage(chatId, messagePrompt.message_id, async (messageTextMsg) => {
379379+ this.pendingAnnouncements[msg.from.id].message = messageTextMsg.text;
380380+381381+ try {
382382+ // Show a preview of the formatted message
383383+ const previewText = this.announcements.formatMessageText(this.pendingAnnouncements[msg.from.id].message);
384384+385385+ // Send a preview message to show how it will look
386386+ await this.bot.sendMessage(
387387+ chatId,
388388+ "Here's a preview of your announcement with formatting:",
389389+ { parse_mode: 'Markdown' }
390390+ ),
391391+392392+ // Send the actual preview
393393+ await this.bot.sendMessage(
394394+ chatId,
395395+ previewText,
396396+ { parse_mode: 'HTML' }
397397+ );
398398+ } catch (error) {
399399+ console.error("Error showing announcement preview:", error);
400400+ await this.bot.sendMessage(
401401+ chatId,
402402+ "Note: There might be issues with your formatting. Please ensure all formatting tags are properly closed."
403403+ );
404404+ }
405405+ // Now ask for a schedule
406406+ this.bot.sendMessage(
407407+ chatId,
408408+ 'Now, let\'s set the schedule for this announcement.\n\n' +
409409+ 'Enter a cron schedule expression. Examples:\n' +
410410+ '- `0 9 * * *` = Every day at 9:00 AM\n' +
411411+ '- `0 18 * * 5` = Every Friday at 6:00 PM\n' +
412412+ '- `0 12 1 * *` = First day of each month at noon\n\n' +
413413+ 'For more options, visit https://crontab.guru/',
414414+ {
415415+ parse_mode: 'Markdown',
416416+ reply_markup: { force_reply: true }
417417+ }
418418+ ).then(schedulePrompt => {
419419+ // Set up a one-time listener for the schedule response
420420+ this.bot.onReplyToMessage(chatId, schedulePrompt.message_id, async (scheduleMsg) => {
421421+ const cronSchedule = scheduleMsg.text;
422422+423423+ // Validate the cron schedule
424424+ if (!this.announcements.isValidCronExpression(cronSchedule)) {
425425+ this.bot.sendMessage(
426426+ chatId,
427427+ '⚠️ That doesn\'t appear to be a valid cron schedule. Please try again using the format shown in the examples.',
428428+ { parse_mode: 'Markdown' }
429429+ ).then(() => {
430430+ // Ask again for a valid schedule
431431+ this.bot.sendMessage(
432432+ chatId,
433433+ 'Please enter a valid cron schedule. Examples:\n' +
434434+ '- `0 9 * * *` = Every day at 9:00 AM\n' +
435435+ '- `0 18 * * 5` = Every Friday at 6:00 PM\n' +
436436+ '- `0 12 1 * *` = First day of each month at noon',
437437+ {
438438+ parse_mode: 'Markdown',
439439+ reply_markup: { force_reply: true }
440440+ }
441441+ ).then((newSchedulePrompt) => {
442442+ // Handle the new schedule response
443443+ this.bot.onReplyToMessage(chatId, newSchedulePrompt.message_id, (newScheduleMsg) => {
444444+ // Replace the schedule with the new one
445445+ const validCronSchedule = newScheduleMsg.text;
446446+447447+ if (!this.announcements.isValidCronExpression(validCronSchedule)) {
448448+ this.bot.sendMessage(
449449+ chatId,
450450+ '⚠️ Still not a valid cron schedule. Using "0 12 * * *" (daily at noon) as a default. You can edit this later.'
451451+ );
452452+ this.pendingAnnouncements[msg.from.id].cronSchedule = "0 12 * * *";
453453+454454+ // Continue to button step
455455+ this.askAboutButton(chatId, msg.from.id);
456456+ } else {
457457+ this.pendingAnnouncements[msg.from.id].cronSchedule = validCronSchedule;
458458+459459+ // Continue to button step
460460+ this.askAboutButton(chatId, msg.from.id);
461461+ }
462462+ });
463463+ });
464464+ });
465465+ return;
466466+ }
467467+468468+ // Store the schedule
469469+ this.pendingAnnouncements[msg.from.id].cronSchedule = cronSchedule;
470470+471471+ // Ask if they want to add a button
472472+ this.bot.sendMessage(
473473+ chatId,
474474+ 'Would you like to add a button with a link to this announcement?',
475475+ {
476476+ reply_markup: {
477477+ inline_keyboard: [
478478+ [
479479+ { text: 'Yes', callback_data: 'add_button' },
480480+ { text: 'No', callback_data: 'skip_button' }
481481+ ]
482482+ ]
483483+ }
484484+ }
485485+ ).then(buttonPrompt => {
486486+ // Callback handler for yes/no button selection
487487+ this.bot.once('callback_query', async (query) => {
488488+ await this.bot.answerCallbackQuery(query.id);
489489+490490+ // Delete the yes/no prompt
491491+ await this.bot.deleteMessage(chatId, buttonPrompt.message_id);
492492+493493+ if (query.data === 'add_button') {
494494+ // User wants to add a button
495495+ this.bot.sendMessage(
496496+ chatId,
497497+ 'Please enter the button text:',
498498+ { reply_markup: { force_reply: true } }
499499+ ).then(buttonTextPrompt => {
500500+ this.bot.onReplyToMessage(chatId, buttonTextPrompt.message_id, async (buttonTextMsg) => {
501501+ const buttonText = buttonTextMsg.text;
502502+503503+ // Now ask for the button URL
504504+ this.bot.sendMessage(
505505+ chatId,
506506+ 'Please enter the button URL:',
507507+ { reply_markup: { force_reply: true } }
508508+ ).then(buttonUrlPrompt => {
509509+ this.bot.onReplyToMessage(chatId, buttonUrlPrompt.message_id, async (buttonUrlMsg) => {
510510+ const buttonUrl = buttonUrlMsg.text;
511511+512512+ // Store the button object
513513+ const button = {
514514+ text: buttonText,
515515+ url: buttonUrl
516516+ };
517517+518518+ // Show confirmation with preview
519519+ await this.showAnnouncementConfirmation(
520520+ chatId,
521521+ msg.from.id,
522522+ this.pendingAnnouncements[msg.from.id].name,
523523+ this.pendingAnnouncements[msg.from.id].message,
524524+ this.pendingAnnouncements[msg.from.id].cronSchedule,
525525+ button
526526+ );
527527+ });
528528+ });
529529+ });
530530+ });
531531+ } else {
532532+ // User doesn't want to add a button
533533+ // Show confirmation with preview
534534+ await this.showAnnouncementConfirmation(
535535+ chatId,
536536+ msg.from.id,
537537+ this.pendingAnnouncements[msg.from.id].name,
538538+ this.pendingAnnouncements[msg.from.id].message,
539539+ this.pendingAnnouncements[msg.from.id].cronSchedule
540540+ );
541541+ }
542542+ });
543543+ });
544544+ });
545545+ });
546546+ });
547547+ });
548548+ });
549549+ });
550550+ });
551551+552552+ // Command to list and manage all announcements
553553+ this.bot.onText(/^\/announcements(?!\S)/, async (msg) => {
554554+ const chatId = msg.chat.id;
555555+556556+ if (!this.isAuthorized(msg.from.id)) {
557557+ this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
558558+ return;
559559+ }
560560+561561+ const announcements = this.announcements.getAnnouncements();
562562+563563+ if (announcements.length === 0) {
564564+ this.bot.sendMessage(
565565+ chatId,
566566+ 'No announcements configured. Use /announce to create a new announcement.'
567567+ );
568568+ return;
569569+ }
570570+571571+ // Format the list of announcements with inline buttons
572572+ let message = '📣 *Text Announcements*\n\n';
573573+574574+ const inlineKeyboard = [];
575575+576576+ for (let i = 0; i < announcements.length; i++) {
577577+ const announcement = announcements[i];
578578+579579+ // Add announcement details to message
580580+ message += `*${i+1}. ${announcement.name}*\n`;
581581+ message += `Schedule: \`${announcement.cronSchedule}\`\n`;
582582+ message += `Last run: ${announcement.lastRun ? new Date(announcement.lastRun).toLocaleString() : 'Never'}\n`;
583583+584584+ // Show button info if present
585585+ if (announcement.button && announcement.button.text && announcement.button.url) {
586586+ message += `Button: "${announcement.button.text}" → ${announcement.button.url}\n`;
587587+ }
588588+589589+ // Format the message preview, replacing line breaks with special character
590590+ const previewMessage = announcement.message
591591+ .replace(/\n/g, '↵') // Replace line breaks with a visible symbol
592592+ .substring(0, 50);
593593+ message += `Message: "${previewMessage}${announcement.message.length > 50 ? '...' : ''}"\n\n`;
594594+595595+ // Add buttons for this announcement
596596+ inlineKeyboard.push([
597597+ {
598598+ text: `▶️ Run #${i+1}`,
599599+ callback_data: `run_announcement_${announcement.id}`
600600+ },
601601+ {
602602+ text: `✏️ Edit #${i+1}`,
603603+ callback_data: `edit_announcement_${announcement.id}`
604604+ },
605605+ {
606606+ text: `❌ Delete #${i+1}`,
607607+ callback_data: `delete_announcement_${announcement.id}`
608608+ }
609609+ ]);
610610+ }
611611+612612+ // Add a button to create a new announcement
613613+ inlineKeyboard.push([
614614+ {
615615+ text: '➕ Add New Announcement',
616616+ callback_data: 'new_announcement'
617617+ }
618618+ ]);
619619+620620+ await this.bot.sendMessage(chatId, message, {
621621+ parse_mode: 'Markdown',
622622+ reply_markup: {
623623+ inline_keyboard: inlineKeyboard
624624+ }
625625+ });
626626+ });
627627+628628+ // Handle URL links
629629+ this.bot.on('message', async (msg) => {
630630+ if (msg.text && msg.text.startsWith('http')) {
631631+ const chatId = msg.chat.id;
632632+633633+ // Skip processing if this is part of an announcement setup
634634+ const isInAnnouncementFlow = this.pendingAnnouncements && this.pendingAnnouncements[msg.from.id];
635635+ const isInButtonEditFlow = this.editingAnnouncementButton && this.editingAnnouncementButton[msg.from.id];
636636+637637+ if (isInAnnouncementFlow || isInButtonEditFlow) {
638638+ // This URL is part of an announcement setup, so we should not process it as a link
639639+ return;
640640+ }
641641+642642+ if (!this.isAuthorized(msg.from.id)) {
643643+ this.bot.sendMessage(chatId, 'You are not authorized to use this bot.');
644644+ return;
645645+ }
646646+647647+ try {
648648+ const url = msg.text.trim();
649649+650650+ this.bot.sendMessage(chatId, 'Processing link...', { reply_to_message_id: msg.message_id });
651651+652652+ const mediaData = await scraperManager.extractFromUrl(url);
653653+654654+ // Check if the scraper returned an error (for temporarily disabled scrapers)
655655+ if (mediaData.error) {
656656+ this.bot.sendMessage(
657657+ chatId,
658658+ mediaData.error,
659659+ { reply_to_message_id: msg.message_id }
660660+ );
661661+ return;
662662+ }
663663+664664+ await queueManager.addToQueue(mediaData);
665665+666666+ const queueLength = await queueManager.getQueueLength();
667667+ const mediaType = mediaData.isVideo ? 'Video' : 'Image';
668668+669669+ this.bot.sendMessage(
670670+ chatId,
671671+ `Added to queue: ${mediaType} - ${mediaData.title}\nCurrent queue length: ${queueLength}`,
672672+ { reply_to_message_id: msg.message_id }
673673+ );
674674+ } catch (error) {
675675+ this.bot.sendMessage(
676676+ chatId,
677677+ `Error processing link: ${error.message}`,
678678+ { reply_to_message_id: msg.message_id }
679679+ );
680680+ }
681681+ }
682682+ });
683683+ }
684684+685685+ /**
686686+ * Register callback query handlers for interactive buttons
687687+ */
688688+ registerCallbacks() {
689689+ this.bot.on('callback_query', async (query) => {
690690+ try {
691691+ const chatId = query.message.chat.id;
692692+ if (!this.isAuthorized(query.from.id)) {
693693+ await this.bot.answerCallbackQuery(query.id, { text: 'You are not authorized to use these controls.' });
694694+ return;
695695+ }
696696+697697+ const data = query.data.split('_');
698698+ const action = data[0];
699699+700700+ switch (action) {
701701+ case 'page': {
702702+ // Handle page navigation
703703+ const page = parseInt(data[1]);
704704+ await this.bot.deleteMessage(chatId, query.message.message_id);
705705+ await this.displayQueuePage(chatId, page, 5);
706706+ await this.bot.answerCallbackQuery(query.id, { text: `Showing page ${page}` });
707707+ break;
708708+ }
709709+710710+ case 'remove': {
711711+ // Handle item removal
712712+ const index = parseInt(data[1]);
713713+ const removed = await queueManager.removeFromQueue(index);
714714+ if (removed) {
715715+ const itemType = removed.isVideo ? 'Video' : 'Image';
716716+ await this.bot.answerCallbackQuery(query.id, { text: `Removed ${itemType}: ${removed.title}` });
717717+718718+ // Update the queue display
719719+ await this.bot.deleteMessage(chatId, query.message.message_id);
720720+ const page = parseInt(data[2]) || 1;
721721+ await this.displayQueuePage(chatId, page, 5);
722722+ } else {
723723+ await this.bot.answerCallbackQuery(query.id, { text: 'Failed to remove item' });
724724+ }
725725+ break;
726726+ }
727727+728728+ case 'top': {
729729+ // Handle move to top (next to post)
730730+ const index = parseInt(data[1]);
731731+ const queue = await queueManager.getQueue();
732732+733733+ if (index > 0 && index < queue.length) {
734734+ // Remove the item from its current position
735735+ const item = queue[index];
736736+ queueManager.queueData.queue.splice(index, 1);
737737+738738+ // Add it to the beginning
739739+ queueManager.queueData.queue.unshift(item);
740740+741741+ // Save changes
742742+ await queueManager.saveQueueToDisk();
743743+744744+ await this.bot.answerCallbackQuery(query.id, { text: `Moved "${item.title}" to top of queue` });
745745+746746+ // Update the queue display
747747+ await this.bot.deleteMessage(chatId, query.message.message_id);
748748+ const page = parseInt(data[2]) || 1;
749749+ await this.displayQueuePage(chatId, page, 5);
750750+ } else {
751751+ await this.bot.answerCallbackQuery(query.id, { text: 'Failed to move item' });
752752+ }
753753+ break;
754754+ }
755755+756756+ case 'preview': {
757757+ // Handle preview item (send a preview of the queued item)
758758+ const index = parseInt(data[1]);
759759+ const queue = await queueManager.getQueue();
760760+761761+ if (index >= 0 && index < queue.length) {
762762+ const item = queue[index];
763763+ await this.bot.answerCallbackQuery(query.id, { text: 'Sending preview...' });
764764+765765+ // Send a temporary message
766766+ const loadingMsg = await this.bot.sendMessage(chatId, 'Preparing preview...');
767767+768768+ try {
769769+ // Generate a preview for the item
770770+ if (item.imageUrl && fs.existsSync(item.imageUrl)) {
771771+ // Send the image as a preview
772772+ const caption = `Preview of: ${item.title}\nFrom: ${item.siteName}\nPosition in queue: ${index + 1}`;
773773+ await this.bot.sendPhoto(chatId, item.imageUrl, { caption });
774774+ } else if (item.imageUrls && Array.isArray(item.imageUrls) && item.imageUrls.length > 0) {
775775+ // Use the first image from multiple images
776776+ const firstImage = item.imageUrls[0];
777777+ if (fs.existsSync(firstImage)) {
778778+ const caption = `Preview of: ${item.title}\nFrom: ${item.siteName}\nPosition in queue: ${index + 1}\n(${item.imageUrls.length} images total)`;
779779+ await this.bot.sendPhoto(chatId, firstImage, { caption });
780780+ }
781781+ }
782782+ } catch (error) {
783783+ console.error('Error sending preview:', error);
784784+ } finally {
785785+ // Delete the loading message
786786+ await this.bot.deleteMessage(chatId, loadingMsg.message_id);
787787+ }
788788+ } else {
789789+ await this.bot.answerCallbackQuery(query.id, { text: 'Item not found' });
790790+ }
791791+ break;
792792+ }
793793+794794+ // New announcement management callback handlers
795795+ case 'run': {
796796+ if (data[1] === 'announcement') {
797797+ const announcementId = data[2];
798798+ await this.bot.answerCallbackQuery(query.id, { text: 'Sending announcement...' });
799799+800800+ try {
801801+ const result = await this.announcements.sendAnnouncementNow(announcementId);
802802+ if (result) {
803803+ await this.bot.sendMessage(chatId, `✅ Announcement sent successfully!`);
804804+ } else {
805805+ await this.bot.sendMessage(chatId, `❌ Failed to send announcement.`);
806806+ }
807807+ } catch (error) {
808808+ await this.bot.sendMessage(chatId, `❌ Error: ${error.message}`);
809809+ }
810810+811811+ // Refresh announcements list
812812+ await this.bot.deleteMessage(chatId, query.message.message_id);
813813+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
814814+ }
815815+ break;
816816+ }
817817+818818+ case 'delete': {
819819+ if (data[1] === 'announcement') {
820820+ const announcementId = data[2];
821821+822822+ // Get the announcement to show its name
823823+ const announcement = this.announcements.getAnnouncementById(announcementId);
824824+ if (!announcement) {
825825+ await this.bot.answerCallbackQuery(query.id, { text: 'Announcement not found.' });
826826+ return;
827827+ }
828828+829829+ // Show confirmation dialog
830830+ await this.bot.answerCallbackQuery(query.id);
831831+832832+ const confirmMessage = await this.bot.sendMessage(
833833+ chatId,
834834+ `Are you sure you want to delete the announcement "${announcement.name}"?`,
835835+ {
836836+ reply_markup: {
837837+ inline_keyboard: [
838838+ [
839839+ { text: '✅ Yes, delete it', callback_data: `confirm_delete_announcement_${announcementId}` },
840840+ { text: '❌ No, cancel', callback_data: 'cancel_delete_announcement' }
841841+ ]
842842+ ]
843843+ }
844844+ }
845845+ );
846846+ }
847847+ break;
848848+ }
849849+850850+ case 'confirm': {
851851+ if (data[1] === 'delete' && data[2] === 'announcement') {
852852+ const announcementId = data[3];
853853+854854+ try {
855855+ const result = await this.announcements.removeAnnouncement(announcementId);
856856+ if (result) {
857857+ await this.bot.answerCallbackQuery(query.id, { text: 'Announcement deleted successfully.' });
858858+ } else {
859859+ await this.bot.answerCallbackQuery(query.id, { text: 'Failed to delete announcement.' });
860860+ }
861861+862862+ // Delete confirmation message
863863+ await this.bot.deleteMessage(chatId, query.message.message_id);
864864+865865+ // Refresh announcements list
866866+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
867867+ } catch (error) {
868868+ await this.bot.answerCallbackQuery(query.id, { text: `Error: ${error.message}` });
869869+ }
870870+ }
871871+ break;
872872+ }
873873+874874+ case 'cancel': {
875875+ if (data[1] === 'delete' && data[2] === 'announcement') {
876876+ await this.bot.answerCallbackQuery(query.id, { text: 'Delete cancelled.' });
877877+ await this.bot.deleteMessage(chatId, query.message.message_id);
878878+ }
879879+ break;
880880+ }
881881+882882+ case 'edit': {
883883+ if (data[1] === 'announcement') {
884884+ const announcementId = data[2];
885885+ const announcement = this.announcements.getAnnouncementById(announcementId);
886886+887887+ if (!announcement) {
888888+ await this.bot.answerCallbackQuery(query.id, { text: 'Announcement not found.' });
889889+ return;
890890+ }
891891+892892+ await this.bot.answerCallbackQuery(query.id);
893893+894894+ // Show edit options
895895+ const editMessage = await this.bot.sendMessage(
896896+ chatId,
897897+ `Editing announcement: *${announcement.name}*\n\nWhat would you like to edit?`,
898898+ {
899899+ parse_mode: 'Markdown',
900900+ reply_markup: {
901901+ inline_keyboard: [
902902+ [
903903+ {
904904+ text: '📝 Edit Message',
905905+ callback_data: `edit_announcement_message_${announcementId}`
906906+ }
907907+ ],
908908+ [
909909+ {
910910+ text: '⏰ Edit Schedule',
911911+ callback_data: `edit_announcement_schedule_${announcementId}`
912912+ }
913913+ ],
914914+ [
915915+ {
916916+ text: '🏷️ Edit Name',
917917+ callback_data: `edit_announcement_name_${announcementId}`
918918+ }
919919+ ],
920920+ [
921921+ {
922922+ text: '🔗 Edit Button',
923923+ callback_data: `edit_announcement_button_${announcementId}`
924924+ }
925925+ ],
926926+ [
927927+ {
928928+ text: '❌ Cancel',
929929+ callback_data: 'cancel_edit_announcement'
930930+ }
931931+ ]
932932+ ]
933933+ }
934934+ }
935935+ );
936936+ } else if (data[1] === 'announcement' && (data[2] === 'message' || data[2] === 'name' || data[2] === 'schedule' || data[2] === 'button')) {
937937+ const field = data[2];
938938+ const announcementId = data[3];
939939+ const announcement = this.announcements.getAnnouncementById(announcementId);
940940+941941+ if (!announcement) {
942942+ await this.bot.answerCallbackQuery(query.id, { text: 'Announcement not found.' });
943943+ return;
944944+ }
945945+946946+ await this.bot.answerCallbackQuery(query.id);
947947+948948+ // Delete the edit options message
949949+ await this.bot.deleteMessage(chatId, query.message.message_id);
950950+951951+ let promptText = '';
952952+ switch (field) {
953953+ case 'message':
954954+ 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.`;
955955+ break;
956956+ case 'name':
957957+ promptText = `Please enter the new name for the announcement "${announcement.name}":`;
958958+ break;
959959+ case 'schedule':
960960+ promptText = `Please enter the new cron schedule for the announcement "${announcement.name}" (use https://crontab.guru/ for help):\n\nCurrent schedule: ${announcement.cronSchedule}`;
961961+ break;
962962+ case 'button':
963963+ // For button editing, we'll first ask if they want to add, edit, or remove a button
964964+ const hasButton = announcement.button && announcement.button.text && announcement.button.url;
965965+966966+ if (hasButton) {
967967+ // Show options to edit or remove existing button
968968+ await this.bot.sendMessage(
969969+ chatId,
970970+ `Current button: "${announcement.button.text}" → ${announcement.button.url}\n\nWhat would you like to do?`,
971971+ {
972972+ reply_markup: {
973973+ inline_keyboard: [
974974+ [
975975+ {
976976+ text: '✏️ Edit Button',
977977+ callback_data: `edit_announcement_button_edit_${announcementId}`
978978+ }
979979+ ],
980980+ [
981981+ {
982982+ text: '❌ Remove Button',
983983+ callback_data: `edit_announcement_button_remove_${announcementId}`
984984+ }
985985+ ],
986986+ [
987987+ {
988988+ text: '↩️ Cancel',
989989+ callback_data: 'cancel_edit_announcement_button'
990990+ }
991991+ ]
992992+ ]
993993+ }
994994+ }
995995+ );
996996+ return;
997997+ } else {
998998+ // No existing button, ask if they want to add one
999999+ await this.bot.sendMessage(
10001000+ chatId,
10011001+ `This announcement doesn't have a button. Would you like to add one?`,
10021002+ {
10031003+ reply_markup: {
10041004+ inline_keyboard: [
10051005+ [
10061006+ {
10071007+ text: '➕ Add Button',
10081008+ callback_data: `edit_announcement_button_add_${announcementId}`
10091009+ }
10101010+ ],
10111011+ [
10121012+ {
10131013+ text: '↩️ Cancel',
10141014+ callback_data: 'cancel_edit_announcement_button'
10151015+ }
10161016+ ]
10171017+ ]
10181018+ }
10191019+ }
10201020+ );
10211021+ return;
10221022+ }
10231023+ }
10241024+10251025+ // For message, name, and schedule we'll send a prompt and handle the reply
10261026+ if (field === 'message' || field === 'name' || field === 'schedule') {
10271027+ // Send the prompt with force_reply
10281028+ const promptMsg = await this.bot.sendMessage(
10291029+ chatId,
10301030+ promptText,
10311031+ { reply_markup: { force_reply: true } }
10321032+ );
10331033+10341034+ // Set up one-time handler for the response
10351035+ this.bot.onReplyToMessage(chatId, promptMsg.message_id, async (responseMsg) => {
10361036+ try {
10371037+ // Get the response text - preserve line breaks and formatting exactly as received
10381038+ const responseText = responseMsg.text;
10391039+10401040+ // Prepare the update object
10411041+ const updates = {};
10421042+ updates[field] = responseText; // Raw text will preserve line breaks
10431043+10441044+ // Update the announcement
10451045+ await this.announcements.updateAnnouncement(announcementId, updates);
10461046+10471047+ // Notify user of success
10481048+ let successMsg = `✅ Announcement ${field} updated successfully!`;
10491049+ if (field === 'message') {
10501050+ successMsg += '\n\nYour message with all line breaks and formatting has been saved.';
10511051+ }
10521052+ await this.bot.sendMessage(chatId, successMsg);
10531053+10541054+ // Refresh the announcements list
10551055+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
10561056+ } catch (error) {
10571057+ await this.bot.sendMessage(
10581058+ chatId,
10591059+ `❌ Error updating announcement: ${error.message}`
10601060+ );
10611061+ }
10621062+ });
10631063+10641064+ // Skip the rest of the code for button
10651065+ return;
10661066+ }
10671067+10681068+ // For button editing, we'll first ask if they want to add, edit, or remove a button
10691069+ const hasButton = announcement.button && announcement.button.text && announcement.button.url;
10701070+10711071+ if (hasButton) {
10721072+ // Show options to edit or remove existing button
10731073+ await this.bot.sendMessage(
10741074+ chatId,
10751075+ `Current button: "${announcement.button.text}" → ${announcement.button.url}\n\nWhat would you like to do?`,
10761076+ {
10771077+ reply_markup: {
10781078+ inline_keyboard: [
10791079+ [
10801080+ {
10811081+ text: '✏️ Edit Button',
10821082+ callback_data: `edit_announcement_button_edit_${announcementId}`
10831083+ }
10841084+ ],
10851085+ [
10861086+ {
10871087+ text: '❌ Remove Button',
10881088+ callback_data: `edit_announcement_button_remove_${announcementId}`
10891089+ }
10901090+ ],
10911091+ [
10921092+ {
10931093+ text: '↩️ Cancel',
10941094+ callback_data: 'cancel_edit_announcement_button'
10951095+ }
10961096+ ]
10971097+ ]
10981098+ }
10991099+ }
11001100+ );
11011101+ return;
11021102+ } else {
11031103+ // No existing button, ask if they want to add one
11041104+ await this.bot.sendMessage(
11051105+ chatId,
11061106+ `This announcement doesn't have a button. Would you like to add one?`,
11071107+ {
11081108+ reply_markup: {
11091109+ inline_keyboard: [
11101110+ [
11111111+ {
11121112+ text: '➕ Add Button',
11131113+ callback_data: `edit_announcement_button_add_${announcementId}`
11141114+ }
11151115+ ],
11161116+ [
11171117+ {
11181118+ text: '↩️ Cancel',
11191119+ callback_data: 'cancel_edit_announcement_button'
11201120+ }
11211121+ ]
11221122+ ]
11231123+ }
11241124+ }
11251125+ );
11261126+ return;
11271127+ }
11281128+ }
11291129+ break;
11301130+ }
11311131+11321132+ case 'new': {
11331133+ if (data[1] === 'announcement') {
11341134+ await this.bot.answerCallbackQuery(query.id);
11351135+11361136+ // Delete the announcements list message
11371137+ await this.bot.deleteMessage(chatId, query.message.message_id);
11381138+11391139+ // Trigger the /announce command
11401140+ await this.bot.sendMessage(
11411141+ chatId,
11421142+ 'Please use the /announce command followed by your announcement text to create a new announcement.'
11431143+ );
11441144+ }
11451145+ break;
11461146+ }
11471147+11481148+ case 'cancel': {
11491149+ if (data[1] === 'edit' && data[2] === 'announcement') {
11501150+ await this.bot.answerCallbackQuery(query.id, { text: 'Edit cancelled.' });
11511151+ await this.bot.deleteMessage(chatId, query.message.message_id);
11521152+11531153+ // Refresh announcements list
11541154+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
11551155+ }
11561156+ break;
11571157+ }
11581158+11591159+ case 'edit': {
11601160+ if (data[1] === 'announcement' && data[2] === 'button') {
11611161+ if (data[3] === 'add' || data[3] === 'edit') {
11621162+ // Add or edit a button
11631163+ const announcementId = data[4];
11641164+ const announcement = this.announcements.getAnnouncementById(announcementId);
11651165+11661166+ if (!announcement) {
11671167+ await this.bot.answerCallbackQuery(query.id, { text: 'Announcement not found.' });
11681168+ return;
11691169+ }
11701170+11711171+ await this.bot.answerCallbackQuery(query.id);
11721172+11731173+ // Delete the options message
11741174+ await this.bot.deleteMessage(chatId, query.message.message_id);
11751175+11761176+ // Store the context for updating
11771177+ this.editingAnnouncementButton = this.editingAnnouncementButton || {};
11781178+ this.editingAnnouncementButton[query.from.id] = { id: announcementId };
11791179+11801180+ // First ask for button text
11811181+ const buttonTextPrompt = await this.bot.sendMessage(
11821182+ chatId,
11831183+ 'Please enter the button text:',
11841184+ { reply_markup: { force_reply: true } }
11851185+ );
11861186+11871187+ this.bot.onReplyToMessage(chatId, buttonTextPrompt.message_id, async (buttonTextMsg) => {
11881188+ const buttonText = buttonTextMsg.text;
11891189+11901190+ // Now ask for the button URL
11911191+ const buttonUrlPrompt = await this.bot.sendMessage(
11921192+ chatId,
11931193+ 'Please enter the button URL:',
11941194+ { reply_markup: { force_reply: true } }
11951195+ );
11961196+11971197+ this.bot.onReplyToMessage(chatId, buttonUrlPrompt.message_id, async (buttonUrlMsg) => {
11981198+ const buttonUrl = buttonUrlMsg.text;
11991199+12001200+ // Create the button object
12011201+ const button = {
12021202+ text: buttonText,
12031203+ url: buttonUrl
12041204+ };
12051205+12061206+ try {
12071207+ // Update the announcement with the new button
12081208+ const updated = await this.announcements.updateAnnouncement(announcementId, { button });
12091209+12101210+ if (updated) {
12111211+ await this.bot.sendMessage(
12121212+ chatId,
12131213+ `✅ Button ${data[3] === 'add' ? 'added' : 'updated'} successfully!`
12141214+ );
12151215+ } else {
12161216+ await this.bot.sendMessage(
12171217+ chatId,
12181218+ `❌ Failed to ${data[3] === 'add' ? 'add' : 'update'} button.`
12191219+ );
12201220+ }
12211221+12221222+ // Clean up
12231223+ delete this.editingAnnouncementButton[query.from.id];
12241224+12251225+ // Refresh announcements list
12261226+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
12271227+ } catch (error) {
12281228+ await this.bot.sendMessage(
12291229+ chatId,
12301230+ `❌ Error updating button: ${error.message}`
12311231+ );
12321232+ }
12331233+ });
12341234+ });
12351235+ } else if (data[3] === 'remove') {
12361236+ // Remove a button
12371237+ const announcementId = data[4];
12381238+12391239+ try {
12401240+ // Remove the button by setting it to null
12411241+ const updated = await this.announcements.updateAnnouncement(announcementId, { button: null });
12421242+12431243+ if (updated) {
12441244+ await this.bot.answerCallbackQuery(query.id, { text: 'Button removed successfully.' });
12451245+ } else {
12461246+ await this.bot.answerCallbackQuery(query.id, { text: 'Failed to remove button.' });
12471247+ }
12481248+12491249+ // Delete the options message
12501250+ await this.bot.deleteMessage(chatId, query.message.message_id);
12511251+12521252+ // Refresh announcements list
12531253+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
12541254+ } catch (error) {
12551255+ await this.bot.answerCallbackQuery(query.id, { text: `Error: ${error.message}` });
12561256+ }
12571257+ }
12581258+ }
12591259+ break;
12601260+ }
12611261+12621262+ case 'cancel': {
12631263+ if (data[1] === 'edit' && data[2] === 'announcement' && data[3] === 'button') {
12641264+ await this.bot.answerCallbackQuery(query.id, { text: 'Button edit cancelled.' });
12651265+ await this.bot.deleteMessage(chatId, query.message.message_id);
12661266+12671267+ // Refresh announcements list
12681268+ await this.bot.onText.handlers.find(h => h.regexp.toString().includes('announcements'))?._callback({ chat: { id: chatId } });
12691269+ }
12701270+ break;
12711271+ }
12721272+ }
12731273+ } catch (error) {
12741274+ console.error('Error handling callback query:', error);
12751275+ await this.bot.answerCallbackQuery(query.id, { text: 'An error occurred' });
12761276+ }
12771277+ });
12781278+ }
12791279+12801280+ /**
12811281+ * Display a page of the queue with interactive buttons
12821282+ * @param {number} chatId - Telegram chat ID
12831283+ * @param {number} page - Page number to display (1-based)
12841284+ * @param {number} pageSize - Number of items per page
12851285+ */
12861286+ async displayQueuePage(chatId, page, pageSize) {
12871287+ const queue = await queueManager.getQueue();
12881288+ const queueLength = queue.length;
12891289+12901290+ if (queueLength === 0) {
12911291+ this.bot.sendMessage(chatId, 'Queue is empty.');
12921292+ return;
12931293+ }
12941294+12951295+ // Calculate total pages
12961296+ const totalPages = Math.ceil(queueLength / pageSize);
12971297+12981298+ // Ensure page is within bounds
12991299+ const currentPage = Math.max(1, Math.min(page, totalPages));
13001300+13011301+ // Calculate start and end indices for this page
13021302+ const startIdx = (currentPage - 1) * pageSize;
13031303+ const endIdx = Math.min(startIdx + pageSize, queueLength);
13041304+13051305+ // Build message with queue items
13061306+ let message = `📋 *Queue Management* (${queueLength} items total)\n`;
13071307+ message += `Showing items ${startIdx + 1}-${endIdx} of ${queueLength}\n\n`;
13081308+13091309+ // Add each queue item
13101310+ for (let i = startIdx; i < endIdx; i++) {
13111311+ const item = queue[i];
13121312+ const itemType = item.isVideo ? '🎬' : '🖼️';
13131313+ const itemIndex = i + 1;
13141314+13151315+ // Show posting status for each service
13161316+ let statusIcons = '';
13171317+ if (item.postedTo) {
13181318+ if (item.postedTo.telegram) statusIcons += '✅TG ';
13191319+ else statusIcons += '❌TG ';
13201320+13211321+ if (queueManager.postServices.includes('discord')) {
13221322+ if (item.postedTo.discord) statusIcons += '✅DS';
13231323+ else statusIcons += '❌DS';
13241324+ }
13251325+ }
13261326+13271327+ message += `${itemIndex}. ${itemType} *${item.title}*\n From: ${item.siteName} ${statusIcons}\n`;
13281328+ }
13291329+13301330+ // Create navigation buttons and item action buttons
13311331+ const inline_keyboard = [];
13321332+13331333+ // Item action buttons
13341334+ for (let i = startIdx; i < endIdx; i++) {
13351335+ const row = [];
13361336+13371337+ // Add "Preview" button
13381338+ row.push({
13391339+ text: `👁️ #${i+1}`,
13401340+ callback_data: `preview_${i}_${currentPage}`
13411341+ });
13421342+13431343+ // Add "Remove" button
13441344+ row.push({
13451345+ text: `❌ #${i+1}`,
13461346+ callback_data: `remove_${i}_${currentPage}`
13471347+ });
13481348+13491349+ // Only add "Move to top" if not already at top
13501350+ if (i > 0) {
13511351+ row.push({
13521352+ text: `⬆️ #${i+1}`,
13531353+ callback_data: `top_${i}_${currentPage}`
13541354+ });
13551355+ } else {
13561356+ row.push({
13571357+ text: `🔼 Next`,
13581358+ callback_data: `preview_0_${currentPage}`
13591359+ });
13601360+ }
13611361+13621362+ inline_keyboard.push(row);
13631363+ }
13641364+13651365+ // Navigation row for paging
13661366+ const navRow = [];
13671367+13681368+ // Previous page button
13691369+ if (currentPage > 1) {
13701370+ navRow.push({
13711371+ text: '◀️ Previous',
13721372+ callback_data: `page_${currentPage - 1}`
13731373+ });
13741374+ }
13751375+13761376+ // Page indicator
13771377+ navRow.push({
13781378+ text: `Page ${currentPage}/${totalPages}`,
13791379+ callback_data: `page_${currentPage}`
13801380+ });
13811381+13821382+ // Next page button
13831383+ if (currentPage < totalPages) {
13841384+ navRow.push({
13851385+ text: 'Next ▶️',
13861386+ callback_data: `page_${currentPage + 1}`
13871387+ });
13881388+ }
13891389+13901390+ if (navRow.length > 0) {
13911391+ inline_keyboard.push(navRow);
13921392+ }
13931393+13941394+ // Send the message with inline keyboard
13951395+ await this.bot.sendMessage(chatId, message, {
13961396+ parse_mode: 'Markdown',
13971397+ reply_markup: {
13981398+ inline_keyboard
13991399+ }
14001400+ });
14011401+ }
14021402+14031403+ isAuthorized(userId) {
14041404+ // If no authorized users are specified, anyone can use the bot
14051405+ if (config.authorizedUsers.length === 0) {
14061406+ return true;
14071407+ }
14081408+14091409+ return config.authorizedUsers.includes(userId.toString());
14101410+ }
14111411+14121412+ /**
14131413+ * Check if the user is the owner of the bot
14141414+ * @param {number} userId - The Telegram user ID to check
14151415+ * @returns {boolean} - Whether the user is the owner
14161416+ */
14171417+ isOwner(userId) {
14181418+ return config.ownerId && userId.toString() === config.ownerId.toString();
14191419+ }
14201420+14211421+ /**
14221422+ * Post media (image or video) to the Telegram channel
14231423+ * @param {Object} mediaData - The media data to post
14241424+ * @returns {Promise<boolean>} - Whether posting was successful
14251425+ */
14261426+ async postMedia(mediaData) {
14271427+ try {
14281428+ // Create inline keyboard with link to source
14291429+ let buttonText = `View on ${mediaData.siteName}`;
14301430+14311431+ // Special butterfly emojis for Bluesky
14321432+ if (mediaData.siteName === 'Bluesky') {
14331433+ buttonText = `🦋 ${buttonText} 🦋`;
14341434+ }
14351435+14361436+ const inlineKeyboard = {
14371437+ inline_keyboard: [
14381438+ [
14391439+ {
14401440+ text: buttonText,
14411441+ url: mediaData.sourceUrl
14421442+ }
14431443+ ]
14441444+ ]
14451445+ };
14461446+14471447+ // Special caption for FurAffinity posts
14481448+ let caption = '';
14491449+ if (mediaData.siteName === 'FurAffinity' && mediaData.title && mediaData.name) {
14501450+ caption = `🖼️: ${mediaData.title}\n🎨: ${mediaData.name}`;
14511451+ }
14521452+14531453+ // Check if we're dealing with multiple images (imageUrls array with more than one item)
14541454+ if (mediaData.imageUrls && Array.isArray(mediaData.imageUrls) && mediaData.imageUrls.length > 1) {
14551455+ console.log(`Posting multiple images: ${mediaData.imageUrls.length} images`);
14561456+14571457+ // Since media groups don't support inline buttons, we'll include the link in the caption
14581458+ const groupCaption = caption ?
14591459+ `${caption}\n\nOriginal: ${mediaData.sourceUrl}` :
14601460+ `${mediaData.title}\n\nOriginal: ${mediaData.sourceUrl}`;
14611461+14621462+ // Prepare media group format for Telegram
14631463+ const mediaGroup = [];
14641464+14651465+ // Process each image in the array
14661466+ for (let i = 0; i < mediaData.imageUrls.length; i++) {
14671467+ const imagePath = mediaData.imageUrls[i];
14681468+14691469+ if (fs.existsSync(imagePath)) {
14701470+ // Add as InputMediaPhoto for the media group - use correct format
14711471+ mediaGroup.push({
14721472+ type: 'photo',
14731473+ media: fs.createReadStream(imagePath),
14741474+ // Only add caption to the first image
14751475+ ...(i === 0 ? { caption: groupCaption } : {})
14761476+ });
14771477+ } else {
14781478+ console.warn(`Image file not found: ${imagePath}`);
14791479+ }
14801480+ }
14811481+14821482+ if (mediaGroup.length > 0) {
14831483+ try {
14841484+ console.log(`Sending media group with ${mediaGroup.length} images`);
14851485+ // Send as a media group (album)
14861486+ await this.bot.sendMediaGroup(config.channelId, mediaGroup);
14871487+ return true;
14881488+ } catch (mediaGroupError) {
14891489+ console.error('Error posting media group:', mediaGroupError);
14901490+ // If posting as a group fails, fall back to posting the first image
14911491+ console.log('Falling back to posting single image');
14921492+ }
14931493+ }
14941494+ }
14951495+14961496+ // Check if we're dealing with a video
14971497+ if (mediaData.isVideo && mediaData.videoUrl) {
14981498+ console.log(`Posting video: ${mediaData.videoUrl}`);
14991499+15001500+ // For videos from local cache, we need to use the file path
15011501+ if (fs.existsSync(mediaData.videoUrl)) {
15021502+ const response = await this.bot.sendVideo(
15031503+ config.channelId,
15041504+ mediaData.videoUrl,
15051505+ {
15061506+ caption: caption, // Add the caption here
15071507+ reply_markup: inlineKeyboard
15081508+ }
15091509+ );
15101510+ return true;
15111511+ } else {
15121512+ // Try to post from URL if not in cache
15131513+ try {
15141514+ const response = await this.bot.sendVideo(
15151515+ config.channelId,
15161516+ mediaData.videoUrl,
15171517+ {
15181518+ caption: caption, // Add the caption here
15191519+ reply_markup: inlineKeyboard
15201520+ }
15211521+ );
15221522+ return true;
15231523+ } catch (videoError) {
15241524+ console.error('Error posting video directly:', videoError);
15251525+15261526+ // Fallback to sending image/thumbnail if video fails
15271527+ if (mediaData.imageUrl && mediaData.imageUrl !== mediaData.videoUrl) {
15281528+ const fallbackCaption = caption ?
15291529+ `${caption}\n(Video post - see original)` :
15301530+ "(Video post - see original)";
15311531+15321532+ const response = await this.bot.sendPhoto(
15331533+ config.channelId,
15341534+ mediaData.imageUrl,
15351535+ {
15361536+ caption: fallbackCaption,
15371537+ reply_markup: inlineKeyboard
15381538+ }
15391539+ );
15401540+ return true;
15411541+ }
15421542+15431543+ throw videoError;
15441544+ }
15451545+ }
15461546+ }
15471547+15481548+ // Handle image posting (including video thumbnails as fallback)
15491549+ console.log(`Posting image: ${mediaData.imageUrl}`);
15501550+15511551+ // For images from local cache, we need to use the file path
15521552+ if (fs.existsSync(mediaData.imageUrl)) {
15531553+ const response = await this.bot.sendPhoto(
15541554+ config.channelId,
15551555+ mediaData.imageUrl,
15561556+ {
15571557+ caption: caption, // Add the caption here
15581558+ reply_markup: inlineKeyboard
15591559+ }
15601560+ );
15611561+ return true;
15621562+ } else {
15631563+ // Try to post from URL if not in cache
15641564+ try {
15651565+ const response = await this.bot.sendPhoto(
15661566+ config.channelId,
15671567+ mediaData.imageUrl,
15681568+ {
15691569+ caption: caption, // Add the caption here
15701570+ reply_markup: inlineKeyboard
15711571+ }
15721572+ );
15731573+ return true;
15741574+ } catch (imageError) {
15751575+ console.error('Error posting image:', imageError);
15761576+15771577+ // Attempt to download and reupload if direct linking fails
15781578+ try {
15791579+ const imageResponse = await axios({
15801580+ method: 'GET',
15811581+ url: mediaData.imageUrl,
15821582+ responseType: 'stream'
15831583+ });
15841584+15851585+ const response = await this.bot.sendPhoto(
15861586+ config.channelId,
15871587+ imageResponse.data,
15881588+ {
15891589+ caption: caption, // Add the caption here
15901590+ reply_markup: inlineKeyboard
15911591+ }
15921592+ );
15931593+15941594+ return true;
15951595+ } catch (secondError) {
15961596+ console.error('Error uploading image after download:', secondError);
15971597+ return false;
15981598+ }
15991599+ }
16001600+ }
16011601+ } catch (error) {
16021602+ console.error('Error posting media:', error);
16031603+ return false;
16041604+ }
16051605+ }
16061606+16071607+ /**
16081608+ * Shutdown the bot gracefully
16091609+ * @returns {Promise<void>}
16101610+ */
16111611+ /**
16121612+ * Helper method to ask about adding a button to an announcement
16131613+ * @param {number} chatId - The chat ID where to send the message
16141614+ * @param {number} userId - The user ID for tracking state
16151615+ */
16161616+ askAboutButton(chatId, userId) {
16171617+ this.bot.sendMessage(
16181618+ chatId,
16191619+ 'Would you like to add a button with a link to this announcement?',
16201620+ {
16211621+ reply_markup: {
16221622+ inline_keyboard: [
16231623+ [
16241624+ { text: 'Yes', callback_data: 'add_button' },
16251625+ { text: 'No', callback_data: 'skip_button' }
16261626+ ]
16271627+ ]
16281628+ }
16291629+ }
16301630+ );
16311631+ }
16321632+16331633+ /**
16341634+ * Helper method to show announcement confirmation
16351635+ * @param {number} chatId - The chat ID where to send the message
16361636+ * @param {number} userId - The user ID for tracking state
16371637+ * @param {string} name - Announcement name
16381638+ * @param {string} message - Announcement message
16391639+ * @param {string} cronSchedule - Cron schedule
16401640+ * @param {Object} button - Button object (optional)
16411641+ */
16421642+ async showAnnouncementConfirmation(chatId, userId, name, message, cronSchedule, button = null) {
16431643+ // Store all the data for the confirmation callback
16441644+ this.confirmAnnouncement = this.confirmAnnouncement || {};
16451645+ this.confirmAnnouncement[userId] = {
16461646+ name,
16471647+ message,
16481648+ cronSchedule,
16491649+ button
16501650+ };
16511651+16521652+ // Create confirmation message with all details
16531653+ let confirmationMessage = "📣 *Announcement Preview*\n\n";
16541654+ confirmationMessage += `*Name*: ${name || "(Auto-generated)"}\n`;
16551655+ confirmationMessage += `*Schedule*: \`${cronSchedule}\`\n`;
16561656+16571657+ if (button) {
16581658+ confirmationMessage += `*Button*: "${button.text}" → ${button.url}\n`;
16591659+ } else {
16601660+ confirmationMessage += "*Button*: None\n";
16611661+ }
16621662+16631663+ confirmationMessage += "\n*Message Preview*:\n------------------\n";
16641664+16651665+ // Send confirmation message
16661666+ await this.bot.sendMessage(
16671667+ chatId,
16681668+ confirmationMessage,
16691669+ { parse_mode: 'Markdown' }
16701670+ );
16711671+16721672+ // Send formatted message preview
16731673+ const formattedMessage = this.announcements.formatMessageText(message);
16741674+ await this.bot.sendMessage(
16751675+ chatId,
16761676+ formattedMessage,
16771677+ { parse_mode: 'HTML' }
16781678+ );
16791679+16801680+ // Ask for confirmation
16811681+ await this.bot.sendMessage(
16821682+ chatId,
16831683+ "Does everything look correct? Ready to create this announcement?",
16841684+ {
16851685+ reply_markup: {
16861686+ inline_keyboard: [
16871687+ [
16881688+ { text: '✅ Create Announcement', callback_data: 'confirm_announcement' },
16891689+ { text: '❌ Cancel', callback_data: 'cancel_announcement' }
16901690+ ]
16911691+ ]
16921692+ }
16931693+ }
16941694+ );
16951695+16961696+ // Set up a one-time listener for the confirmation response
16971697+ this.bot.once('callback_query', async (query) => {
16981698+ if (query.from.id !== userId) return; // Make sure it's the same user
16991699+17001700+ await this.bot.answerCallbackQuery(query.id);
17011701+17021702+ if (query.data === 'confirm_announcement') {
17031703+ try {
17041704+ // Create the announcement
17051705+ const announcement = await this.announcements.addAnnouncement(
17061706+ message,
17071707+ cronSchedule,
17081708+ name,
17091709+ button
17101710+ );
17111711+17121712+ // Send success message
17131713+ let successMessage = `✅ Announcement "${announcement.name}" created!\n\n`;
17141714+ successMessage += `Scheduled for: ${announcement.cronSchedule}\n\n`;
17151715+17161716+ if (button) {
17171717+ successMessage += `Button: "${button.text}" → ${button.url}\n\n`;
17181718+ }
17191719+17201720+ successMessage += "You can manage all announcements with /announcements";
17211721+17221722+ await this.bot.sendMessage(chatId, successMessage);
17231723+17241724+ // Clean up
17251725+ delete this.pendingAnnouncements[userId];
17261726+ delete this.confirmAnnouncement[userId];
17271727+ } catch (error) {
17281728+ this.bot.sendMessage(
17291729+ chatId,
17301730+ `Error creating announcement: ${error.message}\n\nPlease try again.`
17311731+ );
17321732+ }
17331733+ } else {
17341734+ // User canceled
17351735+ await this.bot.sendMessage(
17361736+ chatId,
17371737+ "Announcement creation canceled. You can start over with /announce"
17381738+ );
17391739+17401740+ // Clean up
17411741+ delete this.pendingAnnouncements[userId];
17421742+ delete this.confirmAnnouncement[userId];
17431743+ }
17441744+ });
17451745+ }
17461746+17471747+ /**
17481748+ * Shutdown the bot gracefully
17491749+ * @returns {Promise<void>}
17501750+ */
17511751+ async shutdown() {
17521752+ try {
17531753+ console.log('Stopping Telegram bot polling...');
17541754+ await this.bot.stopPolling();
17551755+ console.log('Telegram bot polling stopped');
17561756+17571757+ return true;
17581758+ } catch (error) {
17591759+ console.error('Error shutting down bot:', error);
17601760+ return false;
17611761+ }
17621762+ }
17631763+}
17641764+17651765+module.exports = StagehandBot;
+84
bot/telegramBotModular.js
···11+const TelegramBot = require('node-telegram-bot-api');
22+const config = require('../config');
33+const queueManager = require('../queue/queueManager');
44+const AnnouncementManager = require('../utils/announcementManager');
55+const CommandRegistry = require('./telegrambot/commandRegistry');
66+77+class StagehandBot {
88+ constructor() {
99+ this.bot = new TelegramBot(config.botToken, { polling: true });
1010+ this.serviceName = 'telegram';
1111+ this.channelId = config.channelId;
1212+ this.announcements = new AnnouncementManager(this);
1313+ this.commandRegistry = new CommandRegistry(this.bot, this.announcements);
1414+ this.init();
1515+ }
1616+1717+ async init() {
1818+ await this.announcements.init();
1919+ this.registerCommands();
2020+ console.log('Telegram bot started...');
2121+ }
2222+2323+ registerCommands() {
2424+ // Use the command registry to register all commands and handlers
2525+ this.commandRegistry.registerAll();
2626+ }
2727+2828+ /**
2929+ * Post media (image or video) to the Telegram channel
3030+ * This method is used by the scheduler and external services
3131+ * @param {Object} mediaData - The media data to post
3232+ * @returns {Promise<boolean>} - Whether posting was successful
3333+ */
3434+ async postMedia(mediaData) {
3535+ return await this.commandRegistry.getMediaHelper().postMedia(mediaData);
3636+ }
3737+3838+ /**
3939+ * Check if a user is authorized
4040+ * @param {number} userId - The user ID to check
4141+ * @returns {boolean} - Whether the user is authorized
4242+ */
4343+ isAuthorized(userId) {
4444+ return this.commandRegistry.getAuthHelper().isAuthorized(userId);
4545+ }
4646+4747+ /**
4848+ * Check if the user is the owner of the bot
4949+ * @param {number} userId - The Telegram user ID to check
5050+ * @returns {boolean} - Whether the user is the owner
5151+ */
5252+ isOwner(userId) {
5353+ return this.commandRegistry.getAuthHelper().isOwner(userId);
5454+ }
5555+5656+ /**
5757+ * Display a page of the queue with interactive buttons
5858+ * @param {number} chatId - Telegram chat ID
5959+ * @param {number} page - Page number to display (1-based)
6060+ * @param {number} pageSize - Number of items per page
6161+ */
6262+ async displayQueuePage(chatId, page, pageSize) {
6363+ return await this.commandRegistry.getQueueHelper().displayQueuePage(chatId, page, pageSize);
6464+ }
6565+6666+ /**
6767+ * Shutdown the bot gracefully
6868+ * @returns {Promise<void>}
6969+ */
7070+ async shutdown() {
7171+ try {
7272+ console.log('Stopping Telegram bot polling...');
7373+ await this.bot.stopPolling();
7474+ console.log('Telegram bot polling stopped');
7575+7676+ return true;
7777+ } catch (error) {
7878+ console.error('Error shutting down bot:', error);
7979+ return false;
8080+ }
8181+ }
8282+}
8383+8484+module.exports = StagehandBot;
+105
bot/telegrambot/commandRegistry.js
···11+// Command imports
22+const StartCommand = require('./commands/start');
33+const HelpCommand = require('./commands/help');
44+const QueueCommand = require('./commands/queue');
55+const SendCommand = require('./commands/send');
66+const ScheduleCommand = require('./commands/schedule');
77+const SetCountCommand = require('./commands/setcount');
88+const ClearCommand = require('./commands/clear');
99+const CleanCacheCommand = require('./commands/cleancache');
1010+const ShuffleCommand = require('./commands/shuffle');
1111+const UpdateCommand = require('./commands/update');
1212+const AnnounceCommand = require('./commands/announce');
1313+const AnnouncementsCommand = require('./commands/announcements');
1414+const StatusCommand = require('./commands/status');
1515+const LinkHandler = require('./commands/linkHandler');
1616+1717+// Helper imports
1818+const AuthHelper = require('./helpers/authHelper');
1919+const QueueHelper = require('./helpers/queueHelper');
2020+const MediaHelper = require('./helpers/mediaHelper');
2121+const CallbackHandler = require('./helpers/callbackHandler');
2222+2323+/**
2424+ * Command registry to manage all telegram bot commands and helpers
2525+ */
2626+class CommandRegistry {
2727+ constructor(bot, announcements, queueMonitor) {
2828+ this.bot = bot;
2929+ this.announcements = announcements;
3030+ this.queueMonitor = queueMonitor;
3131+3232+ // Initialize helpers
3333+ this.authHelper = new AuthHelper();
3434+ this.queueHelper = new QueueHelper(bot);
3535+ this.mediaHelper = new MediaHelper(bot);
3636+ this.callbackHandler = new CallbackHandler(bot, this.authHelper, this.queueHelper, announcements, queueMonitor);
3737+3838+ // Initialize commands
3939+ this.commands = [
4040+ new StartCommand(bot),
4141+ new HelpCommand(bot),
4242+ new QueueCommand(bot, this.authHelper, this.queueHelper),
4343+ new SendCommand(bot, this.authHelper, this.mediaHelper),
4444+ new ScheduleCommand(bot, this.authHelper),
4545+ new SetCountCommand(bot, this.authHelper),
4646+ new ClearCommand(bot, this.authHelper),
4747+ new CleanCacheCommand(bot, this.authHelper),
4848+ new ShuffleCommand(bot, this.authHelper),
4949+ new UpdateCommand(bot, this.authHelper),
5050+ new AnnounceCommand(bot, this.authHelper, announcements),
5151+ new AnnouncementsCommand(bot, this.authHelper, announcements),
5252+ new StatusCommand(bot, this.authHelper, queueMonitor)
5353+ ];
5454+5555+ // Initialize link handler
5656+ this.linkHandler = new LinkHandler(bot, this.authHelper);
5757+ }
5858+5959+ /**
6060+ * Register all commands and handlers
6161+ */
6262+ registerAll() {
6363+ // Register all commands
6464+ this.commands.forEach(command => {
6565+ command.register();
6666+ });
6767+6868+ // Register link handler
6969+ this.linkHandler.register();
7070+7171+ // Register callback handler
7272+ this.callbackHandler.register();
7373+7474+ // Set up cross-references for announcement flows
7575+ const announceCommand = this.commands.find(cmd => cmd instanceof AnnounceCommand);
7676+ if (announceCommand) {
7777+ // Share state between announce command and link handler for proper URL handling
7878+ this.linkHandler.setPendingAnnouncements(announceCommand.pendingAnnouncements);
7979+ this.linkHandler.setEditingAnnouncementButton(this.callbackHandler.getEditingAnnouncementButton());
8080+ }
8181+ }
8282+8383+ /**
8484+ * Get the media helper for external use (like in the main bot class)
8585+ */
8686+ getMediaHelper() {
8787+ return this.mediaHelper;
8888+ }
8989+9090+ /**
9191+ * Get the auth helper for external use
9292+ */
9393+ getAuthHelper() {
9494+ return this.authHelper;
9595+ }
9696+9797+ /**
9898+ * Get the queue helper for external use
9999+ */
100100+ getQueueHelper() {
101101+ return this.queueHelper;
102102+ }
103103+}
104104+105105+module.exports = CommandRegistry;
+340
bot/telegrambot/commands/announce.js
···11+const AnnouncementCreationHelper = require('../helpers/announcementCreationHelper');
22+33+/**
44+ * /announce command handler - uses AnnouncementCreationHelper for workflow
55+ */
66+class AnnounceCommand {
77+ constructor(bot, authHelper, announcements) {
88+ this.bot = bot;
99+ this.authHelper = authHelper;
1010+ this.announcements = announcements;
1111+ this.creationHelper = new AnnouncementCreationHelper(bot, announcements);
1212+ }
1313+1414+ register() {
1515+ this.bot.onText(/^\/announce(?!\S)/, async (msg) => {
1616+ const chatId = msg.chat.id;
1717+1818+ if (!this.authHelper.isAuthorized(msg.from.id)) {
1919+ this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
2020+ return;
2121+ }
2222+2323+ // Use the creation helper to start the workflow
2424+ await this.creationHelper.startCreationWorkflow(chatId, msg.from.id, 'command');
2525+ });
2626+ }
2727+2828+ handleNameStep(chatId, userId, messageId) {
2929+ this.bot.onReplyToMessage(chatId, messageId, async (nameMsg) => {
3030+ const announcementName = nameMsg.text === 'skip' ? '' : nameMsg.text;
3131+ this.pendingAnnouncements[userId].name = announcementName;
3232+3333+ // Now ask for the announcement message text
3434+ this.bot.sendMessage(
3535+ chatId,
3636+ 'Great! Now enter the announcement message content.\n\n' +
3737+ 'Your message can contain multiple lines and formatting:\n' +
3838+ '- *text* for italic\n' +
3939+ '- **text** for bold\n' +
4040+ '- __text__ for underlined\n' +
4141+ '- ~~text~~ for strikethrough\n\n' +
4242+ 'Type your message now:',
4343+ {
4444+ parse_mode: 'Markdown',
4545+ reply_markup: { force_reply: true }
4646+ }
4747+ ).then(messagePrompt => {
4848+ this.handleMessageStep(chatId, userId, messagePrompt.message_id);
4949+ });
5050+ });
5151+ }
5252+5353+ handleMessageStep(chatId, userId, messageId) {
5454+ this.bot.onReplyToMessage(chatId, messageId, async (messageTextMsg) => {
5555+ this.pendingAnnouncements[userId].message = messageTextMsg.text;
5656+5757+ try {
5858+ // Show a preview of the formatted message
5959+ const previewText = this.announcements.formatMessageText(this.pendingAnnouncements[userId].message);
6060+6161+ // Send a preview message to show how it will look
6262+ await this.bot.sendMessage(
6363+ chatId,
6464+ "Here's a preview of your announcement with formatting:",
6565+ { parse_mode: 'Markdown' }
6666+ );
6767+6868+ // Send the actual preview
6969+ await this.bot.sendMessage(
7070+ chatId,
7171+ previewText,
7272+ { parse_mode: 'HTML' }
7373+ );
7474+ } catch (error) {
7575+ console.error("Error showing announcement preview:", error);
7676+ await this.bot.sendMessage(
7777+ chatId,
7878+ "Note: There might be issues with your formatting. Please ensure all formatting tags are properly closed."
7979+ );
8080+ }
8181+8282+ // Now ask for a schedule
8383+ this.bot.sendMessage(
8484+ chatId,
8585+ 'Now, let\'s set the schedule for this announcement.\n\n' +
8686+ 'Enter a cron schedule expression. Examples:\n' +
8787+ '- `0 9 * * *` = Every day at 9:00 AM\n' +
8888+ '- `0 18 * * 5` = Every Friday at 6:00 PM\n' +
8989+ '- `0 12 1 * *` = First day of each month at noon\n\n' +
9090+ 'For more options, visit https://crontab.guru/',
9191+ {
9292+ parse_mode: 'Markdown',
9393+ reply_markup: { force_reply: true }
9494+ }
9595+ ).then(schedulePrompt => {
9696+ this.handleScheduleStep(chatId, userId, schedulePrompt.message_id);
9797+ });
9898+ });
9999+ }
100100+101101+ handleScheduleStep(chatId, userId, messageId) {
102102+ this.bot.onReplyToMessage(chatId, messageId, async (scheduleMsg) => {
103103+ const cronSchedule = scheduleMsg.text;
104104+105105+ // Validate the cron schedule
106106+ if (!this.announcements.isValidCronExpression(cronSchedule)) {
107107+ this.bot.sendMessage(
108108+ chatId,
109109+ '⚠️ That doesn\'t appear to be a valid cron schedule. Please try again using the format shown in the examples.',
110110+ { parse_mode: 'Markdown' }
111111+ ).then(() => {
112112+ // Ask again for a valid schedule
113113+ this.bot.sendMessage(
114114+ chatId,
115115+ 'Please enter a valid cron schedule. Examples:\n' +
116116+ '- `0 9 * * *` = Every day at 9:00 AM\n' +
117117+ '- `0 18 * * 5` = Every Friday at 6:00 PM\n' +
118118+ '- `0 12 1 * *` = First day of each month at noon',
119119+ {
120120+ parse_mode: 'Markdown',
121121+ reply_markup: { force_reply: true }
122122+ }
123123+ ).then((newSchedulePrompt) => {
124124+ // Handle the new schedule response
125125+ this.bot.onReplyToMessage(chatId, newSchedulePrompt.message_id, (newScheduleMsg) => {
126126+ // Replace the schedule with the new one
127127+ const validCronSchedule = newScheduleMsg.text;
128128+129129+ if (!this.announcements.isValidCronExpression(validCronSchedule)) {
130130+ this.bot.sendMessage(
131131+ chatId,
132132+ '⚠️ Still not a valid cron schedule. Using "0 12 * * *" (daily at noon) as a default. You can edit this later.'
133133+ );
134134+ this.pendingAnnouncements[userId].cronSchedule = "0 12 * * *";
135135+136136+ // Continue to button step
137137+ this.askAboutButton(chatId, userId);
138138+ } else {
139139+ this.pendingAnnouncements[userId].cronSchedule = validCronSchedule;
140140+141141+ // Continue to button step
142142+ this.askAboutButton(chatId, userId);
143143+ }
144144+ });
145145+ });
146146+ });
147147+ return;
148148+ }
149149+150150+ // Store the schedule
151151+ this.pendingAnnouncements[userId].cronSchedule = cronSchedule;
152152+153153+ // Ask if they want to add a button
154154+ this.askAboutButton(chatId, userId);
155155+ });
156156+ }
157157+158158+ askAboutButton(chatId, userId) {
159159+ this.bot.sendMessage(
160160+ chatId,
161161+ 'Would you like to add a button with a link to this announcement?',
162162+ {
163163+ reply_markup: {
164164+ inline_keyboard: [
165165+ [
166166+ { text: 'Yes', callback_data: 'add_button' },
167167+ { text: 'No', callback_data: 'skip_button' }
168168+ ]
169169+ ]
170170+ }
171171+ }
172172+ ).then(buttonPrompt => {
173173+ // Callback handler for yes/no button selection
174174+ this.bot.once('callback_query', async (query) => {
175175+ await this.bot.answerCallbackQuery(query.id);
176176+177177+ // Delete the yes/no prompt
178178+ await this.bot.deleteMessage(chatId, buttonPrompt.message_id);
179179+180180+ if (query.data === 'add_button') {
181181+ this.handleButtonCreation(chatId, userId);
182182+ } else {
183183+ // User doesn't want to add a button
184184+ await this.showAnnouncementConfirmation(
185185+ chatId,
186186+ userId,
187187+ this.pendingAnnouncements[userId].name,
188188+ this.pendingAnnouncements[userId].message,
189189+ this.pendingAnnouncements[userId].cronSchedule
190190+ );
191191+ }
192192+ });
193193+ });
194194+ }
195195+196196+ handleButtonCreation(chatId, userId) {
197197+ this.bot.sendMessage(
198198+ chatId,
199199+ 'Please enter the button text:',
200200+ { reply_markup: { force_reply: true } }
201201+ ).then(buttonTextPrompt => {
202202+ this.bot.onReplyToMessage(chatId, buttonTextPrompt.message_id, async (buttonTextMsg) => {
203203+ const buttonText = buttonTextMsg.text;
204204+205205+ // Now ask for the button URL
206206+ this.bot.sendMessage(
207207+ chatId,
208208+ 'Please enter the button URL:',
209209+ { reply_markup: { force_reply: true } }
210210+ ).then(buttonUrlPrompt => {
211211+ this.bot.onReplyToMessage(chatId, buttonUrlPrompt.message_id, async (buttonUrlMsg) => {
212212+ const buttonUrl = buttonUrlMsg.text;
213213+214214+ // Store the button object
215215+ const button = {
216216+ text: buttonText,
217217+ url: buttonUrl
218218+ };
219219+220220+ // Show confirmation with preview
221221+ await this.showAnnouncementConfirmation(
222222+ chatId,
223223+ userId,
224224+ this.pendingAnnouncements[userId].name,
225225+ this.pendingAnnouncements[userId].message,
226226+ this.pendingAnnouncements[userId].cronSchedule,
227227+ button
228228+ );
229229+ });
230230+ });
231231+ });
232232+ });
233233+ }
234234+235235+ async showAnnouncementConfirmation(chatId, userId, name, message, cronSchedule, button = null) {
236236+ // Store all the data for the confirmation callback
237237+ this.confirmAnnouncement[userId] = {
238238+ name,
239239+ message,
240240+ cronSchedule,
241241+ button
242242+ };
243243+244244+ // Create confirmation message with all details
245245+ let confirmationMessage = "📣 *Announcement Preview*\n\n";
246246+ confirmationMessage += `*Name*: ${name || "(Auto-generated)"}\n`;
247247+ confirmationMessage += `*Schedule*: \`${cronSchedule}\`\n`;
248248+249249+ if (button) {
250250+ confirmationMessage += `*Button*: "${button.text}" → ${button.url}\n`;
251251+ } else {
252252+ confirmationMessage += "*Button*: None\n";
253253+ }
254254+255255+ confirmationMessage += "\n*Message Preview*:\n------------------\n";
256256+257257+ // Send confirmation message
258258+ await this.bot.sendMessage(
259259+ chatId,
260260+ confirmationMessage,
261261+ { parse_mode: 'Markdown' }
262262+ );
263263+264264+ // Send formatted message preview
265265+ const formattedMessage = this.announcements.formatMessageText(message);
266266+ await this.bot.sendMessage(
267267+ chatId,
268268+ formattedMessage,
269269+ { parse_mode: 'HTML' }
270270+ );
271271+272272+ // Ask for confirmation
273273+ await this.bot.sendMessage(
274274+ chatId,
275275+ "Does everything look correct? Ready to create this announcement?",
276276+ {
277277+ reply_markup: {
278278+ inline_keyboard: [
279279+ [
280280+ { text: '✅ Create Announcement', callback_data: 'confirm_announcement' },
281281+ { text: '❌ Cancel', callback_data: 'cancel_announcement' }
282282+ ]
283283+ ]
284284+ }
285285+ }
286286+ );
287287+288288+ // Set up a one-time listener for the confirmation response
289289+ this.bot.once('callback_query', async (query) => {
290290+ if (query.from.id !== userId) return; // Make sure it's the same user
291291+292292+ await this.bot.answerCallbackQuery(query.id);
293293+294294+ if (query.data === 'confirm_announcement') {
295295+ try {
296296+ // Create the announcement
297297+ const announcement = await this.announcements.addAnnouncement(
298298+ message,
299299+ cronSchedule,
300300+ name,
301301+ button
302302+ );
303303+304304+ // Send success message
305305+ let successMessage = `✅ Announcement "${announcement.name}" created!\n\n`;
306306+ successMessage += `Scheduled for: ${announcement.cronSchedule}\n\n`;
307307+308308+ if (button) {
309309+ successMessage += `Button: "${button.text}" → ${button.url}\n\n`;
310310+ }
311311+312312+ successMessage += "You can manage all announcements with /announcements";
313313+314314+ await this.bot.sendMessage(chatId, successMessage);
315315+316316+ // Clean up
317317+ delete this.pendingAnnouncements[userId];
318318+ delete this.confirmAnnouncement[userId];
319319+ } catch (error) {
320320+ this.bot.sendMessage(
321321+ chatId,
322322+ `Error creating announcement: ${error.message}\n\nPlease try again.`
323323+ );
324324+ }
325325+ } else {
326326+ // User canceled
327327+ await this.bot.sendMessage(
328328+ chatId,
329329+ "Announcement creation canceled. You can start over with /announce"
330330+ );
331331+332332+ // Clean up
333333+ delete this.pendingAnnouncements[userId];
334334+ delete this.confirmAnnouncement[userId];
335335+ }
336336+ });
337337+ }
338338+}
339339+340340+module.exports = AnnounceCommand;
+89
bot/telegrambot/commands/announcements.js
···11+/**
22+ * /announcements command handler - manage existing announcements
33+ */
44+class AnnouncementsCommand {
55+ constructor(bot, authHelper, announcements) {
66+ this.bot = bot;
77+ this.authHelper = authHelper;
88+ this.announcements = announcements;
99+ }
1010+1111+ register() {
1212+ this.bot.onText(/^\/announcements(?!\S)/, async (msg) => {
1313+ const chatId = msg.chat.id;
1414+1515+ if (!this.authHelper.isAuthorized(msg.from.id)) {
1616+ this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
1717+ return;
1818+ }
1919+2020+ const announcements = this.announcements.getAnnouncements();
2121+2222+ if (announcements.length === 0) {
2323+ this.bot.sendMessage(
2424+ chatId,
2525+ 'No announcements configured. Use /announce to create a new announcement.'
2626+ );
2727+ return;
2828+ }
2929+3030+ // Format the list of announcements with inline buttons
3131+ let message = '📣 *Text Announcements*\n\n';
3232+3333+ const inlineKeyboard = [];
3434+3535+ for (let i = 0; i < announcements.length; i++) {
3636+ const announcement = announcements[i];
3737+3838+ // Add announcement details to message
3939+ message += `*${i+1}. ${announcement.name}*\n`;
4040+ message += `Schedule: \`${announcement.cronSchedule}\`\n`;
4141+ message += `Last run: ${announcement.lastRun ? new Date(announcement.lastRun).toLocaleString() : 'Never'}\n`;
4242+4343+ // Show button info if present
4444+ if (announcement.button && announcement.button.text && announcement.button.url) {
4545+ message += `Button: "${announcement.button.text}" → ${announcement.button.url}\n`;
4646+ }
4747+4848+ // Format the message preview, replacing line breaks with special character
4949+ const previewMessage = announcement.message
5050+ .replace(/\n/g, '↵') // Replace line breaks with a visible symbol
5151+ .substring(0, 50);
5252+ message += `Message: "${previewMessage}${announcement.message.length > 50 ? '...' : ''}"\n\n`;
5353+5454+ // Add buttons for this announcement
5555+ inlineKeyboard.push([
5656+ {
5757+ text: `▶️ Run #${i+1}`,
5858+ callback_data: `run_announcement_${announcement.id}`
5959+ },
6060+ {
6161+ text: `✏️ Edit #${i+1}`,
6262+ callback_data: `edit_announcement_${announcement.id}`
6363+ },
6464+ {
6565+ text: `❌ Delete #${i+1}`,
6666+ callback_data: `delete_announcement_${announcement.id}`
6767+ }
6868+ ]);
6969+ }
7070+7171+ // Add a button to create a new announcement
7272+ inlineKeyboard.push([
7373+ {
7474+ text: '➕ Add New Announcement',
7575+ callback_data: 'new_announcement'
7676+ }
7777+ ]);
7878+7979+ await this.bot.sendMessage(chatId, message, {
8080+ parse_mode: 'Markdown',
8181+ reply_markup: {
8282+ inline_keyboard: inlineKeyboard
8383+ }
8484+ });
8585+ });
8686+ }
8787+}
8888+8989+module.exports = AnnouncementsCommand;
···11+const queueManager = require('../../../queue/queueManager');
22+33+/**
44+ * /clear command handler
55+ */
66+class ClearCommand {
77+ constructor(bot, authHelper) {
88+ this.bot = bot;
99+ this.authHelper = authHelper;
1010+ }
1111+1212+ register() {
1313+ this.bot.onText(/\/clear/, async (msg) => {
1414+ const chatId = msg.chat.id;
1515+1616+ if (!this.authHelper.isAuthorized(msg.from.id)) {
1717+ this.bot.sendMessage(chatId, 'You are not authorized to use this command.');
1818+ return;
1919+ }
2020+2121+ const queueLength = await queueManager.getQueueLength();
2222+2323+ if (queueLength === 0) {
2424+ this.bot.sendMessage(chatId, 'Queue is already empty.');
2525+ return;
2626+ }
2727+2828+ // Clear the queue by removing all items
2929+ for (let i = 0; i < queueLength; i++) {
3030+ await queueManager.removeFromQueue(0);
3131+ }
3232+3333+ this.bot.sendMessage(chatId, `Queue cleared (${queueLength} items removed).`);
3434+ });
3535+ }
3636+}
3737+3838+module.exports = ClearCommand;
+34
bot/telegrambot/commands/help.js
···11+/**
22+ * /help command handler
33+ */
44+class HelpCommand {
55+ constructor(bot) {
66+ this.bot = bot;
77+ }
88+99+ register() {
1010+ this.bot.onText(/\/help/, (msg) => {
1111+ const chatId = msg.chat.id;
1212+ const helpText = `
1313+Stagehand Bot Commands:
1414+/queue - Show current queue status with interactive management
1515+/status - Show detailed queue status and alert information
1616+/send - Post the next image in the queue
1717+/schedule [cron] - Set posting schedule (cron syntax, use https://crontab.guru/ for help)
1818+/setcount [number] - Set number of images per scheduled post (default: 1)
1919+/clear - Clear the queue
2020+/cleancache - Clean expired items from media cache
2121+/announce - Create a new announcement
2222+/announcements - Manage existing announcements
2323+/shuffle - Toggle shuffle mode (shuffles queue after each post)
2424+/update - Update bot from GitHub repository (owner only)
2525+2626+Send any link to a supported site to add it to the queue.
2727+Supported sites: e621, FurAffinity, SoFurry, Weasyl, Bluesky
2828+ `;
2929+ this.bot.sendMessage(chatId, helpText);
3030+ });
3131+ }
3232+}
3333+3434+module.exports = HelpCommand;
···11+/**
22+ * Announcement Creation Helper
33+ * Handles the step-by-step announcement creation workflow
44+ * Used by both the /announce command and the "Add New Announcement" button
55+ */
66+77+class AnnouncementCreationHelper {
88+ constructor(bot, announcements) {
99+ this.bot = bot;
1010+ this.announcements = announcements;
1111+ this.pendingAnnouncements = {};
1212+ this.confirmAnnouncement = {};
1313+ }
1414+1515+ /**
1616+ * Start the announcement creation workflow
1717+ * @param {number} chatId - The chat ID
1818+ * @param {number} userId - The user ID
1919+ * @param {string} source - Source of the workflow ('command' or 'button')
2020+ */
2121+ async startCreationWorkflow(chatId, userId, source = 'command') {
2222+ // Initialize the pending announcements tracker for this user
2323+ this.pendingAnnouncements[userId] = {};
2424+2525+ // Display introduction message with formatting options
2626+ await this.bot.sendMessage(
2727+ chatId,
2828+ '📣 *Create New Announcement*\n\n' +
2929+ 'I\'ll guide you through creating an announcement step by step:\n' +
3030+ '1️⃣ Name your announcement\n' +
3131+ '2️⃣ Write the message content\n' +
3232+ '3️⃣ Set a schedule\n' +
3333+ '4️⃣ Add an optional button (if desired)\n\n' +
3434+ 'You can use these formatting options in your message:\n' +
3535+ '- *text* for italic\n' +
3636+ '- **text** for bold\n' +
3737+ '- __text__ for underlined\n' +
3838+ '- ~~text~~ for strikethrough\n\n' +
3939+ 'Let\'s start! First, what would you like to name this announcement?',
4040+ {
4141+ parse_mode: 'Markdown',
4242+ reply_markup: { force_reply: true }
4343+ }
4444+ ).then(namePrompt => {
4545+ this.handleNameStep(chatId, userId, namePrompt.message_id);
4646+ });
4747+ }
4848+4949+ /**
5050+ * Handle the name input step
5151+ */
5252+ handleNameStep(chatId, userId, messageId) {
5353+ this.bot.onReplyToMessage(chatId, messageId, async (nameMsg) => {
5454+ const announcementName = nameMsg.text === 'skip' ? '' : nameMsg.text;
5555+ this.pendingAnnouncements[userId].name = announcementName;
5656+5757+ // Now ask for the announcement message text
5858+ this.bot.sendMessage(
5959+ chatId,
6060+ 'Great! Now enter the announcement message content.\n\n' +
6161+ 'Your message can contain multiple lines and formatting:\n' +
6262+ '- *text* for italic\n' +
6363+ '- **text** for bold\n' +
6464+ '- __text__ for underlined\n' +
6565+ '- ~~text~~ for strikethrough\n\n' +
6666+ 'Type your message now:',
6767+ {
6868+ parse_mode: 'Markdown',
6969+ reply_markup: { force_reply: true }
7070+ }
7171+ ).then(messagePrompt => {
7272+ this.handleMessageStep(chatId, userId, messagePrompt.message_id);
7373+ });
7474+ });
7575+ }
7676+7777+ /**
7878+ * Handle the message content input step
7979+ */
8080+ handleMessageStep(chatId, userId, messageId) {
8181+ this.bot.onReplyToMessage(chatId, messageId, async (messageTextMsg) => {
8282+ this.pendingAnnouncements[userId].message = messageTextMsg.text;
8383+8484+ try {
8585+ // Show a preview of the formatted message
8686+ const previewText = this.announcements.formatMessageText(this.pendingAnnouncements[userId].message);
8787+8888+ // Send a preview message to show how it will look
8989+ await this.bot.sendMessage(
9090+ chatId,
9191+ "Here's a preview of your announcement with formatting:",
9292+ { parse_mode: 'Markdown' }
9393+ );
9494+9595+ // Send the actual preview
9696+ await this.bot.sendMessage(
9797+ chatId,
9898+ previewText,
9999+ { parse_mode: 'HTML' }
100100+ );
101101+ } catch (error) {
102102+ console.error("Error showing announcement preview:", error);
103103+ await this.bot.sendMessage(
104104+ chatId,
105105+ "Preview couldn't be displayed, but your message has been saved."
106106+ );
107107+ }
108108+109109+ // Ask for schedule
110110+ this.bot.sendMessage(
111111+ chatId,
112112+ 'Perfect! Now let\'s set up the schedule.\n\n' +
113113+ 'Please enter a cron schedule expression. Examples:\n' +
114114+ '- `0 9 * * *` = Every day at 9:00 AM\n' +
115115+ '- `0 18 * * 5` = Every Friday at 6:00 PM\n' +
116116+ '- `0 12 1 * *` = First day of each month at noon\n' +
117117+ '- `*/30 * * * *` = Every 30 minutes\n\n' +
118118+ 'For more options, visit https://crontab.guru/',
119119+ {
120120+ parse_mode: 'Markdown',
121121+ reply_markup: { force_reply: true }
122122+ }
123123+ ).then(schedulePrompt => {
124124+ this.handleScheduleStep(chatId, userId, schedulePrompt.message_id);
125125+ });
126126+ });
127127+ }
128128+129129+ /**
130130+ * Handle the schedule input step
131131+ */
132132+ handleScheduleStep(chatId, userId, messageId) {
133133+ this.bot.onReplyToMessage(chatId, messageId, async (scheduleMsg) => {
134134+ const cronSchedule = scheduleMsg.text;
135135+136136+ // Validate the cron schedule
137137+ if (!this.announcements.isValidCronExpression(cronSchedule)) {
138138+ await this.bot.sendMessage(
139139+ chatId,
140140+ '⚠️ That doesn\'t appear to be a valid cron schedule. Let me help you with that.\n\n' +
141141+ 'Please enter a valid cron schedule. Examples:\n' +
142142+ '- `0 9 * * *` = Every day at 9:00 AM\n' +
143143+ '- `0 18 * * 5` = Every Friday at 6:00 PM\n' +
144144+ '- `0 12 1 * *` = First day of each month at noon',
145145+ {
146146+ parse_mode: 'Markdown',
147147+ reply_markup: { force_reply: true }
148148+ }
149149+ ).then((newSchedulePrompt) => {
150150+ // Handle the new schedule response
151151+ this.bot.onReplyToMessage(chatId, newSchedulePrompt.message_id, (newScheduleMsg) => {
152152+ // Replace the schedule with the new one
153153+ const validCronSchedule = newScheduleMsg.text;
154154+155155+ if (!this.announcements.isValidCronExpression(validCronSchedule)) {
156156+ this.bot.sendMessage(
157157+ chatId,
158158+ '⚠️ Still not a valid cron schedule. Using "0 12 * * *" (daily at noon) as a default. You can edit this later.'
159159+ );
160160+ this.pendingAnnouncements[userId].cronSchedule = "0 12 * * *";
161161+162162+ // Continue to button step
163163+ this.askAboutButton(chatId, userId);
164164+ } else {
165165+ this.pendingAnnouncements[userId].cronSchedule = validCronSchedule;
166166+167167+ // Continue to button step
168168+ this.askAboutButton(chatId, userId);
169169+ }
170170+ });
171171+ });
172172+ return;
173173+ }
174174+175175+ // Store the schedule
176176+ this.pendingAnnouncements[userId].cronSchedule = cronSchedule;
177177+178178+ // Ask if they want to add a button
179179+ this.askAboutButton(chatId, userId);
180180+ });
181181+ }
182182+183183+ /**
184184+ * Ask if user wants to add a button
185185+ */
186186+ askAboutButton(chatId, userId) {
187187+ this.bot.sendMessage(
188188+ chatId,
189189+ 'Would you like to add a button with a link to this announcement?',
190190+ {
191191+ reply_markup: {
192192+ inline_keyboard: [
193193+ [
194194+ { text: 'Yes', callback_data: 'add_button' },
195195+ { text: 'No', callback_data: 'skip_button' }
196196+ ]
197197+ ]
198198+ }
199199+ }
200200+ ).then(buttonPrompt => {
201201+ // Callback handler for yes/no button selection
202202+ this.bot.once('callback_query', async (query) => {
203203+ await this.bot.answerCallbackQuery(query.id);
204204+205205+ // Delete the yes/no prompt
206206+ await this.bot.deleteMessage(chatId, buttonPrompt.message_id);
207207+208208+ if (query.data === 'add_button') {
209209+ this.handleButtonCreation(chatId, userId);
210210+ } else {
211211+ // User doesn't want to add a button
212212+ await this.showAnnouncementConfirmation(
213213+ chatId,
214214+ userId,
215215+ this.pendingAnnouncements[userId].name,
216216+ this.pendingAnnouncements[userId].message,
217217+ this.pendingAnnouncements[userId].cronSchedule
218218+ );
219219+ }
220220+ });
221221+ });
222222+ }
223223+224224+ /**
225225+ * Handle button creation workflow
226226+ */
227227+ handleButtonCreation(chatId, userId) {
228228+ this.bot.sendMessage(
229229+ chatId,
230230+ 'Great! Let\'s create a button for your announcement.\n\n' +
231231+ 'First, what text should appear on the button?',
232232+ {
233233+ reply_markup: { force_reply: true }
234234+ }
235235+ ).then(textPrompt => {
236236+ this.bot.onReplyToMessage(chatId, textPrompt.message_id, (textMsg) => {
237237+ const buttonText = textMsg.text;
238238+239239+ this.bot.sendMessage(
240240+ chatId,
241241+ 'Perfect! Now enter the URL that the button should link to:\n\n' +
242242+ '(Must start with http:// or https://)',
243243+ {
244244+ reply_markup: { force_reply: true }
245245+ }
246246+ ).then(urlPrompt => {
247247+ this.bot.onReplyToMessage(chatId, urlPrompt.message_id, async (urlMsg) => {
248248+ const buttonUrl = urlMsg.text;
249249+250250+ // Validate URL format
251251+ if (!buttonUrl.startsWith('http://') && !buttonUrl.startsWith('https://')) {
252252+ await this.bot.sendMessage(
253253+ chatId,
254254+ '⚠️ Please enter a valid URL starting with http:// or https://'
255255+ );
256256+ return;
257257+ }
258258+259259+ const button = { text: buttonText, url: buttonUrl };
260260+261261+ // Show final confirmation with button
262262+ await this.showAnnouncementConfirmation(
263263+ chatId,
264264+ userId,
265265+ this.pendingAnnouncements[userId].name,
266266+ this.pendingAnnouncements[userId].message,
267267+ this.pendingAnnouncements[userId].cronSchedule,
268268+ button
269269+ );
270270+ });
271271+ });
272272+ });
273273+ });
274274+ }
275275+276276+ /**
277277+ * Show the final confirmation screen
278278+ */
279279+ async showAnnouncementConfirmation(chatId, userId, name, message, cronSchedule, button = null) {
280280+ // Store all the data for the confirmation callback
281281+ if (!this.confirmAnnouncement) {
282282+ this.confirmAnnouncement = {};
283283+ }
284284+ this.confirmAnnouncement[userId] = {
285285+ name,
286286+ message,
287287+ cronSchedule,
288288+ button
289289+ };
290290+291291+ // Create confirmation message with all details
292292+ let confirmationMessage = "📣 *Announcement Preview*\n\n";
293293+ confirmationMessage += `*Name*: ${name || "(Auto-generated)"}\n`;
294294+ confirmationMessage += `*Schedule*: \`${cronSchedule}\`\n`;
295295+296296+ if (button) {
297297+ confirmationMessage += `*Button*: "${button.text}" → ${button.url}\n`;
298298+ } else {
299299+ confirmationMessage += "*Button*: None\n";
300300+ }
301301+302302+ confirmationMessage += "\n*Message Preview*:\n------------------\n";
303303+304304+ // Send confirmation message
305305+ await this.bot.sendMessage(
306306+ chatId,
307307+ confirmationMessage,
308308+ { parse_mode: 'Markdown' }
309309+ );
310310+311311+ // Send formatted message preview
312312+ const formattedMessage = this.announcements.formatMessageText(message);
313313+ await this.bot.sendMessage(
314314+ chatId,
315315+ formattedMessage,
316316+ { parse_mode: 'HTML' }
317317+ );
318318+319319+ // Ask for confirmation
320320+ await this.bot.sendMessage(
321321+ chatId,
322322+ "Does everything look correct? Ready to create this announcement?",
323323+ {
324324+ reply_markup: {
325325+ inline_keyboard: [
326326+ [
327327+ { text: '✅ Create Announcement', callback_data: 'confirm_announcement' },
328328+ { text: '❌ Cancel', callback_data: 'cancel_announcement' }
329329+ ]
330330+ ]
331331+ }
332332+ }
333333+ );
334334+335335+ // Set up a one-time listener for the confirmation response
336336+ this.bot.once('callback_query', async (query) => {
337337+ if (query.from.id !== userId) return; // Make sure it's the same user
338338+339339+ await this.bot.answerCallbackQuery(query.id);
340340+341341+ if (query.data === 'confirm_announcement') {
342342+ try {
343343+ // Create the announcement
344344+ const announcement = await this.announcements.addAnnouncement(
345345+ message,
346346+ cronSchedule,
347347+ name,
348348+ button
349349+ );
350350+351351+ // Send success message
352352+ let successMessage = `✅ Announcement "${announcement.name}" created!\n\n`;
353353+ successMessage += `Scheduled for: ${announcement.cronSchedule}\n\n`;
354354+355355+ if (button) {
356356+ successMessage += `Button: "${button.text}" → ${button.url}\n\n`;
357357+ }
358358+359359+ successMessage += "You can manage all announcements with /announcements";
360360+361361+ await this.bot.sendMessage(chatId, successMessage);
362362+363363+ // Clean up
364364+ delete this.pendingAnnouncements[userId];
365365+ delete this.confirmAnnouncement[userId];
366366+ } catch (error) {
367367+ this.bot.sendMessage(
368368+ chatId,
369369+ `Error creating announcement: ${error.message}\n\nPlease try again.`
370370+ );
371371+ }
372372+ } else {
373373+ // User canceled
374374+ await this.bot.sendMessage(
375375+ chatId,
376376+ "Announcement creation canceled. You can start over with /announce"
377377+ );
378378+379379+ // Clean up
380380+ delete this.pendingAnnouncements[userId];
381381+ delete this.confirmAnnouncement[userId];
382382+ }
383383+ });
384384+ }
385385+386386+ /**
387387+ * Clean up any pending data for a user
388388+ */
389389+ cleanup(userId) {
390390+ delete this.pendingAnnouncements[userId];
391391+ delete this.confirmAnnouncement[userId];
392392+ }
393393+}
394394+395395+module.exports = AnnouncementCreationHelper;
+26
bot/telegrambot/helpers/authHelper.js
···11+const config = require('../../../config');
22+33+/**
44+ * Authorization helper for checking user permissions
55+ */
66+class AuthHelper {
77+ isAuthorized(userId) {
88+ // If no authorized users are specified, anyone can use the bot
99+ if (config.authorizedUsers.length === 0) {
1010+ return true;
1111+ }
1212+1313+ return config.authorizedUsers.includes(userId.toString());
1414+ }
1515+1616+ /**
1717+ * Check if the user is the owner of the bot
1818+ * @param {number} userId - The Telegram user ID to check
1919+ * @returns {boolean} - Whether the user is the owner
2020+ */
2121+ isOwner(userId) {
2222+ return config.ownerId && userId.toString() === config.ownerId.toString();
2323+ }
2424+}
2525+2626+module.exports = AuthHelper;
+733
bot/telegrambot/helpers/callbackHandler.js
···11+const queueManager = require('../../../queue/queueManager');
22+const AnnouncementCreationHelper = require('./announcementCreationHelper');
33+44+/**
55+ * Callback query handler for interactive buttons
66+ */
77+class CallbackHandler {
88+ constructor(bot, authHelper, queueHelper, announcements, queueMonitor = null) {
99+ this.bot = bot;
1010+ this.authHelper = authHelper;
1111+ this.queueHelper = queueHelper;
1212+ this.announcements = announcements;
1313+ this.queueMonitor = queueMonitor;
1414+ this.editingAnnouncementButton = {};
1515+ this.creationHelper = new AnnouncementCreationHelper(bot, announcements);
1616+ }
1717+1818+ register() {
1919+ this.bot.on('callback_query', async (query) => {
2020+ try {
2121+ const chatId = query.message.chat.id;
2222+ if (!this.authHelper.isAuthorized(query.from.id)) {
2323+ await this.bot.answerCallbackQuery(query.id, { text: 'You are not authorized to use these controls.' });
2424+ return;
2525+ }
2626+2727+ const data = query.data.split('_');
2828+ const action = data[0];
2929+3030+ switch (action) {
3131+ case 'page':
3232+ await this.handlePageNavigation(query, data, chatId);
3333+ break;
3434+ case 'remove':
3535+ await this.handleItemRemoval(query, data, chatId);
3636+ break;
3737+ case 'top':
3838+ await this.handleMoveToTop(query, data, chatId);
3939+ break;
4040+ case 'preview':
4141+ await this.handlePreview(query, data, chatId);
4242+ break;
4343+ case 'run':
4444+ await this.handleRunAnnouncement(query, data, chatId);
4545+ break;
4646+ case 'delete':
4747+ await this.handleDeleteAnnouncement(query, data, chatId);
4848+ break;
4949+ case 'confirm':
5050+ await this.handleConfirmDelete(query, data, chatId);
5151+ break;
5252+ case 'cancel':
5353+ await this.handleCancel(query, data, chatId);
5454+ break;
5555+ case 'edit':
5656+ // Handle different edit patterns
5757+ if (data[1] === 'button') {
5858+ await this.handleEditButtonAction(query, data, chatId);
5959+ } else {
6060+ await this.handleEditAnnouncement(query, data, chatId);
6161+ }
6262+ break;
6363+ case 'new':
6464+ await this.handleNewAnnouncement(query, data, chatId);
6565+ break;
6666+ case 'refresh':
6767+ await this.handleRefreshStatus(query, data, chatId);
6868+ break;
6969+ case 'view':
7070+ await this.handleViewQueue(query, data, chatId);
7171+ break;
7272+ case 'test':
7373+ await this.handleTestAlerts(query, data, chatId);
7474+ break;
7575+ case 'reset':
7676+ await this.handleResetAlerts(query, data, chatId);
7777+ break;
7878+ case 'add':
7979+ // Handle add_button callback for announcement workflow
8080+ if (query.data === 'add_button') {
8181+ // This will be handled by the AnnouncementCreationHelper's listener
8282+ return;
8383+ }
8484+ break;
8585+ case 'skip':
8686+ // Handle skip_button callback for announcement workflow
8787+ if (query.data === 'skip_button') {
8888+ // This will be handled by the AnnouncementCreationHelper's listener
8989+ return;
9090+ }
9191+ break;
9292+ default:
9393+ // Handle workflow-specific callbacks that don't follow the standard pattern
9494+ if (query.data === 'confirm_announcement' || query.data === 'cancel_announcement') {
9595+ // These will be handled by the AnnouncementCreationHelper's listener
9696+ return;
9797+ }
9898+ break;
9999+ }
100100+ } catch (error) {
101101+ console.error('Error handling callback query:', error);
102102+ await this.bot.answerCallbackQuery(query.id, { text: 'An error occurred' });
103103+ }
104104+ });
105105+ }
106106+107107+ async handlePageNavigation(query, data, chatId) {
108108+ const page = parseInt(data[1]);
109109+ await this.bot.deleteMessage(chatId, query.message.message_id);
110110+ await this.queueHelper.displayQueuePage(chatId, page, 5);
111111+ await this.bot.answerCallbackQuery(query.id, { text: `Showing page ${page}` });
112112+ }
113113+114114+ async handleItemRemoval(query, data, chatId) {
115115+ const index = parseInt(data[1]);
116116+ const removed = await queueManager.removeFromQueue(index);
117117+ if (removed) {
118118+ const itemType = removed.isVideo ? 'Video' : 'Image';
119119+ await this.bot.answerCallbackQuery(query.id, { text: `Removed ${itemType}: ${removed.title}` });
120120+121121+ // Update the queue display
122122+ await this.bot.deleteMessage(chatId, query.message.message_id);
123123+ const page = parseInt(data[2]) || 1;
124124+ await this.queueHelper.displayQueuePage(chatId, page, 5);
125125+ } else {
126126+ await this.bot.answerCallbackQuery(query.id, { text: 'Failed to remove item' });
127127+ }
128128+ }
129129+130130+ async handleMoveToTop(query, data, chatId) {
131131+ const index = parseInt(data[1]);
132132+ const item = await this.queueHelper.handleMoveToTop(index);
133133+134134+ if (item) {
135135+ await this.bot.answerCallbackQuery(query.id, { text: `Moved "${item.title}" to top of queue` });
136136+137137+ // Update the queue display
138138+ await this.bot.deleteMessage(chatId, query.message.message_id);
139139+ const page = parseInt(data[2]) || 1;
140140+ await this.queueHelper.displayQueuePage(chatId, page, 5);
141141+ } else {
142142+ await this.bot.answerCallbackQuery(query.id, { text: 'Failed to move item' });
143143+ }
144144+ }
145145+146146+ async handlePreview(query, data, chatId) {
147147+ const index = parseInt(data[1]);
148148+ await this.bot.answerCallbackQuery(query.id, { text: 'Sending preview...' });
149149+ await this.queueHelper.handlePreview(chatId, index);
150150+ }
151151+152152+ async handleRunAnnouncement(query, data, chatId) {
153153+ if (data[1] === 'announcement') {
154154+ let announcementId = data[2];
155155+156156+ // Handle URL decoding in case Telegram encodes the callback data
157157+ try {
158158+ announcementId = decodeURIComponent(announcementId);
159159+ } catch (e) {
160160+ // If decoding fails, use the original value
161161+ }
162162+163163+ await this.bot.answerCallbackQuery(query.id, { text: 'Sending announcement...' });
164164+165165+ try {
166166+ const result = await this.announcements.sendAnnouncementNow(announcementId);
167167+ if (result) {
168168+ await this.bot.sendMessage(chatId, `✅ Announcement sent successfully!`);
169169+ } else {
170170+ await this.bot.sendMessage(chatId, `❌ Failed to send announcement.`);
171171+ }
172172+ } catch (error) {
173173+ await this.bot.sendMessage(chatId, `❌ Error: ${error.message}`);
174174+ }
175175+176176+ // Refresh announcements list
177177+ await this.bot.deleteMessage(chatId, query.message.message_id);
178178+ // Note: In the full implementation, you'd need to trigger the announcements command here
179179+ }
180180+ }
181181+182182+ async handleDeleteAnnouncement(query, data, chatId) {
183183+ if (data[1] === 'announcement') {
184184+ let announcementId = data[2];
185185+186186+ // Handle URL decoding in case Telegram encodes the callback data
187187+ try {
188188+ announcementId = decodeURIComponent(announcementId);
189189+ } catch (e) {
190190+ // If decoding fails, use the original value
191191+ }
192192+193193+ // Get the announcement to show its name
194194+ const announcement = this.announcements.getAnnouncementById(announcementId);
195195+ if (!announcement) {
196196+ await this.bot.answerCallbackQuery(query.id, { text: 'Announcement not found.' });
197197+ return;
198198+ }
199199+200200+ // Show confirmation dialog
201201+ await this.bot.answerCallbackQuery(query.id);
202202+203203+ await this.bot.sendMessage(
204204+ chatId,
205205+ `Are you sure you want to delete the announcement "${announcement.name}"?`,
206206+ {
207207+ reply_markup: {
208208+ inline_keyboard: [
209209+ [
210210+ { text: '✅ Yes, delete it', callback_data: `confirm_delete_announcement_${announcementId}` },
211211+ { text: '❌ No, cancel', callback_data: 'cancel_delete_announcement' }
212212+ ]
213213+ ]
214214+ }
215215+ }
216216+ );
217217+ }
218218+ }
219219+220220+ async handleConfirmDelete(query, data, chatId) {
221221+ if (data[1] === 'delete' && data[2] === 'announcement') {
222222+ let announcementId = data[3];
223223+224224+ // Handle URL decoding in case Telegram encodes the callback data
225225+ try {
226226+ announcementId = decodeURIComponent(announcementId);
227227+ } catch (e) {
228228+ // If decoding fails, use the original value
229229+ }
230230+231231+ try {
232232+ const result = await this.announcements.removeAnnouncement(announcementId);
233233+ if (result) {
234234+ await this.bot.answerCallbackQuery(query.id, { text: 'Announcement deleted successfully.' });
235235+ } else {
236236+ await this.bot.answerCallbackQuery(query.id, { text: 'Failed to delete announcement.' });
237237+ }
238238+239239+ // Delete confirmation message
240240+ await this.bot.deleteMessage(chatId, query.message.message_id);
241241+242242+ // Note: In the full implementation, you'd need to refresh the announcements list here
243243+ } catch (error) {
244244+ await this.bot.answerCallbackQuery(query.id, { text: `Error: ${error.message}` });
245245+ }
246246+ }
247247+ }
248248+249249+ async handleCancel(query, data, chatId) {
250250+ if (data[1] === 'delete' && data[2] === 'announcement') {
251251+ await this.bot.answerCallbackQuery(query.id, { text: 'Delete cancelled.' });
252252+ await this.bot.deleteMessage(chatId, query.message.message_id);
253253+ } else if (data[1] === 'edit' && data[2] === 'announcement') {
254254+ await this.bot.answerCallbackQuery(query.id, { text: 'Edit cancelled.' });
255255+ await this.bot.deleteMessage(chatId, query.message.message_id);
256256+ // Note: In the full implementation, you'd need to refresh the announcements list here
257257+ }
258258+ }
259259+260260+ async handleEditAnnouncement(query, data, chatId) {
261261+ if (data[1] === 'announcement') {
262262+ let announcementId;
263263+ let editType = null;
264264+265265+ // Handle different edit callback patterns:
266266+ // 1. edit_announcement_${id} - Show edit menu
267267+ // 2. edit_announcement_message_${id} - Edit message
268268+ // 3. edit_announcement_schedule_${id} - Edit schedule
269269+ // 4. edit_announcement_name_${id} - Edit name
270270+ // 5. edit_announcement_button_${id} - Edit button
271271+272272+ if (data.length === 3) {
273273+ // Pattern: edit_announcement_${id}
274274+ announcementId = data[2];
275275+ } else if (data.length === 4) {
276276+ // Pattern: edit_announcement_${type}_${id}
277277+ editType = data[2];
278278+ announcementId = data[3];
279279+ } else {
280280+ await this.bot.answerCallbackQuery(query.id, { text: 'Invalid edit command.' });
281281+ return;
282282+ }
283283+284284+ // Handle URL decoding in case Telegram encodes the callback data
285285+ try {
286286+ announcementId = decodeURIComponent(announcementId);
287287+ } catch (e) {
288288+ // If decoding fails, use the original value
289289+ }
290290+291291+ // Ensure announcements are initialized
292292+ if (!this.announcements || !this.announcements.initialized) {
293293+ await this.bot.answerCallbackQuery(query.id, { text: 'Announcements system not ready. Please try again.' });
294294+ return;
295295+ }
296296+297297+ const announcement = this.announcements.getAnnouncementById(announcementId);
298298+299299+ if (!announcement) {
300300+ await this.bot.answerCallbackQuery(query.id, { text: 'Announcement not found.' });
301301+ return;
302302+ }
303303+304304+ await this.bot.answerCallbackQuery(query.id);
305305+306306+ if (!editType) {
307307+ // Show edit options menu
308308+ await this.showEditOptionsMenu(chatId, announcement);
309309+ } else {
310310+ // Handle specific edit action
311311+ await this.handleSpecificEdit(chatId, announcement, editType);
312312+ }
313313+ }
314314+ }
315315+316316+ async showEditOptionsMenu(chatId, announcement) {
317317+ // Show edit options
318318+ await this.bot.sendMessage(
319319+ chatId,
320320+ `Editing announcement: *${announcement.name}*\n\nWhat would you like to edit?`,
321321+ {
322322+ parse_mode: 'Markdown',
323323+ reply_markup: {
324324+ inline_keyboard: [
325325+ [
326326+ {
327327+ text: '📝 Edit Message',
328328+ callback_data: `edit_announcement_message_${announcement.id}`
329329+ }
330330+ ],
331331+ [
332332+ {
333333+ text: '⏰ Edit Schedule',
334334+ callback_data: `edit_announcement_schedule_${announcement.id}`
335335+ }
336336+ ],
337337+ [
338338+ {
339339+ text: '🏷️ Edit Name',
340340+ callback_data: `edit_announcement_name_${announcement.id}`
341341+ }
342342+ ],
343343+ [
344344+ {
345345+ text: '🔗 Edit Button',
346346+ callback_data: `edit_announcement_button_${announcement.id}`
347347+ }
348348+ ],
349349+ [
350350+ {
351351+ text: '❌ Cancel',
352352+ callback_data: 'cancel_edit_announcement'
353353+ }
354354+ ]
355355+ ]
356356+ }
357357+ }
358358+ );
359359+ }
360360+361361+ async handleSpecificEdit(chatId, announcement, editType) {
362362+ // Store the editing context for this user/chat
363363+ const userId = chatId; // Using chatId as userId since this is a private chat
364364+ this.editingAnnouncementButton[userId] = {
365365+ announcementId: announcement.id,
366366+ editType: editType
367367+ };
368368+369369+ switch (editType) {
370370+ case 'message':
371371+ await this.bot.sendMessage(
372372+ chatId,
373373+ `📝 *Editing message for "${announcement.name}"*\n\n` +
374374+ `Current message:\n${announcement.message}\n\n` +
375375+ `Please send the new message content. You can use formatting:\n` +
376376+ `- *text* for italic\n` +
377377+ `- **text** for bold\n` +
378378+ `- __text__ for underlined\n` +
379379+ `- ~~text~~ for strikethrough`,
380380+ {
381381+ parse_mode: 'Markdown',
382382+ reply_markup: { force_reply: true }
383383+ }
384384+ ).then(prompt => {
385385+ this.handleEditMessageInput(chatId, userId, prompt.message_id, announcement);
386386+ });
387387+ break;
388388+ case 'schedule':
389389+ await this.bot.sendMessage(
390390+ chatId,
391391+ `⏰ *Editing schedule for "${announcement.name}"*\n\n` +
392392+ `Current schedule: \`${announcement.cronSchedule}\`\n\n` +
393393+ `Please enter a new cron schedule expression. Examples:\n` +
394394+ `- \`0 9 * * *\` = Every day at 9:00 AM\n` +
395395+ `- \`0 18 * * 5\` = Every Friday at 6:00 PM\n` +
396396+ `- \`0 12 1 * *\` = First day of each month at noon\n\n` +
397397+ `For more options, visit https://crontab.guru/`,
398398+ {
399399+ parse_mode: 'Markdown',
400400+ reply_markup: { force_reply: true }
401401+ }
402402+ ).then(prompt => {
403403+ this.handleEditScheduleInput(chatId, userId, prompt.message_id, announcement);
404404+ });
405405+ break;
406406+ case 'name':
407407+ await this.bot.sendMessage(
408408+ chatId,
409409+ `🏷️ *Editing name for "${announcement.name}"*\n\n` +
410410+ `Current name: ${announcement.name}\n\n` +
411411+ `Please enter the new name for this announcement:`,
412412+ {
413413+ parse_mode: 'Markdown',
414414+ reply_markup: { force_reply: true }
415415+ }
416416+ ).then(prompt => {
417417+ this.handleEditNameInput(chatId, userId, prompt.message_id, announcement);
418418+ });
419419+ break;
420420+ case 'button':
421421+ await this.bot.sendMessage(
422422+ chatId,
423423+ `🔗 *Editing button for "${announcement.name}"*\n\n` +
424424+ `Current button: ${announcement.button ? `"${announcement.button.text}" -> ${announcement.button.url}` : 'None'}\n\n` +
425425+ `Would you like to add/edit a button or remove the existing one?`,
426426+ {
427427+ parse_mode: 'Markdown',
428428+ reply_markup: {
429429+ inline_keyboard: [
430430+ [
431431+ { text: '✏️ Add/Edit Button', callback_data: `edit_button_add_${announcement.id}` },
432432+ { text: '🗑️ Remove Button', callback_data: `edit_button_remove_${announcement.id}` }
433433+ ],
434434+ [
435435+ { text: '❌ Cancel', callback_data: 'cancel_edit_announcement' }
436436+ ]
437437+ ]
438438+ }
439439+ }
440440+ );
441441+ break;
442442+ default:
443443+ await this.bot.sendMessage(chatId, 'Unknown edit type.');
444444+ }
445445+ }
446446+447447+ async handleNewAnnouncement(query, data, chatId) {
448448+ await this.bot.answerCallbackQuery(query.id);
449449+450450+ // Delete the announcements list message
451451+ await this.bot.deleteMessage(chatId, query.message.message_id);
452452+453453+ // Use the creation helper to start the workflow
454454+ await this.creationHelper.startCreationWorkflow(chatId, query.from.id, 'button');
455455+ }
456456+457457+ async handleRefreshStatus(query, data, chatId) {
458458+ if (data[1] === 'status') {
459459+ await this.bot.answerCallbackQuery(query.id, { text: 'Refreshing status...' });
460460+461461+ // Delete the old message and trigger a new status check
462462+ await this.bot.deleteMessage(chatId, query.message.message_id);
463463+464464+ // Send /status command programmatically
465465+ await this.bot.sendMessage(chatId, '/status');
466466+ }
467467+ }
468468+469469+ async handleViewQueue(query, data, chatId) {
470470+ if (data[1] === 'queue') {
471471+ await this.bot.answerCallbackQuery(query.id, { text: 'Opening queue view...' });
472472+473473+ // Display the queue using queue helper
474474+ await this.queueHelper.displayQueuePage(chatId, 1, 5);
475475+ }
476476+ }
477477+478478+ async handleTestAlerts(query, data, chatId) {
479479+ if (data[1] === 'alerts' && this.authHelper.isOwner(query.from.id)) {
480480+ await this.bot.answerCallbackQuery(query.id, { text: 'Forcing alert check...' });
481481+482482+ if (this.queueMonitor) {
483483+ await this.queueMonitor.forceCheck();
484484+ await this.bot.sendMessage(chatId, '🔔 Alert check completed. Check for any alert messages.');
485485+ } else {
486486+ await this.bot.sendMessage(chatId, '❌ Queue monitor not available.');
487487+ }
488488+ } else {
489489+ await this.bot.answerCallbackQuery(query.id, { text: 'Admin only feature.', show_alert: true });
490490+ }
491491+ }
492492+493493+ async handleResetAlerts(query, data, chatId) {
494494+ if (data[1] === 'alerts' && this.authHelper.isOwner(query.from.id)) {
495495+ await this.bot.answerCallbackQuery(query.id, { text: 'Resetting alert flags...' });
496496+497497+ if (this.queueMonitor) {
498498+ await this.queueMonitor.resetAlerts();
499499+ await this.bot.sendMessage(chatId, '🔄 Alert flags have been reset. New alerts will be sent when thresholds are reached again.');
500500+ } else {
501501+ await this.bot.sendMessage(chatId, '❌ Queue monitor not available.');
502502+ }
503503+ } else {
504504+ await this.bot.answerCallbackQuery(query.id, { text: 'Admin only feature.', show_alert: true });
505505+ }
506506+ }
507507+508508+ handleEditMessageInput(chatId, userId, messageId, announcement) {
509509+ this.bot.onReplyToMessage(chatId, messageId, async (reply) => {
510510+ try {
511511+ const newMessage = reply.text;
512512+513513+ // Update the announcement
514514+ await this.announcements.updateAnnouncement(announcement.id, { message: newMessage });
515515+516516+ // Show success message with preview
517517+ await this.bot.sendMessage(
518518+ chatId,
519519+ `✅ *Message updated successfully!*\n\nHere's a preview of your updated announcement:`,
520520+ { parse_mode: 'Markdown' }
521521+ );
522522+523523+ // Show formatted preview
524524+ const previewText = this.announcements.formatMessageText(newMessage);
525525+ await this.bot.sendMessage(chatId, previewText, { parse_mode: 'HTML' });
526526+527527+ // Clean up editing state
528528+ delete this.editingAnnouncementButton[userId];
529529+530530+ } catch (error) {
531531+ console.error('Error updating announcement message:', error);
532532+ await this.bot.sendMessage(chatId, `❌ Error updating message: ${error.message}`);
533533+ }
534534+ });
535535+ }
536536+537537+ handleEditScheduleInput(chatId, userId, messageId, announcement) {
538538+ this.bot.onReplyToMessage(chatId, messageId, async (reply) => {
539539+ try {
540540+ const newSchedule = reply.text;
541541+542542+ // Validate the cron schedule
543543+ if (!this.announcements.isValidCronExpression(newSchedule)) {
544544+ await this.bot.sendMessage(
545545+ chatId,
546546+ '⚠️ That doesn\'t appear to be a valid cron schedule. Please try again using the format shown in the examples.',
547547+ { parse_mode: 'Markdown' }
548548+ );
549549+ // Ask again
550550+ this.bot.sendMessage(
551551+ chatId,
552552+ 'Please enter a valid cron schedule. Examples:\n' +
553553+ '- `0 9 * * *` = Every day at 9:00 AM\n' +
554554+ '- `0 18 * * 5` = Every Friday at 6:00 PM\n' +
555555+ '- `0 12 1 * *` = First day of each month at noon',
556556+ {
557557+ parse_mode: 'Markdown',
558558+ reply_markup: { force_reply: true }
559559+ }
560560+ ).then(newPrompt => {
561561+ this.handleEditScheduleInput(chatId, userId, newPrompt.message_id, announcement);
562562+ });
563563+ return;
564564+ }
565565+566566+ // Update the announcement
567567+ await this.announcements.updateAnnouncement(announcement.id, { cronSchedule: newSchedule });
568568+569569+ await this.bot.sendMessage(
570570+ chatId,
571571+ `✅ *Schedule updated successfully!*\n\nNew schedule: \`${newSchedule}\``,
572572+ { parse_mode: 'Markdown' }
573573+ );
574574+575575+ // Clean up editing state
576576+ delete this.editingAnnouncementButton[userId];
577577+578578+ } catch (error) {
579579+ console.error('Error updating announcement schedule:', error);
580580+ await this.bot.sendMessage(chatId, `❌ Error updating schedule: ${error.message}`);
581581+ }
582582+ });
583583+ }
584584+585585+ handleEditNameInput(chatId, userId, messageId, announcement) {
586586+ this.bot.onReplyToMessage(chatId, messageId, async (reply) => {
587587+ try {
588588+ const newName = reply.text;
589589+590590+ // Update the announcement
591591+ await this.announcements.updateAnnouncement(announcement.id, { name: newName });
592592+593593+ await this.bot.sendMessage(
594594+ chatId,
595595+ `✅ *Name updated successfully!*\n\nNew name: ${newName}`,
596596+ { parse_mode: 'Markdown' }
597597+ );
598598+599599+ // Clean up editing state
600600+ delete this.editingAnnouncementButton[userId];
601601+602602+ } catch (error) {
603603+ console.error('Error updating announcement name:', error);
604604+ await this.bot.sendMessage(chatId, `❌ Error updating name: ${error.message}`);
605605+ }
606606+ });
607607+ }
608608+609609+ async handleEditButtonAction(query, data, chatId) {
610610+ // Handle button edit actions: edit_button_add_${id} or edit_button_remove_${id}
611611+ const action = data[2]; // 'add' or 'remove'
612612+ let announcementId = data[3];
613613+614614+ // Handle URL decoding in case Telegram encodes the callback data
615615+ try {
616616+ announcementId = decodeURIComponent(announcementId);
617617+ } catch (e) {
618618+ // If decoding fails, use the original value
619619+ }
620620+621621+ const announcement = this.announcements.getAnnouncementById(announcementId);
622622+ if (!announcement) {
623623+ await this.bot.answerCallbackQuery(query.id, { text: 'Announcement not found.' });
624624+ return;
625625+ }
626626+627627+ await this.bot.answerCallbackQuery(query.id);
628628+629629+ if (action === 'remove') {
630630+ // Remove the button
631631+ try {
632632+ await this.announcements.updateAnnouncement(announcement.id, { button: null });
633633+ await this.bot.sendMessage(
634634+ chatId,
635635+ `✅ *Button removed successfully!*\n\nThe announcement "${announcement.name}" no longer has a button.`,
636636+ { parse_mode: 'Markdown' }
637637+ );
638638+ } catch (error) {
639639+ console.error('Error removing button:', error);
640640+ await this.bot.sendMessage(chatId, `❌ Error removing button: ${error.message}`);
641641+ }
642642+ } else if (action === 'add') {
643643+ // Start the button creation process
644644+ const userId = chatId; // Using chatId as userId since this is a private chat
645645+ this.editingAnnouncementButton[userId] = {
646646+ announcementId: announcement.id,
647647+ editType: 'button',
648648+ step: 'text'
649649+ };
650650+651651+ await this.bot.sendMessage(
652652+ chatId,
653653+ `🔗 *Adding/Editing button for "${announcement.name}"*\n\n` +
654654+ `Please enter the button text (what users will see on the button):`,
655655+ {
656656+ parse_mode: 'Markdown',
657657+ reply_markup: { force_reply: true }
658658+ }
659659+ ).then(prompt => {
660660+ this.handleEditButtonTextInput(chatId, userId, prompt.message_id, announcement);
661661+ });
662662+ }
663663+ }
664664+665665+ handleEditButtonTextInput(chatId, userId, messageId, announcement) {
666666+ this.bot.onReplyToMessage(chatId, messageId, async (reply) => {
667667+ const buttonText = reply.text;
668668+669669+ // Store the button text and ask for URL
670670+ this.editingAnnouncementButton[userId].buttonText = buttonText;
671671+ this.editingAnnouncementButton[userId].step = 'url';
672672+673673+ await this.bot.sendMessage(
674674+ chatId,
675675+ `Great! Now please enter the URL that the button should link to:`,
676676+ {
677677+ reply_markup: { force_reply: true }
678678+ }
679679+ ).then(prompt => {
680680+ this.handleEditButtonUrlInput(chatId, userId, prompt.message_id, announcement);
681681+ });
682682+ });
683683+ }
684684+685685+ handleEditButtonUrlInput(chatId, userId, messageId, announcement) {
686686+ this.bot.onReplyToMessage(chatId, messageId, async (reply) => {
687687+ try {
688688+ const buttonUrl = reply.text;
689689+ const buttonText = this.editingAnnouncementButton[userId].buttonText;
690690+691691+ // Validate URL format (basic check)
692692+ if (!buttonUrl.startsWith('http://') && !buttonUrl.startsWith('https://')) {
693693+ await this.bot.sendMessage(
694694+ chatId,
695695+ '⚠️ Please enter a valid URL starting with http:// or https://',
696696+ { reply_markup: { force_reply: true } }
697697+ ).then(newPrompt => {
698698+ this.handleEditButtonUrlInput(chatId, userId, newPrompt.message_id, announcement);
699699+ });
700700+ return;
701701+ }
702702+703703+ // Update the announcement with the new button
704704+ const button = { text: buttonText, url: buttonUrl };
705705+ await this.announcements.updateAnnouncement(announcement.id, { button: button });
706706+707707+ await this.bot.sendMessage(
708708+ chatId,
709709+ `✅ *Button updated successfully!*\n\nButton: "${buttonText}" -> ${buttonUrl}`,
710710+ { parse_mode: 'Markdown' }
711711+ );
712712+713713+ // Clean up editing state
714714+ delete this.editingAnnouncementButton[userId];
715715+716716+ } catch (error) {
717717+ console.error('Error updating announcement button:', error);
718718+ await this.bot.sendMessage(chatId, `❌ Error updating button: ${error.message}`);
719719+ }
720720+ });
721721+ }
722722+723723+ // Allow access to editingAnnouncementButton for coordination with other modules
724724+ getEditingAnnouncementButton() {
725725+ return this.editingAnnouncementButton;
726726+ }
727727+728728+ setEditingAnnouncementButton(editingAnnouncementButton) {
729729+ this.editingAnnouncementButton = editingAnnouncementButton;
730730+ }
731731+}
732732+733733+module.exports = CallbackHandler;
···11+# Bot Architecture - Migration Summary
22+33+## ✅ Migration Completed - Modular Version Now Active
44+55+The Telegram bot has been successfully migrated from a monolithic 1757-line file to a modular architecture with 17 separate modules. **The modular version is now the active implementation.**
66+77+## 🚨 Important Notice
88+99+- **Current Active Version**: Modular architecture (in `telegramBot.js`)
1010+- **Legacy Backup**: Original monolithic version (in `telegramBot.js.backup`)
1111+- **Future Development**: Only the modular version will receive updates and new features
1212+- **No Action Required**: Existing code continues to work without changes
1313+1414+## 📁 New Modular Structure
1515+1616+```
1717+bot/
1818+├── telegramBot.js # Main bot file (now using modular architecture)
1919+├── telegramBotModular.js # Original modular implementation
2020+├── telegramBot.js.backup # Backup of original monolithic version
2121+└── telegrambot/
2222+ ├── commandRegistry.js # Central command coordinator
2323+ ├── README.md # Detailed architecture documentation
2424+ ├── commands/ # Individual command modules (13 files)
2525+ │ ├── announce.js
2626+ │ ├── announcements.js
2727+ │ ├── cleancache.js
2828+ │ ├── clear.js
2929+ │ ├── help.js
3030+ │ ├── index.js
3131+ │ ├── linkHandler.js
3232+ │ ├── queue.js
3333+ │ ├── schedule.js
3434+ │ ├── send.js
3535+ │ ├── setcount.js
3636+ │ ├── shuffle.js
3737+ │ ├── start.js
3838+ │ └── update.js
3939+ └── helpers/ # Shared functionality modules (4 files)
4040+ ├── authHelper.js
4141+ ├── callbackHandler.js
4242+ ├── index.js
4343+ ├── mediaHelper.js
4444+ └── queueHelper.js
4545+```
4646+4747+## 🔄 Migration Script
4848+4949+The `migrate-bot.sh` script in the root directory allows easy switching between versions:
5050+5151+```bash
5252+# Check current status
5353+./migrate-bot.sh status
5454+5555+# Switch to modular version (already active)
5656+./migrate-bot.sh switch-to-modular
5757+5858+# Switch back to original version if needed
5959+./migrate-bot.sh switch-to-original
6060+```
6161+6262+## ✅ Validation Results
6363+6464+- ✅ All 17 JavaScript files pass syntax validation
6565+- ✅ Modular architecture maintains exact same API as original
6666+- ✅ Command registry successfully coordinates all modules
6767+- ✅ Dependencies properly injected between components
6868+- ✅ Migration script works correctly from root directory
6969+- ✅ Backward compatibility preserved
7070+7171+## 🚀 Current Status
7272+7373+- **Active Version**: Modular architecture ✅
7474+- **File Count**: 17 focused modules (reduced from 1 monolithic file)
7575+- **Code Organization**: Commands and helpers properly separated
7676+- **Maintainability**: Significantly improved
7777+- **API Compatibility**: 100% backward compatible
7878+- **Legacy Support**: Original version backed up but no longer maintained
7979+8080+## 📝 For Users
8181+8282+**No action required!** Your bot will automatically use the new modular architecture on the next restart. All functionality remains exactly the same.
8383+8484+## 📝 For Developers
8585+8686+**Important**: Future development should use the modular structure in `bot/telegrambot/`. The backup file (`telegramBot.js.backup`) is for emergency rollback only and will not receive updates.
8787+8888+## 🔧 Troubleshooting
8989+9090+If you encounter any issues:
9191+9292+1. Use `./migrate-bot.sh status` to check current version
9393+2. Use `./migrate-bot.sh switch-to-original` to revert if needed
9494+3. Check `bot/telegrambot/README.md` for detailed architecture documentation
9595+4. All original functionality is preserved in the modular version
9696+9797+The migration is complete and the bot is ready for use!
+139
docs/bot-architecture.md
···11+# Bot Architecture - Modular Structure Documentation
22+33+This directory contains the modularized version of the Telegram bot functionality, breaking down the monolithic `telegramBot.js` file into smaller, focused modules.
44+55+## Directory Structure
66+77+```
88+telegrambot/
99+├── commands/ # Individual command handlers
1010+│ ├── index.js # Export all commands
1111+│ ├── start.js # /start command
1212+│ ├── help.js # /help command
1313+│ ├── queue.js # /queue command
1414+│ ├── send.js # /send command
1515+│ ├── schedule.js # /schedule command
1616+│ ├── setcount.js # /setcount command
1717+│ ├── clear.js # /clear command
1818+│ ├── cleancache.js # /cleancache command
1919+│ ├── shuffle.js # /shuffle command
2020+│ ├── update.js # /update command
2121+│ ├── announce.js # /announce command (complex interactive)
2222+│ ├── announcements.js # /announcements command
2323+│ └── linkHandler.js # URL link processing
2424+├── helpers/ # Shared helper functions
2525+│ ├── index.js # Export all helpers
2626+│ ├── authHelper.js # User authorization checking
2727+│ ├── queueHelper.js # Queue display and management
2828+│ ├── mediaHelper.js # Media posting functionality
2929+│ └── callbackHandler.js # Callback query handling
3030+├── commandRegistry.js # Central command registration
3131+└── README.md # This file
3232+```
3333+3434+## Current Status
3535+3636+**The modular bot is now the default!** As of the latest update, `telegramBot.js` has been replaced with the modular implementation.
3737+3838+### What This Means
3939+4040+- ✅ **No changes needed** - Your existing code continues to work exactly as before
4141+- ✅ **Better maintainability** - The bot is now organized into focused modules
4242+- ✅ **Backup available** - The original monolithic version is saved as `telegramBot.js.backup`
4343+- ⚠️ **Legacy support** - The backup file will not receive future updates
4444+4545+### Migration Script
4646+4747+Use the migration script in the root directory to switch between versions:
4848+4949+```bash
5050+# Check current status
5151+./migrate-bot.sh status
5252+5353+# Switch back to original version (if needed)
5454+./migrate-bot.sh switch-to-original
5555+5656+# Switch to modular version (already active)
5757+./migrate-bot.sh switch-to-modular
5858+```
5959+6060+## Architecture Overview
6161+6262+### File Structure
6363+6464+### Command Structure
6565+6666+Each command follows this standardized pattern:
6767+6868+```javascript
6969+class CommandName {
7070+ constructor(bot, authHelper, ...otherHelpers) {
7171+ this.bot = bot;
7272+ this.authHelper = authHelper;
7373+ // ... other dependencies
7474+ }
7575+7676+ register() {
7777+ this.bot.onText(/\/commandname/, async (msg) => {
7878+ // Command logic here
7979+ });
8080+ }
8181+}
8282+8383+module.exports = CommandName;
8484+```
8585+8686+### Helper Structure
8787+8888+Helpers provide shared functionality:
8989+9090+```javascript
9191+class HelperName {
9292+ constructor(bot, ...dependencies) {
9393+ this.bot = bot;
9494+ // ... other dependencies
9595+ }
9696+9797+ someMethod() {
9898+ // Helper logic here
9999+ }
100100+}
101101+102102+module.exports = HelperName;
103103+```
104104+105105+## Benefits
106106+107107+1. **Separation of Concerns**: Each command and helper has a single responsibility
108108+2. **Easier Testing**: Individual modules can be tested in isolation
109109+3. **Better Maintainability**: Changes to one command don't affect others
110110+4. **Reusability**: Helper functions can be shared across commands
111111+5. **Cleaner Code**: Smaller files are easier to read and understand
112112+113113+## Adding New Commands
114114+115115+1. Create a new file in `commands/` directory
116116+2. Follow the command structure pattern
117117+3. Add the command to `commands/index.js`
118118+4. Add the command to `commandRegistry.js`
119119+120120+## Adding New Helpers
121121+122122+1. Create a new file in `helpers/` directory
123123+2. Follow the helper structure pattern
124124+3. Add the helper to `helpers/index.js`
125125+4. Use the helper in relevant commands via the command registry
126126+127127+## Migration Notes
128128+129129+### Important Changes
130130+131131+- **Active Version**: The modular architecture is now the default implementation
132132+- **Compatibility**: Full API compatibility maintained - no code changes required
133133+- **Legacy Backup**: Original monolithic version preserved as `telegramBot.js.backup`
134134+- **Future Updates**: Only the modular version will receive new features and updates
135135+- **Rollback Option**: Use `./migrate-bot.sh switch-to-original` if issues arise
136136+137137+### For Developers
138138+139139+When adding new features or fixing bugs, work with the modular structure in this directory. The monolithic backup file should be considered legacy and will not be maintained going forward.
+152
docs/queue-monitoring-configuration.md
···11+# Queue Monitoring System - Configuration Guide
22+33+This document shows how to configure the queue monitoring and alert system.
44+55+## Basic Setup
66+77+The queue monitoring system is enabled by default. To customize the behavior, add these variables to your `.env` file:
88+99+```bash
1010+# Enable queue monitoring (default: true)
1111+QUEUE_ALERTS_ENABLED=true
1212+1313+# Alert when queue drops to 10 items or fewer (default: 10)
1414+QUEUE_LOW_THRESHOLD=10
1515+1616+# Alert when queue is empty or critical (default: 0)
1717+QUEUE_EMPTY_THRESHOLD=0
1818+1919+# Hours between repeated alerts (default: 24)
2020+QUEUE_ALERT_COOLDOWN_HOURS=24
2121+```
2222+2323+## Configuration Examples
2424+2525+### Conservative Setup (Early Warnings)
2626+Good for high-traffic channels that post frequently:
2727+2828+```bash
2929+QUEUE_ALERTS_ENABLED=true
3030+QUEUE_LOW_THRESHOLD=25
3131+QUEUE_EMPTY_THRESHOLD=5
3232+QUEUE_ALERT_COOLDOWN_HOURS=24
3333+```
3434+3535+This will:
3636+- Send low queue alerts when 25 or fewer items remain
3737+- Send critical alerts when 5 or fewer items remain
3838+- Send empty alerts when queue is completely empty
3939+- Wait 24 hours before sending duplicate alerts
4040+4141+### Minimal Setup (Last-Minute Alerts)
4242+Good for low-traffic channels or testing:
4343+4444+```bash
4545+QUEUE_ALERTS_ENABLED=true
4646+QUEUE_LOW_THRESHOLD=5
4747+QUEUE_EMPTY_THRESHOLD=1
4848+QUEUE_ALERT_COOLDOWN_HOURS=24
4949+```
5050+5151+This will:
5252+- Send low queue alerts when 5 or fewer items remain
5353+- Send critical alerts when 1 item remains
5454+- Send empty alerts when queue is completely empty
5555+- Wait 24 hours before sending duplicate alerts
5656+5757+### Disabled Monitoring
5858+To completely disable queue monitoring:
5959+6060+```bash
6161+QUEUE_ALERTS_ENABLED=false
6262+```
6363+6464+## How Alerts Work
6565+6666+1. **Real-time Monitoring**: The system checks queue levels every 30 seconds
6767+2. **Smart Notifications**: Alerts are only sent once per threshold breach
6868+3. **24-Hour Cooldown**: Each alert type respects a 24-hour minimum between notifications
6969+4. **Automatic Reset**: Alert flags reset when queue levels recover above thresholds
7070+5. **Multi-User Support**: All authorized users receive alerts simultaneously
7171+6. **Spam Prevention**: Multiple safeguards prevent notification flooding
7272+7373+## Testing Your Setup
7474+7575+1. Use `/status` command to view current configuration
7676+2. Temporarily lower thresholds for testing
7777+3. Use admin controls to test alert system
7878+4. Add/remove items from queue to trigger alerts
7979+8080+## Testing Configuration
8181+8282+### Short Cooldown for Testing
8383+For development and testing purposes, you can use a shorter cooldown period:
8484+8585+```bash
8686+QUEUE_ALERTS_ENABLED=true
8787+QUEUE_LOW_THRESHOLD=3
8888+QUEUE_EMPTY_THRESHOLD=1
8989+# 6 minutes between alerts for testing
9090+QUEUE_ALERT_COOLDOWN_HOURS=0.1
9191+```
9292+9393+### Manual Testing Steps
9494+1. Set low thresholds and short cooldown
9595+2. Use `/status` command to check current configuration
9696+3. Manually adjust queue items to test alert triggers
9797+4. Verify alerts are sent with proper timing
9898+5. Test alert reset functionality
9999+6. Reset to production values when testing complete
100100+101101+### Production Cooldown Settings
102102+For production environments, consider these cooldown options:
103103+104104+```bash
105105+# Standard 24-hour cooldown (recommended)
106106+QUEUE_ALERT_COOLDOWN_HOURS=24
107107+108108+# More frequent for critical environments
109109+QUEUE_ALERT_COOLDOWN_HOURS=12
110110+111111+# Conservative for low-maintenance setups
112112+QUEUE_ALERT_COOLDOWN_HOURS=48
113113+```
114114+115115+## Alert Message Examples
116116+117117+### Low Queue Alert
118118+```
119119+⚠️ Queue Running Low
120120+121121+The queue currently has only 8 items remaining.
122122+123123+Consider adding more content to maintain posting schedule.
124124+```
125125+126126+### Empty Queue Alert
127127+```
128128+🚨 Queue is Empty
129129+130130+The queue is completely empty! No content will be posted until new items are added.
131131+132132+Use the bot to add new content immediately.
133133+```
134134+135135+### Critical Queue Alert
136136+```
137137+🚨 Queue Critically Low
138138+139139+The queue has only 2 items left and has reached the critical threshold.
140140+141141+Immediate attention required!
142142+```
143143+144144+## Monitoring Best Practices
145145+146146+1. **Set Reasonable Thresholds**: Consider your posting frequency and content addition rate
147147+2. **Configure Appropriate Cooldowns**: Balance between timely notifications and spam prevention
148148+3. **Monitor Regularly**: Use `/status` command to check queue health and alert history
149149+4. **Plan Ahead**: Add content before reaching low thresholds
150150+5. **Test Periodically**: Verify alerts work as expected with realistic scenarios
151151+6. **Adjust as Needed**: Fine-tune thresholds and cooldown periods based on usage patterns
152152+7. **Document Changes**: Keep track of configuration adjustments for your team
+222
docs/queue-monitoring-implementation.md
···11+# Queue Monitoring System - Implementation Summary
22+33+## Overview
44+55+I have successfully implemented a comprehensive queue monitoring and alert system for the Stagehand Telegram bot. This system provides real-time monitoring of queue levels and automatically notifies authorized users when the queue needs attention.
66+77+## Features Implemented
88+99+### 1. Queue Monitor (`utils/queueMonitor.js`)
1010+1111+**Core Functionality:**
1212+- Real-time queue monitoring (checks every 30 seconds)
1313+- Configurable low and empty queue thresholds
1414+- Smart alert management with 24-hour cooldown between alerts
1515+- Automatic alert flag reset when queue recovers
1616+- Multi-user notification system
1717+- Time-based spam prevention
1818+1919+**Key Methods:**
2020+- `startMonitoring()` / `stopMonitoring()` - Control monitoring lifecycle
2121+- `checkQueueStatus()` - Core monitoring logic
2222+- `sendLowQueueAlert()` - Send low queue notifications
2323+- `sendEmptyQueueAlert()` - Send critical/empty queue notifications
2424+- `getQueueStatus()` - Comprehensive status information
2525+- `forceCheck()` - Manual alert testing
2626+- `resetAlerts()` - Reset alert flags
2727+- `updateConfig()` - Runtime configuration updates
2828+2929+### 2. Status Command (`bot/telegrambot/commands/status.js`)
3030+3131+**Features:**
3232+- Detailed queue status display
3333+- Alert system configuration and status
3434+- Health recommendations
3535+- Interactive controls with callback buttons
3636+- Admin-only testing and reset functions
3737+3838+**Information Displayed:**
3939+- Total queue items and service-specific counts
4040+- Shuffle mode status
4141+- Alert system configuration and current state
4242+- Active alerts and recent notifications
4343+- Queue health recommendations
4444+4545+**Interactive Controls:**
4646+- 🔄 Refresh Status
4747+- 📋 View Queue
4848+- 🔔 Test Alerts (admin only)
4949+- 🔄 Reset Alerts (admin only)
5050+5151+### 3. Configuration System
5252+5353+**Environment Variables Added:**
5454+```bash
5555+# Enable/disable queue monitoring (default: true)
5656+QUEUE_ALERTS_ENABLED=true
5757+5858+# Threshold for low queue alerts (default: 10)
5959+QUEUE_LOW_THRESHOLD=10
6060+6161+# Threshold for empty/critical alerts (default: 0)
6262+QUEUE_EMPTY_THRESHOLD=0
6363+6464+# Hours between repeated alerts for same condition (default: 24)
6565+QUEUE_ALERT_COOLDOWN_HOURS=24
6666+```
6767+6868+**Updated Files:**
6969+- `.env.example` - Added configuration examples
7070+- `config.js` - Added queue alert configuration parsing
7171+7272+### 4. Integration with Existing System
7373+7474+**Updated Components:**
7575+- `bot/telegramBot.js` - Integrated queue monitor initialization
7676+- `bot/telegrambot/commandRegistry.js` - Added status command registration
7777+- `bot/telegrambot/helpers/callbackHandler.js` - Added status command callbacks
7878+- `bot/telegrambot/commands/help.js` - Added /status command documentation
7979+8080+## Alert System Behavior
8181+8282+### Low Queue Alert
8383+**Trigger:** Queue ≤ QUEUE_LOW_THRESHOLD AND > QUEUE_EMPTY_THRESHOLD
8484+**Message Example:**
8585+```
8686+⚠️ Queue Running Low
8787+8888+The queue currently has only 8 items remaining.
8989+9090+Consider adding more content to maintain posting schedule.
9191+```
9292+9393+### Empty/Critical Queue Alert
9494+**Trigger:** Queue ≤ QUEUE_EMPTY_THRESHOLD
9595+**Message Examples:**
9696+```
9797+🚨 Queue is Empty
9898+9999+The queue is completely empty! No content will be posted until new items are added.
100100+101101+Use the bot to add new content immediately.
102102+```
103103+104104+```
105105+🚨 Queue Critically Low
106106+107107+The queue has only 2 items left and has reached the critical threshold.
108108+109109+Immediate attention required!
110110+```
111111+112112+### Alert Reset Logic
113113+- **24-Hour Cooldown**: Each alert type can only be sent once every 24 hours
114114+- **Smart Flag Reset**: Alert flags reset when queue grows above thresholds
115115+- **Time-Based Prevention**: Prevents alert spam even if queue fluctuates around thresholds
116116+- **Low alert resets**: When queue > QUEUE_LOW_THRESHOLD
117117+- **Empty alert resets**: When queue > QUEUE_EMPTY_THRESHOLD
118118+- **Manual reset available**: For admins via `/status` command
119119+- **Configurable cooldown**: Can be adjusted via `alertCooldownHours` setting
120120+121121+## New Commands
122122+123123+### `/status`
124124+- **Purpose:** Display comprehensive queue status and alert information
125125+- **Access:** All authorized users
126126+- **Features:** Interactive controls, real-time status, health recommendations
127127+- **Admin Features:** Test alerts, reset alert flags
128128+129129+## Testing
130130+131131+### Test Script (`test-queue-monitor.js`)
132132+- Standalone test script for queue monitoring functionality
133133+- Tests alert logic, configuration updates, and status reporting
134134+- Mock Telegram bot for safe testing
135135+- Comprehensive test coverage
136136+137137+### Usage:
138138+```bash
139139+node test-queue-monitor.js
140140+```
141141+142142+## Documentation
143143+144144+### Files Created/Updated:
145145+- `QUEUE-MONITORING.md` - Comprehensive configuration guide
146146+- `README.md` - Updated with queue monitoring documentation
147147+- Alert system examples and best practices
148148+149149+## Technical Implementation Details
150150+151151+### Architecture
152152+- **Modular Design:** Queue monitor is a separate utility class
153153+- **Event-Driven:** Uses intervals for periodic checking
154154+- **Stateful:** Tracks alert states and timestamps to prevent spam
155155+- **Time-Based Cooldowns:** 24-hour minimum between duplicate alerts
156156+- **Configurable:** Runtime configuration updates supported
157157+- **Spam Prevention:** Multiple layers of protection against alert flooding
158158+159159+### Error Handling
160160+- Graceful degradation when queue manager unavailable
161161+- Comprehensive error logging
162162+- Safe fallbacks for configuration issues
163163+- Robust notification delivery with per-user error handling
164164+165165+### Performance Considerations
166166+- Lightweight monitoring (30-second intervals)
167167+- Efficient queue status checking
168168+- Minimal memory footprint
169169+- Non-blocking alert delivery
170170+171171+## Usage Examples
172172+173173+### Basic Setup
174174+```bash
175175+# Default configuration (alerts at 10 and 0 items)
176176+QUEUE_ALERTS_ENABLED=true
177177+QUEUE_LOW_THRESHOLD=10
178178+QUEUE_EMPTY_THRESHOLD=0
179179+```
180180+181181+### Conservative Setup
182182+```bash
183183+# Early warnings for high-traffic channels
184184+QUEUE_ALERTS_ENABLED=true
185185+QUEUE_LOW_THRESHOLD=25
186186+QUEUE_EMPTY_THRESHOLD=5
187187+```
188188+189189+### Testing/Development
190190+```bash
191191+# Minimal alerts for testing with short cooldown
192192+QUEUE_ALERTS_ENABLED=true
193193+QUEUE_LOW_THRESHOLD=3
194194+QUEUE_EMPTY_THRESHOLD=1
195195+# For testing, you can set shorter cooldown
196196+# QUEUE_ALERT_COOLDOWN_HOURS=0.1 # 6 minutes for testing
197197+```
198198+199199+## Benefits
200200+201201+1. **Proactive Management:** Users are notified before content runs out
202202+2. **Automated Monitoring:** No manual checking required
203203+3. **Smart Notifications:** Prevents alert spam with 24-hour cooldowns and intelligent reset logic
204204+4. **Multi-User Support:** All authorized users stay informed
205205+5. **Configurable:** Adapts to different usage patterns and cooldown preferences
206206+6. **Admin Controls:** Testing and management capabilities for bot owners
207207+7. **Comprehensive Status:** Detailed information via `/status` command
208208+8. **Spam Prevention:** Multiple safeguards prevent notification flooding
209209+210210+## Future Enhancements
211211+212212+Potential improvements that could be added:
213213+- Webhook notifications for external systems
214214+- Historical queue level tracking
215215+- Predictive alerts based on posting frequency
216216+- Custom alert messages per threshold
217217+- Integration with announcement system for public notifications
218218+- Queue trend analysis and reporting
219219+220220+## Conclusion
221221+222222+The queue monitoring system provides a robust, intelligent solution for maintaining content availability in the Stagehand bot. It automatically alerts users when attention is needed while providing comprehensive tools for monitoring and management. The system is highly configurable, well-integrated with the existing architecture, and thoroughly tested.
+92
migrate-bot.sh
···11+#!/bin/bash
22+33+# Migration script to switch between monolithic and modular telegram bot
44+55+CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
66+BOT_DIR="$CURRENT_DIR/bot"
77+BACKUP_FILE="$BOT_DIR/telegramBot.js.backup"
88+MODULAR_FILE="$BOT_DIR/telegramBotModular.js"
99+ORIGINAL_FILE="$BOT_DIR/telegramBot.js"
1010+1111+usage() {
1212+ echo "Usage: $0 [switch-to-modular|switch-to-original|status]"
1313+ echo ""
1414+ echo "Commands:"
1515+ echo " switch-to-modular Switch to using the modular telegram bot"
1616+ echo " switch-to-original Switch back to the original telegram bot"
1717+ echo " status Show which version is currently active"
1818+ echo ""
1919+}
2020+2121+switch_to_modular() {
2222+ if [ ! -f "$MODULAR_FILE" ]; then
2323+ echo "Error: Modular bot file not found at $MODULAR_FILE"
2424+ exit 1
2525+ fi
2626+2727+ # Backup original if not already backed up
2828+ if [ ! -f "$BACKUP_FILE" ]; then
2929+ echo "Backing up original telegramBot.js..."
3030+ cp "$ORIGINAL_FILE" "$BACKUP_FILE"
3131+ fi
3232+3333+ echo "Switching to modular telegram bot..."
3434+ cp "$MODULAR_FILE" "$ORIGINAL_FILE"
3535+ echo "✅ Switched to modular version"
3636+ echo "💡 Restart your bot service to apply changes"
3737+}
3838+3939+switch_to_original() {
4040+ if [ ! -f "$BACKUP_FILE" ]; then
4141+ echo "Error: No backup file found. Cannot restore original version."
4242+ echo "The original version may already be active or was never backed up."
4343+ exit 1
4444+ fi
4545+4646+ echo "Switching back to original telegram bot..."
4747+ cp "$BACKUP_FILE" "$ORIGINAL_FILE"
4848+ echo "✅ Switched to original version"
4949+ echo "💡 Restart your bot service to apply changes"
5050+}
5151+5252+show_status() {
5353+ echo "Telegram Bot Version Status:"
5454+ echo "============================"
5555+5656+ if [ -f "$BACKUP_FILE" ]; then
5757+ # Check if current file matches modular or original
5858+ if cmp -s "$ORIGINAL_FILE" "$MODULAR_FILE"; then
5959+ echo "✅ Currently using: MODULAR version"
6060+ elif cmp -s "$ORIGINAL_FILE" "$BACKUP_FILE"; then
6161+ echo "✅ Currently using: ORIGINAL version"
6262+ else
6363+ echo "⚠️ Currently using: UNKNOWN version (manual changes detected)"
6464+ fi
6565+ echo "📁 Backup available: Yes"
6666+ else
6767+ echo "✅ Currently using: ORIGINAL version (no backup created)"
6868+ echo "📁 Backup available: No"
6969+ fi
7070+7171+ echo ""
7272+ echo "Available files:"
7373+ [ -f "$ORIGINAL_FILE" ] && echo " - telegramBot.js (active)"
7474+ [ -f "$MODULAR_FILE" ] && echo " - telegramBotModular.js"
7575+ [ -f "$BACKUP_FILE" ] && echo " - telegramBot.js.backup"
7676+}
7777+7878+case "${1:-}" in
7979+ switch-to-modular)
8080+ switch_to_modular
8181+ ;;
8282+ switch-to-original)
8383+ switch_to_original
8484+ ;;
8585+ status)
8686+ show_status
8787+ ;;
8888+ *)
8989+ usage
9090+ exit 1
9191+ ;;
9292+esac
···1414 this.autoSaveInterval = null;
1515 this.queueData = { queue: [] };
1616 this.postServices = ['telegram'];
1717+ this.shuffleMode = false; // Whether queue should be shuffled after item removal
17181819 // Add Discord to services if enabled
1920 if (config.discord?.enabled) {
···233234 if (allPosted) {
234235 const removed = this.queueData.queue.splice(index, 1)[0];
235236 console.log(`All services posted item: ${removed.title}, removed from queue`);
237237+238238+ // If shuffle mode is enabled and there are items remaining in the queue, shuffle them
239239+ if (this.shuffleMode && this.queueData.queue.length > 1) {
240240+ await this.shuffleQueue();
241241+ } else {
242242+ await this.saveQueueToDisk();
243243+ }
244244+ } else {
245245+ await this.saveQueueToDisk();
236246 }
237247238238- await this.saveQueueToDisk();
239248 return true;
240249 } catch (error) {
241250 console.error('Error marking item as posted:', error);
···409418 async shutdown() {
410419 this.stopScheduler();
411420 return this.saveQueueToDisk();
421421+ }
422422+423423+ /**
424424+ * Toggle shuffle mode on/off
425425+ * @returns {boolean} - New state of shuffle mode
426426+ */
427427+ toggleShuffleMode() {
428428+ this.shuffleMode = !this.shuffleMode;
429429+ console.log(`Queue shuffle mode ${this.shuffleMode ? 'enabled' : 'disabled'}`);
430430+ return this.shuffleMode;
431431+ }
432432+433433+ /**
434434+ * Get current state of shuffle mode
435435+ * @returns {boolean} - Current state of shuffle mode
436436+ */
437437+ isShuffleModeEnabled() {
438438+ return this.shuffleMode;
439439+ }
440440+441441+ /**
442442+ * Shuffle the remaining items in the queue using Fisher-Yates algorithm
443443+ * @returns {Promise<boolean>} - Whether shuffle was successful
444444+ */
445445+ async shuffleQueue() {
446446+ try {
447447+ if (this.queueData.queue.length <= 1) {
448448+ // No need to shuffle if queue is empty or has only one item
449449+ return true;
450450+ }
451451+452452+ const queue = this.queueData.queue;
453453+454454+ // Fisher-Yates (Knuth) shuffle algorithm
455455+ for (let i = queue.length - 1; i > 0; i--) {
456456+ // Generate random index between 0 and i (inclusive)
457457+ const j = Math.floor(Math.random() * (i + 1));
458458+ // Swap elements at i and j
459459+ [queue[i], queue[j]] = [queue[j], queue[i]];
460460+ }
461461+462462+ console.log(`Queue shuffled (${queue.length} items)`);
463463+ await this.saveQueueToDisk();
464464+ return true;
465465+ } catch (error) {
466466+ console.error('Error shuffling queue:', error);
467467+ return false;
468468+ }
412469 }
413470}
414471
-25
test-updater.js
···11-/**
22- * Test script for the updater's change statistics functionality
33- */
44-const updater = require('./utils/updater');
55-66-// Log test title
77-console.log('=== Testing Updater Change Statistics ===');
88-99-// Test with different commit ranges
1010-async function runTests() {
1111- // Test with the most recent commit
1212- await updater.testChangeStats(1);
1313-1414- // Test with the last 3 commits if they exist
1515- await updater.testChangeStats(3);
1616-1717- // For testing the actual update functionality:
1818- // Uncomment the following line to test the manual update with real fetching
1919- // const updateResult = await updater.manualUpdate();
2020- // console.log('Update result:', updateResult ? 'Updates applied' : 'No updates available');
2121-}
2222-2323-runTests().then(() => {
2424- console.log('Tests completed');
2525-});
+8
utils/announcementManager.js
···176176 announcement.name = updates.name;
177177 }
178178179179+ if (updates.button !== undefined) {
180180+ // Validate button if provided (null is allowed for removal)
181181+ if (updates.button !== null && (!updates.button.text || !updates.button.url)) {
182182+ throw new Error('Button must have both text and url properties');
183183+ }
184184+ announcement.button = updates.button;
185185+ }
186186+179187 if (updates.cronSchedule !== undefined) {
180188 if (!this.isValidCronExpression(updates.cronSchedule)) {
181189 throw new Error('Invalid cron expression');
+407
utils/queueMonitor.js
···11+/**
22+ * Queue Monitor for tracking queue levels and sending alerts
33+ * Handles low queue and empty queue notifications to authorized users
44+ */
55+66+const config = require('../config');
77+const fs = require('fs').promises;
88+const path = require('path');
99+1010+class QueueMonitor {
1111+ constructor(telegramBot) {
1212+ this.telegramBot = telegramBot;
1313+ this.queueManager = null; // Will be set after initialization
1414+1515+ // Alert state tracking
1616+ this.lastQueueLength = 0;
1717+ this.lowQueueAlertSent = false;
1818+ this.emptyQueueAlertSent = false;
1919+ this.lastLowQueueAlertTime = 0;
2020+ this.lastEmptyQueueAlertTime = 0;
2121+2222+ // Persistence file path
2323+ this.stateFilePath = path.join(__dirname, '..', 'queue', 'alert-state.json');
2424+2525+ // Configuration
2626+ this.lowThreshold = config.queueLowThreshold;
2727+ this.emptyThreshold = config.queueEmptyThreshold;
2828+ this.alertsEnabled = config.queueAlertsEnabled;
2929+ this.alertCooldownHours = config.queueAlertCooldownHours || 24; // 24 hour cooldown between alerts
3030+3131+ // Monitoring interval (check every 30 seconds)
3232+ this.monitoringInterval = null;
3333+ this.intervalMs = 30 * 1000;
3434+3535+ // Periodic save interval (save state every 5 minutes)
3636+ this.periodicSaveInterval = null;
3737+ this.saveIntervalMs = 5 * 60 * 1000; // 5 minutes in milliseconds
3838+3939+ console.log(`Queue Monitor initialized - Low threshold: ${this.lowThreshold}, Empty threshold: ${this.emptyThreshold}, Alerts enabled: ${this.alertsEnabled}`);
4040+ }
4141+4242+ /**
4343+ * Initialize the queue monitor with queue manager reference
4444+ * @param {Object} queueManager - The queue manager instance
4545+ */
4646+ async init(queueManager) {
4747+ this.queueManager = queueManager;
4848+4949+ // Load persisted alert state
5050+ await this.loadAlertState();
5151+5252+ if (this.alertsEnabled) {
5353+ this.startMonitoring();
5454+ }
5555+5656+ // Start periodic saving regardless of whether alerts are enabled
5757+ this.startPeriodicSave();
5858+ }
5959+6060+ /**
6161+ * Start monitoring the queue for changes
6262+ */
6363+ startMonitoring() {
6464+ if (this.monitoringInterval) {
6565+ clearInterval(this.monitoringInterval);
6666+ }
6767+6868+ this.monitoringInterval = setInterval(() => {
6969+ this.checkQueueStatus();
7070+ }, this.intervalMs);
7171+7272+ console.log('Queue monitoring started');
7373+ }
7474+7575+ /**
7676+ * Start periodic saving of alert state (every 5 minutes)
7777+ */
7878+ startPeriodicSave() {
7979+ if (this.periodicSaveInterval) {
8080+ clearInterval(this.periodicSaveInterval);
8181+ }
8282+8383+ this.periodicSaveInterval = setInterval(async () => {
8484+ await this.saveAlertState();
8585+ console.log('Queue alert state saved (periodic backup)');
8686+ }, this.saveIntervalMs);
8787+8888+ console.log('Periodic alert state saving started (every 5 minutes)');
8989+ }
9090+9191+ /**
9292+ * Stop monitoring the queue
9393+ */
9494+ stopMonitoring() {
9595+ if (this.monitoringInterval) {
9696+ clearInterval(this.monitoringInterval);
9797+ this.monitoringInterval = null;
9898+ }
9999+100100+ console.log('Queue monitoring stopped');
101101+ }
102102+103103+ /**
104104+ * Stop periodic saving of alert state
105105+ */
106106+ stopPeriodicSave() {
107107+ if (this.periodicSaveInterval) {
108108+ clearInterval(this.periodicSaveInterval);
109109+ this.periodicSaveInterval = null;
110110+ }
111111+112112+ console.log('Periodic alert state saving stopped');
113113+ }
114114+115115+ /**
116116+ * Check current queue status and send alerts if necessary
117117+ */
118118+ async checkQueueStatus() {
119119+ if (!this.queueManager || !this.alertsEnabled) {
120120+ return;
121121+ }
122122+123123+ try {
124124+ const currentLength = await this.queueManager.getQueueLength();
125125+ const currentTime = Date.now();
126126+ const cooldownMs = this.alertCooldownHours * 60 * 60 * 1000; // Convert hours to milliseconds
127127+128128+ let stateChanged = false;
129129+130130+ // Only reset alert flags when queue has grown above thresholds
131131+ // This prevents continuous resetting when queue is at/below thresholds
132132+ if (currentLength > this.lowThreshold) {
133133+ if (this.lowQueueAlertSent || this.emptyQueueAlertSent) {
134134+ this.lowQueueAlertSent = false;
135135+ this.emptyQueueAlertSent = false;
136136+ stateChanged = true;
137137+ }
138138+ } else if (currentLength > this.emptyThreshold && currentLength <= this.lowThreshold) {
139139+ // Only reset empty alert flag if we're between empty and low thresholds
140140+ if (this.emptyQueueAlertSent) {
141141+ this.emptyQueueAlertSent = false;
142142+ stateChanged = true;
143143+ }
144144+ }
145145+146146+ // Check for low queue alert (with 24-hour cooldown)
147147+ const canSendLowAlert = !this.lowQueueAlertSent &&
148148+ (currentTime - this.lastLowQueueAlertTime) >= cooldownMs;
149149+150150+ if (currentLength <= this.lowThreshold && currentLength > this.emptyThreshold && canSendLowAlert) {
151151+ await this.sendLowQueueAlert(currentLength);
152152+ this.lowQueueAlertSent = true;
153153+ this.lastLowQueueAlertTime = currentTime;
154154+ stateChanged = true;
155155+ }
156156+157157+ // Check for empty queue alert (with 24-hour cooldown)
158158+ const canSendEmptyAlert = !this.emptyQueueAlertSent &&
159159+ (currentTime - this.lastEmptyQueueAlertTime) >= cooldownMs;
160160+161161+ if (currentLength <= this.emptyThreshold && canSendEmptyAlert) {
162162+ await this.sendEmptyQueueAlert(currentLength);
163163+ this.emptyQueueAlertSent = true;
164164+ this.lastEmptyQueueAlertTime = currentTime;
165165+ stateChanged = true;
166166+ }
167167+168168+ // Save state if any alert flags or timestamps changed
169169+ if (stateChanged) {
170170+ await this.saveAlertState();
171171+ }
172172+173173+ this.lastQueueLength = currentLength;
174174+ } catch (error) {
175175+ console.error('Error checking queue status:', error);
176176+ }
177177+ }
178178+179179+ /**
180180+ * Send low queue alert to all authorized users
181181+ * @param {number} queueLength - Current queue length
182182+ */
183183+ async sendLowQueueAlert(queueLength) {
184184+ const message = `⚠️ *Queue Running Low*\n\nThe queue currently has only *${queueLength}* item${queueLength !== 1 ? 's' : ''} remaining.\n\nConsider adding more content to maintain posting schedule.`;
185185+186186+ await this.sendAlertToAuthorizedUsers(message);
187187+ console.log(`Low queue alert sent (${queueLength} items remaining)`);
188188+ }
189189+190190+ /**
191191+ * Send empty queue alert to all authorized users
192192+ * @param {number} queueLength - Current queue length (should be 0 or empty threshold)
193193+ */
194194+ async sendEmptyQueueAlert(queueLength) {
195195+ const message = queueLength === 0
196196+ ? `🚨 *Queue is Empty*\n\nThe queue is completely empty! No content will be posted until new items are added.\n\nUse the bot to add new content immediately.`
197197+ : `🚨 *Queue Critically Low*\n\nThe queue has only *${queueLength}* item${queueLength !== 1 ? 's' : ''} left and has reached the critical threshold.\n\nImmediate attention required!`;
198198+199199+ await this.sendAlertToAuthorizedUsers(message);
200200+ console.log(`Empty queue alert sent (${queueLength} items remaining)`);
201201+ }
202202+203203+ /**
204204+ * Send alert message to all authorized users
205205+ * @param {string} message - The alert message to send
206206+ */
207207+ async sendAlertToAuthorizedUsers(message) {
208208+ if (!this.telegramBot || !config.authorizedUsers) {
209209+ return;
210210+ }
211211+212212+ const authorizedUsers = config.authorizedUsers;
213213+214214+ for (const userId of authorizedUsers) {
215215+ try {
216216+ await this.telegramBot.bot.sendMessage(userId, message, {
217217+ parse_mode: 'Markdown',
218218+ disable_notification: false
219219+ });
220220+ } catch (error) {
221221+ console.error(`Failed to send alert to user ${userId}:`, error.message);
222222+ }
223223+ }
224224+ }
225225+226226+ /**
227227+ * Get current queue status information
228228+ * @returns {Object} - Queue status object
229229+ */
230230+ async getQueueStatus() {
231231+ if (!this.queueManager) {
232232+ return null;
233233+ }
234234+235235+ try {
236236+ const currentLength = await this.queueManager.getQueueLength();
237237+ const queue = await this.queueManager.getQueue();
238238+239239+ // Calculate how many items are ready to post for each service
240240+ const readyItems = {
241241+ telegram: 0,
242242+ discord: 0
243243+ };
244244+245245+ queue.forEach(item => {
246246+ if (item.postedTo) {
247247+ if (!item.postedTo.telegram) readyItems.telegram++;
248248+ if (this.queueManager.postServices.includes('discord') && !item.postedTo.discord) {
249249+ readyItems.discord++;
250250+ }
251251+ } else {
252252+ // If no postedTo field, assume all services need to post
253253+ readyItems.telegram++;
254254+ if (this.queueManager.postServices.includes('discord')) {
255255+ readyItems.discord++;
256256+ }
257257+ }
258258+ });
259259+260260+ return {
261261+ totalItems: currentLength,
262262+ readyForTelegram: readyItems.telegram,
263263+ readyForDiscord: readyItems.discord,
264264+ shuffleMode: this.queueManager.isShuffleModeEnabled(),
265265+ alertsEnabled: this.alertsEnabled,
266266+ lowThreshold: this.lowThreshold,
267267+ emptyThreshold: this.emptyThreshold,
268268+ lowQueueAlertSent: this.lowQueueAlertSent,
269269+ emptyQueueAlertSent: this.emptyQueueAlertSent,
270270+ lastLowQueueAlertTime: this.lastLowQueueAlertTime,
271271+ lastEmptyQueueAlertTime: this.lastEmptyQueueAlertTime,
272272+ alertCooldownHours: this.alertCooldownHours
273273+ };
274274+ } catch (error) {
275275+ console.error('Error getting queue status:', error);
276276+ return null;
277277+ }
278278+ }
279279+280280+ /**
281281+ * Manually trigger a queue status check (for testing or forced updates)
282282+ */
283283+ async forceCheck() {
284284+ await this.checkQueueStatus();
285285+ }
286286+287287+ /**
288288+ * Reset alert flags (useful for testing or manual reset)
289289+ */
290290+ async resetAlerts() {
291291+ this.lowQueueAlertSent = false;
292292+ this.emptyQueueAlertSent = false;
293293+ this.lastLowQueueAlertTime = 0;
294294+ this.lastEmptyQueueAlertTime = 0;
295295+ await this.saveAlertState();
296296+ console.log('Queue alert flags and timestamps reset');
297297+ }
298298+299299+ /**
300300+ * Update configuration settings
301301+ * @param {Object} newConfig - New configuration object
302302+ */
303303+ updateConfig(newConfig) {
304304+ if (newConfig.lowThreshold !== undefined) {
305305+ this.lowThreshold = newConfig.lowThreshold;
306306+ }
307307+ if (newConfig.emptyThreshold !== undefined) {
308308+ this.emptyThreshold = newConfig.emptyThreshold;
309309+ }
310310+ if (newConfig.alertCooldownHours !== undefined) {
311311+ this.alertCooldownHours = newConfig.alertCooldownHours;
312312+ }
313313+ if (newConfig.alertsEnabled !== undefined) {
314314+ this.alertsEnabled = newConfig.alertsEnabled;
315315+316316+ if (this.alertsEnabled && !this.monitoringInterval) {
317317+ this.startMonitoring();
318318+ } else if (!this.alertsEnabled && this.monitoringInterval) {
319319+ this.stopMonitoring();
320320+ }
321321+322322+ // Note: Periodic saving continues regardless of alert settings
323323+ // to ensure state persistence for cooldown timers
324324+ }
325325+326326+ console.log(`Queue monitor config updated - Low: ${this.lowThreshold}, Empty: ${this.emptyThreshold}, Cooldown: ${this.alertCooldownHours}h, Enabled: ${this.alertsEnabled}`);
327327+ }
328328+329329+ /**
330330+ * Load alert state from persistent storage
331331+ */
332332+ async loadAlertState() {
333333+ try {
334334+ const data = await fs.readFile(this.stateFilePath, 'utf8');
335335+ const state = JSON.parse(data);
336336+337337+ // Restore alert flags and timestamps
338338+ this.lowQueueAlertSent = state.lowQueueAlertSent || false;
339339+ this.emptyQueueAlertSent = state.emptyQueueAlertSent || false;
340340+ this.lastLowQueueAlertTime = state.lastLowQueueAlertTime || 0;
341341+ this.lastEmptyQueueAlertTime = state.lastEmptyQueueAlertTime || 0;
342342+343343+ console.log('Queue alert state loaded from persistent storage');
344344+345345+ // Log current cooldown status if alerts were previously sent
346346+ const currentTime = Date.now();
347347+ const cooldownMs = this.alertCooldownHours * 60 * 60 * 1000;
348348+349349+ if (this.lastLowQueueAlertTime > 0) {
350350+ const timeSinceLastLow = currentTime - this.lastLowQueueAlertTime;
351351+ const lowCooldownRemaining = Math.max(0, cooldownMs - timeSinceLastLow);
352352+ if (lowCooldownRemaining > 0) {
353353+ const hoursRemaining = Math.ceil(lowCooldownRemaining / (60 * 60 * 1000));
354354+ console.log(`Low queue alert cooldown: ${hoursRemaining} hours remaining`);
355355+ }
356356+ }
357357+358358+ if (this.lastEmptyQueueAlertTime > 0) {
359359+ const timeSinceLastEmpty = currentTime - this.lastEmptyQueueAlertTime;
360360+ const emptyCooldownRemaining = Math.max(0, cooldownMs - timeSinceLastEmpty);
361361+ if (emptyCooldownRemaining > 0) {
362362+ const hoursRemaining = Math.ceil(emptyCooldownRemaining / (60 * 60 * 1000));
363363+ console.log(`Empty queue alert cooldown: ${hoursRemaining} hours remaining`);
364364+ }
365365+ }
366366+367367+ } catch (error) {
368368+ if (error.code === 'ENOENT') {
369369+ console.log('No existing alert state file found, starting with fresh state');
370370+ } else {
371371+ console.error('Error loading alert state:', error);
372372+ }
373373+ // Continue with default values if file doesn't exist or is corrupted
374374+ }
375375+ }
376376+377377+ /**
378378+ * Save alert state to persistent storage
379379+ */
380380+ async saveAlertState() {
381381+ try {
382382+ const state = {
383383+ lowQueueAlertSent: this.lowQueueAlertSent,
384384+ emptyQueueAlertSent: this.emptyQueueAlertSent,
385385+ lastLowQueueAlertTime: this.lastLowQueueAlertTime,
386386+ lastEmptyQueueAlertTime: this.lastEmptyQueueAlertTime,
387387+ lastSaved: Date.now()
388388+ };
389389+390390+ await fs.writeFile(this.stateFilePath, JSON.stringify(state, null, 2));
391391+ } catch (error) {
392392+ console.error('Error saving alert state:', error);
393393+ }
394394+ }
395395+396396+ /**
397397+ * Shutdown the queue monitor
398398+ */
399399+ async shutdown() {
400400+ this.stopMonitoring();
401401+ this.stopPeriodicSave();
402402+ await this.saveAlertState();
403403+ console.log('Queue monitor shutdown complete');
404404+ }
405405+}
406406+407407+module.exports = QueueMonitor;