diff --git a/lib/commands.js b/lib/commands.js index 4cb710a..b8753f9 100644 --- a/lib/commands.js +++ b/lib/commands.js @@ -66,6 +66,8 @@ async function handleAddFeed(ctx, msg, args) { if (detectResult?.error) { if (detectResult.error === 402) { await safeReply(ctx, msg, "❌ Cannot add feed: Payment Required (HTTP 402). The feed publisher requires payment or authentication to access this feed."); + } else if (detectResult.error === 429) { + await safeReply(ctx, msg, "❌ Cannot add feed: Too Many Requests (HTTP 429). The feed publisher is rate-limiting requests."); } else if (detectResult.error === 404) { await safeReply(ctx, msg, "❌ Cannot add feed: Not Found (HTTP 404). The feed URL does not exist."); } else { diff --git a/lib/feeds.js b/lib/feeds.js index 209b251..ba28d08 100644 --- a/lib/feeds.js +++ b/lib/feeds.js @@ -128,6 +128,12 @@ async function parseXmlFeed(parser, feed) { const response = await fetch(feed.url, { headers, signal: controller.signal }); if (response.status === 304) return []; + if (response.status === 402 || response.status === 429) { + const err = new Error(`Feed ${feed.url} returned HTTP ${response.status}`); + err.feedGone = true; + err.status = response.status; + throw err; + } if (!response.ok) throw new Error(`HTTP Error ${response.status}: ${response.statusText}`); const text = await response.text(); @@ -177,6 +183,12 @@ async function parseJsonFeed(feed) { const response = await fetch(feed.url, { headers, signal: controller.signal }); if (response.status === 304) return []; + if (response.status === 402 || response.status === 429) { + const err = new Error(`Feed ${feed.url} returned HTTP ${response.status}`); + err.feedGone = true; + err.status = response.status; + throw err; + } if (!response.ok) throw new Error(`HTTP Error ${response.status}`); const feedData = await response.json(); @@ -257,10 +269,33 @@ async function _processFeedItems(ctx, feed, shouldPost) { return shouldPost ? { newItemsCount } : undefined; } +async function _removeFeedAndNotify(ctx, feed, status) { + const reason = status === 402 ? "now requires payment" : "is rate-limited"; + console.log(`Feed ${feed.url} ${reason} — removing`); + + await ctx.db.execute("DELETE FROM feeds WHERE url = ? AND channel_id = ?", [feed.url, feed.channel_id]); + ctx.feeds.delete(`${feed.url}-${feed.channel_id}`); + ctx.setBotStatus(); + + try { + const serverData = await ctx.stoat.getServer(feed.server_id); + if (serverData?.owner) { + const dmChannel = await ctx.stoat.openDM(serverData.owner); + await dmChannel.sendMessage(`⚠️ AutoFeeds removed the feed \`${feed.url}\` from <#${feed.channel_id}> because the feed ${reason} (HTTP ${status}).`); + } + } catch (err) { + console.error("Failed to notify server owner about feed removal:", err); + } +} + async function checkFeed(ctx, feed) { try { return await _processFeedItems(ctx, feed, true); } catch (error) { + if (error.feedGone) { + await _removeFeedAndNotify(ctx, feed, error.status); + return { newItemsCount: 0, error: true }; + } console.error(`Error checking feed ${feed.url}:`, error); return { newItemsCount: 0, error: true }; }