Compare commits

..
66 Commits
Author SHA1 Message Date
YoursFunny 91cad86325 use http2 in httpx 2024-06-20 02:11:39 +08:00
YoursFunny cfdb05e476 add use uvloop 2024-06-20 01:41:40 +08:00
YoursFunny 6df6a950c8 update to httpx 2024-06-19 23:19:42 +08:00
YoursFunny 78d185723b add type checking import 2024-06-19 22:47:10 +08:00
YoursFunny 1252ec8804 fix import logger 2024-06-19 01:42:50 +08:00
YoursFunny cde1091c85 update call concurrently 2024-06-19 01:39:59 +08:00
YoursFunny 8b2b6b49ab update regex pattern 2024-06-19 01:03:16 +08:00
YoursFunny 86b4a972c9 bump version 2024-06-19 00:58:23 +08:00
YoursFunny 483a3de649 fix edit message caption 2024-05-13 00:55:49 +08:00
YoursFunny 48bc81898d fix edit message tuple 2024-05-13 00:47:37 +08:00
YoursFunny f660bc2c5e fix edit message url 2024-05-13 00:38:36 +08:00
YoursFunny 7747dab4a3 fix edit message caption 2024-05-13 00:32:17 +08:00
YoursFunny f2fac6a9e0 fix edit message 2024-05-13 00:24:03 +08:00
YoursFunny e3dcb6e59b fix send gif 2024-05-13 00:19:24 +08:00
YoursFunny a1d1dc5ea3 fix template replace 2024-05-13 00:00:08 +08:00
YoursFunny e629899a72 fix missing return 2024-05-12 23:51:19 +08:00
YoursFunny d079271ef9 fix action 2024-05-12 23:42:18 +08:00
YoursFunny 1526c7900c add set template 2024-05-12 23:34:11 +08:00
YoursFunny 79d68b2e82 fix int admin id 2024-04-28 01:57:10 +08:00
YoursFunny e700ae65ae add message user dict debug 2024-04-28 01:36:36 +08:00
YoursFunny 9ac6065366 remove dict key if disable edit forward 2024-04-28 01:33:50 +08:00
YoursFunny 40877b52eb fix confirm 2024-04-28 01:26:43 +08:00
YoursFunny 0ca9b55ea8 try edit reply markup 2024-04-28 01:18:35 +08:00
YoursFunny ef17b05c14 fix html escape only message 2024-04-28 01:12:02 +08:00
YoursFunny 85d0257c51 feat: change forward confirm reply markup 2024-04-28 01:08:22 +08:00
YoursFunny 7f90670f1c fix edit message html escape 2024-04-28 01:01:59 +08:00
YoursFunny 755c405196 update ptb version 2024-04-26 15:49:52 +08:00
YoursFunny 0ffa9903c4 add hint 2024-04-26 15:19:04 +08:00
YoursFunny 248e7d48e0 fix caption of multiple inline query results 2024-04-26 15:15:13 +08:00
YoursFunny e915cd13de fix html escape 2024-04-26 15:13:38 +08:00
YoursFunny a7d144f2ee fix multiple media forward 2024-03-25 16:54:08 +08:00
YoursFunny 081ee595b1 remove edit reply 2024-03-24 22:54:23 +08:00
YoursFunny 4272454f9a fix url match 2024-03-24 22:33:36 +08:00
YoursFunny 7d289c9db7 fix multiple line substitution 2024-03-24 22:32:22 +08:00
YoursFunny 500ba14565 fix caption text substitute 2024-03-24 22:15:06 +08:00
YoursFunny 1cb64b64e2 fix edit enable 2024-03-24 21:59:55 +08:00
YoursFunny 8ad9078c5e fix edit enable 2024-03-24 21:54:50 +08:00
YoursFunny ebfaaad75a fix message handler filter 2024-03-24 21:46:32 +08:00
YoursFunny fdd21e22ed add command handler 2024-03-24 21:20:34 +08:00
YoursFunny ee92b8d8aa add edit before forward 2024-03-24 21:17:05 +08:00
YoursFunny 50161243ea simplify send action 2024-03-24 16:45:42 +08:00
YoursFunny 7846873b9c fix video title 2024-03-24 01:03:39 +08:00
YoursFunny 6d06e28598 fix gif media 2024-03-23 15:01:17 +08:00
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
YoursFunny 5059a4d04a update docker.yml 2024-03-22 17:43:00 +08:00
8 changed files with 277 additions and 94 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 -1
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
@@ -40,7 +42,6 @@ jobs:
name: Build and push name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: .
push: true push: true
build-args: | build-args: |
APP_NAME=${{ env.APP_NAME }} APP_NAME=${{ env.APP_NAME }}
+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
+15 -4
View File
@@ -1,9 +1,16 @@
import logging import logging
import re
import os import os
import re
try:
import uvloop, asyncio
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
uvloop = None
BOT_TOKEN = os.getenv("BOT_TOKEN") BOT_TOKEN = os.getenv("BOT_TOKEN")
ADMIN = os.getenv("ADMIN").split(",") ADMIN = [int(i) for i in os.getenv("BOT_ADMIN").split(",")]
WEBHOOK = os.getenv("WEBHOOK", False) WEBHOOK = os.getenv("WEBHOOK", False)
if WEBHOOK: if WEBHOOK:
@@ -14,12 +21,16 @@ 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|fixvx|vxtwitter)\.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)
message_url_regex = re.compile(r"\[.+]", re.S)
logging.basicConfig( logging.basicConfig(
level=os.getenv("LOG_LEVEL", "WARNING"), level=os.getenv("LOG_LEVEL", "WARNING"),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
) )
logger = logging.getLogger(__name__)
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
+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
+163 -46
View File
@@ -1,77 +1,157 @@
from aiohttp import ClientSession import html
from telegram import Update, Chat from functools import wraps
from telegram.constants import ParseMode, ChatAction from typing import TYPE_CHECKING
from telegram.ext import (
Application, from telegram import InlineKeyboardButton, InlineKeyboardMarkup
ApplicationBuilder, from telegram.constants import ChatAction, ChatType, ParseMode
ContextTypes, from telegram.ext import (ApplicationBuilder, CallbackQueryHandler, CommandHandler, Defaults,
Defaults, InlineQueryHandler, MessageHandler, PicklePersistence, filters)
filters,
InlineQueryHandler,
PicklePersistence,
MessageHandler, CommandHandler
)
import common import common
from tweet import TGTweet from tweet import TGTweet
if TYPE_CHECKING:
from telegram import Chat, Message, Update
from telegram.ext import Application, ContextTypes
logger = common.get_logger(__name__)
def send_action(action):
def decorator(func):
@wraps(func)
async def command_func(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
await update.effective_chat.send_action(action)
return await func(update, context, *args, **kwargs)
return command_func
return decorator
async def inline_query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def inline_query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
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: 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)
async def reply_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: @send_action(ChatAction.UPLOAD_PHOTO)
await update.effective_chat.send_action(ChatAction.UPLOAD_PHOTO) async def url_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
# url = update.message.text.split(" ")[0]
url = update.message.text url = update.message.text
async with TGTweet(context.bot_data['client'], url) as tweet: 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_to_send = 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 not tweet.is_single_gif else await update.effective_message.reply_animation(
media[0][0],
caption=tweet.message_text,
reply_to_message_id=update.message.message_id,
has_spoiler=media[0][1]
) )
if not isinstance(message_to_send, tuple):
message_to_send = (message_to_send,)
url = tweet.url
if context.user_data.get('edit_before_forward', False):
message_reply = await update.effective_message.reply_text(
"Reply to edit message. [URL]",
reply_markup=InlineKeyboardMarkup.from_button(
InlineKeyboardButton("↩️ Confirm", callback_data="forward")
),
reply_to_message_id=update.message.message_id,
)
context.user_data['message_reply'] = message_reply
context.user_data['message_to_send'] = message_to_send
context.user_data['message_url'] = url
return
if 'forward_channel_id' in context.user_data: if 'forward_channel_id' in context.user_data:
try: await forward_message(update, context, message_to_send)
await update.effective_chat.copy_messages(
chat_id=context.user_data['forward_channel_id'],
message_ids=[m.id for m in message_sent],
)
except Exception as e:
await update.effective_message.reply_text(str(e))
async def forward_message(
update: Update,
context: ContextTypes.DEFAULT_TYPE,
message_to_send: tuple[Message, ...],
) -> None:
try:
await update.effective_chat.copy_messages(
context.user_data['forward_channel_id'],
[m.id for m in message_to_send]
)
except Exception as e:
await update.effective_message.reply_text(str(e))
async def edit_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if 'message_reply' not in context.user_data:
return
if update.message.reply_to_message != context.user_data['message_reply']:
return
template = context.user_data.get('template', None)
message_url = '<a href="{0}">{1}</a>'
url = context.user_data['message_url']
if template:
update_text = template.replace("[]", message_url.format(
url,
html.escape(update.message.text)
))
else:
update_text = html.escape(update.message.text)
match = common.message_url_regex.search(update_text)
if match:
match = match.span()
update_text = update_text[:match[0]] + message_url.format(
url,
update_text[match[0] + 1:match[1] - 1]
) + update_text[match[1]:]
message_to_send = context.user_data['message_to_send']
await message_to_send[0].edit_caption(update_text)
async def query_forward_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
message_to_send = context.user_data['message_to_send']
await forward_message(update, context, message_to_send)
await update.callback_query.answer('✅ Forwarded')
await update.callback_query.delete_message()
del context.user_data['message_reply']
del context.user_data['message_to_send']
del context.user_data['message_url']
@send_action(ChatAction.TYPING)
async def cmd_set_forward_channel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cmd_set_forward_channel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.effective_chat.send_action(ChatAction.TYPING)
if not context.args: if not context.args:
await update.effective_message.reply_text("Please provide a channel username or id.") await update.effective_message.reply_text("Please provide a channel username or id.")
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.")
@send_action(ChatAction.TYPING)
async def cmd_remove_forward_channel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def cmd_remove_forward_channel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.effective_chat.send_action(ChatAction.TYPING)
if 'forward_channel_id' in context.user_data: if 'forward_channel_id' in context.user_data:
del context.user_data['forward_channel_id'] del context.user_data['forward_channel_id']
await update.effective_message.reply_text("Remove successfully.") await update.effective_message.reply_text("Remove successfully.")
@@ -79,6 +159,41 @@ async def cmd_remove_forward_channel(update: Update, context: ContextTypes.DEFAU
await update.effective_message.reply_text("No channel to remove.") await update.effective_message.reply_text("No channel to remove.")
@send_action(ChatAction.TYPING)
async def cmd_edit_before_forward(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if context.user_data.get('forward_channel_id', None) is None:
await update.effective_message.reply_text("Please enable forward channel first.")
return
ebf_status = context.user_data.get('edit_before_forward', False)
if ebf_status:
context.user_data['edit_before_forward'] = False
context.user_data.pop('message_reply', None)
context.user_data.pop('message_to_send', None)
context.user_data.pop('message_url', None)
await update.effective_message.reply_text("Disable edit before forward.")
return
context.user_data['edit_before_forward'] = True
await update.effective_message.reply_text("Enable edit before forward.")
@send_action(ChatAction.TYPING)
async def cmd_set_template(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
reply = update.effective_message.reply_to_message
if not reply:
await update.effective_message.reply_text("Please reply to a message to set as template.")
return
if '[]' not in reply.text_html:
await update.effective_message.reply_text("Please reply to a message with [] to set as template.")
return
context.user_data['template'] = reply.text_html
await update.effective_message.reply_text("Template set.")
@send_action(ChatAction.TYPING)
async def cmd_user_dict(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.effective_message.reply_text(str(context.user_data))
async def post_init(application: Application) -> None: async def post_init(application: Application) -> None:
# commands = [ # commands = [
# BotCommand('start', CMD_START), # BotCommand('start', CMD_START),
@@ -87,7 +202,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.init_client()
async def post_stop(application: Application) -> None: async def post_stop(application: Application) -> None:
@@ -95,13 +210,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_client()
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,20 +223,23 @@ 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) .concurrent_updates(True)
# .get_updates_proxy(proxy_url)
.build() .build()
) )
# user_filter = filters.User() user_filter = filters.User()
# user_filter.add_user_ids(common.admin) user_filter.add_user_ids(common.ADMIN)
handlers = [ handlers = [
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),
MessageHandler(filters.Regex(common.x_url_regex) & filters.ChatType.PRIVATE, url_media),
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),
CommandHandler("edit_before_forward", cmd_edit_before_forward),
CommandHandler("set_template", cmd_set_template),
MessageHandler(~filters.COMMAND & filters.ChatType.PRIVATE, edit_message),
CallbackQueryHandler(query_forward_message, pattern="forward"),
CommandHandler("bot_dict", cmd_user_dict, filters=user_filter),
] ]
application.add_handlers(handlers) application.add_handlers(handlers)
+3 -2
View File
@@ -1,2 +1,3 @@
python-telegram-bot[webhooks]~=21.0.1 python-telegram-bot[webhooks]~=21.3
aiohttp[speedups]~=3.9.3 httpx[http2]~=0.27.0
uvloop~=0.19.0; sys_platform != 'win32'
+90 -37
View File
@@ -1,26 +1,36 @@
import html
from typing import Generator from typing import Generator
from aiohttp import ClientSession from httpx import AsyncClient
from telegram import ( from telegram import (InlineQueryResultMpeg4Gif, InlineQueryResultPhoto, InlineQueryResultVideo, InputMediaPhoto,
InlineQueryResultPhoto, InputMediaVideo)
InlineQueryResultVideo,
InputMediaPhoto, from common import get_logger, x_media_regex, x_tco_regex, x_url_regex
InputMediaVideo
) logger = get_logger(__name__)
from common import x_url_regex, x_media_regex, x_tco_regex
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}
""" """
async def fetch_json(session: ClientSession, url: str) -> dict: def create_client() -> 'AsyncClient':
async with session.get(url) as response: return AsyncClient(http2=True)
assert response.status == 200
return await response.json()
async def close_client(_client: 'AsyncClient') -> None:
await _client.aclose()
async def fetch_json(_client: 'AsyncClient', url: str) -> dict:
logger.info(f"Fetching {url}")
response = await _client.get(url)
assert response.status_code == response.is_success, f"Failed to fetch {url}, status code {response.status_code}"
return response.json()
class TweetMedia: class TweetMedia:
@@ -29,20 +39,27 @@ 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
case "gif":
return self._url
case _:
return self._url
@property @property
def thumb(self) -> str: def thumb(self) -> str:
@@ -51,6 +68,10 @@ class TweetMedia:
return f"{twimg_url}{self._uri}?format=jpg&name=thumb" return f"{twimg_url}{self._uri}?format=jpg&name=thumb"
case "video": case "video":
return self._thumb return self._thumb
case "gif":
return self._thumb
case _:
return self._thumb
@property @property
def type(self) -> str: def type(self) -> str:
@@ -104,28 +125,38 @@ class Tweet:
class TGTweet(Tweet): class TGTweet(Tweet):
def __init__(self, session: ClientSession, url: str): _httpx_client: AsyncClient
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 self._is_single_gif: bool = False
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 init_client(cls) -> None:
cls._httpx_client = create_client()
@classmethod
async def close_client(cls) -> None:
await close_client(cls._httpx_client)
async def _fetch_tweet(self, api_param: tuple[str]) -> dict:
return await fetch_json(self._httpx_client, 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,48 +177,61 @@ 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
def is_single_gif(self) -> bool:
return self._is_single_gif
@property @property
def message_text(self) -> str: def message_text(self) -> str:
return message_raw_text.format( return message_raw_text.format(
url=self.url, url=self.url,
author_url=self.author_url, author_url=self.author_url,
author=self.author, author=html.escape(self.author),
text=self.text text=html.escape(self.text)
) )
@property @property
def inline_query_generator(self) -> Generator[InlineQueryResultPhoto | InlineQueryResultVideo, None, None]: def inline_query_generator(self) -> Generator[
InlineQueryResultPhoto | InlineQueryResultVideo | InlineQueryResultMpeg4Gif, 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, caption=self.message_text
description=self.text,
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, title=self.text,
description=self.text, caption=self.message_text
caption=self.message_text if not i else None )
elif tweet_media.type == "gif":
yield InlineQueryResultMpeg4Gif(
id=str(i),
mpeg4_url=tweet_media.url,
thumbnail_url=tweet_media.thumb,
caption=self.message_text
) )
@property @property
def pm_media_generator(self) -> Generator[InputMediaPhoto | InputMediaVideo, None, None]: def pm_media_generator(self) -> Generator[InputMediaPhoto | InputMediaVideo | tuple[str, bool], 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 +243,12 @@ class TGTweet(Tweet):
has_spoiler=self.sensitive, has_spoiler=self.sensitive,
thumbnail=tweet_media.thumb thumbnail=tweet_media.thumb
) )
elif tweet_media.type == "gif":
if len(self.media) == 1:
self._is_single_gif = True
yield tweet_media.url, self.sensitive
yield InputMediaVideo(
media=tweet_media.url,
has_spoiler=self.sensitive,
thumbnail=tweet_media.thumb
)