this repo has no description
0
fork

Configure Feed

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

feat: Enhance link processing to support multiple links and improve shuffle command persistence

+141 -38
+12 -8
README.md
··· 263 263 - `/send` - Post the next image in the queue immediately 264 264 - `/schedule [cron]` - Set posting schedule using cron syntax 265 265 - `/setcount [number]` - Set number of images per post interval 266 - - `/shuffle` - Toggle shuffle mode (randomizes queue after each post) 266 + - `/shuffle` - Toggle shuffle mode (randomizes queue after each post, persists between restarts) 267 267 - `/clear` - Clear the entire queue 268 268 - `/cleancache` - Clean expired items from media cache 269 269 - `/announce` - Create a new announcement ··· 274 274 #### Direct Links 275 275 Send a link from any supported website to the bot in a direct message. The bot will extract the image and add it to the queue. 276 276 277 - #### Forwarded Messages 278 - The bot can also process forwarded messages that contain links to supported websites. This includes: 277 + #### Multiple Links in Messages 278 + The bot can process any message containing multiple links to supported websites. This includes: 279 279 280 - - **Text Links**: Any URLs found in the forwarded message text will be extracted and processed 281 - - **Button Links**: URLs embedded in inline keyboard buttons (common in channel posts) are automatically detected and processed 282 - - **Multiple Links**: If a forwarded message contains multiple valid links, all will be processed simultaneously 280 + - **Multiple URLs in Text**: Messages containing several URLs will have all valid links processed 281 + - **Mixed Content**: Messages with both text and inline keyboard buttons containing URLs 282 + - **Embedded URLs**: Links that appear anywhere within message text (not just at the beginning) 283 + - **Button Links**: URLs embedded in inline keyboard buttons are automatically detected and processed 283 284 - **Error Handling**: Invalid links are silently discarded - the bot will only show an error if zero valid links are found 284 285 285 - **Usage**: Simply forward any message containing supported website links to the bot. The bot will automatically detect it's a forwarded message and extract all valid URLs for processing. 286 + #### Forwarded Messages 287 + The bot can also process forwarded messages that contain links to supported websites with the same capabilities as regular messages. 288 + 289 + **Usage**: Send any message containing supported website links to the bot. The bot will automatically detect and process all valid URLs while silently discarding invalid ones. 286 290 287 291 ### Interactive Queue Management 288 292 ··· 293 297 - **Item Removal**: Remove specific items from the queue with a single click 294 298 - **Reordering**: Move any item to the top of the queue to be posted next 295 299 - **Pagination**: Easily navigate through pages of queued items 296 - - **Shuffle Mode**: Automatically randomize the queue after each post with `/shuffle` command 300 + - **Shuffle Mode**: Automatically randomize the queue after each post with `/shuffle` command (setting persists between restarts) 297 301 298 302 The interface shows important information about each queued item including: 299 303 - Item position in queue
+2
bot/telegrambot/commands/help.js
··· 24 24 /update - Update bot from GitHub repository (owner only) 25 25 26 26 Send any link to a supported site to add it to the queue. 27 + Send messages with multiple links to process them all at once. 27 28 Forward messages with links or buttons to extract and queue multiple items. 29 + Invalid links are silently discarded - only valid content is queued. 28 30 Supported sites: e621, FurAffinity, SoFurry, Weasyl, Bluesky 29 31 `; 30 32 this.bot.sendMessage(chatId, helpText);
+101 -21
bot/telegrambot/commands/linkHandler.js
··· 14 14 15 15 register() { 16 16 this.bot.on('message', async (msg) => { 17 - // Check for direct URL messages 18 - if (msg.text && msg.text.startsWith('http')) { 17 + // Skip processing if this is part of an announcement setup 18 + const isInAnnouncementFlow = this.pendingAnnouncements && this.pendingAnnouncements[msg.from.id]; 19 + const isInButtonEditFlow = this.editingAnnouncementButton && this.editingAnnouncementButton[msg.from.id]; 20 + 21 + if (isInAnnouncementFlow || isInButtonEditFlow) { 22 + return; 23 + } 24 + 25 + // Check authorization first for all URL processing 26 + if (!this.authHelper.isAuthorized(msg.from.id)) { 27 + // Only show unauthorized message if the message contains URLs 28 + const urls = this.extractUrlsFromMessage(msg); 29 + if (urls.length > 0) { 30 + this.bot.sendMessage(msg.chat.id, 'You are not authorized to use this bot.'); 31 + } 32 + return; 33 + } 34 + 35 + // Check for direct URL messages (single URL starting with http) 36 + if (msg.text && msg.text.startsWith('http') && !this.hasMultipleLinks(msg)) { 19 37 await this.processDirectUrl(msg); 20 38 return; 21 39 } ··· 25 43 await this.processForwardedMessage(msg); 26 44 return; 27 45 } 46 + 47 + // Check for any message with URLs (multiple links, buttons, or embedded URLs) 48 + if (this.hasMultipleLinks(msg)) { 49 + await this.processMultiLinkMessage(msg); 50 + return; 51 + } 28 52 }); 29 53 } 30 54 ··· 34 58 async processDirectUrl(msg) { 35 59 const chatId = msg.chat.id; 36 60 37 - // Skip processing if this is part of an announcement setup 38 - const isInAnnouncementFlow = this.pendingAnnouncements && this.pendingAnnouncements[msg.from.id]; 39 - const isInButtonEditFlow = this.editingAnnouncementButton && this.editingAnnouncementButton[msg.from.id]; 40 - 41 - if (isInAnnouncementFlow || isInButtonEditFlow) { 42 - // This URL is part of an announcement setup, so we should not process it as a link 43 - return; 44 - } 45 - 46 - if (!this.authHelper.isAuthorized(msg.from.id)) { 47 - this.bot.sendMessage(chatId, 'You are not authorized to use this bot.'); 48 - return; 49 - } 50 - 51 61 try { 52 62 const url = msg.text.trim(); 53 63 ··· 90 100 async processForwardedMessage(msg) { 91 101 const chatId = msg.chat.id; 92 102 93 - if (!this.authHelper.isAuthorized(msg.from.id)) { 94 - this.bot.sendMessage(chatId, 'You are not authorized to use this bot.'); 95 - return; 96 - } 97 - 98 103 // Extract URLs from the forwarded message 99 104 const urls = this.extractUrlsFromMessage(msg); 100 105 ··· 191 196 } 192 197 193 198 return urls; 199 + } 200 + 201 + /** 202 + * Check if a message has multiple links or inline keyboard buttons 203 + */ 204 + hasMultipleLinks(msg) { 205 + // Count URLs in text 206 + const textUrls = msg.text ? this.extractUrlsFromText(msg.text) : []; 207 + 208 + // Check for inline keyboard buttons 209 + const hasButtons = msg.reply_markup && msg.reply_markup.inline_keyboard && 210 + msg.reply_markup.inline_keyboard.some(row => 211 + row.some(button => button.url) 212 + ); 213 + 214 + // Return true if we have multiple URLs in text, or any buttons, or a single URL not at the start 215 + return textUrls.length > 1 || 216 + hasButtons || 217 + (textUrls.length === 1 && !msg.text.startsWith('http')); 218 + } 219 + 220 + /** 221 + * Process a message that may contain multiple links in text or buttons 222 + */ 223 + async processMultiLinkMessage(msg) { 224 + const chatId = msg.chat.id; 225 + 226 + // Extract URLs from the message 227 + const urls = this.extractUrlsFromMessage(msg); 228 + 229 + if (urls.length === 0) { 230 + // No URLs found - silently ignore 231 + return; 232 + } 233 + 234 + try { 235 + this.bot.sendMessage(chatId, `Processing message with ${urls.length} link(s)...`, { reply_to_message_id: msg.message_id }); 236 + 237 + const results = await scraperManager.extractFromUrls(urls); 238 + 239 + if (results.validResults.length === 0) { 240 + this.bot.sendMessage( 241 + chatId, 242 + '❌ No valid links found in the message.', 243 + { reply_to_message_id: msg.message_id } 244 + ); 245 + return; 246 + } 247 + 248 + // Add all valid results to queue 249 + for (const mediaData of results.validResults) { 250 + await queueManager.addToQueue(mediaData); 251 + } 252 + 253 + const queueLength = await queueManager.getQueueLength(); 254 + let responseMessage = `✅ Added ${results.validResults.length} item(s) to queue`; 255 + 256 + if (results.invalidUrls.length > 0) { 257 + responseMessage += ` (${results.invalidUrls.length} invalid link(s) silently discarded)`; 258 + } 259 + 260 + responseMessage += `\nCurrent queue length: ${queueLength}`; 261 + 262 + this.bot.sendMessage( 263 + chatId, 264 + responseMessage, 265 + { reply_to_message_id: msg.message_id } 266 + ); 267 + } catch (error) { 268 + this.bot.sendMessage( 269 + chatId, 270 + `Error processing message: ${error.message}`, 271 + { reply_to_message_id: msg.message_id } 272 + ); 273 + } 194 274 } 195 275 196 276 // Allow other modules to set these properties for proper URL handling during announcements
+2 -2
bot/telegrambot/commands/shuffle.js
··· 21 21 const isEnabled = queueManager.toggleShuffleMode(); 22 22 23 23 if (isEnabled) { 24 - this.bot.sendMessage(chatId, '🔀 Shuffle mode enabled! Queue will be randomized after each post.'); 24 + this.bot.sendMessage(chatId, '🔀 Shuffle mode enabled! Queue will be randomized after each post.\n💾 Setting saved and will persist after restarts.'); 25 25 } else { 26 - this.bot.sendMessage(chatId, '📋 Shuffle mode disabled. Queue will maintain its order.'); 26 + this.bot.sendMessage(chatId, '📋 Shuffle mode disabled. Queue will maintain its order.\n💾 Setting saved and will persist after restarts.'); 27 27 } 28 28 }); 29 29 }
+2 -1
bot/telegrambot/helpers/queueHelper.js
··· 35 35 const endIdx = Math.min(startIdx + pageSize, queueLength); 36 36 37 37 // Build message with queue items 38 - let message = `📋 *Queue Management* (${queueLength} items total)\n`; 38 + const shuffleStatus = queueManager.isShuffleModeEnabled() ? '🔀 Shuffle: ON' : '📋 Shuffle: OFF'; 39 + let message = `📋 *Queue Management* (${queueLength} items total) - ${shuffleStatus}\n`; 39 40 message += `Showing items ${startIdx + 1}-${endIdx} of ${queueLength}\n\n`; 40 41 41 42 // Add each queue item
+2 -2
queue/alert-state.json
··· 1 1 { 2 2 "lowQueueAlertSent": true, 3 - "emptyQueueAlertSent": true, 3 + "emptyQueueAlertSent": false, 4 4 "lastLowQueueAlertTime": 1749155645693, 5 5 "lastEmptyQueueAlertTime": 1749155676860, 6 - "lastSaved": 1749155688499 6 + "lastSaved": 1749157762521 7 7 }
+20 -4
queue/queueManager.js
··· 12 12 this.imagesPerInterval = config.imagesPerInterval; 13 13 this.scheduledTask = null; 14 14 this.autoSaveInterval = null; 15 - this.queueData = { queue: [] }; 15 + this.queueData = { queue: [], shuffleMode: false }; 16 16 this.postServices = ['telegram']; 17 17 this.shuffleMode = false; // Whether queue should be shuffled after item removal 18 18 ··· 30 30 31 31 // Log queue status on startup 32 32 const queueLength = this.queueData.queue.length; 33 - console.log(`Queue loaded with ${queueLength} item${queueLength !== 1 ? 's' : ''}`); 33 + const shuffleStatus = this.shuffleMode ? 'enabled' : 'disabled'; 34 + console.log(`Queue loaded with ${queueLength} item${queueLength !== 1 ? 's' : ''}, shuffle mode ${shuffleStatus}`); 34 35 35 36 // Initialize service tracking for any existing items that don't have it 36 37 this.initializeServiceTracking(); ··· 98 99 const content = await fs.readJson(this.queueFile); 99 100 if (content && Array.isArray(content.queue)) { 100 101 this.queueData = content; 102 + // Load shuffle mode from saved data, default to false if not present 103 + this.shuffleMode = content.shuffleMode || false; 101 104 return; 102 105 } 103 106 } catch (mainError) { ··· 110 113 const backupContent = await fs.readJson(this.queueBackupFile); 111 114 if (backupContent && Array.isArray(backupContent.queue)) { 112 115 this.queueData = backupContent; 116 + // Load shuffle mode from backup data, default to false if not present 117 + this.shuffleMode = backupContent.shuffleMode || false; 113 118 await this.saveQueueToDisk(); // Update the main file 114 119 console.log('Queue successfully recovered from backup'); 115 120 return; ··· 122 127 } 123 128 124 129 // If we reach here, initialize an empty queue 125 - this.queueData = { queue: [] }; 130 + this.queueData = { queue: [], shuffleMode: false }; 131 + this.shuffleMode = false; 126 132 await this.saveQueueToDisk(); 127 133 } catch (error) { 128 134 console.error('Error during queue initialization:', error); 129 - this.queueData = { queue: [] }; 135 + this.queueData = { queue: [], shuffleMode: false }; 136 + this.shuffleMode = false; 130 137 } 131 138 } 132 139 ··· 135 142 */ 136 143 async saveQueueToDisk() { 137 144 try { 145 + // Update shuffle mode in queueData before saving 146 + this.queueData.shuffleMode = this.shuffleMode; 147 + 138 148 // First, update the backup file 139 149 await fs.writeJson(this.queueBackupFile, this.queueData, { spaces: 2 }); 140 150 ··· 427 437 toggleShuffleMode() { 428 438 this.shuffleMode = !this.shuffleMode; 429 439 console.log(`Queue shuffle mode ${this.shuffleMode ? 'enabled' : 'disabled'}`); 440 + 441 + // Save the new shuffle state to disk 442 + this.saveQueueToDisk() 443 + .then(() => console.log('Shuffle mode state saved')) 444 + .catch(err => console.error('Error saving shuffle mode state:', err)); 445 + 430 446 return this.shuffleMode; 431 447 } 432 448