twitter forward feature

This commit is contained in:
esef committed 2026-06-22 22:36:26 +03:00
1 parent 1ce7ea15d9
commit 16e9b54a76
8 files changed
+501 -57

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
gifs/ gifs/
credentials.env credentials.env
.vscode/__pycache__/ .vscode/__pycache__/
data/
+8
View File
@@ -6,6 +6,7 @@ import logging
import asyncio import asyncio
from tgbridge.forwarder import TelegramForwarder from tgbridge.forwarder import TelegramForwarder
from twitterbridge.rss import TwitterRSSForwarder
load_dotenv('credentials.env') load_dotenv('credentials.env')
@@ -20,10 +21,16 @@ forwarder = TelegramForwarder(
webhook_url=os.getenv('DISCORD_WEBHOOK_URL'), webhook_url=os.getenv('DISCORD_WEBHOOK_URL'),
) )
twitter = TwitterRSSForwarder(
rss_url=os.getenv('TWITTER_RSS_URL'),
webhook_url=os.getenv('TWITTER_DISCORD_WEBHOOK'),
)
@bot.event @bot.event
async def on_ready(): async def on_ready():
print(f'✅ {bot.user} is online!') print(f'✅ {bot.user} is online!')
asyncio.create_task(forwarder.start()) asyncio.create_task(forwarder.start())
asyncio.create_task(twitter.start())
async def main(): async def main():
async with bot: async with bot:
@@ -38,6 +45,7 @@ async def main():
await bot.start(os.getenv('DISCORD_TOKEN')) await bot.start(os.getenv('DISCORD_TOKEN'))
finally: finally:
await forwarder.stop() await forwarder.stop()
twitter.stop()
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
asyncio.run(main()) asyncio.run(main())
+28 -57
View File
@@ -1,83 +1,71 @@
# this is where telegram-to-discord forwarding lives # this is where telegram-to-discord forwarding lives
import logging
import io import io
import json import json
import aiohttp import aiohttp
from telegram import Update from telegram.ext import Application, MessageHandler, filters
from telegram.ext import Application, MessageHandler, filters, ContextTypes
logger = logging.getLogger('telegram.forwarder')
class TelegramForwarder: class TelegramForwarder:
"""Watches a Telegram group/channel and forwards messages to a Discord webhook.""" def __init__(self, token, group_id, webhook_url):
def __init__(self, token: str, group_id: int, webhook_url: str):
self.token = token self.token = token
self.group_id = group_id self.group_id = group_id
self.webhook_url = webhook_url self.webhook_url = webhook_url
self._app: Application | None = None self._app = None
async def start(self): async def start(self):
"""Build the telegram application and start polling.""" self._app = Application.builder().token(self.token).build()
self._app = (
Application.builder()
.token(self.token)
.build()
)
self._app.add_handler(MessageHandler(filters.ALL, self._on_message)) self._app.add_handler(MessageHandler(filters.ALL, self._on_message))
await self._app.initialize() await self._app.initialize()
await self._app.start() await self._app.start()
await self._app.updater.start_polling(drop_pending_updates=True) await self._app.updater.start_polling(drop_pending_updates=True)
logger.info('✅ Telegram forwarder started — watching group %s', self.group_id) print(f'✅ telegram forwarder started — watching group {self.group_id}')
async def stop(self): async def stop(self):
"""Graceful shutdown."""
if self._app: if self._app:
await self._app.updater.stop() await self._app.updater.stop()
await self._app.stop() await self._app.stop()
await self._app.shutdown() await self._app.shutdown()
logger.info('Telegram forwarder stopped') print('telegram forwarder stopped')
# ── internal helpers ───────────────────────────────────────────────────── # internal helpers
def _sender_name(self, update):
def _sender_name(self, update: Update) -> str:
"""Extract a display name from the message sender."""
user = update.effective_user user = update.effective_user
if user: if user:
name = user.first_name or '' name = user.first_name or ''
if user.last_name: if user.last_name:
name += f' {user.last_name}' name += f' {user.last_name}'
return name.strip() or user.username or 'Unknown' return name.strip() or user.username or 'unknown'
# channel posts have no user — use chat title # channel posts have no user — use chat title
chat = update.effective_chat chat = update.effective_chat
if chat and chat.title: if chat and chat.title:
return chat.title return chat.title
return 'Unknown' return 'unknown'
def _forward_prefix(self, message) -> str: def _forward_prefix(self, message):
"""Build a prefix for forwarded messages."""
if message.forward_origin: if message.forward_origin:
origin = message.forward_origin origin = message.forward_origin
# user forward # user forward
if hasattr(origin, 'sender_user') and origin.sender_user: if hasattr(origin, 'sender_user') and origin.sender_user:
u = origin.sender_user u = origin.sender_user
name = u.first_name or '' name = u.first_name or ''
if u.last_name: if u.last_name:
name += f' {u.last_name}' name += f' {u.last_name}'
return f'↩️ Forwarded from **{name.strip() or u.username or "Unknown"}**:\n' return f'↩️ Forwarded from **{name.strip() or u.username or "unknown"}**:\n'
# channel forward # channel forward
if hasattr(origin, 'chat') and origin.chat: if hasattr(origin, 'chat') and origin.chat:
return f'↩️ Forwarded from **{origin.chat.title or "Unknown channel"}**:\n' return f'↩️ Forwarded from **{origin.chat.title or "unknown channel"}**:\n'
# hidden user # hidden user
if hasattr(origin, 'sender_user_name') and origin.sender_user_name: if hasattr(origin, 'sender_user_name') and origin.sender_user_name:
return f'↩️ Forwarded from **{origin.sender_user_name}**:\n' return f'↩️ Forwarded from **{origin.sender_user_name}**:\n'
return '↩️ Forwarded:\n' return '↩️ Forwarded:\n'
return '' return ''
async def _download_file(self, file_id: str) -> tuple[bytes, str]: async def _download_file(self, file_id):
"""Download a file from Telegram, return (bytes, filename)."""
tg_file = await self._app.bot.get_file(file_id) tg_file = await self._app.bot.get_file(file_id)
buf = io.BytesIO() buf = io.BytesIO()
await tg_file.download_to_memory(buf) await tg_file.download_to_memory(buf)
@@ -85,35 +73,28 @@ class TelegramForwarder:
filename = tg_file.file_path.split('/')[-1] if tg_file.file_path else 'file' filename = tg_file.file_path.split('/')[-1] if tg_file.file_path else 'file'
return buf.read(), filename return buf.read(), filename
async def _send_to_webhook(self, username: str, content: str = '', async def _send_to_webhook(self, username, content='', file_data=None, filename=None):
file_data: bytes | None = None,
filename: str | None = None):
"""POST a message (and optional file) to the Discord webhook."""
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
data = aiohttp.FormData() data = aiohttp.FormData()
payload = {'username': username} payload = {'username': username}
if content: if content:
payload['content'] = content[:2000] # discord limit payload['content'] = content[:2000]
if file_data and filename: if file_data and filename:
data.add_field('payload_json', json.dumps(payload), data.add_field('payload_json', json.dumps(payload), content_type='application/json')
content_type='application/json')
data.add_field('file', file_data, filename=filename) data.add_field('file', file_data, filename=filename)
else: else:
# simple json post
async with session.post(self.webhook_url, json=payload) as resp: async with session.post(self.webhook_url, json=payload) as resp:
if resp.status not in (200, 204): if resp.status not in (200, 204):
logger.error('Webhook POST failed: %s %s', resp.status, await resp.text()) print(f'webhook post failed: {resp.status} {await resp.text()}')
return return
async with session.post(self.webhook_url, data=data) as resp: async with session.post(self.webhook_url, data=data) as resp:
if resp.status not in (200, 204): if resp.status not in (200, 204):
logger.error('Webhook POST failed: %s %s', resp.status, await resp.text()) print(f'webhook post failed: {resp.status} {await resp.text()}')
# ── main message handler ───────────────────────────────────────────────── # main message handler
async def _on_message(self, update, context):
async def _on_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle every incoming message from the watched group."""
message = update.effective_message message = update.effective_message
if not message: if not message:
return return
@@ -127,53 +108,43 @@ class TelegramForwarder:
caption = message.caption or '' caption = message.caption or ''
try: try:
# ── photo ────────────────────────────────────────────────────
if message.photo: if message.photo:
# largest resolution is last in the array
photo = message.photo[-1] photo = message.photo[-1]
file_data, filename = await self._download_file(photo.file_id) file_data, filename = await self._download_file(photo.file_id)
text = f'{prefix}{caption}'.strip() text = f'{prefix}{caption}'.strip()
await self._send_to_webhook(sender, text, file_data, filename) await self._send_to_webhook(sender, text, file_data, filename)
# ── video ────────────────────────────────────────────────────
elif message.video: elif message.video:
file_data, filename = await self._download_file(message.video.file_id) file_data, filename = await self._download_file(message.video.file_id)
text = f'{prefix}{caption}'.strip() text = f'{prefix}{caption}'.strip()
await self._send_to_webhook(sender, text, file_data, filename) await self._send_to_webhook(sender, text, file_data, filename)
# ── document (files, gifs sent as docs, etc) ─────────────────
elif message.document: elif message.document:
file_data, filename = await self._download_file(message.document.file_id) file_data, filename = await self._download_file(message.document.file_id)
text = f'{prefix}{caption}'.strip() text = f'{prefix}{caption}'.strip()
await self._send_to_webhook(sender, text, file_data, filename) await self._send_to_webhook(sender, text, file_data, filename)
# ── animation (GIF) ──────────────────────────────────────────
elif message.animation: elif message.animation:
file_data, filename = await self._download_file(message.animation.file_id) file_data, filename = await self._download_file(message.animation.file_id)
text = f'{prefix}{caption}'.strip() text = f'{prefix}{caption}'.strip()
await self._send_to_webhook(sender, text, file_data, filename) await self._send_to_webhook(sender, text, file_data, filename)
# ── sticker ──────────────────────────────────────────────────
elif message.sticker: elif message.sticker:
# stickers have no caption — send emoji or name
sticker_text = message.sticker.emoji or '(sticker)' sticker_text = message.sticker.emoji or '(sticker)'
text = f'{prefix}{sticker_text}' text = f'{prefix}{sticker_text}'
# try to download the sticker image
try: try:
file_data, filename = await self._download_file(message.sticker.file_id) file_data, filename = await self._download_file(message.sticker.file_id)
await self._send_to_webhook(sender, text, file_data, filename) await self._send_to_webhook(sender, text, file_data, filename)
except Exception: except Exception:
await self._send_to_webhook(sender, text) await self._send_to_webhook(sender, text)
# ── text ─────────────────────────────────────────────────────
elif message.text: elif message.text:
text = f'{prefix}{message.text}' text = f'{prefix}{message.text}'
await self._send_to_webhook(sender, text) await self._send_to_webhook(sender, text)
else: else:
# voice, audio, contact, location, etc — just note it
text = f'{prefix}*(unsupported message type)*' text = f'{prefix}*(unsupported message type)*'
await self._send_to_webhook(sender, text) await self._send_to_webhook(sender, text)
except Exception: except Exception as e:
logger.exception('Failed to forward message from %s', sender) print(f'failed to forward message from {sender}: {e}')
+1
View File
@@ -0,0 +1 @@
Binary file not shown.
Binary file not shown.
+363
View File
@@ -0,0 +1,363 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
<channel>
<atom:link href="https://nitter.net/CalabiyauLeaks/rss" rel="self" type="application/rss+xml" />
<title>Strinova Leaks/News / @CalabiyauLeaks</title>
<link>https://nitter.net/CalabiyauLeaks</link>
<description>Twitter feed for: @CalabiyauLeaks. Generated by https://nitter.net
</description>
<language>en-us</language>
<ttl>40</ttl>
<image>
<title>Strinova Leaks/News / @CalabiyauLeaks</title>
<link>https://nitter.net/CalabiyauLeaks</link>
<url>https://nitter.net/pic/pbs.twimg.com%2Fprofile_images%2F1834591186776186880%2FR6_I7Gu3_400x400.jpg</url>
<width>128</width>
<height>128</height>
</image>
<item>
<title>RT by @CalabiyauLeaks: 月曜日はHP上限-50% #ストリノヴァ #Strinova</title>
<dc:creator>@Strinova_JP</dc:creator>
<description><![CDATA[<p>月曜日はHP上限-50%<br>
<a href="https://nitter.net/search?f=tweets&q=%23ストリノヴァ">#ストリノヴァ</a> <a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHLYuizKboAAhFPG.png" style="max-width:250px;" />]]></description>
<pubDate>Mon, 22 Jun 2026 03:02:21 GMT</pubDate>
<guid isPermaLink="false">2068892319978545317</guid>
<link>https://nitter.net/Strinova_JP/status/2068892319978545317#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: 【汐 - 新外観予告】 レジェンダリー外観 -「百万ボルトベイビー」 ✨6月30日登場!✨ 「大会のスーパースターたるあーしには、 スーパーヒーローのスーツがお似合いっしょ!」 #ストリノヴァ #Strinova</title>
<dc:creator>@Strinova_JP</dc:creator>
<description><![CDATA[<p>【汐 - 新外観予告】<br>
レジェンダリー外観 -「百万ボルトベイビー」<br>
✨6月30日登場!✨ <br>
<br>
「大会のスーパースターたるあーしには、 スーパーヒーローのスーツがお似合いっしょ!」<br>
<a href="https://nitter.net/search?f=tweets&q=%23ストリノヴァ">#ストリノヴァ</a> <a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHLGEiCoaMAAjD4l.jpg" style="max-width:250px;" />]]></description>
<pubDate>Sun, 21 Jun 2026 09:00:01 GMT</pubDate>
<guid isPermaLink="false">2068619941817790692</guid>
<link>https://nitter.net/Strinova_JP/status/2068619941817790692#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: Cielle Legendary Outfit &amp; Weapon Skin | Preview ⚡ All systems go — Zappy&apos;s ready to steal the show! Cielle hits the Infinite Apex Grand Prix in her “Winged Princess” battle suit — elegant, playful, and built to steal the spotlight. Available starting July 2, 00:00 UTC. ⏰ #Strinova</title>
<dc:creator>@Strinova_EN</dc:creator>
<description><![CDATA[<p>Cielle Legendary Outfit & Weapon Skin | Preview ⚡<br>
<br>
All systems go — Zappy's ready to steal the show!<br>
Cielle hits the Infinite Apex Grand Prix in her “Winged Princess” battle suit — elegant, playful, and built to steal the spotlight.<br>
Available starting July 2, 00:00 UTC. ⏰<br>
<br>
<a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHLFPUmGaYAALiUm.jpg" style="max-width:250px;" />]]></description>
<pubDate>Sun, 21 Jun 2026 09:00:02 GMT</pubDate>
<guid isPermaLink="false">2068619945588453653</guid>
<link>https://nitter.net/Strinova_EN/status/2068619945588453653#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: Happy Birthday, Cielle. 🎂 She follows her own compass — she doesn’t need to be anyone’s harbor or sail. May today find her gently, like sunlight resting on a quiet table, making everything feel a little softer than usual. 🎂 Drop your birthday wishes for Cielle below and include your Game ID for a chance to win 288 Bablo Crystals! 10 winners will be selected. Deadline: June 27, 23:59 UTC #Strinova</title>
<dc:creator>@Strinova_EN</dc:creator>
<description><![CDATA[<p>Happy Birthday, Cielle. 🎂<br>
<br>
She follows her own compass — she doesn’t need to be anyone’s harbor or sail.<br>
May today find her gently, like sunlight resting on a quiet table, making everything feel a little softer than usual.<br>
<br>
🎂 Drop your birthday wishes for Cielle below and include your Game ID for a chance to win 288 Bablo Crystals!<br>
10 winners will be selected.<br>
Deadline: June 27, 23:59 UTC<br>
<a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHLFQssAa8AEraVt.jpg" style="max-width:250px;" />]]></description>
<pubDate>Sun, 21 Jun 2026 07:00:00 GMT</pubDate>
<guid isPermaLink="false">2068589736730464261</guid>
<link>https://nitter.net/Strinova_EN/status/2068589736730464261#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: ⋱\🎂𝗛𝗮𝗽𝗽𝘆 𝗕𝗶𝗿𝘁𝗵𝗱𝗮𝘆🎂/⋰ 【汐 お誕生日おめでとう!❣】 -タダで食べる満腹ディナー🍽️ 「ふぅ……っ! やっと生き返ったぁー!」 「お嬢様~、今回の依頼料、ちゃんと払ってくれたら、あーし絶対自分にご褒美あげなきゃっしょ〜」 「オードリーでいいって言ってるでしょ……まあいいわ。今日が何の日か、分かってる?」 「え? んー……うっわ、やば! もしかして『汐』って名前の誕生日、もう来ちゃった感じ!?」 「ちょっ、ケーキ食べて、お散歩して、絵を買うっていうあーしの完璧なプランが!! なんで忘れてたのよぉ!」 「あんたね……はあ、任務が急すぎたせいもあるし。行くわよ、今日は私がおごるから。」 「にっしっしー 、じゃあ、あーしとあんたがこれからも元気に生きていけることと、このタダ飯、次もその次もあるように! 行くっしょ!」 💌今日は特別な日。汐の誕生日に、たくさんの祝福を届けましょう🎂🌟 🎁コメントでお祝いメッセージを送ってくれた方の中から、抽選で10名様に「パブロクリスタル×288」をプレゼント! 📅応募期間: 2026年6月21日(日)~2026年6月27日(土) ※当選されたプレイヤーには、イベント終了後7営業日以内に公式Xアカウントよりダイレクトメッセージでご連絡し、必要情報のご提供をお願いいたします。DMの受信箱をご確認ください。 #ストリノヴァ #Strinova</title>
<dc:creator>@Strinova_JP</dc:creator>
<description><![CDATA[<p>⋱\🎂𝗛𝗮𝗽𝗽𝘆 𝗕𝗶𝗿𝘁𝗵𝗱𝗮𝘆🎂/⋰ <br>
【汐 お誕生日おめでとう!❣】<br>
<br>
-タダで食べる満腹ディナー🍽️ <br>
「ふぅ……っ! やっと生き返ったぁー!」 <br>
「お嬢様~、今回の依頼料、ちゃんと払ってくれたら、あーし絶対自分にご褒美あげなきゃっしょ〜」 <br>
「オードリーでいいって言ってるでしょ……まあいいわ。今日が何の日か、分かってる?」 <br>
「え? んー……うっわ、やば! もしかして『汐』って名前の誕生日、もう来ちゃった感じ!?」 <br>
「ちょっ、ケーキ食べて、お散歩して、絵を買うっていうあーしの完璧なプランが!! なんで忘れてたのよぉ!」 <br>
「あんたね……はあ、任務が急すぎたせいもあるし。行くわよ、今日は私がおごるから。」 <br>
「にっしっしー 、じゃあ、あーしとあんたがこれからも元気に生きていけることと、このタダ飯、次もその次もあるように! 行くっしょ!」 <br>
<br>
💌今日は特別な日。汐の誕生日に、たくさんの祝福を届けましょう🎂🌟<br>
<br>
🎁コメントでお祝いメッセージを送ってくれた方の中から、抽選で10名様に「パブロクリスタル×288」をプレゼント! <br>
<br>
📅応募期間:<br>
2026年6月21日(日)~2026年6月27日(土)<br>
<br>
※当選されたプレイヤーには、イベント終了後7営業日以内に公式Xアカウントよりダイレクトメッセージでご連絡し、必要情報のご提供をお願いいたします。DMの受信箱をご確認ください。 <br>
<a href="https://nitter.net/search?f=tweets&q=%23ストリノヴァ">#ストリノヴァ</a> <a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHLT9TW1a8AAdW51.jpg" style="max-width:250px;" />]]></description>
<pubDate>Sun, 21 Jun 2026 04:52:28 GMT</pubDate>
<guid isPermaLink="false">2068557643543908673</guid>
<link>https://nitter.net/Strinova_JP/status/2068557643543908673#m</link>
</item>
<item>
<title>Happy Birthday, Cielle! &quot;Ah... Finally done with my work!&quot; &quot;Miss, once you&apos;re done settling the bill, I must treat myself to a good meal~&quot; &quot;I&apos;ve already said, you can just call me Audrey... Forget it, you do know what day it is today?&quot; &quot;What? Erm... Wait, is it already Cielle&apos;s birthday?&quot; &quot;Oh dear! Cake, a job, buying paintings, my plans have been ruined! How did I get so busy I forgot!&quot; &quot;Tsk tsk tsk... Whatever, our tasks were much too rushed. Come on, this time it&apos;s on me.&quot; &quot;Hehe, then here&apos;s to the both of us living happily, and here&apos;s to this free meal having a next time, and a next next time!&quot;</title>
<dc:creator>@CalabiyauLeaks</dc:creator>
<description><![CDATA[<p>Happy Birthday, Cielle!<br>
<br>
"Ah... Finally done with my work!"<br>
"Miss, once you're done settling the bill, I must treat myself to a good meal~"<br>
"I've already said, you can just call me Audrey... Forget it, you do know what day it is today?"<br>
"What? Erm... Wait, is it already Cielle's birthday?"<br>
"Oh dear! Cake, a job, buying paintings, my plans have been ruined! How did I get so busy I forgot!"<br>
"Tsk tsk tsk... Whatever, our tasks were much too rushed. Come on, this time it's on me."<br>
"Hehe, then here's to the both of us living happily, and here's to this free meal having a next time, and a next next time!"</p>
<img src="https://nitter.net/pic/media%2FHLTnXlHaIAEpidH.jpg" style="max-width:250px;" />]]></description>
<pubDate>Sun, 21 Jun 2026 03:33:54 GMT</pubDate>
<guid isPermaLink="false">2068537870600827185</guid>
<link>https://nitter.net/CalabiyauLeaks/status/2068537870600827185#m</link>
</item>
<item>
<title>(CN) The [Crystalline Escape: Green-Carpeted Summer] event is about to begin! New maps submitted by Navigators have been added into the Crystalline Escape map pool, so come and enjoy the thrill of being chased with your friends! Event Duration: June 23rd 2026 after the update - July 21st Event Rules: Complete cumulative quests and obtain rewards. Event Rewards: Memory Sequences, Limited-time Chat Bubble, Event Records, Basestrings etc.</title>
<dc:creator>@CalabiyauLeaks</dc:creator>
<description><![CDATA[<p>(CN)<br>
The [Crystalline Escape: Green-Carpeted Summer] event is about to begin! New maps submitted by Navigators have been added into the Crystalline Escape map pool, so come and enjoy the thrill of being chased with your friends!<br>
<br>
Event Duration: June 23rd 2026 after the update - July 21st<br>
<br>
Event Rules: Complete cumulative quests and obtain rewards.<br>
<br>
Event Rewards: Memory Sequences, Limited-time Chat Bubble, Event Records, Basestrings etc.</p>
<img src="https://nitter.net/pic/media%2FHLPxwZMaYAElH0Z.png" style="max-width:250px;" />]]></description>
<pubDate>Sat, 20 Jun 2026 09:24:03 GMT</pubDate>
<guid isPermaLink="false">2068263602520195208</guid>
<link>https://nitter.net/CalabiyauLeaks/status/2068263602520195208#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: 【ミシェルの観察日記 】 みなさん、こんにちは—!ミシェルです! 今日は、ミシェルがみんなに紹介するのは欧泊のフラヴィア先輩のお話です。 フラヴィア先輩はね、めっちゃ頼りになるし、普段もすごく面倒見がよくて、本当にいい人なんだよ! #ストリノヴァ #Strinova</title>
<dc:creator>@Strinova_JP</dc:creator>
<description><![CDATA[<p>【ミシェルの観察日記 】<br>
<br>
みなさん、こんにちは—!ミシェルです!<br>
今日は、ミシェルがみんなに紹介するのは欧泊のフラヴィア先輩のお話です。<br>
フラヴィア先輩はね、めっちゃ頼りになるし、普段もすごく面倒見がよくて、本当にいい人なんだよ!<br>
<a href="https://nitter.net/search?f=tweets&q=%23ストリノヴァ">#ストリノヴァ</a> <a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHLJeUlFbwAAR10y.jpg" style="max-width:250px;" />]]></description>
<pubDate>Sat, 20 Jun 2026 03:00:02 GMT</pubDate>
<guid isPermaLink="false">2068166957728461101</guid>
<link>https://nitter.net/Strinova_JP/status/2068166957728461101#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: 【ミシェルのおしゃべりタイム】 やっほー!皆さん、こんにちは~ミシェルです! 最近ワールドカップが始まったけど、みんなは試合見てるかな?めっちゃ盛り上がってるよね!🔥 ミシェルもね、パワーにゃんと先輩と一緒に観戦したんだ〜!ゴール前のハラハラする攻防戦は、もうゲーム中のHPミリ残しみたいな緊張感で、パワーにゃんをギューッて抱きしめちゃった!🐱💦 めっちゃ楽しそうだし、ミシェルもサッカーしたくなってきました!!⚽️ 💬今週の質問: もしストリノヴァで3vs3のサッカー大会やるなら、みんなは誰をチームに編成する? また、キャプテンは誰にするの?みんなの考える最強のチーム編成、コメントでミシェルに教えてね!📝 (PS: へへっ、ミシェルってばどう見てもリーダー適性Sランクだよねっ!任せて!) 📅 期間:2026年6月19日(金)~ 6月25日(木) 🎁 コメント参加で抽選10名様に「パブロクリスタル×288」をプレゼントいたします。 *当選された方には、イベント終了後7営業日以内に公式Xよりダイレクトメッセージでご連絡いたします。DMの受信箱をご確認ください。 #ストリノヴァ #Strinova</title>
<dc:creator>@Strinova_JP</dc:creator>
<description><![CDATA[<p>【ミシェルのおしゃべりタイム】<br>
<br>
やっほー!皆さん、こんにちは~ミシェルです!<br>
最近ワールドカップが始まったけど、みんなは試合見てるかな?めっちゃ盛り上がってるよね!🔥<br>
<br>
ミシェルもね、パワーにゃんと先輩と一緒に観戦したんだ〜!ゴール前のハラハラする攻防戦は、もうゲーム中のHPミリ残しみたいな緊張感で、パワーにゃんをギューッて抱きしめちゃった!🐱💦<br>
<br>
めっちゃ楽しそうだし、ミシェルもサッカーしたくなってきました!!⚽️<br>
<br>
💬今週の質問:<br>
もしストリノヴァで3vs3のサッカー大会やるなら、みんなは誰をチームに編成する?<br>
また、キャプテンは誰にするの?みんなの考える最強のチーム編成、コメントでミシェルに教えてね!📝 (PS: へへっ、ミシェルってばどう見てもリーダー適性Sランクだよねっ!任せて!)<br>
<br>
📅 期間:2026年6月19日(金)~ 6月25日(木)<br>
🎁 コメント参加で抽選10名様に「パブロクリスタル×288」をプレゼントいたします。<br>
*当選された方には、イベント終了後7営業日以内に公式Xよりダイレクトメッセージでご連絡いたします。DMの受信箱をご確認ください。<br>
<a href="https://nitter.net/search?f=tweets&q=%23ストリノヴァ">#ストリノヴァ</a> <a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHLJdGnibgAAWcQh.jpg" style="max-width:250px;" />]]></description>
<pubDate>Fri, 19 Jun 2026 09:00:00 GMT</pubDate>
<guid isPermaLink="false">2067895160776892876</guid>
<link>https://nitter.net/Strinova_JP/status/2067895160776892876#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: 【初夏のひととき🍃】 静かな水面を進む船の上——  今日も賑やかな声が響いている。 「ねえねえ!みんな、ちょっと休憩しない?   パワーにゃんと一緒に作ったものを用意したよ〜!」 「ミシェルが作るものって……本当に大丈夫かな?」 「えっ?フューシャはまた変な想像してるの〜?」 「ミシェル!もう、ファンのみんなへの撮影邪魔しないで!!」 「えへへ〜香奈美も忙しそうだし、ちょっと味見してみてよ〜  私の獲物はこれ……(もぐもぐ)……おいしい」 #ストリノヴァ #Strinova</title>
<dc:creator>@Strinova_JP</dc:creator>
<description><![CDATA[<p>【初夏のひととき🍃】<br>
静かな水面を進む船の上——<br>
 今日も賑やかな声が響いている。<br>
<br>
「ねえねえ!みんな、ちょっと休憩しない?<br>
  パワーにゃんと一緒に作ったものを用意したよ〜!」<br>
「ミシェルが作るものって……本当に大丈夫かな?」<br>
「えっ?フューシャはまた変な想像してるの〜?」<br>
「ミシェル!もう、ファンのみんなへの撮影邪魔しないで!!」<br>
「えへへ〜香奈美も忙しそうだし、ちょっと味見してみてよ〜<br>
 私の獲物はこれ……(もぐもぐ)……おいしい」<br>
<a href="https://nitter.net/search?f=tweets&q=%23ストリノヴァ">#ストリノヴァ</a> <a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHLJdMcwb0AATcga.jpg" style="max-width:250px;" />]]></description>
<pubDate>Fri, 19 Jun 2026 04:00:00 GMT</pubDate>
<guid isPermaLink="false">2067819664390897679</guid>
<link>https://nitter.net/Strinova_JP/status/2067819664390897679#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: Happy Dragon Boat Festival! 🐲🛶 No missions today — just good vibes on the water. Hop aboard the Mighty Meow with Michele, Fuchsia, and Kanami, and enjoy a sunny summer afternoon together. 🌊☀️ #Strinova #StrinovaWallpaper</title>
<dc:creator>@Strinova_EN</dc:creator>
<description><![CDATA[<p>Happy Dragon Boat Festival! 🐲🛶<br>
<br>
No missions today — just good vibes on the water.<br>
Hop aboard the Mighty Meow with Michele, Fuchsia, and Kanami, and enjoy a sunny summer afternoon together. 🌊☀️<br>
<br>
<a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a> <a href="https://nitter.net/search?f=tweets&q=%23StrinovaWallpaper">#StrinovaWallpaper</a></p>
<img src="https://nitter.net/pic/media%2FHLFIsp9agAE3TNy.jpg" style="max-width:250px;" />]]></description>
<pubDate>Fri, 19 Jun 2026 04:00:00 GMT</pubDate>
<guid isPermaLink="false">2067819664202412270</guid>
<link>https://nitter.net/Strinova_EN/status/2067819664202412270#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: 【インフェクションモード:特性&ステータス強化システム登場!】 インフェクションモードに、新たな戦略要素が追加! 特性とステータス強化を組み合わせて、自分だけのスタイルを構築しよう。 今回の戦場では、あなたはどんな戦い方を選びますか? 📅開催期間 2026年6月25日(アップデート後)~ 2026年8月20日 📢イベントルール - 開催期間中、インフェクションモードをプレイして戦備クエストを達成すると、戦備レベルを上げることができます。 - 戦備レベルが上がると、強力なインフェクション用の特性カードやステータス強化アイテムが解放されるほか、晶原戦備ショップ専用の交換アイテム「超弦安定剤」を獲得できます。 - 晶原戦備ショップでは、戦備レベルアップ時に獲得した「超弦安定剤」を使用することで、新しいカードやカードパック、サイコロなどの報酬を獲得できます。なお、晶原戦備ショップで解放したアイテムは永久に保持されます。 - 各シーズンの晶原戦備で新しく追加されるカードは、開催期間中は晶原戦備ショップでのみ交換可能です。該当シーズンの晶原戦備が終了した後は「完全収録パック」に追加され、カード図鑑からカードピースを使って合成できるようになります。 ※ 各シーズンの晶原戦備が終了すると、解放済みの特性カードやステータス強化、および未使用の「超弦安定剤」は自動的に回収されます。開催期間にはご注意ください。 #ストリノヴァ #Strinova</title>
<dc:creator>@Strinova_JP</dc:creator>
<description><![CDATA[<p>【インフェクションモード:特性&ステータス強化システム登場!】<br>
<br>
インフェクションモードに、新たな戦略要素が追加!<br>
特性とステータス強化を組み合わせて、自分だけのスタイルを構築しよう。<br>
今回の戦場では、あなたはどんな戦い方を選びますか?<br>
<br>
📅開催期間<br>
2026年6月25日(アップデート後)~ 2026年8月20日<br>
<br>
📢イベントルール<br>
- 開催期間中、インフェクションモードをプレイして戦備クエストを達成すると、戦備レベルを上げることができます。<br>
- 戦備レベルが上がると、強力なインフェクション用の特性カードやステータス強化アイテムが解放されるほか、晶原戦備ショップ専用の交換アイテム「超弦安定剤」を獲得できます。<br>
- 晶原戦備ショップでは、戦備レベルアップ時に獲得した「超弦安定剤」を使用することで、新しいカードやカードパック、サイコロなどの報酬を獲得できます。なお、晶原戦備ショップで解放したアイテムは永久に保持されます。<br>
- 各シーズンの晶原戦備で新しく追加されるカードは、開催期間中は晶原戦備ショップでのみ交換可能です。該当シーズンの晶原戦備が終了した後は「完全収録パック」に追加され、カード図鑑からカードピースを使って合成できるようになります。<br>
※ 各シーズンの晶原戦備が終了すると、解放済みの特性カードやステータス強化、および未使用の「超弦安定剤」は自動的に回収されます。開催期間にはご注意ください。<br>
<a href="https://nitter.net/search?f=tweets&q=%23ストリノヴァ">#ストリノヴァ</a> <a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHLFU4KIaIAA9iph.jpg" style="max-width:250px;" />]]></description>
<pubDate>Thu, 18 Jun 2026 09:00:03 GMT</pubDate>
<guid isPermaLink="false">2067532783338315955</guid>
<link>https://nitter.net/Strinova_JP/status/2067532783338315955#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: New Outbreak Supply Event Preview Gear up for Outbreak Mode — Outbreak Talents are joining the battlefield! 🌟 From June 25th to August 20th, play Outbreak mode to accumulate progress and unlock free rewards, including powerful Talents, Attribute Enhancements and new Cards. Jump in, power up, and claim every Supply reward before the event ends! #Strinova</title>
<dc:creator>@Strinova_EN</dc:creator>
<description><![CDATA[<p>New Outbreak Supply Event Preview<br>
<br>
Gear up for Outbreak Mode — Outbreak Talents are joining the battlefield! 🌟<br>
From June 25th to August 20th, play Outbreak mode to accumulate progress and unlock free rewards, including powerful Talents, Attribute Enhancements and new Cards.<br>
Jump in, power up, and claim every Supply reward before the event ends!<br>
<a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHLEzzfGaEAA_J6V.jpg" style="max-width:250px;" />]]></description>
<pubDate>Thu, 18 Jun 2026 09:00:03 GMT</pubDate>
<guid isPermaLink="false">2067532784110027106</guid>
<link>https://nitter.net/Strinova_EN/status/2067532784110027106#m</link>
</item>
<item>
<title>(CN, Global) The mid-season PV for Season 26SP3 - Shadow Divergence is out! Here&apos;s what the new update will have to offer: - Preview of the next Legendary skin, Cielle - Million-Volt Baby. (0:04 - 1:33) - Preview of the Epic outfits that will be included in the [Cielle - Million-Volt Baby] banner, Galatea - Warp Edge and Leona - Dimension Ascension. (1:34 - 2:27) - Preview of the Outbreak Talent system, which are special cards you draw upon entering the round/getting infected. (2:28 - 2:56) - Preview of the Greatsword. (2:57 - 3:16) - Preview of the Ninjato - Gilded Constellations. (3:17 - 3:30) - Preview of the Calabiyau x Tang Sancai collaboration, including a spray, a chip badge, and the Stinger - Glazed Sand Relic. (3:31 - 3:48) - Preview of the next few shop skins: Kokona - Dawnlight Swanfeather, Baimo - Freedom Regulations and Fuchsia - Maid Trainee. (3:49 - 4:20) - Preview of the next Orbit Resonance skin, Yugiri - Midnight Velvet. (4:21 - 4:32)</title>
<dc:creator>@CalabiyauLeaks</dc:creator>
<description><![CDATA[<p>(CN, Global)<br>
The mid-season PV for Season 26SP3 - Shadow Divergence is out! Here's what the new update will have to offer:<br>
<br>
- Preview of the next Legendary skin, Cielle - Million-Volt Baby. (0:04 - 1:33)<br>
- Preview of the Epic outfits that will be included in the [Cielle - Million-Volt Baby] banner, Galatea - Warp Edge and Leona - Dimension Ascension. (1:34 - 2:27)<br>
- Preview of the Outbreak Talent system, which are special cards you draw upon entering the round/getting infected. (2:28 - 2:56)<br>
- Preview of the Greatsword. (2:57 - 3:16)<br>
- Preview of the Ninjato - Gilded Constellations. (3:17 - 3:30)<br>
- Preview of the Calabiyau x Tang Sancai collaboration, including a spray, a chip badge, and the Stinger - Glazed Sand Relic. (3:31 - 3:48)<br>
- Preview of the next few shop skins: Kokona - Dawnlight Swanfeather, Baimo - Freedom Regulations and Fuchsia - Maid Trainee. (3:49 - 4:20)<br>
- Preview of the next Orbit Resonance skin, Yugiri - Midnight Velvet. (4:21 - 4:32)</p>
<a href="https://nitter.net/CalabiyauLeaks/status/2067495990832349224#m">
<br>Video<br>
<img src="https://nitter.net/pic/amplify_video_thumb%2F2067489417787494401%2Fimg%2FxPZKE_ojAZBiFubZ.jpg" style="max-width:250px;" />
</a>]]></description>
<pubDate>Thu, 18 Jun 2026 06:33:51 GMT</pubDate>
<guid isPermaLink="false">2067495990832349224</guid>
<link>https://nitter.net/CalabiyauLeaks/status/2067495990832349224#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: ✨【 新近接武器登場!】✨ 新武器 - 「大剣」 🗡️ 無名の鍛冶師が鍛えた、風格のある大剣 ◆特徴 -攻撃速度はやや遅いが、その分高いダメージを発揮 -ダッシュ攻撃および重撃に専用モーションを搭載 #ストリノヴァ #Strinova</title>
<dc:creator>@Strinova_JP</dc:creator>
<description><![CDATA[<p>✨【 新近接武器登場!】✨<br>
<br>
新武器 - 「大剣」 🗡️<br>
無名の鍛冶師が鍛えた、風格のある大剣<br>
<br>
◆特徴<br>
-攻撃速度はやや遅いが、その分高いダメージを発揮<br>
-ダッシュ攻撃および重撃に専用モーションを搭載<br>
<br>
<a href="https://nitter.net/search?f=tweets&q=%23ストリノヴァ">#ストリノヴァ</a> <a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHLAMDTHbQAAHaWo.jpg" style="max-width:250px;" />]]></description>
<pubDate>Wed, 17 Jun 2026 11:00:03 GMT</pubDate>
<guid isPermaLink="false">2067200596352876587</guid>
<link>https://nitter.net/Strinova_JP/status/2067200596352876587#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: New Melee Weapon Preview: Greatsword A refined sword forged by an unknown master. The Greatsword will be available after the June 25 update. Prepare to wield elegance and power on the battlefield. #Strinova</title>
<dc:creator>@Strinova_EN</dc:creator>
<description><![CDATA[<p>New Melee Weapon Preview: Greatsword<br>
<br>
A refined sword forged by an unknown master.<br>
The Greatsword will be available after the June 25 update.<br>
Prepare to wield elegance and power on the battlefield.<br>
<a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHLAiybhb0AAqfIg.jpg" style="max-width:250px;" />]]></description>
<pubDate>Wed, 17 Jun 2026 11:00:03 GMT</pubDate>
<guid isPermaLink="false">2067200596340506904</guid>
<link>https://nitter.net/Strinova_EN/status/2067200596340506904#m</link>
</item>
<item>
<title>(CN) A Calabiyau x MSI offline event is going to commence this July. More details will be posted soon, so stay tuned!</title>
<dc:creator>@CalabiyauLeaks</dc:creator>
<description><![CDATA[<p>(CN)<br>
A Calabiyau x MSI offline event is going to commence this July. More details will be posted soon, so stay tuned!</p>
<img src="https://nitter.net/pic/media%2FHK7HX7Ba0AAoLAj.jpg" style="max-width:250px;" />]]></description>
<pubDate>Tue, 16 Jun 2026 09:03:45 GMT</pubDate>
<guid isPermaLink="false">2066808939241693445</guid>
<link>https://nitter.net/CalabiyauLeaks/status/2066808939241693445#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: 🐴【イベント:極彩の華】🐴 イベント期間中、週間クエストを完了すると、警報器の外観「絶えぬ狼煙」などの報酬を無料で獲得できます。 【仕上げの金彩】を購入してアップグレードすると、ミツバチのエピック外観「伝統の三彩釉」と限定メダルを入手できます。💎 📅 開催期間:2026年6月25日(木) ~ 2026年7月22日(水) #ストリノヴァ #Strinova</title>
<dc:creator>@Strinova_JP</dc:creator>
<description><![CDATA[<p>🐴【イベント:極彩の華】🐴<br>
<br>
イベント期間中、週間クエストを完了すると、警報器の外観「絶えぬ狼煙」などの報酬を無料で獲得できます。<br>
【仕上げの金彩】を購入してアップグレードすると、ミツバチのエピック外観「伝統の三彩釉」と限定メダルを入手できます。💎<br>
<br>
📅 開催期間:2026年6月25日(木) ~ 2026年7月22日(水)<br>
<a href="https://nitter.net/search?f=tweets&q=%23ストリノヴァ">#ストリノヴァ</a> <a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHK5-UgFbIAAGHSr.jpg" style="max-width:250px;" />]]></description>
<pubDate>Tue, 16 Jun 2026 04:00:00 GMT</pubDate>
<guid isPermaLink="false">2066732499208966290</guid>
<link>https://nitter.net/Strinova_JP/status/2066732499208966290#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: Event Preview丨Strinova X TANG SANCAI The ancient colors of Sancai are ready to shine on the battlefield. From June 25th to July 22nd, complete the [Pristine Canvas] quest to get the Tattletale Skin – Eternal Flame and other rewards for free. Upgrade to [Gilded Masterpiece] to instantly obtain the Stinger Epic Skin – Sancai Desert Echo, plus a limited badge! Let the melody of Sancai begin. #Strinova</title>
<dc:creator>@Strinova_EN</dc:creator>
<description><![CDATA[<p>Event Preview丨Strinova X TANG SANCAI<br>
<br>
The ancient colors of Sancai are ready to shine on the battlefield.<br>
<br>
From June 25th to July 22nd, complete the [Pristine Canvas] quest to get the Tattletale Skin – Eternal Flame and other rewards for free.<br>
Upgrade to [Gilded Masterpiece] to instantly obtain the Stinger Epic Skin – Sancai Desert Echo, plus a limited badge!<br>
<br>
Let the melody of Sancai begin.<br>
<br>
<a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHK6AlvOaMAA1rry.jpg" style="max-width:250px;" />]]></description>
<pubDate>Tue, 16 Jun 2026 04:00:17 GMT</pubDate>
<guid isPermaLink="false">2066732572051493081</guid>
<link>https://nitter.net/Strinova_EN/status/2066732572051493081#m</link>
</item>
<item>
<title>RT by @CalabiyauLeaks: 【Ninjato - Gilded Constellations】coming soon🌟 Navigators who own the「Ninjato - Prism Blade」can obtain the Legendary Skin「Ninjato - Gilded Constellations」and the Epic Case「JE-66 Stellar Eminence」through the limited-time 【Gilded Constellations Pack】 &quot;May you wield your blade to cleave through the cold void and darkness, and let the brilliance of civilization become an ode that travels across the stars.&quot; ⏰Duration: June 25th - July 23rd (UTC+0) #Strinova</title>
<dc:creator>@Strinova_EN</dc:creator>
<description><![CDATA[<p>【Ninjato - Gilded Constellations】coming soon🌟<br>
<br>
Navigators who own the「Ninjato - Prism Blade」can obtain the Legendary Skin「Ninjato - Gilded Constellations」and the Epic Case「JE-66 Stellar Eminence」through the limited-time 【Gilded Constellations Pack】<br>
<br>
"May you wield your blade to cleave through the cold void and darkness, and let the brilliance of civilization become an ode that travels across the stars."<br>
<br>
⏰Duration: June 25th - July 23rd (UTC+0)<br>
<br>
<a href="https://nitter.net/search?f=tweets&q=%23Strinova">#Strinova</a></p>
<img src="https://nitter.net/pic/media%2FHK2mTiebYAAC7O8.jpg" style="max-width:250px;" />]]></description>
<pubDate>Mon, 15 Jun 2026 12:02:57 GMT</pubDate>
<guid isPermaLink="false">2066491651011625040</guid>
<link>https://nitter.net/Strinova_EN/status/2066491651011625040#m</link>
</item>
</channel>
</rss>
+100
View File
@@ -0,0 +1,100 @@
# this is where twitter/x rss-to-discord forwarding lives
import os
import asyncio
import aiohttp
import feedparser
POLL_INTERVAL = 43200 # 12 hours
GUID_FILE = 'data/last_tweet.txt'
class TwitterRSSForwarder:
def __init__(self, rss_url, webhook_url):
self.rss_url = rss_url
self.webhook_url = webhook_url
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(GUID_FILE):
with open(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(GUID_FILE, 'w') as f:
f.write(self.last_guid or '')
async def _poll(self):
feed = feedparser.parse(self.rss_url)
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]}...')