Compare commits

...
62 Commits
Author SHA1 Message Date
YoursFunny 9c2606160a format 2024-10-19 22:41:41 +08:00
YoursFunny 3fe8a1cf3b new bsky support 2024-10-19 22:25:07 +08:00
YoursFunny 62a2820af3 add params support in fetch_json 2024-10-19 22:23:51 +08:00
YoursFunny 80b630d28d minor fixes 2024-10-19 22:22:27 +08:00
YoursFunny 4173972407 fix empty env bot_admin 2024-10-19 22:21:56 +08:00
YoursFunny 0d2648162c bump version 2024-10-19 22:19:44 +08:00
YoursFunny 0847ada3b7 update gitignore 2024-10-18 00:21:19 +08:00
YoursFunny f0c287a148 fix await 2024-08-15 17:45:14 +08:00
YoursFunny 3658bfd2aa fix handler 2024-08-15 17:42:19 +08:00
YoursFunny ae1b04dee8 add extract url from message 2024-08-15 17:34:40 +08:00
YoursFunny 641f7218a4 minor fixes 2024-08-15 16:45:36 +08:00
YoursFunny 61dfa16011 fix edit message str format 2024-08-15 16:45:06 +08:00
YoursFunny 2986e80076 fix edit message 2024-08-14 01:52:01 +08:00
YoursFunny f112ecdf63 fix text 2024-08-14 01:30:08 +08:00
YoursFunny 79d50ae9a3 fix 2024-08-14 01:19:50 +08:00
YoursFunny 416331303d fix 2024-08-14 01:14:01 +08:00
YoursFunny 55771e01fe fix 2024-08-14 01:13:47 +08:00
YoursFunny afca276f49 minor fixes 2024-08-14 00:57:20 +08:00
YoursFunny 06d25854d2 fix error import 2024-08-14 00:37:01 +08:00
YoursFunny 515aa9711d new set template 2024-08-14 00:29:02 +08:00
YoursFunny 733adfc4a6 refactor using custom callback context 2024-08-13 23:20:24 +08:00
YoursFunny 03066853de refactor 2024-08-13 19:17:58 +08:00
YoursFunny 566c17a855 fix pixiv url 2024-07-19 19:34:18 +08:00
YoursFunny 87f0d16028 fix regex https pattern 2024-07-16 14:28:49 +08:00
YoursFunny 197993e522 bump version 2024-07-16 14:27:15 +08:00
YoursFunny 02abcd899e update pixiv regex 2024-07-16 14:21:43 +08:00
YoursFunny ebb48d2bb5 fix forward channel check admin user 2024-07-09 16:27:13 +08:00
YoursFunny be77a33e9d use large instead of origin for pixiv 2024-06-24 20:02:35 +08:00
YoursFunny 0439adefa0 add refresh token if expire 2024-06-24 15:57:30 +08:00
YoursFunny 81e5d2dbd1 add log 2024-06-24 15:47:12 +08:00
YoursFunny e7ccd70e56 fix deal with invalid url 2024-06-24 02:17:26 +08:00
YoursFunny 23128af96a fix regex 2024-06-24 02:14:50 +08:00
YoursFunny fe1724aca4 fix annotations 2024-06-24 02:06:25 +08:00
YoursFunny 0433650563 fix init pixiv 2024-06-24 02:03:28 +08:00
YoursFunny a617a7e1a0 fix requirement conflict 2024-06-24 01:48:45 +08:00
YoursFunny e35e3895ba fix requirement conflict 2024-06-24 01:45:23 +08:00
YoursFunny a4a9f2eaa0 add pixiv package requirement 2024-06-24 01:29:29 +08:00
YoursFunny 1c779e98a8 add pixiv support 2024-06-24 01:25:03 +08:00
YoursFunny 4a5aa296ce add pixiv refresh token setting 2024-06-23 22:14:58 +08:00
YoursFunny c1c1d42892 refactor, clear structure 2024-06-23 22:06:27 +08:00
YoursFunny cade370538 refactor 2024-06-23 21:52:12 +08:00
YoursFunny a6325d5bc8 fix check instance 2024-06-20 15:46:47 +08:00
YoursFunny 6c0e581135 fix regex match group 2024-06-20 15:40:18 +08:00
YoursFunny 494fc50446 fix missing aexit 2024-06-20 15:29:30 +08:00
YoursFunny a4e568667c fix type import error 2024-06-20 15:25:17 +08:00
YoursFunny 8231eeb20e refactor tweet, better type hint 2024-06-20 15:13:09 +08:00
YoursFunny 74f42a2df3 fix status code check 2024-06-20 02:51:20 +08:00
YoursFunny ae1f5a8c8c rename 2024-06-20 02:48:06 +08:00
YoursFunny 38adbf0cb8 use cached property 2024-06-20 02:40:35 +08:00
YoursFunny 55ae73ee6b use uuid for query 2024-06-20 02:21:14 +08:00
YoursFunny d6589fec5b fix annotations 2024-06-20 02:20:10 +08:00
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
17 changed files with 1078 additions and 362 deletions
+8 -1
View File
@@ -3,4 +3,11 @@ __pycache__/
cert/
data/
docker-compose.yml
x.py
utils/x.py
.env
# Added by cargo
/target
+1 -1
View File
@@ -7,7 +7,7 @@ ENV PYTHONUNBUFFERED=1
RUN set -eux; \
apt-get update; \
apt-get install -y gosu; \
apt-get install -y git gosu; \
rm -rf /var/lib/apt/lists/*; \
# verify that the binary works
gosu nobody true
+11 -14
View File
@@ -1,9 +1,17 @@
import logging
import os
import re
try:
import uvloop
import asyncio
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
uvloop = None
BOT_TOKEN = os.getenv("BOT_TOKEN")
ADMIN = [int(i) for i in os.getenv("BOT_ADMIN").split(",")]
ADMIN = [int(i) for i in os.getenv("BOT_ADMIN", "").split(",") if i]
PIXIV_REFRESH_TOKEN = os.getenv("PIXIV_REFRESH_TOKEN")
WEBHOOK = os.getenv("WEBHOOK", False)
if WEBHOOK:
@@ -13,14 +21,3 @@ if WEBHOOK:
WEBHOOK_KEY = os.getenv("WEBHOOK_KEY", "cert/private.key")
WEBHOOK_CERT = os.getenv("WEBHOOK_CERT", "cert/cert.pem")
WEBHOOK_SECRET_TOKEN = os.getenv("WEBHOOK_SECRET_TOKEN")
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_tco_regex = re.compile(r"(?:https?://)t\.co/.+$", re.M)
message_url_regex = re.compile(r"\[.+]", re.S)
logging.basicConfig(
level=os.getenv("LOG_LEVEL", "WARNING"),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
+3 -2
View File
@@ -1,12 +1,13 @@
services:
tgxmb: image: yoursfunny/telegram-twitter-media-bot: latest
restart: always
ports:
- "8443:8443"
# ports:
# - "8443:8443"
environment:
LOCAL_USER_ID: '1000'
BOT_TOKEN: ''
BOT_ADMIN: ''
PIXIV_REFRESH_TOKEN: ''
WEBHOOK: false
WEBHOOK_LISTEN: '127.0.0.1'
WEBHOOK_PORT: 8443
+147 -94
View File
@@ -1,136 +1,175 @@
from __future__ import annotations
import html
from functools import wraps
from typing import TYPE_CHECKING
from aiohttp import ClientSession
from telegram import Update, Chat, InlineKeyboardMarkup, InlineKeyboardButton, Message
from telegram.constants import ParseMode, ChatAction, ChatType
from telegram.ext import (
Application,
ApplicationBuilder,
ContextTypes,
Defaults,
filters,
InlineQueryHandler,
PicklePersistence,
MessageHandler,
CommandHandler, CallbackQueryHandler
)
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, MessageEntity
from telegram.constants import ChatAction, ChatType, ParseMode
from telegram.ext import (ApplicationBuilder, CallbackQueryHandler, CommandHandler, ContextTypes, Defaults,
InlineQueryHandler, MessageHandler, PicklePersistence, filters)
import common
from tweet import TGTweet
import utils.regex as regex
from utils.context import ChatData, CustomContext, EditMessage
from utils.logger import get_logger
from utils.net import NetClient
from utils.pixiv import ProcessPixiv
from utils.telegram import Telegram
if TYPE_CHECKING:
from telegram import Message, Update
from telegram.ext import Application
logger = 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)
async def command_func(update: Update, context: CustomContext, *args, **kwargs):
try:
await update.effective_chat.send_action(action)
finally:
return await func(update, context, *args, **kwargs)
return command_func
return decorator
async def inline_query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
def extract_urls(message: Message) -> set[str]:
types = [MessageEntity.URL, MessageEntity.TEXT_LINK]
res = message.parse_entities(types)
res.update(message.parse_caption_entities(types))
res.update({key: key.url for key in res if key.type == MessageEntity.TEXT_LINK})
return set(res.values())
async def inline_query(update: Update, context: CustomContext) -> None:
query = update.inline_query.query
if query == "":
return
common.logger.info(f"Query: {query}")
async with TGTweet(query) as tweet:
result = list(tweet.inline_query_generator)
await update.inline_query.answer(result)
logger.info(f"Query: {query}")
async with Telegram(query) as tweet:
if not tweet:
return
await update.inline_query.answer(tweet.inline_query_result())
@send_action(ChatAction.UPLOAD_PHOTO)
async def url_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
url = update.message.text
common.logger.info(f"Receiving url: {url}")
async with TGTweet(url) as tweet:
media = list(tweet.pm_media_generator)
async def url_media(update: Update, context: CustomContext, url: str) -> None:
async with Telegram(url) as tweet:
if not tweet:
return
media = tweet.message_media_result()
if not media:
await update.effective_message.reply_text(
"No media found or media type is not supported.",
reply_to_message_id=update.message.message_id,
)
return
message_to_send = await update.effective_message.reply_media_group(
media,
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(
) if not isinstance(media[0], tuple) 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):
if context.chat_data.edit_before_forward:
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 edit message.",
reply_markup=InlineKeyboardMarkup.from_column(
[InlineKeyboardButton(name, callback_data=f"template|{name}") for name in
context.chat_data.template.keys()] + [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
context.chat_data.edit_message[message_reply.id] = EditMessage(
url=url,
forward=message_to_send
)
return
if 'forward_channel_id' in context.user_data:
if context.chat_data.forward_channel_id:
await forward_message(update, context, message_to_send)
async def handel_url_media(update: Update, context: CustomContext) -> None:
url = update.message.text
logger.info(f"Receiving url: {url}")
await url_media(update, context, url)
async def forward_message(
update: Update,
context: ContextTypes.DEFAULT_TYPE,
context: CustomContext,
message_to_send: tuple[Message, ...],
) -> None:
try:
await update.effective_chat.copy_messages(
context.user_data['forward_channel_id'],
context.chat_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)
if template:
update_text = template.replace("[]", 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]] + '<a href="{0}">{1}</a>'.format(
context.user_data['message_url'],
update_text[match[0] + 1:match[1] - 1]
) + update_text[match[1]:]
message_to_send = context.user_data['message_to_send']
if isinstance(message_to_send, tuple):
await message_to_send[0].edit_text(update_text)
else:
await message_to_send.edit_caption(update_text)
async def edit_message(update: Update, context: CustomContext) -> bool:
if not (reply := update.message.reply_to_message):
return False
_edit_message = context.chat_data.edit_message.get(reply.id, None)
if not _edit_message:
return False
new_text = '<a href="{0}">{1}</a>'.format(
_edit_message.url,
html.escape(update.message.text)
)
update_text = context.chat_data.template[template].replace("[]", new_text) if (
template := _edit_message.template) else new_text
await _edit_message.forward[0].edit_caption(update_text)
return True
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)
async def handle_message(update: Update, context: CustomContext) -> None:
if await edit_message(update, context):
return
if not (urls := extract_urls(update.message)):
return
for url in urls:
await url_media(update, context, url)
async def query_forward_message(update: Update, context: CustomContext) -> None:
_edit_message = context.chat_data.edit_message[update.effective_message.id]
await forward_message(update, context, _edit_message.forward)
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']
del _edit_message
async def query_template(update: Update, context: CustomContext) -> None:
query = update.callback_query
await query.answer()
name = query.data.split("|")[1]
_edit_message = context.chat_data.edit_message[query.message.message_id]
_edit_message.template = name
await _edit_message.forward[0].edit_caption(context.chat_data.template[name])
@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: CustomContext) -> None:
if not context.args:
await update.effective_message.reply_text("Please provide a channel username or id.")
return
channel = context.args[0]
try:
channel: Chat = await context.bot.get_chat(channel)
channel = await context.bot.get_chat(channel)
except Exception as e:
await update.effective_message.reply_text(str(e))
return
@@ -142,55 +181,60 @@ async def cmd_set_forward_channel(update: Update, context: ContextTypes.DEFAULT_
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 = filter(lambda x: x.user.id == update.effective_user.id, channel_admin)
user = next(user, None)
if not user:
await update.effective_message.reply_text("You are not an admin of the channel.")
return
user_bot = filter(lambda x: x.user.id == context.bot.id, channel_admin)
user_bot = next(user_bot, None)
if user_bot.can_post_messages:
context.user_data['forward_channel_id'] = channel.id
context.chat_data.forward_channel_id = channel.id
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:
if 'forward_channel_id' in context.user_data:
del context.user_data['forward_channel_id']
async def cmd_remove_forward_channel(update: Update, context: CustomContext) -> None:
if context.chat_data.forward_channel_id:
context.chat_data.forward_channel_id = None
await update.effective_message.reply_text("Remove successfully.")
return
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:
async def cmd_edit_before_forward(update: Update, context: CustomContext) -> None:
if context.chat_data.forward_channel_id 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)
if context.chat_data.edit_before_forward:
context.chat_data.edit_before_forward = False
context.chat_data.edit_message.clear()
await update.effective_message.reply_text("Disable edit before forward.")
return
context.user_data['edit_before_forward'] = True
context.chat_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:
async def cmd_set_template(update: Update, context: CustomContext) -> 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:
if '[]' not in (template := 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
if not context.args:
await update.effective_message.reply_text("Please provide a name for the template.")
return
context.chat_data.template[''.join(context.args)] = template
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 cmd_user_dict(update: Update, context: CustomContext) -> None:
await update.effective_message.reply_text(html.escape(str(context.chat_data)), disable_web_page_preview=True)
async def post_init(application: Application) -> None:
@@ -201,15 +245,18 @@ async def post_init(application: Application) -> None:
DESCRIPTION = "A bot to fetch tweets from Twitter."
await application.bot.set_my_description(DESCRIPTION)
await application.bot.set_my_short_description(DESCRIPTION)
TGTweet.set_session(ClientSession())
NetClient.init_client()
if common.PIXIV_REFRESH_TOKEN:
await ProcessPixiv.init_client(common.PIXIV_REFRESH_TOKEN)
async def post_stop(application: Application) -> None:
await application.bot.send_message(common.ADMIN[0], "Shutting down...")
if common.ADMIN:
await application.bot.send_message(common.ADMIN[0], "Shutting down...")
async def post_shutdown(application: Application) -> None:
await TGTweet.close_session()
await NetClient.close_client()
def main():
@@ -219,9 +266,12 @@ def main():
.token(common.BOT_TOKEN)
.defaults(defaults)
.persistence(persistence)
.context_types(ContextTypes(context=CustomContext, chat_data=ChatData))
.post_init(post_init)
.post_stop(post_stop)
.post_shutdown(post_shutdown)
.concurrent_updates(True)
.http_version('2')
.build()
)
@@ -229,15 +279,18 @@ def main():
user_filter.add_user_ids(common.ADMIN)
handlers = [
MessageHandler(filters.Regex(common.x_url_regex) & filters.ChatType.PRIVATE, url_media),
InlineQueryHandler(inline_query, common.x_url_regex),
InlineQueryHandler(inline_query),
MessageHandler((filters.Regex(regex.x_url) | filters.Regex(regex.pixiv_url)) | filters.Regex(
regex.bsky_url) & filters.ChatType.PRIVATE,
handel_url_media),
CommandHandler("set_forward_channel", cmd_set_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),
MessageHandler(~filters.COMMAND & filters.ChatType.PRIVATE, handle_message),
CallbackQueryHandler(query_forward_message, pattern="forward"),
CommandHandler("bot_dict", cmd_user_dict, filters=user_filter),
CallbackQueryHandler(query_template, pattern=r"^template\|"),
CommandHandler("bot_dict", cmd_user_dict),
]
application.add_handlers(handlers)
+4 -2
View File
@@ -1,2 +1,4 @@
python-telegram-bot[webhooks]~=21.1.1
aiohttp[speedups]~=3.9.3
python-telegram-bot[webhooks]~=21.6
httpx[http2]~=0.27
uvloop~=0.19.0; sys_platform != 'win32'
async-pixiv @ git+https://github.com/TheFunny/async-pixiv@main
-248
View File
@@ -1,248 +0,0 @@
import html
from typing import Generator
from aiohttp import ClientSession
from telegram import (
InlineQueryResultPhoto,
InlineQueryResultVideo,
InlineQueryResultMpeg4Gif,
InputMediaPhoto,
InputMediaVideo
)
from common import x_url_regex, x_media_regex, x_tco_regex, logger
twimg_url = 'https://pbs.twimg.com/'
vx_api_url = 'https://api.vxtwitter.com/{0}/status/{1}'
message_raw_text = """{url}
<a href="{author_url}">{author}</a>: {text}
"""
async def fetch_json(session: ClientSession, url: str) -> dict:
logger.info(f"Fetching {url}")
async with session.get(url) as response:
assert response.status == 200, f"Failed to fetch {url}, status code {response.status}"
return await response.json()
class TweetMedia:
def __init__(self, url: str, thumb: str, media_type: str):
self._url: str = url
self._thumb: str = thumb
self._type: str = media_type
def __str__(self):
return f"Media[url: {self.url} thumb: {self.thumb} type: {self.type}]"
@property
def _uri(self) -> str | None:
match = x_media_regex.match(self._url)
if match:
return match.group(2).removesuffix('.jpg').removesuffix('.png')
return None
@property
def url(self) -> str:
match self._type:
case "image":
return f"{twimg_url}{self._uri}?format=jpg&name=4096x4096"
case "video":
return self._url
case "gif":
return self._url
case _:
return self._url
@property
def thumb(self) -> str:
match self._type:
case "image":
return f"{twimg_url}{self._uri}?format=jpg&name=thumb"
case "video":
return self._thumb
case "gif":
return self._thumb
case _:
return self._thumb
@property
def type(self) -> str:
return self._type
class Tweet:
def __init__(
self,
tweet_id: str,
author: str,
author_id: str,
text: str,
media: list[TweetMedia],
sensitive: bool = False
):
self._id: str = tweet_id
self._author: str = author
self._author_id: str = author_id
self._text: str = text
self._media: list[TweetMedia] = media
self._sensitive: bool = sensitive
@property
def id(self) -> str:
return self._id
@property
def url(self) -> str:
return f"https://twitter.com/{self._author_id}/status/{self._id}"
@property
def author(self) -> str:
return self._author
@property
def author_url(self) -> str:
return f"https://twitter.com/{self._author_id}"
@property
def text(self) -> str:
return self._text
@property
def media(self) -> list[TweetMedia]:
return self._media
@property
def sensitive(self) -> bool:
return self._sensitive
class TGTweet(Tweet):
_session: ClientSession
def __init__(self, url: str):
self._url: str = url
self._api_param: tuple[str] = self._tweet_id
self._is_single_gif: bool = False
assert self._api_param
async def __aenter__(self):
self._tweet: dict = await self._fetch_tweet(self._api_param)
super().__init__(*self._init_properties)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
@classmethod
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
def _tweet_id(self) -> tuple[str] | None:
match = x_url_regex.match(self._url)
if match:
return match.groups()
return None
@property
def _tweet_text(self) -> str:
match = x_tco_regex.search(self._tweet['text'])
return self._tweet['text'][:match.start()].strip(" ") if match else self._tweet['text']
@property
def _tweet_media(self) -> list[TweetMedia]:
return [
TweetMedia(
url=x['url'],
thumb=x['thumbnail_url'],
media_type=x['type']
)
for x in self._tweet['media_extended']
]
@property
def _init_properties(self) -> tuple:
id = self._tweet['tweetID']
author = self._tweet['user_name']
author_id = self._tweet['user_screen_name']
text = self._tweet_text
media = self._tweet_media
sensitive = self._tweet['possibly_sensitive']
return id, author, author_id, text, media, sensitive
@property
def is_single_gif(self) -> bool:
return self._is_single_gif
@property
def message_text(self) -> str:
return message_raw_text.format(
url=self.url,
author_url=self.author_url,
author=html.escape(self.author),
text=html.escape(self.text)
)
@property
def inline_query_generator(self) -> Generator[
InlineQueryResultPhoto | InlineQueryResultVideo | InlineQueryResultMpeg4Gif, None, None
]:
for i, tweet_media in enumerate(self.media):
logger.info(str(tweet_media))
if tweet_media.type == "image":
yield InlineQueryResultPhoto(
id=str(i),
photo_url=tweet_media.url,
thumbnail_url=tweet_media.thumb,
caption=self.message_text
)
elif tweet_media.type == "video":
yield InlineQueryResultVideo(
id=str(i),
video_url=tweet_media.url,
mime_type="video/mp4",
thumbnail_url=tweet_media.thumb,
title=self.text,
caption=self.message_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
)
@property
def pm_media_generator(self) -> Generator[InputMediaPhoto | InputMediaVideo | tuple[str, bool], None, None]:
for tweet_media in self.media:
logger.info(str(tweet_media))
if tweet_media.type == "image":
yield InputMediaPhoto(
media=tweet_media.url,
has_spoiler=self.sensitive
)
elif tweet_media.type == "video":
yield InputMediaVideo(
media=tweet_media.url,
has_spoiler=self.sensitive,
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
)
View File
+154
View File
@@ -0,0 +1,154 @@
from __future__ import annotations
from functools import cached_property
from typing import TYPE_CHECKING
from utils.net import NetClient
from utils.regex import bsky_url
if TYPE_CHECKING:
from utils.types import BskyEmbedImages, BskyInfo, BskyEmbedVideo, BskyEmbedExternal
bsky_api_url = 'https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread'
class BskyMedia:
__slots__ = ('_url', '_thumb', '_type', '__dict__')
def __init__(self, url: str, thumb: str, media_type: str):
self._url: str = url
self._thumb: str = thumb
self._type: str = media_type
def __str__(self):
return f"BskyMedia(url={self.url}, thumb={self.thumb}, type={self.type})"
@property
def url(self) -> str:
return self._url
@property
def thumb(self) -> str:
return self._thumb
@property
def type(self) -> str:
return self._type
class Bsky:
__slots__ = ('_id', '_author', '_author_id', '_text', '_media', '_sensitive', '__dict__')
def __init__(
self,
id: str,
author: str,
author_id: str,
text: str,
media: list[BskyMedia],
sensitive: bool = False
):
self._id: str = id
self._author: str = author
self._author_id: str = author_id
self._text: str = text
self._media: list[BskyMedia] = media
self._sensitive: bool = sensitive
@property
def id(self) -> str:
return self._id
@cached_property
def url(self) -> str:
return f"https://bsky.app/profile/{self._author_id}/post/{self._id}"
@property
def author(self) -> str:
return self._author
@cached_property
def author_url(self) -> str:
return f"https://bsky.app/profile/{self._author_id}"
@property
def text(self) -> str:
return self._text
@property
def media(self) -> list[BskyMedia]:
return self._media
@property
def sensitive(self) -> bool:
return self._sensitive
class ProcessBsky:
__slots__ = ('_url', '_id', '_bsky')
def __init__(self, url: str):
self._url: str = url
async def __aenter__(self):
bsky = await self._fetch_bsky()
if not bsky['thread'].get('post'):
raise ValueError(f"BSky post not found: {bsky}")
self._bsky = bsky['thread']['post']
return Bsky(
id=self._id,
author=self._bsky['author']['displayName'],
author_id=self._bsky['author']['handle'],
text=self._bsky['record']['text'],
media=self._bsky_media,
sensitive=False # TODO: implement sensitive
)
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
async def _fetch_bsky(self) -> BskyInfo:
match = bsky_url.match(self._url)
if not match:
raise ValueError(f"Invalid Bsky URL: {self._url}")
auther_id, self._id = match.groups()
return await NetClient.fetch_json(
bsky_api_url,
params={'uri': f'at://{auther_id}/app.bsky.feed.post/{self._id}', 'depth': 0}
)
@property
def _bsky_media(self) -> list[BskyMedia]:
if not self._bsky.get('embed'):
return []
match (embed := self._bsky['embed'])['$type']:
case 'app.bsky.embed.images#view':
embed: BskyEmbedImages
return [
BskyMedia(
url=image['fullsize'],
thumb=image['thumb'],
media_type='image'
)
for image in embed['images']
]
case 'app.bsky.embed.video#view':
embed: BskyEmbedVideo
return [
BskyMedia(
url=embed['playlist'],
thumb=embed['thumbnail'],
media_type='video'
)
]
case 'app.bsky.embed.external#view':
embed: BskyEmbedExternal
return [
BskyMedia(
url=embed['external']['uri'],
thumb=embed['external']['thumb'],
media_type='external'
)
]
case _:
raise NotImplementedError(f"Unknown Bsky embed type: {embed['type']}")
+42
View File
@@ -0,0 +1,42 @@
import dataclasses
from typing import Optional
from telegram import Message
from telegram.ext import Application, CallbackContext, ExtBot
@dataclasses.dataclass(repr=False)
class EditMessage:
url: str
forward: tuple[Message, ...]
template: str = ""
def __str__(self):
forward = ", ".join(f"Message({f.id})" for f in self.forward)
return f"EditMessage(url={self.url}, forward={forward}, template={self.template})"
__repr__ = __str__
class ChatData:
def __init__(self):
self.forward_channel_id: Optional[int] = None
self.edit_before_forward: bool = False
self.edit_message: dict[int, EditMessage] = {}
self.template: dict[str, str] = {}
def __str__(self):
return f"ChatData(forward_channel_id={self.forward_channel_id}, edit_before_forward={self.edit_before_forward}, " \
f"edit_message={self.edit_message}, template={self.template})"
__repr__ = __str__
class CustomContext(CallbackContext[ExtBot, dict, ChatData, dict]):
def __init__(
self,
application: Application,
chat_id: Optional[int] = None,
user_id: Optional[int] = None
):
super().__init__(application=application, chat_id=chat_id, user_id=user_id)
+11
View File
@@ -0,0 +1,11 @@
import logging
import os
logging.basicConfig(
level=os.getenv("LOG_LEVEL", "WARNING"),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
+37
View File
@@ -0,0 +1,37 @@
from __future__ import annotations
from httpx import AsyncClient
def create_client() -> AsyncClient:
return AsyncClient(http2=True)
async def close_client(_client: AsyncClient) -> None:
return await _client.aclose()
async def fetch_json(_client: AsyncClient, url: str, params: dict = None) -> dict:
response = await _client.get(url, params=params)
assert response.is_success, f"Failed to fetch {url}, status code {response.status_code}"
return response.json()
class NetClient:
_httpx_client: AsyncClient
@classmethod
def init_client(cls) -> None:
cls._httpx_client = create_client()
@classmethod
async def close_client(cls) -> None:
await close_client(cls._httpx_client)
@classmethod
def get_client(cls) -> AsyncClient:
return cls._httpx_client
@classmethod
async def fetch_json(cls, url: str, params: dict = None) -> dict:
return await fetch_json(cls._httpx_client, url, params)
+141
View File
@@ -0,0 +1,141 @@
from __future__ import annotations
from typing import Literal, TYPE_CHECKING
from async_pixiv import PixivClient
from async_pixiv.error import ApiError
if TYPE_CHECKING:
from async_pixiv.model.illust import Illust
class PixivMedia:
__slots__ = ('_url', '_thumb')
def __init__(self, url: str, thumb: str):
self._url: str = url
self._thumb: str = thumb
def __str__(self):
return f"PixivMedia(url={self.url}, thumb={self.thumb}, large={self.large})"
@property
def url(self) -> str:
return self._url
@property
def thumb(self) -> str:
return self._thumb
@property
def large(self) -> str:
url = self._url.replace("img-original", "img-master").removesuffix(".jpg").removesuffix(".png")
return url + "_master1200.jpg"
class Pixiv:
__slots__ = ('_illust',)
def __init__(self, illust: Illust):
self._illust: Illust = illust
@property
def url(self) -> str:
return str(self._illust.link).rstrip('/')
@property
def type(self) -> Literal["illust", "manga", "ugoira"]:
return self._illust.type.value
@property
def title(self) -> str:
return self._illust.title
@property
def author(self) -> str:
return self._illust.user.name
@property
def author_url(self) -> str:
return str(self._illust.user.link).rstrip('/')
@property
def description(self) -> str:
return self._illust.caption
@property
def tags(self) -> list[str]:
return [tag.name for tag in self._illust.tags]
@property
def is_multiple_pages(self) -> bool:
return self._illust.page_count > 1
@property
def is_nsfw(self) -> bool:
return self._illust.is_nsfw
@property
def is_ai(self) -> bool:
return self._illust.ai_type.value == 2
@property
def images(self) -> list[PixivMedia]:
if self.is_multiple_pages:
return [
PixivMedia(
url=page.image_urls.original,
thumb=page.image_urls.medium
)
for page in self._illust.meta_pages
]
else:
return [
PixivMedia( # should observe if image_url.original is always None for single page
url=self._illust.image_urls.original or self._illust.meta_single_page.original,
thumb=self._illust.image_urls.medium
)
]
class _ProcessPixiv:
_client: PixivClient
@classmethod
async def init_client(cls, token: str) -> None:
cls._client = PixivClient()
await cls._client.login_with_token(token)
cls._token = token
@classmethod
async def close_client(cls) -> None:
await cls._client.close()
@classmethod
async def refresh_token(cls):
await cls._client.login_with_token(cls._token)
class ProcessPixiv(_ProcessPixiv):
__slots__ = ('_url', '_illust')
def __init__(self, url: str):
self._url: str = url
async def __aenter__(self):
self._illust = await self._fetch_illust()
return Pixiv(self._illust)
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
async def _fetch_illust(self) -> Illust:
illust_id = self._parse_illust_id()
try:
return (await self._client.ILLUST.detail(illust_id)).illust
except ApiError:
await self.refresh_token()
return (await self._client.ILLUST.detail(illust_id)).illust # TODO use retry here
def _parse_illust_id(self) -> int:
return int(self._url.split("/")[-1])
+10
View File
@@ -0,0 +1,10 @@
import re
x_url = re.compile(r"^(?:https?://)?(?:www\.|mobile\.)?(?:x|twitter|fixvx|vxtwitter)\.com/(.+)/status/(\d+)")
x_media_url = re.compile(r"^(?:https?://)?(pbs|video)\.twimg\.com/(.*)")
x_tco_url = re.compile(r"(?:https?://)?t\.co/.+$", re.M)
message_url = re.compile(r"\[.+]", re.S)
pixiv_url = re.compile(r"^(?:https?://)?(?:www\.)?pixiv\.net/(?:en/)?(?:artworks/|i/)(\d+)")
bsky_url = re.compile(r"^(?:https?://)?(?:bsky)\.app/profile/(.+)/post/(.+)")
+287
View File
@@ -0,0 +1,287 @@
from __future__ import annotations
import html
from functools import cached_property
from typing import Generator, TYPE_CHECKING
from uuid import uuid4
from telegram import InlineQueryResultMpeg4Gif, InlineQueryResultPhoto, InlineQueryResultVideo, InputMediaPhoto, \
InputMediaVideo
from common import PIXIV_REFRESH_TOKEN
from .bsky import ProcessBsky
from .logger import get_logger
from .pixiv import ProcessPixiv
from .regex import bsky_url, pixiv_url, x_url
from .tweet import ProcessTweet
if TYPE_CHECKING:
from .types import TypeInlineQueryResult, TypeMessageMediaResult
logger = get_logger(__name__)
message_raw_text_tweet = """{url}
<a href="{author_url}">{author}</a>: {text}
"""
message_raw_text_pixiv = """<a href="{url}">{text}</a> / <a href="{author_url}">{author}</a>
{tags}
"""
class Telegram:
def __init__(self, url: str):
self._url = url
async def __aenter__(self):
if x_url.match(self._url):
async with TelegramTweet(self._url) as tweet:
return tweet
elif PIXIV_REFRESH_TOKEN and pixiv_url.match(self._url):
async with TelegramPixiv(self._url) as pixiv:
return pixiv
elif bsky_url.match(self._url):
async with TelegramBsky(self._url) as bsky:
return bsky
else:
return None # TODO add raise and catch
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
class TelegramTweet:
message_raw_text = message_raw_text_tweet
__slots__ = ('_url', '_tweet', '__dict__')
def __init__(self, url: str):
self._url: str = url
async def __aenter__(self):
async with ProcessTweet(self._url) as tweet:
self._tweet = tweet
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
@property
def url(self) -> str:
return self._tweet.url
@cached_property
def message_text(self) -> str:
tweet = self._tweet
return self.message_raw_text.format(
url=tweet.url,
author_url=tweet.author_url,
author=html.escape(tweet.author),
text=html.escape(tweet.text)
)
def inline_query_result(self) -> tuple[TypeInlineQueryResult, ...]:
return tuple(self.inline_query_generator())
def message_media_result(self) -> tuple[TypeMessageMediaResult, ...]:
return tuple(self.message_media_generator())
def inline_query_generator(self) -> Generator[TypeInlineQueryResult, None, None]:
tweet = self._tweet
for tweet_media in tweet.media:
logger.info(str(tweet_media))
if tweet_media.type == "image":
yield InlineQueryResultPhoto(
id=str(uuid4()),
photo_url=tweet_media.url,
thumbnail_url=tweet_media.thumb,
caption=self.message_text
)
elif tweet_media.type == "video":
yield InlineQueryResultVideo(
id=str(uuid4()),
video_url=tweet_media.url,
mime_type="video/mp4",
thumbnail_url=tweet_media.thumb,
title=tweet.text,
caption=self.message_text
)
elif tweet_media.type == "gif":
yield InlineQueryResultMpeg4Gif(
id=str(uuid4()),
mpeg4_url=tweet_media.url,
thumbnail_url=tweet_media.thumb,
caption=self.message_text
)
def message_media_generator(self) -> Generator[TypeMessageMediaResult, None, None]:
tweet = self._tweet
for tweet_media in tweet.media:
logger.info(str(tweet_media))
if tweet_media.type == "image":
yield InputMediaPhoto(
media=tweet_media.url,
has_spoiler=tweet.sensitive
)
elif tweet_media.type == "video":
yield InputMediaVideo(
media=tweet_media.url,
has_spoiler=tweet.sensitive,
thumbnail=tweet_media.thumb
)
elif tweet_media.type == "gif":
if len(tweet.media) == 1:
yield tweet_media.url, tweet.sensitive
yield InputMediaVideo(
media=tweet_media.url,
has_spoiler=tweet.sensitive,
thumbnail=tweet_media.thumb
)
class TelegramPixiv:
message_raw_text = message_raw_text_pixiv
__slots__ = ('_url', '_pixiv')
def __init__(self, url: str):
self._url = url
async def __aenter__(self):
async with ProcessPixiv(self._url) as pixiv:
self._pixiv = pixiv
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
@property
def url(self) -> str:
return self._pixiv.url
@property
def message_text(self) -> str:
pixiv = self._pixiv
return self.message_raw_text.format(
url=pixiv.url,
author_url=pixiv.author_url,
author=html.escape(pixiv.author),
text=html.escape(pixiv.title),
tags=html.escape(" ".join(f"#{name}" for name in pixiv.tags))
)
def inline_query_result(self) -> tuple[TypeInlineQueryResult, ...]:
return tuple(i for i in self.inline_query_generator() if i)
def message_media_result(self) -> tuple[TypeMessageMediaResult, ...]:
return tuple(i for i in self.message_media_generator() if i)
def inline_query_generator(self) -> Generator[TypeInlineQueryResult, None, None]:
pixiv = self._pixiv
for media in pixiv.images:
logger.info(str(media))
if pixiv.type in ("illust", "manga"):
yield InlineQueryResultPhoto(
id=str(uuid4()),
photo_url=media.large,
thumbnail_url=media.thumb,
caption=self.message_text
)
else:
yield
def message_media_generator(self) -> Generator[TypeMessageMediaResult, None, None]:
pixiv = self._pixiv
for media in pixiv.images:
logger.info(str(media))
if pixiv.type in ("illust", "manga"):
yield InputMediaPhoto(
media=media.large,
has_spoiler=pixiv.is_nsfw
)
else:
yield
class TelegramBsky:
message_raw_text = message_raw_text_tweet
__slots__ = ('_url', '_bsky', '__dict__')
def __init__(self, url: str):
self._url: str = url
async def __aenter__(self):
async with ProcessBsky(self._url) as bsky:
self._bsky = bsky
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
@property
def url(self) -> str:
return self._bsky.url
@cached_property
def message_text(self) -> str:
bsky = self._bsky
return self.message_raw_text.format(
url=bsky.url,
author_url=bsky.author_url,
author=html.escape(bsky.author),
text=html.escape(bsky.text)
)
def inline_query_result(self) -> tuple[TypeInlineQueryResult, ...]:
return tuple(i for i in self.inline_query_generator() if i)
def message_media_result(self) -> tuple[TypeMessageMediaResult, ...]:
return tuple(i for i in self.message_media_generator() if i)
def inline_query_generator(self) -> Generator[TypeInlineQueryResult, None, None]:
bsky = self._bsky
for bsky_media in bsky.media:
logger.info(str(bsky_media))
if bsky_media.type == "image":
yield InlineQueryResultPhoto(
id=str(uuid4()),
photo_url=bsky_media.url,
thumbnail_url=bsky_media.thumb,
caption=self.message_text
)
elif bsky_media.type == "video":
# yield InlineQueryResultVideo(
# id=str(uuid4()),
# video_url=bsky_media.url,
# mime_type="video/mp4",
# thumbnail_url=bsky_media.thumb,
# title=bsky.text,
# caption=self.message_text
# )
yield
elif bsky_media.type == "external":
yield InlineQueryResultVideo(
id=str(uuid4()),
video_url=bsky_media.url,
mime_type="image/gif",
thumbnail_url=bsky_media.thumb,
title=bsky.text,
caption=self.message_text
)
def message_media_generator(self) -> Generator[TypeMessageMediaResult, None, None]:
bsky = self._bsky
for bsky_media in bsky.media:
logger.info(str(bsky_media))
if bsky_media.type == "image":
yield InputMediaPhoto(
media=bsky_media.url,
has_spoiler=bsky.sensitive
)
elif bsky_media.type == "video":
# yield InputMediaVideo(
# media=bsky_media.url,
# has_spoiler=bsky.sensitive,
# thumbnail=bsky_media.thumb
# )
yield
elif bsky_media.type == "external":
yield bsky_media.url, bsky.sensitive
+152
View File
@@ -0,0 +1,152 @@
from __future__ import annotations
from functools import cached_property
from typing import TYPE_CHECKING
from .net import NetClient
from .regex import x_media_url, x_tco_url, x_url
if TYPE_CHECKING:
from .types import TweetInfo
twimg_url = 'https://pbs.twimg.com/'
vx_api_url = 'https://api.vxtwitter.com/{0}/status/{1}'
class TweetMedia:
__slots__ = ('_url', '_thumb', '_type', '__dict__')
def __init__(self, url: str, thumb: str, media_type: str):
self._url: str = url
self._thumb: str = thumb
self._type: str = media_type
def __str__(self):
return f"TweetMedia(url={self.url}, thumb={self.thumb}, type={self.type})"
@cached_property
def _uri(self) -> str | None:
if match := x_media_url.match(self._url):
return match.group(2).removesuffix('.jpg').removesuffix('.png')
return None
@cached_property
def url(self) -> str:
match self._type:
case "image":
return f"{twimg_url}{self._uri}?format=jpg&name=4096x4096"
case "video":
return self._url
case "gif":
return self._url
case _:
return self._url
@cached_property
def thumb(self) -> str:
match self._type:
case "image":
return f"{twimg_url}{self._uri}?format=jpg&name=thumb"
case "video":
return self._thumb
case "gif":
return self._thumb
case _:
return self._thumb
@property
def type(self) -> str:
return self._type
class Tweet:
__slots__ = ('_id', '_author', '_author_id', '_text', '_media', '_sensitive', '__dict__')
def __init__(
self,
tweet_id: str,
author: str,
author_id: str,
text: str,
media: list[TweetMedia],
sensitive: bool = False
):
self._id: str = tweet_id
self._author: str = author
self._author_id: str = author_id
self._text: str = text
self._media: list[TweetMedia] = media
self._sensitive: bool = sensitive
@property
def id(self) -> str:
return self._id
@cached_property
def url(self) -> str:
return f"https://twitter.com/{self._author_id}/status/{self._id}"
@property
def author(self) -> str:
return self._author
@cached_property
def author_url(self) -> str:
return f"https://twitter.com/{self._author_id}"
@property
def text(self) -> str:
return self._text
@property
def media(self) -> list[TweetMedia]:
return self._media
@property
def sensitive(self) -> bool:
return self._sensitive
class ProcessTweet:
__slots__ = ('_url', '_tweet')
def __init__(self, url: str):
self._url: str = url
async def __aenter__(self):
self._tweet = await self._fetch_tweet()
return Tweet(
tweet_id=self._tweet["tweetID"],
author=self._tweet["user_name"],
author_id=self._tweet["user_screen_name"],
text=self._tweet_text,
media=self._tweet_media,
sensitive=self._tweet["possibly_sensitive"]
)
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
async def _fetch_tweet(self) -> TweetInfo:
match = x_url.match(self._url)
assert match, f"Invalid URL: {self._url}"
auther_id, tweet_id = match.groups()
return await NetClient.fetch_json(vx_api_url.format(auther_id, tweet_id))
@property
def _tweet_text(self) -> str:
match = x_tco_url.search(self._tweet['text'])
return self._tweet['text'][:match.start()].strip(" ") if match else self._tweet['text']
@property
def _tweet_media(self) -> list[TweetMedia]:
return [
TweetMedia(
url=tweet_media['url'],
thumb=tweet_media['thumbnail_url'],
media_type=tweet_media['type']
)
for tweet_media in self._tweet['media_extended']
]
+70
View File
@@ -0,0 +1,70 @@
from __future__ import annotations
from typing import Literal, TypedDict
from telegram import InlineQueryResultMpeg4Gif, InlineQueryResultPhoto, InlineQueryResultVideo, InputMediaPhoto, \
InputMediaVideo
TypeInlineQueryResult = InlineQueryResultMpeg4Gif | InlineQueryResultPhoto | InlineQueryResultVideo
InputMediaAnimation = tuple[str, bool]
TypeMessageMediaResult = InputMediaPhoto | InputMediaVideo | InputMediaAnimation
class TweetInfo(TypedDict):
tweetID: str
user_name: str
user_screen_name: str
text: str
media_extended: list[dict]
possibly_sensitive: bool
class BskyInfo(TypedDict):
thread: BskyThread
class BskyThread(TypedDict):
post: BskyPost
class BskyPost(TypedDict):
author: BskyAuthor
record: BskyPostRecord
embed: BskyEmbedImages | BskyEmbedVideo | BskyEmbedExternal
class BskyAuthor(TypedDict):
handle: str
displayName: str
class BskyPostRecord(TypedDict):
text: str
class BskyEmbedImage(TypedDict):
thumb: str
fullsize: str
BskyEmbedImages = TypedDict('BskyEmbedImages', {
'$type': Literal['app.bsky.embed.images#view'],
'images': list[BskyEmbedImage]
})
BskyEmbedVideo = TypedDict('BskyEmbedVideo', {
'$type': Literal['app.bsky.embed.video#view'],
'playlist': str,
'thumbnail': str
})
class BskyEmbedExternalItem(TypedDict):
uri: str
thumb: str
BskyEmbedExternal = TypedDict('BskyEmbedExternal', {
'$type': Literal['app.bsky.embed.external#view'],
'external': BskyEmbedExternalItem
})