twitterbridge: decide new vs old by status id, not feed position

RSSHub's guid is the timeline entry's own id (a retweet's id), so ids follow
the order the account posted, while feed order and pubDate follow the
original tweet. A fresh retweet of an old post can sort to the bottom; it is
now posted, and only ids older than every known one are treated as deletion
slide-ins. !leaks sorts by id too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
esefandClaude Opus 5.5 committed 2026-10-06 21:29:08 +03:00
1 parent f8b3501637
commit 2af2adc780
2 files changed
+31 -12

No files matched your search

+3 -1
View File
@@ -3,7 +3,7 @@ import aiohttp
import feedparser import feedparser
from discord.ext import commands from discord.ext import commands
from twitterbridge.rss import embed_link from twitterbridge.rss import embed_link, tweet_id
REQUEST_TIMEOUT = 15 REQUEST_TIMEOUT = 15
@@ -51,6 +51,8 @@ class TwitterLeaks(commands.Cog):
return await ctx.reply(msg[:2000]) return await ctx.reply(msg[:2000])
response = "**Latest 5 Leaks:**\n" response = "**Latest 5 Leaks:**\n"
# the feed isn't chronological; status ids are
entries = sorted(entries, key=lambda e: tweet_id(e.get('id') or e.get('link', '')) or 0, reverse=True)
for entry in entries[:5]: for entry in entries[:5]:
response += f"\n{embed_link(entry.get('link', ''))}\n" response += f"\n{embed_link(entry.get('link', ''))}\n"
+28 -11
View File
@@ -43,6 +43,14 @@ def _guid(entry) -> str:
return entry.get('id') or entry.get('link', '') return entry.get('id') or entry.get('link', '')
def tweet_id(guid: str) -> int | None:
"""Status id from a guid. RSSHub uses the timeline entry's own id (a retweet's
id, not the original's), so ids grow in the order the account posted them,
unlike the feed order and pubDate (the original tweet's date)."""
tail = guid.rstrip('/').rsplit('/', 1)[-1]
return int(tail) if tail.isdigit() else None
class TwitterRSSForwarder: class TwitterRSSForwarder:
"""Polls the Twitter RSS feed and forwards new items to Discord webhooks. """Polls the Twitter RSS feed and forwards new items to Discord webhooks.
@@ -164,7 +172,7 @@ class TwitterRSSForwarder:
return return
seen_set = set(seen) seen_set = set(seen)
known = [i for i, g in enumerate(ids) if g in seen_set] known = [g for g in ids if g in seen_set]
if not known: if not known:
# nothing in the feed is familiar (long outage or a feed change): # nothing in the feed is familiar (long outage or a feed change):
# re-baseline instead of flooding the channel with the whole page # re-baseline instead of flooding the channel with the whole page
@@ -174,19 +182,28 @@ class TwitterRSSForwarder:
self._save_state() self._save_state()
return return
# Unseen entries above the lowest known one are new (the feed isn't strictly # Unseen entries newer than the oldest known one in this feed are new, wherever
# chronological, so they can sit below newer known tweets). Unseen entries # the feed puts them. Unseen entries older than every known one are old tweets
# below every known one are old tweets that slid in when something above # that slid in when something was deleted, so they are only remembered.
# was deleted, so they are only remembered. known_ids = [i for i in map(tweet_id, known) if i is not None]
lowest_known = known[-1] oldest_known = min(known_ids) if known_ids else None
new = [e for e in entries[:lowest_known] if _guid(e) not in seen_set] new, slid_in = [], []
tail = [g for g in ids[lowest_known:] if g not in seen_set] for e in entries:
if tail: g = _guid(e)
seen.extend(tail) if g in seen_set:
continue
tid = tweet_id(g)
if tid is not None and oldest_known is not None and tid < oldest_known:
slid_in.append(g)
else:
new.append(e)
if slid_in:
seen.extend(slid_in)
del seen[:-SEEN_LIMIT] del seen[:-SEEN_LIMIT]
self._save_state() self._save_state()
for entry in reversed(new): # oldest first new.sort(key=lambda e: tweet_id(_guid(e)) or 0)
for entry in new: # oldest first
if not await self._post(name, webhook, entry): if not await self._post(name, webhook, entry):
break # leave the rest unseen; retried next poll break # leave the rest unseen; retried next poll
seen.append(_guid(entry)) seen.append(_guid(entry))