Compare commits

...
22 Commits
Author SHA1 Message Date
YoursFunny 4083b09093 add gif support 2024-03-23 14:54:22 +08:00
YoursFunny 8fb53e8412 fix session 2024-03-23 00:17:59 +08:00
YoursFunny 5c0ee8b491 fix data 2024-03-22 23:06:21 +08:00
YoursFunny af561ed0a0 fix pers_data 2024-03-22 23:03:04 +08:00
YoursFunny 985fe3fbfe update api format and logger 2024-03-22 22:45:52 +08:00
YoursFunny 19fb5b9822 fix logger 2024-03-22 22:34:59 +08:00
YoursFunny 1185d4bc5f fix logger 2024-03-22 22:26:31 +08:00
YoursFunny c3faecf640 add logger 2024-03-22 22:18:43 +08:00
YoursFunny 76e0ffe58c fix image suffix 2024-03-22 21:32:32 +08:00
YoursFunny a38c1b05b4 fix twitter id 2024-03-22 21:11:19 +08:00
YoursFunny b3c9b1ace7 fix api request 2024-03-22 21:03:25 +08:00
YoursFunny 6a7ab11148 fix query id duplicate 2024-03-22 20:13:46 +08:00
YoursFunny 18f1f00259 fix docker compose file 2024-03-22 19:46:30 +08:00
YoursFunny 04da6eb3d9 fix reply message 2024-03-22 19:45:26 +08:00
YoursFunny 7a35b06d4e fix channel check 2024-03-22 19:04:30 +08:00
YoursFunny a5ae2321c5 fix regex pattern 2024-03-22 19:02:38 +08:00
YoursFunny d263071946 add assert message 2024-03-22 18:57:59 +08:00
YoursFunny 0491d8c3fc fix pers_data 2024-03-22 18:55:48 +08:00
YoursFunny 3afea136a4 update docker.yml 2024-03-22 18:41:09 +08:00
YoursFunny 87108b2ac8 fix await 2024-03-22 18:41:07 +08:00
YoursFunny 4613e4c825 remove version in compose 2024-03-22 17:56:59 +08:00
YoursFunny eb1016f137 fix typo 2024-03-22 17:49:38 +08:00
7 changed files with 77 additions and 48 deletions
+1 -1
View File
@@ -27,5 +27,5 @@
.python-version .python-version
LICENSE LICENSE
README.md README.md
pers_data data/
cert/ cert/
+2
View File
@@ -4,6 +4,8 @@ on:
push: push:
tags: tags:
- v* - v*
branches:
- master
env: env:
APP_NAME: telegram-twitter-media-bot APP_NAME: telegram-twitter-media-bot
+1 -1
View File
@@ -1,6 +1,6 @@
**/.idea **/.idea
__pycache__/ __pycache__/
cert/ cert/
pers_data data/
docker-compose.yml docker-compose.yml
x.py x.py
+3 -3
View File
@@ -1,9 +1,9 @@
import logging import logging
import re
import os import os
import re
BOT_TOKEN = os.getenv("BOT_TOKEN") BOT_TOKEN = os.getenv("BOT_TOKEN")
ADMIN = os.getenv("ADMIN").split(",") ADMIN = os.getenv("BOT_ADMIN").split(",")
WEBHOOK = os.getenv("WEBHOOK", False) WEBHOOK = os.getenv("WEBHOOK", False)
if WEBHOOK: if WEBHOOK:
@@ -14,7 +14,7 @@ if WEBHOOK:
WEBHOOK_CERT = os.getenv("WEBHOOK_CERT", "cert/cert.pem") WEBHOOK_CERT = os.getenv("WEBHOOK_CERT", "cert/cert.pem")
WEBHOOK_SECRET_TOKEN = os.getenv("WEBHOOK_SECRET_TOKEN") WEBHOOK_SECRET_TOKEN = os.getenv("WEBHOOK_SECRET_TOKEN")
x_url_regex = re.compile(r"^(?:https?://)(?:www\.|mobile\.|)(x|twitter)\.com/.+/status/(\d+)") x_url_regex = re.compile(r"^(?:https?://)(?:www\.|mobile\.|)(?:x|twitter)\.com/(.+)/status/(\d+)")
x_media_regex = re.compile(r"^(?:https?://)(pbs|video)\.twimg\.com/(.*)") x_media_regex = re.compile(r"^(?:https?://)(pbs|video)\.twimg\.com/(.*)")
x_tco_regex = re.compile(r"(?:https?://)t\.co/.+$", re.M) x_tco_regex = re.compile(r"(?:https?://)t\.co/.+$", re.M)
+2 -2
View File
@@ -1,4 +1,3 @@
version: '3.7'
services: services:
tgxmb: image: yoursfunny/telegram-twitter-media-bot: latest tgxmb: image: yoursfunny/telegram-twitter-media-bot: latest
restart: always restart: always
@@ -16,6 +15,7 @@ services:
WEBHOOK_CERT: './cert/cert.pem' WEBHOOK_CERT: './cert/cert.pem'
WEBHOOK_SECRET_TOKEN: 'secret-token' WEBHOOK_SECRET_TOKEN: 'secret-token'
# LOG_LEVEL: 'WARNING' # LOG_LEVEL: 'WARNING'
# volumes: volumes:
- ./data: /app/data
# - ./cert:/app/cert # - ./cert:/app/cert
container_name: tgxmb container_name: tgxmb
+20 -19
View File
@@ -1,6 +1,6 @@
from aiohttp import ClientSession from aiohttp import ClientSession
from telegram import Update, Chat from telegram import Update, Chat
from telegram.constants import ParseMode, ChatAction from telegram.constants import ParseMode, ChatAction, ChatType
from telegram.ext import ( from telegram.ext import (
Application, Application,
ApplicationBuilder, ApplicationBuilder,
@@ -9,7 +9,8 @@ from telegram.ext import (
filters, filters,
InlineQueryHandler, InlineQueryHandler,
PicklePersistence, PicklePersistence,
MessageHandler, CommandHandler MessageHandler,
CommandHandler
) )
import common import common
@@ -20,7 +21,8 @@ async def inline_query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
query = update.inline_query.query query = update.inline_query.query
if query == "": if query == "":
return return
async with TGTweet(context.bot_data['client'], query) as tweet: common.logger.info(f"Query: {query}")
async with TGTweet(query) as tweet:
result = list(tweet.inline_query_generator) result = list(tweet.inline_query_generator)
await update.inline_query.answer(result) await update.inline_query.answer(result)
@@ -29,11 +31,13 @@ async def reply_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non
await update.effective_chat.send_action(ChatAction.UPLOAD_PHOTO) await update.effective_chat.send_action(ChatAction.UPLOAD_PHOTO)
# url = update.message.text.split(" ")[0] # url = update.message.text.split(" ")[0]
url = update.message.text url = update.message.text
async with TGTweet(context.bot_data['client'], url) as tweet: common.logger.info(f"Receiving url: {url}")
async with TGTweet(url) as tweet:
media = list(tweet.pm_media_generator) media = list(tweet.pm_media_generator)
message_sent = await update.effective_message.reply_media_group( message_sent = await update.effective_message.reply_media_group(
media, caption=tweet.message_text, media,
# reply_to_message_id=update.message.message_id caption=tweet.message_text,
reply_to_message_id=update.message.message_id
) )
if 'forward_channel_id' in context.user_data: if 'forward_channel_id' in context.user_data:
try: try:
@@ -52,19 +56,20 @@ async def cmd_set_forward_channel(update: Update, context: ContextTypes.DEFAULT_
return return
channel = context.args[0] channel = context.args[0]
try: try:
channel: Chat = context.bot.get_chat(channel) channel: Chat = await context.bot.get_chat(channel)
except Exception as e: except Exception as e:
await update.effective_message.reply_text(str(e)) await update.effective_message.reply_text(str(e))
return return
if channel.type != "CHANNEL": if channel.type != ChatType.CHANNEL:
await update.effective_message.reply_text("That is not a channel.") await update.effective_message.reply_text("That is not a channel.")
return return
channel_admin = await channel.get_administrators() try:
channel_admin = await channel.get_administrators()
except Exception as e:
await update.effective_message.reply_text(str(e) + "\nPlease add the bot to the channel and set as admin")
return
user_bot = filter(lambda x: x.user.id == context.bot.id, channel_admin) user_bot = filter(lambda x: x.user.id == context.bot.id, channel_admin)
user_bot = next(user_bot, None) user_bot = next(user_bot, None)
if not user_bot:
await update.effective_message.reply_text("I am not an admin in that channel.")
return
if user_bot.can_post_messages: if user_bot.can_post_messages:
context.user_data['forward_channel_id'] = channel.id context.user_data['forward_channel_id'] = channel.id
await update.effective_message.reply_text("Add successfully.") await update.effective_message.reply_text("Add successfully.")
@@ -87,7 +92,7 @@ async def post_init(application: Application) -> None:
DESCRIPTION = "A bot to fetch tweets from Twitter." DESCRIPTION = "A bot to fetch tweets from Twitter."
await application.bot.set_my_description(DESCRIPTION) await application.bot.set_my_description(DESCRIPTION)
await application.bot.set_my_short_description(DESCRIPTION) await application.bot.set_my_short_description(DESCRIPTION)
application.bot_data['client'] = ClientSession() TGTweet.set_session(ClientSession())
async def post_stop(application: Application) -> None: async def post_stop(application: Application) -> None:
@@ -95,13 +100,12 @@ async def post_stop(application: Application) -> None:
async def post_shutdown(application: Application) -> None: async def post_shutdown(application: Application) -> None:
await application.bot_data['client'].close() await TGTweet.close_session()
def main(): def main():
defaults = Defaults(parse_mode=ParseMode.HTML, allow_sending_without_reply=True) defaults = Defaults(parse_mode=ParseMode.HTML, allow_sending_without_reply=True)
persistence = PicklePersistence(filepath='pers_data') persistence = PicklePersistence(filepath='data/pers.pkl')
# proxy_url = 'http://127.0.0.1:10809'
application = (ApplicationBuilder() application = (ApplicationBuilder()
.token(common.BOT_TOKEN) .token(common.BOT_TOKEN)
.defaults(defaults) .defaults(defaults)
@@ -109,8 +113,6 @@ def main():
.post_init(post_init) .post_init(post_init)
.post_stop(post_stop) .post_stop(post_stop)
.post_shutdown(post_shutdown) .post_shutdown(post_shutdown)
# .proxy(proxy_url)
# .get_updates_proxy(proxy_url)
.build() .build()
) )
@@ -119,7 +121,6 @@ def main():
handlers = [ handlers = [
MessageHandler(filters.Regex(common.x_url_regex) & filters.ChatType.PRIVATE, reply_media), MessageHandler(filters.Regex(common.x_url_regex) & filters.ChatType.PRIVATE, reply_media),
# MessageHandler(None, reply_media),
InlineQueryHandler(inline_query, common.x_url_regex), InlineQueryHandler(inline_query, common.x_url_regex),
CommandHandler("set_forward_channel", cmd_set_forward_channel), CommandHandler("set_forward_channel", cmd_set_forward_channel),
CommandHandler("remove_forward_channel", cmd_remove_forward_channel), CommandHandler("remove_forward_channel", cmd_remove_forward_channel),
+48 -22
View File
@@ -5,12 +5,13 @@ from telegram import (
InlineQueryResultPhoto, InlineQueryResultPhoto,
InlineQueryResultVideo, InlineQueryResultVideo,
InputMediaPhoto, InputMediaPhoto,
InputMediaVideo InputMediaVideo, InputMediaAnimation, InlineQueryResultMpeg4Gif
) )
from common import x_url_regex, x_media_regex, x_tco_regex
from common import x_url_regex, x_media_regex, x_tco_regex, logger
twimg_url = 'https://pbs.twimg.com/' twimg_url = 'https://pbs.twimg.com/'
vx_api_url = 'https://api.vxtwitter.com/status/' vx_api_url = 'https://api.vxtwitter.com/{0}/status/{1}'
message_raw_text = """{url} message_raw_text = """{url}
<a href="{author_url}">{author}</a>: {text} <a href="{author_url}">{author}</a>: {text}
@@ -18,8 +19,9 @@ message_raw_text = """{url}
async def fetch_json(session: ClientSession, url: str) -> dict: async def fetch_json(session: ClientSession, url: str) -> dict:
logger.info(f"Fetching {url}")
async with session.get(url) as response: async with session.get(url) as response:
assert response.status == 200 assert response.status == 200, f"Failed to fetch {url}, status code {response.status}"
return await response.json() return await response.json()
@@ -29,18 +31,21 @@ class TweetMedia:
self._thumb: str = thumb self._thumb: str = thumb
self._type: str = media_type self._type: str = media_type
def __str__(self):
return f"Media[url: {self.url} thumb: {self.thumb} type: {self.type}]"
@property @property
def _uri(self) -> str | None: def _uri(self) -> str | None:
match = x_media_regex.match(self._url) match = x_media_regex.match(self._url)
if match: if match:
return match.group(2).removesuffix('.jpg') return match.group(2).removesuffix('.jpg').removesuffix('.png')
return None return None
@property @property
def url(self) -> str: def url(self) -> str:
match self._type: match self._type:
case "image": case "image":
return f"{twimg_url}{self._uri}?format=jpg&name=orig" return f"{twimg_url}{self._uri}?format=jpg&name=4096x4096"
case "video": case "video":
return self._url return self._url
@@ -104,28 +109,37 @@ class Tweet:
class TGTweet(Tweet): class TGTweet(Tweet):
def __init__(self, session: ClientSession, url: str): _session: ClientSession
self._session: ClientSession = session
def __init__(self, url: str):
self._url: str = url self._url: str = url
self._id: str = self._tweet_id self._api_param: tuple[str] = self._tweet_id
assert self._id assert self._api_param
async def __aenter__(self): async def __aenter__(self):
self._tweet: dict = await self._fetch_tweet(self._id) self._tweet: dict = await self._fetch_tweet(self._api_param)
super().__init__(*self._init_properties) super().__init__(*self._init_properties)
return self return self
async def __aexit__(self, exc_type, exc_val, exc_tb): async def __aexit__(self, exc_type, exc_val, exc_tb):
pass pass
async def _fetch_tweet(self, tweet_id: str) -> dict: @classmethod
return await fetch_json(self._session, vx_api_url + tweet_id) def set_session(cls, session: ClientSession) -> None:
cls._session = session
@classmethod
async def close_session(cls) -> None:
await cls._session.close()
async def _fetch_tweet(self, api_param: tuple[str]) -> dict:
return await fetch_json(self._session, vx_api_url.format(*api_param))
@property @property
def _tweet_id(self) -> str | None: def _tweet_id(self) -> tuple[str] | None:
match = x_url_regex.match(self._url) match = x_url_regex.match(self._url)
if match: if match:
return match.group(1).strip('/') return match.groups()
return None return None
@property @property
@@ -146,12 +160,13 @@ class TGTweet(Tweet):
@property @property
def _init_properties(self) -> tuple: def _init_properties(self) -> tuple:
id = self._tweet['tweetID']
author = self._tweet['user_name'] author = self._tweet['user_name']
author_id = self._tweet['user_screen_name'] author_id = self._tweet['user_screen_name']
text = self._tweet_text text = self._tweet_text
media = self._tweet_media media = self._tweet_media
sensitive = self._tweet['possibly_sensitive'] sensitive = self._tweet['possibly_sensitive']
return self._id, author, author_id, text, media, sensitive return id, author, author_id, text, media, sensitive
@property @property
def message_text(self) -> str: def message_text(self) -> str:
@@ -165,29 +180,34 @@ class TGTweet(Tweet):
@property @property
def inline_query_generator(self) -> Generator[InlineQueryResultPhoto | InlineQueryResultVideo, None, None]: def inline_query_generator(self) -> Generator[InlineQueryResultPhoto | InlineQueryResultVideo, None, None]:
for i, tweet_media in enumerate(self.media): for i, tweet_media in enumerate(self.media):
logger.info(str(tweet_media))
if tweet_media.type == "image": if tweet_media.type == "image":
yield InlineQueryResultPhoto( yield InlineQueryResultPhoto(
id=self.id, id=str(i),
photo_url=tweet_media.url, photo_url=tweet_media.url,
thumbnail_url=tweet_media.thumb, thumbnail_url=tweet_media.thumb,
title=self.url,
description=self.text,
caption=self.message_text if not i else None caption=self.message_text if not i else None
) )
elif tweet_media.type == "video": elif tweet_media.type == "video":
yield InlineQueryResultVideo( yield InlineQueryResultVideo(
id=self.id, id=str(i),
video_url=tweet_media.url, video_url=tweet_media.url,
mime_type="video/mp4", mime_type="video/mp4",
thumbnail_url=tweet_media.thumb, thumbnail_url=tweet_media.thumb,
title=self.url, caption=self.message_text if not i else None
description=self.text, )
elif tweet_media.type == "gif":
yield InlineQueryResultMpeg4Gif(
id=str(i),
mpeg4_url=tweet_media.url,
thumbnail_url=tweet_media.thumb,
caption=self.message_text if not i else None caption=self.message_text if not i else None
) )
@property @property
def pm_media_generator(self) -> Generator[InputMediaPhoto | InputMediaVideo, None, None]: def pm_media_generator(self) -> Generator[InputMediaPhoto | InputMediaVideo, None, None]:
for tweet_media in self.media: for tweet_media in self.media:
logger.info(str(tweet_media))
if tweet_media.type == "image": if tweet_media.type == "image":
yield InputMediaPhoto( yield InputMediaPhoto(
media=tweet_media.url, media=tweet_media.url,
@@ -199,3 +219,9 @@ class TGTweet(Tweet):
has_spoiler=self.sensitive, has_spoiler=self.sensitive,
thumbnail=tweet_media.thumb thumbnail=tweet_media.thumb
) )
elif tweet_media.type == "gif":
yield InputMediaAnimation(
media=tweet_media.url,
has_spoiler=self.sensitive,
thumbnail=tweet_media.thumb
)