···55## Features
6677- Extract images from various supported websites
88+- Process forwarded messages containing links (including button links)
89- Queue images for scheduled posting
910- Customizable posting schedule using cron syntax
1011- Access control to limit who can use the bot
···270271271272### Adding Images to Queue
272273274274+#### Direct Links
273275Send 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.
276276+277277+#### Forwarded Messages
278278+The bot can also process forwarded messages that contain links to supported websites. This includes:
279279+280280+- **Text Links**: Any URLs found in the forwarded message text will be extracted and processed
281281+- **Button Links**: URLs embedded in inline keyboard buttons (common in channel posts) are automatically detected and processed
282282+- **Multiple Links**: If a forwarded message contains multiple valid links, all will be processed simultaneously
283283+- **Error Handling**: Invalid links are silently discarded - the bot will only show an error if zero valid links are found
284284+285285+**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.
274286275287### Interactive Queue Management
276288
+1
bot/telegrambot/commands/help.js
···2424/update - Update bot from GitHub repository (owner only)
25252626Send any link to a supported site to add it to the queue.
2727+Forward messages with links or buttons to extract and queue multiple items.
2728Supported sites: e621, FurAffinity, SoFurry, Weasyl, Bluesky
2829 `;
2930 this.bot.sendMessage(chatId, helpText);
+174-50
bot/telegrambot/commands/linkHandler.js
···14141515 register() {
1616 this.bot.on('message', async (msg) => {
1717+ // Check for direct URL messages
1718 if (msg.text && msg.text.startsWith('http')) {
1818- const chatId = msg.chat.id;
1919-2020- // Skip processing if this is part of an announcement setup
2121- const isInAnnouncementFlow = this.pendingAnnouncements && this.pendingAnnouncements[msg.from.id];
2222- const isInButtonEditFlow = this.editingAnnouncementButton && this.editingAnnouncementButton[msg.from.id];
2323-2424- if (isInAnnouncementFlow || isInButtonEditFlow) {
2525- // This URL is part of an announcement setup, so we should not process it as a link
2626- return;
2727- }
2828-2929- if (!this.authHelper.isAuthorized(msg.from.id)) {
3030- this.bot.sendMessage(chatId, 'You are not authorized to use this bot.');
3131- return;
3232- }
3333-3434- try {
3535- const url = msg.text.trim();
3636-3737- this.bot.sendMessage(chatId, 'Processing link...', { reply_to_message_id: msg.message_id });
3838-3939- const mediaData = await scraperManager.extractFromUrl(url);
4040-4141- // Check if the scraper returned an error (for temporarily disabled scrapers)
4242- if (mediaData.error) {
4343- this.bot.sendMessage(
4444- chatId,
4545- mediaData.error,
4646- { reply_to_message_id: msg.message_id }
4747- );
4848- return;
4949- }
5050-5151- await queueManager.addToQueue(mediaData);
5252-5353- const queueLength = await queueManager.getQueueLength();
5454- const mediaType = mediaData.isVideo ? 'Video' : 'Image';
5555-5656- this.bot.sendMessage(
5757- chatId,
5858- `Added to queue: ${mediaType} - ${mediaData.title}\nCurrent queue length: ${queueLength}`,
5959- { reply_to_message_id: msg.message_id }
6060- );
6161- } catch (error) {
6262- this.bot.sendMessage(
6363- chatId,
6464- `Error processing link: ${error.message}`,
6565- { reply_to_message_id: msg.message_id }
6666- );
6767- }
1919+ await this.processDirectUrl(msg);
2020+ return;
2121+ }
2222+2323+ // Check for forwarded messages
2424+ if (msg.forward_from || msg.forward_from_chat) {
2525+ await this.processForwardedMessage(msg);
2626+ return;
6827 }
6928 });
2929+ }
3030+3131+ /**
3232+ * Process a direct URL message
3333+ */
3434+ async processDirectUrl(msg) {
3535+ const chatId = msg.chat.id;
3636+3737+ // Skip processing if this is part of an announcement setup
3838+ const isInAnnouncementFlow = this.pendingAnnouncements && this.pendingAnnouncements[msg.from.id];
3939+ const isInButtonEditFlow = this.editingAnnouncementButton && this.editingAnnouncementButton[msg.from.id];
4040+4141+ if (isInAnnouncementFlow || isInButtonEditFlow) {
4242+ // This URL is part of an announcement setup, so we should not process it as a link
4343+ return;
4444+ }
4545+4646+ if (!this.authHelper.isAuthorized(msg.from.id)) {
4747+ this.bot.sendMessage(chatId, 'You are not authorized to use this bot.');
4848+ return;
4949+ }
5050+5151+ try {
5252+ const url = msg.text.trim();
5353+5454+ this.bot.sendMessage(chatId, 'Processing link...', { reply_to_message_id: msg.message_id });
5555+5656+ const mediaData = await scraperManager.extractFromUrl(url);
5757+5858+ // Check if the scraper returned an error (for temporarily disabled scrapers)
5959+ if (mediaData.error) {
6060+ this.bot.sendMessage(
6161+ chatId,
6262+ mediaData.error,
6363+ { reply_to_message_id: msg.message_id }
6464+ );
6565+ return;
6666+ }
6767+6868+ await queueManager.addToQueue(mediaData);
6969+7070+ const queueLength = await queueManager.getQueueLength();
7171+ const mediaType = mediaData.isVideo ? 'Video' : 'Image';
7272+7373+ this.bot.sendMessage(
7474+ chatId,
7575+ `Added to queue: ${mediaType} - ${mediaData.title}\nCurrent queue length: ${queueLength}`,
7676+ { reply_to_message_id: msg.message_id }
7777+ );
7878+ } catch (error) {
7979+ this.bot.sendMessage(
8080+ chatId,
8181+ `Error processing link: ${error.message}`,
8282+ { reply_to_message_id: msg.message_id }
8383+ );
8484+ }
8585+ }
8686+8787+ /**
8888+ * Process a forwarded message that may contain links in text or buttons
8989+ */
9090+ async processForwardedMessage(msg) {
9191+ const chatId = msg.chat.id;
9292+9393+ if (!this.authHelper.isAuthorized(msg.from.id)) {
9494+ this.bot.sendMessage(chatId, 'You are not authorized to use this bot.');
9595+ return;
9696+ }
9797+9898+ // Extract URLs from the forwarded message
9999+ const urls = this.extractUrlsFromMessage(msg);
100100+101101+ if (urls.length === 0) {
102102+ // No URLs found - silently ignore
103103+ return;
104104+ }
105105+106106+ try {
107107+ this.bot.sendMessage(chatId, `Processing forwarded message with ${urls.length} link(s)...`, { reply_to_message_id: msg.message_id });
108108+109109+ const results = await scraperManager.extractFromUrls(urls);
110110+111111+ if (results.validResults.length === 0) {
112112+ this.bot.sendMessage(
113113+ chatId,
114114+ '❌ No valid links found in the forwarded message.',
115115+ { reply_to_message_id: msg.message_id }
116116+ );
117117+ return;
118118+ }
119119+120120+ // Add all valid results to queue
121121+ for (const mediaData of results.validResults) {
122122+ await queueManager.addToQueue(mediaData);
123123+ }
124124+125125+ const queueLength = await queueManager.getQueueLength();
126126+ let responseMessage = `✅ Added ${results.validResults.length} item(s) to queue`;
127127+128128+ if (results.invalidUrls.length > 0) {
129129+ responseMessage += ` (${results.invalidUrls.length} invalid link(s) silently discarded)`;
130130+ }
131131+132132+ responseMessage += `\nCurrent queue length: ${queueLength}`;
133133+134134+ this.bot.sendMessage(
135135+ chatId,
136136+ responseMessage,
137137+ { reply_to_message_id: msg.message_id }
138138+ );
139139+ } catch (error) {
140140+ this.bot.sendMessage(
141141+ chatId,
142142+ `Error processing forwarded message: ${error.message}`,
143143+ { reply_to_message_id: msg.message_id }
144144+ );
145145+ }
146146+ }
147147+148148+ /**
149149+ * Extract URLs from a message's text and inline keyboard buttons
150150+ */
151151+ extractUrlsFromMessage(msg) {
152152+ const urls = [];
153153+154154+ // Extract URLs from message text
155155+ if (msg.text) {
156156+ const textUrls = this.extractUrlsFromText(msg.text);
157157+ urls.push(...textUrls);
158158+ }
159159+160160+ // Extract URLs from inline keyboard buttons
161161+ if (msg.reply_markup && msg.reply_markup.inline_keyboard) {
162162+ const buttonUrls = this.extractUrlsFromInlineKeyboard(msg.reply_markup.inline_keyboard);
163163+ urls.push(...buttonUrls);
164164+ }
165165+166166+ // Remove duplicates
167167+ return [...new Set(urls)];
168168+ }
169169+170170+ /**
171171+ * Extract URLs from text using regex
172172+ */
173173+ extractUrlsFromText(text) {
174174+ const urlRegex = /https?:\/\/[^\s]+/g;
175175+ const matches = text.match(urlRegex);
176176+ return matches || [];
177177+ }
178178+179179+ /**
180180+ * Extract URLs from inline keyboard buttons
181181+ */
182182+ extractUrlsFromInlineKeyboard(inlineKeyboard) {
183183+ const urls = [];
184184+185185+ for (const row of inlineKeyboard) {
186186+ for (const button of row) {
187187+ if (button.url) {
188188+ urls.push(button.url);
189189+ }
190190+ }
191191+ }
192192+193193+ return urls;
70194 }
7119572196 // Allow other modules to set these properties for proper URL handling during announcements