diff --git a/.gitignore b/.gitignore index 0f5e6b5..63a077b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ gifs/ credentials.env .vscode/__pycache__/ +data/ diff --git a/main.py b/main.py index a8e9af4..7a7683b 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ import logging import asyncio from tgbridge.forwarder import TelegramForwarder +from twitterbridge.rss import TwitterRSSForwarder load_dotenv('credentials.env') @@ -20,10 +21,16 @@ forwarder = TelegramForwarder( 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 async def on_ready(): print(f'✅ {bot.user} is online!') asyncio.create_task(forwarder.start()) + asyncio.create_task(twitter.start()) async def main(): async with bot: @@ -38,6 +45,7 @@ async def main(): await bot.start(os.getenv('DISCORD_TOKEN')) finally: await forwarder.stop() + twitter.stop() logging.basicConfig(level=logging.INFO) asyncio.run(main()) \ No newline at end of file diff --git a/tgbridge/forwarder.py b/tgbridge/forwarder.py index ab718e0..8453a66 100644 --- a/tgbridge/forwarder.py +++ b/tgbridge/forwarder.py @@ -1,83 +1,71 @@ # this is where telegram-to-discord forwarding lives -import logging import io import json import aiohttp -from telegram import Update -from telegram.ext import Application, MessageHandler, filters, ContextTypes - -logger = logging.getLogger('telegram.forwarder') - +from telegram.ext import Application, MessageHandler, filters class TelegramForwarder: - """Watches a Telegram group/channel and forwards messages to a Discord webhook.""" - - def __init__(self, token: str, group_id: int, webhook_url: str): + def __init__(self, token, group_id, webhook_url): self.token = token self.group_id = group_id self.webhook_url = webhook_url - self._app: Application | None = None + self._app = None 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)) await self._app.initialize() await self._app.start() 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): - """Graceful shutdown.""" if self._app: await self._app.updater.stop() await self._app.stop() await self._app.shutdown() - logger.info('Telegram forwarder stopped') + print('telegram forwarder stopped') - # ── internal helpers ───────────────────────────────────────────────────── - - def _sender_name(self, update: Update) -> str: - """Extract a display name from the message sender.""" + # internal helpers + def _sender_name(self, update): user = update.effective_user if user: name = user.first_name or '' if 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 chat = update.effective_chat if chat and chat.title: return chat.title - return 'Unknown' + return 'unknown' - def _forward_prefix(self, message) -> str: - """Build a prefix for forwarded messages.""" + def _forward_prefix(self, message): if message.forward_origin: origin = message.forward_origin + # user forward if hasattr(origin, 'sender_user') and origin.sender_user: u = origin.sender_user name = u.first_name or '' if 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 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 if hasattr(origin, 'sender_user_name') and origin.sender_user_name: return f'↩️ Forwarded from **{origin.sender_user_name}**:\n' + return '↩️ Forwarded:\n' return '' - async def _download_file(self, file_id: str) -> tuple[bytes, str]: - """Download a file from Telegram, return (bytes, filename).""" + async def _download_file(self, file_id): tg_file = await self._app.bot.get_file(file_id) buf = io.BytesIO() 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' return buf.read(), filename - async def _send_to_webhook(self, username: str, content: str = '', - file_data: bytes | None = None, - filename: str | None = None): - """POST a message (and optional file) to the Discord webhook.""" + async def _send_to_webhook(self, username, content='', file_data=None, filename=None): async with aiohttp.ClientSession() as session: data = aiohttp.FormData() payload = {'username': username} if content: - payload['content'] = content[:2000] # discord limit + payload['content'] = content[:2000] if file_data and filename: - data.add_field('payload_json', json.dumps(payload), - content_type='application/json') + data.add_field('payload_json', json.dumps(payload), content_type='application/json') data.add_field('file', file_data, filename=filename) else: - # simple json post async with session.post(self.webhook_url, json=payload) as resp: 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 async with session.post(self.webhook_url, data=data) as resp: 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 ───────────────────────────────────────────────── - - async def _on_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """Handle every incoming message from the watched group.""" + # main message handler + async def _on_message(self, update, context): message = update.effective_message if not message: return @@ -127,53 +108,43 @@ class TelegramForwarder: caption = message.caption or '' try: - # ── photo ──────────────────────────────────────────────────── if message.photo: - # largest resolution is last in the array photo = message.photo[-1] file_data, filename = await self._download_file(photo.file_id) text = f'{prefix}{caption}'.strip() await self._send_to_webhook(sender, text, file_data, filename) - # ── video ──────────────────────────────────────────────────── elif message.video: file_data, filename = await self._download_file(message.video.file_id) text = f'{prefix}{caption}'.strip() await self._send_to_webhook(sender, text, file_data, filename) - # ── document (files, gifs sent as docs, etc) ───────────────── elif message.document: file_data, filename = await self._download_file(message.document.file_id) text = f'{prefix}{caption}'.strip() await self._send_to_webhook(sender, text, file_data, filename) - # ── animation (GIF) ────────────────────────────────────────── elif message.animation: file_data, filename = await self._download_file(message.animation.file_id) text = f'{prefix}{caption}'.strip() await self._send_to_webhook(sender, text, file_data, filename) - # ── sticker ────────────────────────────────────────────────── elif message.sticker: - # stickers have no caption — send emoji or name sticker_text = message.sticker.emoji or '(sticker)' text = f'{prefix}{sticker_text}' - # try to download the sticker image try: file_data, filename = await self._download_file(message.sticker.file_id) await self._send_to_webhook(sender, text, file_data, filename) except Exception: await self._send_to_webhook(sender, text) - # ── text ───────────────────────────────────────────────────── elif message.text: text = f'{prefix}{message.text}' await self._send_to_webhook(sender, text) else: - # voice, audio, contact, location, etc — just note it text = f'{prefix}*(unsupported message type)*' await self._send_to_webhook(sender, text) - except Exception: - logger.exception('Failed to forward message from %s', sender) + except Exception as e: + print(f'failed to forward message from {sender}: {e}') diff --git a/twitterbridge/__init__.py b/twitterbridge/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/twitterbridge/__init__.py @@ -0,0 +1 @@ + diff --git a/twitterbridge/__pycache__/__init__.cpython-312.pyc b/twitterbridge/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..2c7be73 Binary files /dev/null and b/twitterbridge/__pycache__/__init__.cpython-312.pyc differ diff --git a/twitterbridge/__pycache__/rss.cpython-312.pyc b/twitterbridge/__pycache__/rss.cpython-312.pyc new file mode 100644 index 0000000..9bb71a9 Binary files /dev/null and b/twitterbridge/__pycache__/rss.cpython-312.pyc differ diff --git a/twitterbridge/rss b/twitterbridge/rss new file mode 100644 index 0000000..a3d4c30 --- /dev/null +++ b/twitterbridge/rss @@ -0,0 +1,363 @@ + + + + + Strinova Leaks/News / @CalabiyauLeaks + https://nitter.net/CalabiyauLeaks + Twitter feed for: @CalabiyauLeaks. Generated by https://nitter.net + + en-us + 40 + + Strinova Leaks/News / @CalabiyauLeaks + https://nitter.net/CalabiyauLeaks + https://nitter.net/pic/pbs.twimg.com%2Fprofile_images%2F1834591186776186880%2FR6_I7Gu3_400x400.jpg + 128 + 128 + + + RT by @CalabiyauLeaks: 月曜日はHP上限-50% #ストリノヴァ #Strinova + @Strinova_JP + 月曜日はHP上限-50%
+#ストリノヴァ #Strinova

+]]>
+ Mon, 22 Jun 2026 03:02:21 GMT + 2068892319978545317 + https://nitter.net/Strinova_JP/status/2068892319978545317#m +
+ + RT by @CalabiyauLeaks: 【汐 - 新外観予告】 レジェンダリー外観 -「百万ボルトベイビー」 ✨6月30日登場!✨ 「大会のスーパースターたるあーしには、 スーパーヒーローのスーツがお似合いっしょ!」 #ストリノヴァ #Strinova + @Strinova_JP + 【汐 - 新外観予告】
+レジェンダリー外観 -「百万ボルトベイビー」
+✨6月30日登場!✨
+
+「大会のスーパースターたるあーしには、 スーパーヒーローのスーツがお似合いっしょ!」
+#ストリノヴァ #Strinova

+]]>
+ Sun, 21 Jun 2026 09:00:01 GMT + 2068619941817790692 + https://nitter.net/Strinova_JP/status/2068619941817790692#m +
+ + RT by @CalabiyauLeaks: Cielle Legendary Outfit & Weapon Skin | Preview ⚡ All systems go — Zappy'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 + @Strinova_EN + Cielle Legendary Outfit & Weapon Skin | Preview ⚡
+
+All systems go — Zappy'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

+]]>
+ Sun, 21 Jun 2026 09:00:02 GMT + 2068619945588453653 + https://nitter.net/Strinova_EN/status/2068619945588453653#m +
+ + 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 + @Strinova_EN + 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

+]]>
+ Sun, 21 Jun 2026 07:00:00 GMT + 2068589736730464261 + https://nitter.net/Strinova_EN/status/2068589736730464261#m +
+ + RT by @CalabiyauLeaks: ⋱\🎂𝗛𝗮𝗽𝗽𝘆 𝗕𝗶𝗿𝘁𝗵𝗱𝗮𝘆🎂/⋰ 【汐 お誕生日おめでとう!❣】 -タダで食べる満腹ディナー🍽️ 「ふぅ……っ! やっと生き返ったぁー!」 「お嬢様~、今回の依頼料、ちゃんと払ってくれたら、あーし絶対自分にご褒美あげなきゃっしょ〜」 「オードリーでいいって言ってるでしょ……まあいいわ。今日が何の日か、分かってる?」 「え? んー……うっわ、やば! もしかして『汐』って名前の誕生日、もう来ちゃった感じ!?」 「ちょっ、ケーキ食べて、お散歩して、絵を買うっていうあーしの完璧なプランが!! なんで忘れてたのよぉ!」 「あんたね……はあ、任務が急すぎたせいもあるし。行くわよ、今日は私がおごるから。」 「にっしっしー 、じゃあ、あーしとあんたがこれからも元気に生きていけることと、このタダ飯、次もその次もあるように! 行くっしょ!」 💌今日は特別な日。汐の誕生日に、たくさんの祝福を届けましょう🎂🌟 🎁コメントでお祝いメッセージを送ってくれた方の中から、抽選で10名様に「パブロクリスタル×288」をプレゼント! 📅応募期間: 2026年6月21日(日)~2026年6月27日(土) ※当選されたプレイヤーには、イベント終了後7営業日以内に公式Xアカウントよりダイレクトメッセージでご連絡し、必要情報のご提供をお願いいたします。DMの受信箱をご確認ください。 #ストリノヴァ #Strinova + @Strinova_JP + ⋱\🎂𝗛𝗮𝗽𝗽𝘆 𝗕𝗶𝗿𝘁𝗵𝗱𝗮𝘆🎂/⋰
+【汐 お誕生日おめでとう!❣】
+
+ -タダで食べる満腹ディナー🍽️
+「ふぅ……っ! やっと生き返ったぁー!」
+「お嬢様~、今回の依頼料、ちゃんと払ってくれたら、あーし絶対自分にご褒美あげなきゃっしょ〜」
+「オードリーでいいって言ってるでしょ……まあいいわ。今日が何の日か、分かってる?」
+「え? んー……うっわ、やば! もしかして『汐』って名前の誕生日、もう来ちゃった感じ!?」
+「ちょっ、ケーキ食べて、お散歩して、絵を買うっていうあーしの完璧なプランが!! なんで忘れてたのよぉ!」
+「あんたね……はあ、任務が急すぎたせいもあるし。行くわよ、今日は私がおごるから。」
+「にっしっしー 、じゃあ、あーしとあんたがこれからも元気に生きていけることと、このタダ飯、次もその次もあるように! 行くっしょ!」
+
+💌今日は特別な日。汐の誕生日に、たくさんの祝福を届けましょう🎂🌟
+
+ 🎁コメントでお祝いメッセージを送ってくれた方の中から、抽選で10名様に「パブロクリスタル×288」をプレゼント!
+
+📅応募期間:
+2026年6月21日(日)~2026年6月27日(土)
+
+※当選されたプレイヤーには、イベント終了後7営業日以内に公式Xアカウントよりダイレクトメッセージでご連絡し、必要情報のご提供をお願いいたします。DMの受信箱をご確認ください。
+#ストリノヴァ #Strinova

+]]>
+ Sun, 21 Jun 2026 04:52:28 GMT + 2068557643543908673 + https://nitter.net/Strinova_JP/status/2068557643543908673#m +
+ + Happy Birthday, Cielle! "Ah... Finally done with my work!" "Miss, once you're done settling the bill, I must treat myself to a good meal~" "I've already said, you can just call me Audrey... Forget it, you do know what day it is today?" "What? Erm... Wait, is it already Cielle's birthday?" "Oh dear! Cake, a job, buying paintings, my plans have been ruined! How did I get so busy I forgot!" "Tsk tsk tsk... Whatever, our tasks were much too rushed. Come on, this time it's on me." "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!" + @CalabiyauLeaks + Happy Birthday, Cielle!
+
+"Ah... Finally done with my work!"
+"Miss, once you're done settling the bill, I must treat myself to a good meal~"
+"I've already said, you can just call me Audrey... Forget it, you do know what day it is today?"
+"What? Erm... Wait, is it already Cielle's birthday?"
+"Oh dear! Cake, a job, buying paintings, my plans have been ruined! How did I get so busy I forgot!"
+"Tsk tsk tsk... Whatever, our tasks were much too rushed. Come on, this time it's on me."
+"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!"

+]]>
+ Sun, 21 Jun 2026 03:33:54 GMT + 2068537870600827185 + https://nitter.net/CalabiyauLeaks/status/2068537870600827185#m +
+ + (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. + @CalabiyauLeaks + (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.

+]]>
+ Sat, 20 Jun 2026 09:24:03 GMT + 2068263602520195208 + https://nitter.net/CalabiyauLeaks/status/2068263602520195208#m +
+ + RT by @CalabiyauLeaks: 【ミシェルの観察日記 】 みなさん、こんにちは—!ミシェルです! 今日は、ミシェルがみんなに紹介するのは欧泊のフラヴィア先輩のお話です。 フラヴィア先輩はね、めっちゃ頼りになるし、普段もすごく面倒見がよくて、本当にいい人なんだよ! #ストリノヴァ #Strinova + @Strinova_JP + 【ミシェルの観察日記 】
+
+みなさん、こんにちは—!ミシェルです!
+今日は、ミシェルがみんなに紹介するのは欧泊のフラヴィア先輩のお話です。
+フラヴィア先輩はね、めっちゃ頼りになるし、普段もすごく面倒見がよくて、本当にいい人なんだよ!
+#ストリノヴァ #Strinova

+]]>
+ Sat, 20 Jun 2026 03:00:02 GMT + 2068166957728461101 + https://nitter.net/Strinova_JP/status/2068166957728461101#m +
+ + RT by @CalabiyauLeaks: 【ミシェルのおしゃべりタイム】 やっほー!皆さん、こんにちは~ミシェルです! 最近ワールドカップが始まったけど、みんなは試合見てるかな?めっちゃ盛り上がってるよね!🔥 ミシェルもね、パワーにゃんと先輩と一緒に観戦したんだ〜!ゴール前のハラハラする攻防戦は、もうゲーム中のHPミリ残しみたいな緊張感で、パワーにゃんをギューッて抱きしめちゃった!🐱💦 めっちゃ楽しそうだし、ミシェルもサッカーしたくなってきました!!⚽️ 💬今週の質問: もしストリノヴァで3vs3のサッカー大会やるなら、みんなは誰をチームに編成する? また、キャプテンは誰にするの?みんなの考える最強のチーム編成、コメントでミシェルに教えてね!📝 (PS: へへっ、ミシェルってばどう見てもリーダー適性Sランクだよねっ!任せて!) 📅 期間:2026年6月19日(金)~ 6月25日(木) 🎁 コメント参加で抽選10名様に「パブロクリスタル×288」をプレゼントいたします。 *当選された方には、イベント終了後7営業日以内に公式Xよりダイレクトメッセージでご連絡いたします。DMの受信箱をご確認ください。 #ストリノヴァ #Strinova + @Strinova_JP + 【ミシェルのおしゃべりタイム】
+
+やっほー!皆さん、こんにちは~ミシェルです!
+ 最近ワールドカップが始まったけど、みんなは試合見てるかな?めっちゃ盛り上がってるよね!🔥
+
+ミシェルもね、パワーにゃんと先輩と一緒に観戦したんだ〜!ゴール前のハラハラする攻防戦は、もうゲーム中のHPミリ残しみたいな緊張感で、パワーにゃんをギューッて抱きしめちゃった!🐱💦
+
+めっちゃ楽しそうだし、ミシェルもサッカーしたくなってきました!!⚽️
+
+💬今週の質問:
+もしストリノヴァで3vs3のサッカー大会やるなら、みんなは誰をチームに編成する?
+また、キャプテンは誰にするの?みんなの考える最強のチーム編成、コメントでミシェルに教えてね!📝 (PS: へへっ、ミシェルってばどう見てもリーダー適性Sランクだよねっ!任せて!)
+
+📅 期間:2026年6月19日(金)~ 6月25日(木)
+🎁 コメント参加で抽選10名様に「パブロクリスタル×288」をプレゼントいたします。
+*当選された方には、イベント終了後7営業日以内に公式Xよりダイレクトメッセージでご連絡いたします。DMの受信箱をご確認ください。
+#ストリノヴァ #Strinova

+]]>
+ Fri, 19 Jun 2026 09:00:00 GMT + 2067895160776892876 + https://nitter.net/Strinova_JP/status/2067895160776892876#m +
+ + RT by @CalabiyauLeaks: 【初夏のひととき🍃】 静かな水面を進む船の上——  今日も賑やかな声が響いている。 「ねえねえ!みんな、ちょっと休憩しない?   パワーにゃんと一緒に作ったものを用意したよ〜!」 「ミシェルが作るものって……本当に大丈夫かな?」 「えっ?フューシャはまた変な想像してるの〜?」 「ミシェル!もう、ファンのみんなへの撮影邪魔しないで!!」 「えへへ〜香奈美も忙しそうだし、ちょっと味見してみてよ〜  私の獲物はこれ……(もぐもぐ)……おいしい」 #ストリノヴァ #Strinova + @Strinova_JP + 【初夏のひととき🍃】
+静かな水面を進む船の上——
+ 今日も賑やかな声が響いている。
+
+「ねえねえ!みんな、ちょっと休憩しない?
+  パワーにゃんと一緒に作ったものを用意したよ〜!」
+「ミシェルが作るものって……本当に大丈夫かな?」
+「えっ?フューシャはまた変な想像してるの〜?」
+「ミシェル!もう、ファンのみんなへの撮影邪魔しないで!!」
+「えへへ〜香奈美も忙しそうだし、ちょっと味見してみてよ〜
+ 私の獲物はこれ……(もぐもぐ)……おいしい」
+#ストリノヴァ #Strinova

+]]>
+ Fri, 19 Jun 2026 04:00:00 GMT + 2067819664390897679 + https://nitter.net/Strinova_JP/status/2067819664390897679#m +
+ + 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 + @Strinova_EN + 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

+]]>
+ Fri, 19 Jun 2026 04:00:00 GMT + 2067819664202412270 + https://nitter.net/Strinova_EN/status/2067819664202412270#m +
+ + RT by @CalabiyauLeaks: 【インフェクションモード:特性&ステータス強化システム登場!】 インフェクションモードに、新たな戦略要素が追加! 特性とステータス強化を組み合わせて、自分だけのスタイルを構築しよう。 今回の戦場では、あなたはどんな戦い方を選びますか? 📅開催期間 2026年6月25日(アップデート後)~ 2026年8月20日 📢イベントルール - 開催期間中、インフェクションモードをプレイして戦備クエストを達成すると、戦備レベルを上げることができます。 - 戦備レベルが上がると、強力なインフェクション用の特性カードやステータス強化アイテムが解放されるほか、晶原戦備ショップ専用の交換アイテム「超弦安定剤」を獲得できます。 - 晶原戦備ショップでは、戦備レベルアップ時に獲得した「超弦安定剤」を使用することで、新しいカードやカードパック、サイコロなどの報酬を獲得できます。なお、晶原戦備ショップで解放したアイテムは永久に保持されます。 - 各シーズンの晶原戦備で新しく追加されるカードは、開催期間中は晶原戦備ショップでのみ交換可能です。該当シーズンの晶原戦備が終了した後は「完全収録パック」に追加され、カード図鑑からカードピースを使って合成できるようになります。 ※ 各シーズンの晶原戦備が終了すると、解放済みの特性カードやステータス強化、および未使用の「超弦安定剤」は自動的に回収されます。開催期間にはご注意ください。 #ストリノヴァ #Strinova + @Strinova_JP + 【インフェクションモード:特性&ステータス強化システム登場!】
+
+インフェクションモードに、新たな戦略要素が追加!
+特性とステータス強化を組み合わせて、自分だけのスタイルを構築しよう。
+今回の戦場では、あなたはどんな戦い方を選びますか?
+
+📅開催期間
+2026年6月25日(アップデート後)~ 2026年8月20日
+
+📢イベントルール
+- 開催期間中、インフェクションモードをプレイして戦備クエストを達成すると、戦備レベルを上げることができます。
+- 戦備レベルが上がると、強力なインフェクション用の特性カードやステータス強化アイテムが解放されるほか、晶原戦備ショップ専用の交換アイテム「超弦安定剤」を獲得できます。
+- 晶原戦備ショップでは、戦備レベルアップ時に獲得した「超弦安定剤」を使用することで、新しいカードやカードパック、サイコロなどの報酬を獲得できます。なお、晶原戦備ショップで解放したアイテムは永久に保持されます。
+- 各シーズンの晶原戦備で新しく追加されるカードは、開催期間中は晶原戦備ショップでのみ交換可能です。該当シーズンの晶原戦備が終了した後は「完全収録パック」に追加され、カード図鑑からカードピースを使って合成できるようになります。
+※ 各シーズンの晶原戦備が終了すると、解放済みの特性カードやステータス強化、および未使用の「超弦安定剤」は自動的に回収されます。開催期間にはご注意ください。
+#ストリノヴァ #Strinova

+]]>
+ Thu, 18 Jun 2026 09:00:03 GMT + 2067532783338315955 + https://nitter.net/Strinova_JP/status/2067532783338315955#m +
+ + 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 + @Strinova_EN + 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

+]]>
+ Thu, 18 Jun 2026 09:00:03 GMT + 2067532784110027106 + https://nitter.net/Strinova_EN/status/2067532784110027106#m +
+ + (CN, Global) The mid-season PV for Season 26SP3 - Shadow Divergence is out! Here'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) + @CalabiyauLeaks + (CN, Global)
+The mid-season PV for Season 26SP3 - Shadow Divergence is out! Here'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)

+ +
Video
+ +
]]>
+ Thu, 18 Jun 2026 06:33:51 GMT + 2067495990832349224 + https://nitter.net/CalabiyauLeaks/status/2067495990832349224#m +
+ + RT by @CalabiyauLeaks: ✨【 新近接武器登場!】✨ 新武器 - 「大剣」 🗡️ 無名の鍛冶師が鍛えた、風格のある大剣 ◆特徴 -攻撃速度はやや遅いが、その分高いダメージを発揮 -ダッシュ攻撃および重撃に専用モーションを搭載 #ストリノヴァ #Strinova + @Strinova_JP + ✨【 新近接武器登場!】✨
+
+新武器 - 「大剣」 🗡️
+無名の鍛冶師が鍛えた、風格のある大剣
+
+◆特徴
+ -攻撃速度はやや遅いが、その分高いダメージを発揮
+ -ダッシュ攻撃および重撃に専用モーションを搭載
+
+#ストリノヴァ #Strinova

+]]>
+ Wed, 17 Jun 2026 11:00:03 GMT + 2067200596352876587 + https://nitter.net/Strinova_JP/status/2067200596352876587#m +
+ + 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 + @Strinova_EN + 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

+]]>
+ Wed, 17 Jun 2026 11:00:03 GMT + 2067200596340506904 + https://nitter.net/Strinova_EN/status/2067200596340506904#m +
+ + (CN) A Calabiyau x MSI offline event is going to commence this July. More details will be posted soon, so stay tuned! + @CalabiyauLeaks + (CN)
+A Calabiyau x MSI offline event is going to commence this July. More details will be posted soon, so stay tuned!

+]]>
+ Tue, 16 Jun 2026 09:03:45 GMT + 2066808939241693445 + https://nitter.net/CalabiyauLeaks/status/2066808939241693445#m +
+ + RT by @CalabiyauLeaks: 🐴【イベント:極彩の華】🐴 イベント期間中、週間クエストを完了すると、警報器の外観「絶えぬ狼煙」などの報酬を無料で獲得できます。 【仕上げの金彩】を購入してアップグレードすると、ミツバチのエピック外観「伝統の三彩釉」と限定メダルを入手できます。💎 📅 開催期間:2026年6月25日(木) ~ 2026年7月22日(水) #ストリノヴァ #Strinova + @Strinova_JP + 🐴【イベント:極彩の華】🐴
+
+イベント期間中、週間クエストを完了すると、警報器の外観「絶えぬ狼煙」などの報酬を無料で獲得できます。
+【仕上げの金彩】を購入してアップグレードすると、ミツバチのエピック外観「伝統の三彩釉」と限定メダルを入手できます。💎
+
+📅 開催期間:2026年6月25日(木) ~ 2026年7月22日(水)
+#ストリノヴァ #Strinova

+]]>
+ Tue, 16 Jun 2026 04:00:00 GMT + 2066732499208966290 + https://nitter.net/Strinova_JP/status/2066732499208966290#m +
+ + 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 + @Strinova_EN + 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

+]]>
+ Tue, 16 Jun 2026 04:00:17 GMT + 2066732572051493081 + https://nitter.net/Strinova_EN/status/2066732572051493081#m +
+ + 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】 "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." ⏰Duration: June 25th - July 23rd (UTC+0) #Strinova + @Strinova_EN + 【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】
+
+"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."
+
+⏰Duration: June 25th - July 23rd (UTC+0)
+
+#Strinova

+]]>
+ Mon, 15 Jun 2026 12:02:57 GMT + 2066491651011625040 + https://nitter.net/Strinova_EN/status/2066491651011625040#m +
+ +
+
diff --git a/twitterbridge/rss.py b/twitterbridge/rss.py new file mode 100644 index 0000000..a752f95 --- /dev/null +++ b/twitterbridge/rss.py @@ -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]}...')