import { getFirstHtml, getImgTitle } from "@common"; import { config } from "@constants"; import dayjs from "dayjs"; import customParseFormat from "dayjs/plugin/customParseFormat"; import { Client, MessageEmbed, TextChannel } from "discord.js"; dayjs.extend(customParseFormat); /** * Retry mechanism for failed date checks * * This implementation allows the bot to retry fetching and posting content * when a date check fails. It will retry at hourly intervals for a number of attempts * specified by the RETRY_ATTEMPTS environment variable (defaults to 3). * * This is useful when: * 1. The website hasn't updated with today's post yet * 2. There are temporary network issues * 3. The website structure changed temporarily */ // Sleep function to delay between retry attempts const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); export async function sendDiscordMessage( client: Client, url: string, dateCheck?: dayjs.Dayjs ): Promise { const { img, title } = await getImgTitle(url); if (dateCheck) { const dateRegex = /\d{1,2}.\d{1,2}.\d{4}/g; const date = dateRegex.exec(title)?.[0]; const dayjsDate = dayjs(date, config.LEGICA_DATE_FORMAT); if (!dateCheck.isSame(dayjsDate, "D")) throw new Error( `Post failed date check, date from post ${date}, date checked ${dateCheck.format( config.LEGICA_DATE_FORMAT )}` ); } try { const promises = client.channels.cache.map(async (channel) => { try { if (channel.type !== "text") return null; const embeddedMessage = new MessageEmbed().setTitle(title).setImage(img); const msg = await (channel as TextChannel).send(embeddedMessage); const reactions = [ "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", ]; for (const reaction of reactions) { try { await msg.react(reaction); } catch { console.error(`Reaction ${reaction} to channel ${channel.id} failed.`); } } } catch (err) { console.error(`Message to channel ${channel.id} failed.`); } }); await Promise.all(promises); } catch (err) { console.error(err); } } /** * Fetches and sends the next legica-dana post to all Discord channels * * This function implements a retry mechanism that will: * 1. Try to fetch and post the latest content * 2. If it fails (especially due to date check), wait for 1 hour * 3. Retry up to the number of times specified in RETRY_ATTEMPTS env var * * @param client The Discord client used to send messages * @throws Error if all retry attempts fail */ export async function sendNextMessage(client: Client): Promise { // Get max retry attempts from config const maxRetries = config.RETRY_ATTEMPTS; let attempts = 0; let lastError: Error | null = null; // Keep trying until we've reached max attempts while (attempts < maxRetries) { try { // Get the URL of the latest post const href = await getFirstHtml(); if (!href) throw new Error("URL cannot be empty!"); // Try to send the message await sendDiscordMessage(client, href, dayjs()); // If successful, return return; } catch (error: unknown) { attempts++; const typedError = error instanceof Error ? error : new Error(String(error)); lastError = typedError; // Log the retry attempt console.error( `Attempt ${attempts}/${maxRetries} failed: ${typedError.message}` ); // If we've reached max attempts, throw the last error if (attempts >= maxRetries) { throw new Error( `Failed after ${attempts} attempts. Last error: ${ lastError?.message || "Unknown error" }` ); } // Wait for 1 hour before retrying (3600000 ms) console.log( `Waiting 1 hour before retry attempt ${attempts + 1}/${maxRetries}...` ); await sleep(3600000); } } }