this repo has no description
0
fork

Configure Feed

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

feat: Enhance link handling to support processing of forwarded messages and multiple links

+231 -55
+12
README.md
··· 5 5 ## Features 6 6 7 7 - Extract images from various supported websites 8 + - Process forwarded messages containing links (including button links) 8 9 - Queue images for scheduled posting 9 10 - Customizable posting schedule using cron syntax 10 11 - Access control to limit who can use the bot ··· 270 271 271 272 ### Adding Images to Queue 272 273 274 + #### Direct Links 273 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 + 277 + #### Forwarded Messages 278 + The bot can also process forwarded messages that contain links to supported websites. This includes: 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 283 + - **Error Handling**: Invalid links are silently discarded - the bot will only show an error if zero valid links are found 284 + 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. 274 286 275 287 ### Interactive Queue Management 276 288
+1
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 + Forward messages with links or buttons to extract and queue multiple items. 27 28 Supported sites: e621, FurAffinity, SoFurry, Weasyl, Bluesky 28 29 `; 29 30 this.bot.sendMessage(chatId, helpText);
+174 -50
bot/telegrambot/commands/linkHandler.js
··· 14 14 15 15 register() { 16 16 this.bot.on('message', async (msg) => { 17 + // Check for direct URL messages 17 18 if (msg.text && msg.text.startsWith('http')) { 18 - const chatId = msg.chat.id; 19 - 20 - // Skip processing if this is part of an announcement setup 21 - const isInAnnouncementFlow = this.pendingAnnouncements && this.pendingAnnouncements[msg.from.id]; 22 - const isInButtonEditFlow = this.editingAnnouncementButton && this.editingAnnouncementButton[msg.from.id]; 23 - 24 - if (isInAnnouncementFlow || isInButtonEditFlow) { 25 - // This URL is part of an announcement setup, so we should not process it as a link 26 - return; 27 - } 28 - 29 - if (!this.authHelper.isAuthorized(msg.from.id)) { 30 - this.bot.sendMessage(chatId, 'You are not authorized to use this bot.'); 31 - return; 32 - } 33 - 34 - try { 35 - const url = msg.text.trim(); 36 - 37 - this.bot.sendMessage(chatId, 'Processing link...', { reply_to_message_id: msg.message_id }); 38 - 39 - const mediaData = await scraperManager.extractFromUrl(url); 40 - 41 - // Check if the scraper returned an error (for temporarily disabled scrapers) 42 - if (mediaData.error) { 43 - this.bot.sendMessage( 44 - chatId, 45 - mediaData.error, 46 - { reply_to_message_id: msg.message_id } 47 - ); 48 - return; 49 - } 50 - 51 - await queueManager.addToQueue(mediaData); 52 - 53 - const queueLength = await queueManager.getQueueLength(); 54 - const mediaType = mediaData.isVideo ? 'Video' : 'Image'; 55 - 56 - this.bot.sendMessage( 57 - chatId, 58 - `Added to queue: ${mediaType} - ${mediaData.title}\nCurrent queue length: ${queueLength}`, 59 - { reply_to_message_id: msg.message_id } 60 - ); 61 - } catch (error) { 62 - this.bot.sendMessage( 63 - chatId, 64 - `Error processing link: ${error.message}`, 65 - { reply_to_message_id: msg.message_id } 66 - ); 67 - } 19 + await this.processDirectUrl(msg); 20 + return; 21 + } 22 + 23 + // Check for forwarded messages 24 + if (msg.forward_from || msg.forward_from_chat) { 25 + await this.processForwardedMessage(msg); 26 + return; 68 27 } 69 28 }); 29 + } 30 + 31 + /** 32 + * Process a direct URL message 33 + */ 34 + async processDirectUrl(msg) { 35 + const chatId = msg.chat.id; 36 + 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 + try { 52 + const url = msg.text.trim(); 53 + 54 + this.bot.sendMessage(chatId, 'Processing link...', { reply_to_message_id: msg.message_id }); 55 + 56 + const mediaData = await scraperManager.extractFromUrl(url); 57 + 58 + // Check if the scraper returned an error (for temporarily disabled scrapers) 59 + if (mediaData.error) { 60 + this.bot.sendMessage( 61 + chatId, 62 + mediaData.error, 63 + { reply_to_message_id: msg.message_id } 64 + ); 65 + return; 66 + } 67 + 68 + await queueManager.addToQueue(mediaData); 69 + 70 + const queueLength = await queueManager.getQueueLength(); 71 + const mediaType = mediaData.isVideo ? 'Video' : 'Image'; 72 + 73 + this.bot.sendMessage( 74 + chatId, 75 + `Added to queue: ${mediaType} - ${mediaData.title}\nCurrent queue length: ${queueLength}`, 76 + { reply_to_message_id: msg.message_id } 77 + ); 78 + } catch (error) { 79 + this.bot.sendMessage( 80 + chatId, 81 + `Error processing link: ${error.message}`, 82 + { reply_to_message_id: msg.message_id } 83 + ); 84 + } 85 + } 86 + 87 + /** 88 + * Process a forwarded message that may contain links in text or buttons 89 + */ 90 + async processForwardedMessage(msg) { 91 + const chatId = msg.chat.id; 92 + 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 + // Extract URLs from the forwarded message 99 + const urls = this.extractUrlsFromMessage(msg); 100 + 101 + if (urls.length === 0) { 102 + // No URLs found - silently ignore 103 + return; 104 + } 105 + 106 + try { 107 + this.bot.sendMessage(chatId, `Processing forwarded message with ${urls.length} link(s)...`, { reply_to_message_id: msg.message_id }); 108 + 109 + const results = await scraperManager.extractFromUrls(urls); 110 + 111 + if (results.validResults.length === 0) { 112 + this.bot.sendMessage( 113 + chatId, 114 + '❌ No valid links found in the forwarded message.', 115 + { reply_to_message_id: msg.message_id } 116 + ); 117 + return; 118 + } 119 + 120 + // Add all valid results to queue 121 + for (const mediaData of results.validResults) { 122 + await queueManager.addToQueue(mediaData); 123 + } 124 + 125 + const queueLength = await queueManager.getQueueLength(); 126 + let responseMessage = `✅ Added ${results.validResults.length} item(s) to queue`; 127 + 128 + if (results.invalidUrls.length > 0) { 129 + responseMessage += ` (${results.invalidUrls.length} invalid link(s) silently discarded)`; 130 + } 131 + 132 + responseMessage += `\nCurrent queue length: ${queueLength}`; 133 + 134 + this.bot.sendMessage( 135 + chatId, 136 + responseMessage, 137 + { reply_to_message_id: msg.message_id } 138 + ); 139 + } catch (error) { 140 + this.bot.sendMessage( 141 + chatId, 142 + `Error processing forwarded message: ${error.message}`, 143 + { reply_to_message_id: msg.message_id } 144 + ); 145 + } 146 + } 147 + 148 + /** 149 + * Extract URLs from a message's text and inline keyboard buttons 150 + */ 151 + extractUrlsFromMessage(msg) { 152 + const urls = []; 153 + 154 + // Extract URLs from message text 155 + if (msg.text) { 156 + const textUrls = this.extractUrlsFromText(msg.text); 157 + urls.push(...textUrls); 158 + } 159 + 160 + // Extract URLs from inline keyboard buttons 161 + if (msg.reply_markup && msg.reply_markup.inline_keyboard) { 162 + const buttonUrls = this.extractUrlsFromInlineKeyboard(msg.reply_markup.inline_keyboard); 163 + urls.push(...buttonUrls); 164 + } 165 + 166 + // Remove duplicates 167 + return [...new Set(urls)]; 168 + } 169 + 170 + /** 171 + * Extract URLs from text using regex 172 + */ 173 + extractUrlsFromText(text) { 174 + const urlRegex = /https?:\/\/[^\s]+/g; 175 + const matches = text.match(urlRegex); 176 + return matches || []; 177 + } 178 + 179 + /** 180 + * Extract URLs from inline keyboard buttons 181 + */ 182 + extractUrlsFromInlineKeyboard(inlineKeyboard) { 183 + const urls = []; 184 + 185 + for (const row of inlineKeyboard) { 186 + for (const button of row) { 187 + if (button.url) { 188 + urls.push(button.url); 189 + } 190 + } 191 + } 192 + 193 + return urls; 70 194 } 71 195 72 196 // Allow other modules to set these properties for proper URL handling during announcements
+5 -5
queue/alert-state.json
··· 1 1 { 2 - "lowQueueAlertSent": false, 3 - "emptyQueueAlertSent": false, 4 - "lastLowQueueAlertTime": 1748136883217, 5 - "lastEmptyQueueAlertTime": 1748136540595, 6 - "lastSaved": 1749076222424 2 + "lowQueueAlertSent": true, 3 + "emptyQueueAlertSent": true, 4 + "lastLowQueueAlertTime": 1749155645693, 5 + "lastEmptyQueueAlertTime": 1749155676860, 6 + "lastSaved": 1749155688499 7 7 }
+39
utils/scraperManager.js
··· 38 38 39 39 return await scraper.extract(url); 40 40 } 41 + 42 + /** 43 + * Extract image data from multiple URLs, handling failures gracefully 44 + * @param {string[]} urls - Array of URLs to extract from 45 + * @returns {Promise<{validResults: Array, invalidUrls: string[]}>} - Results with valid extractions and invalid URLs 46 + */ 47 + async extractFromUrls(urls) { 48 + const validResults = []; 49 + const invalidUrls = []; 50 + 51 + for (const url of urls) { 52 + try { 53 + const scraper = this.findScraper(url); 54 + 55 + if (!scraper) { 56 + invalidUrls.push(url); 57 + continue; 58 + } 59 + 60 + const mediaData = await scraper.extract(url); 61 + 62 + // Check if the scraper returned an error (for temporarily disabled scrapers) 63 + if (mediaData.error) { 64 + invalidUrls.push(url); 65 + continue; 66 + } 67 + 68 + validResults.push(mediaData); 69 + } catch (error) { 70 + // Silently add to invalid URLs list 71 + invalidUrls.push(url); 72 + } 73 + } 74 + 75 + return { 76 + validResults, 77 + invalidUrls 78 + }; 79 + } 41 80 } 42 81 43 82 module.exports = new ScraperManager();