# this is where twitter/x rss-to-discord forwarding lives import os import asyncio import aiohttp import feedparser POLL_INTERVAL = 300 # 5 minutes GUID_FILE = 'data/last_tweet.txt' class TwitterRSSForwarder: def __init__(self, rss_url, webhook_url, guid_file='data/last_tweet.txt'): self.rss_url = rss_url self.webhook_url = webhook_url self.guid_file = guid_file self.last_guid = None self._running = False async def start(self): self._running = True self._load_last_guid() print(f'✅ twitter rss forwarder started — polling {self.rss_url}') while self._running: try: await self._poll() except Exception as e: print(f'twitter rss poll failed: {e}') await asyncio.sleep(POLL_INTERVAL) def stop(self): self._running = False print('twitter rss forwarder stopped') def _load_last_guid(self): if os.path.exists(self.guid_file): with open(self.guid_file, 'r') as f: self.last_guid = f.read().strip() or None def _save_last_guid(self): os.makedirs('data', exist_ok=True) with open(self.guid_file, 'w') as f: f.write(self.last_guid or '') async def _poll(self): headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} async with aiohttp.ClientSession(headers=headers) as session: async with session.get(self.rss_url) as resp: text = await resp.text() feed = feedparser.parse(text) if feed.bozo and not feed.entries: print(f'twitter rss feed error: {feed.bozo_exception}') return if not feed.entries: return # first run — just record the latest guid, don't post anything if self.last_guid is None: self.last_guid = feed.entries[0].get('id', '') self._save_last_guid() print(f'twitter rss first run — recorded guid: {self.last_guid}') return # collect new entries (entries are newest-first in rss) new_entries = [] for entry in feed.entries: guid = entry.get('id', '') if guid == self.last_guid: break new_entries.append(entry) if not new_entries: return # post oldest first so discord order matches timeline for entry in reversed(new_entries): await self._post_to_discord(entry) # update last seen guid to the newest self.last_guid = new_entries[0].get('id', '') self._save_last_guid() async def _post_to_discord(self, entry): title = entry.get('title', '') link = entry.get('link', '') # nitter links -> real twitter links if 'nitter.net' in link: link = link.replace('nitter.net', 'twitter.com').replace('#m', '') content = f'{title}\n{link}'.strip() payload = { 'username': '@CalabiyauLeaks', 'content': content[:2000], } async with aiohttp.ClientSession() as session: async with session.post(self.webhook_url, json=payload) as resp: if resp.status not in (200, 204): print(f'twitter webhook post failed: {resp.status} {await resp.text()}') else: print(f'twitter rss posted: {title[:60]}...')