Compare commits

...
21 Commits
Author SHA1 Message Date
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
14 changed files with 397 additions and 150 deletions
+1 -1
View File
@@ -3,4 +3,4 @@ __pycache__/
cert/ cert/
data/ data/
docker-compose.yml docker-compose.yml
x.py utils/x.py
+1 -1
View File
@@ -7,7 +7,7 @@ ENV PYTHONUNBUFFERED=1
RUN set -eux; \ RUN set -eux; \
apt-get update; \ apt-get update; \
apt-get install -y gosu; \ apt-get install -y git gosu; \
rm -rf /var/lib/apt/lists/*; \ rm -rf /var/lib/apt/lists/*; \
# verify that the binary works # verify that the binary works
gosu nobody true gosu nobody true
+4 -17
View File
@@ -1,9 +1,8 @@
import logging
import os import os
import re
try: try:
import uvloop, asyncio import uvloop
import asyncio
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError: except ImportError:
@@ -12,6 +11,8 @@ except ImportError:
BOT_TOKEN = os.getenv("BOT_TOKEN") 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(",")]
PIXIV_REFRESH_TOKEN = os.getenv("PIXIV_REFRESH_TOKEN")
WEBHOOK = os.getenv("WEBHOOK", False) WEBHOOK = os.getenv("WEBHOOK", False)
if WEBHOOK: if WEBHOOK:
WEBHOOK_LISTEN = os.getenv("WEBHOOK_LISTEN", "0.0.0.0") WEBHOOK_LISTEN = os.getenv("WEBHOOK_LISTEN", "0.0.0.0")
@@ -20,17 +21,3 @@ if WEBHOOK:
WEBHOOK_KEY = os.getenv("WEBHOOK_KEY", "cert/private.key") WEBHOOK_KEY = os.getenv("WEBHOOK_KEY", "cert/private.key")
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|fixvx|vxtwitter)\.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'
)
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
+3 -2
View File
@@ -1,12 +1,13 @@
services: services:
tgxmb: image: yoursfunny/telegram-twitter-media-bot: latest tgxmb: image: yoursfunny/telegram-twitter-media-bot: latest
restart: always restart: always
ports: # ports:
- "8443:8443" # - "8443:8443"
environment: environment:
LOCAL_USER_ID: '1000' LOCAL_USER_ID: '1000'
BOT_TOKEN: '' BOT_TOKEN: ''
BOT_ADMIN: '' BOT_ADMIN: ''
PIXIV_REFRESH_TOKEN: ''
WEBHOOK: false WEBHOOK: false
WEBHOOK_LISTEN: '127.0.0.1' WEBHOOK_LISTEN: '127.0.0.1'
WEBHOOK_PORT: 8443 WEBHOOK_PORT: 8443
+32 -15
View File
@@ -10,13 +10,17 @@ from telegram.ext import (ApplicationBuilder, CallbackQueryHandler, CommandHandl
InlineQueryHandler, MessageHandler, PicklePersistence, filters) InlineQueryHandler, MessageHandler, PicklePersistence, filters)
import common import common
from tweet import TelegramTweet import utils.regex as regex
from utils.logger import get_logger
from utils.net import NetClient
from utils.pixiv import ProcessPixiv
from utils.telegram import Telegram
if TYPE_CHECKING: if TYPE_CHECKING:
from telegram import Chat, Message, Update from telegram import Message, Update
from telegram.ext import Application, ContextTypes from telegram.ext import Application, ContextTypes
logger = common.get_logger(__name__) logger = get_logger(__name__)
def send_action(action): def send_action(action):
@@ -36,22 +40,26 @@ async def inline_query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
if query == "": if query == "":
return return
logger.info(f"Query: {query}") logger.info(f"Query: {query}")
async with TelegramTweet(query) as tweet: async with Telegram(query) as tweet:
result = list(tweet.inline_query_generator) if not tweet:
await update.inline_query.answer(result) return
await update.inline_query.answer(tweet.inline_query_result())
@send_action(ChatAction.UPLOAD_PHOTO) @send_action(ChatAction.UPLOAD_PHOTO)
async def url_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def url_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
url = update.message.text url = update.message.text
logger.info(f"Receiving url: {url}") logger.info(f"Receiving url: {url}")
async with TelegramTweet(url) as tweet: async with Telegram(url) as tweet:
media = list(tweet.message_media_generator) media = tweet.message_media_result()
if not media:
await update.effective_message.reply_text("No media found or media type is not supported.")
return
message_to_send = await update.effective_message.reply_media_group( message_to_send = await update.effective_message.reply_media_group(
media, media,
caption=tweet.message_text, caption=tweet.message_text,
reply_to_message_id=update.message.message_id, 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], media[0][0],
caption=tweet.message_text, caption=tweet.message_text,
reply_to_message_id=update.message.message_id, reply_to_message_id=update.message.message_id,
@@ -105,7 +113,7 @@ async def edit_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
)) ))
else: else:
update_text = html.escape(update.message.text) update_text = html.escape(update.message.text)
match = common.message_url_regex.search(update_text) match = regex.message_url.search(update_text)
if match: if match:
match = match.span() match = match.span()
update_text = update_text[:match[0]] + message_url.format( update_text = update_text[:match[0]] + message_url.format(
@@ -133,7 +141,7 @@ 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 = await context.bot.get_chat(channel) channel = 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
@@ -145,6 +153,11 @@ async def cmd_set_forward_channel(update: Update, context: ContextTypes.DEFAULT_
except Exception as e: except Exception as e:
await update.effective_message.reply_text(str(e) + "\nPlease add the bot to the channel and set as admin") await update.effective_message.reply_text(str(e) + "\nPlease add the bot to the channel and set as admin")
return 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 = filter(lambda x: x.user.id == context.bot.id, channel_admin)
user_bot = next(user_bot, None) user_bot = next(user_bot, None)
if user_bot.can_post_messages: if user_bot.can_post_messages:
@@ -204,7 +217,9 @@ 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)
TelegramTweet.init_client() NetClient.init_client()
if common.PIXIV_REFRESH_TOKEN:
await ProcessPixiv.init_client(common.PIXIV_REFRESH_TOKEN)
async def post_stop(application: Application) -> None: async def post_stop(application: Application) -> None:
@@ -212,7 +227,7 @@ async def post_stop(application: Application) -> None:
async def post_shutdown(application: Application) -> None: async def post_shutdown(application: Application) -> None:
await TelegramTweet.close_client() await NetClient.close_client()
def main(): def main():
@@ -226,6 +241,7 @@ def main():
.post_stop(post_stop) .post_stop(post_stop)
.post_shutdown(post_shutdown) .post_shutdown(post_shutdown)
.concurrent_updates(True) .concurrent_updates(True)
.http_version('2')
.build() .build()
) )
@@ -233,8 +249,9 @@ def main():
user_filter.add_user_ids(common.ADMIN) user_filter.add_user_ids(common.ADMIN)
handlers = [ handlers = [
InlineQueryHandler(inline_query, common.x_url_regex), InlineQueryHandler(inline_query),
MessageHandler(filters.Regex(common.x_url_regex) & filters.ChatType.PRIVATE, url_media), MessageHandler((filters.Regex(regex.x_url) | filters.Regex(regex.pixiv_url)) & 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("edit_before_forward", cmd_edit_before_forward),
+3 -2
View File
@@ -1,3 +1,4 @@
python-telegram-bot[webhooks]~=21.3 python-telegram-bot[webhooks]~=21.3
httpx[http2]~=0.27.0 httpx[http2]~=0.27
uvloop~=0.19.0; sys_platform != 'win32' uvloop~=0.19.0; sys_platform != 'win32'
async-pixiv @ git+https://github.com/TheFunny/async-pixiv@main
View File
+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) -> dict:
response = await _client.get(url)
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) -> dict:
return await fetch_json(cls._httpx_client, url)
+212
View File
@@ -0,0 +1,212 @@
from __future__ import annotations
import html
from typing import Generator, Literal, TYPE_CHECKING
from uuid import uuid4
from async_pixiv import PixivClient
from async_pixiv.error import ApiError
from telegram import InlineQueryResultPhoto, InputMediaPhoto
from utils.logger import get_logger
if TYPE_CHECKING:
from async_pixiv.model.illust import Illust
from utils.types import TypeInlineQueryResult, TypeMessageMediaResult
logger = get_logger(__name__)
message_raw_text = """<a href="{url}">{text}</a> / <a href="{author_url}">{author}</a>
{tags}
"""
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
__slots__ = ('_url', '_illust')
@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)
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])
class TelegramPixiv:
__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
def url(self) -> str:
return self._url
@property
def message_text(self) -> str:
pixiv = self._pixiv
return 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(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]:
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
+8
View File
@@ -0,0 +1,8 @@
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+)")
+24
View File
@@ -0,0 +1,24 @@
from __future__ import annotations
from common import PIXIV_REFRESH_TOKEN
from .pixiv import TelegramPixiv
from .regex import pixiv_url, x_url
from .tweet import TelegramTweet
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
else:
return None # TODO add raise and catch
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
+44 -112
View File
@@ -2,26 +2,18 @@ from __future__ import annotations
import html import html
from functools import cached_property from functools import cached_property
from typing import TYPE_CHECKING from typing import Generator, TYPE_CHECKING
from uuid import uuid4 from uuid import uuid4
from httpx import AsyncClient
from telegram import (InlineQueryResultMpeg4Gif, InlineQueryResultPhoto, InlineQueryResultVideo, InputMediaPhoto, from telegram import (InlineQueryResultMpeg4Gif, InlineQueryResultPhoto, InlineQueryResultVideo, InputMediaPhoto,
InputMediaVideo) InputMediaVideo)
from common import get_logger, x_media_regex, x_tco_regex, x_url_regex from .logger import get_logger
from .net import NetClient
from .regex import x_media_url, x_tco_url, x_url
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Generator, TypedDict from .types import TweetInfo, TypeInlineQueryResult, TypeMessageMediaResult
class TweetInfo(TypedDict):
tweetID: str
user_name: str
user_screen_name: str
text: str
media_extended: list[dict]
possibly_sensitive: bool
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -34,21 +26,6 @@ message_raw_text = """{url}
""" """
def create_client() -> AsyncClient:
return AsyncClient(http2=True)
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.is_success, f"Failed to fetch {url}, status code {response.status_code}"
return response.json()
class TweetMedia: class TweetMedia:
__slots__ = ('_url', '_thumb', '_type', '__dict__') __slots__ = ('_url', '_thumb', '_type', '__dict__')
@@ -58,12 +35,11 @@ class TweetMedia:
self._type: str = media_type self._type: str = media_type
def __str__(self): def __str__(self):
return f"Media[url: {self.url} thumb: {self.thumb} type: {self.type}]" return f"TweetMedia(url={self.url}, thumb={self.thumb}, type={self.type})"
@cached_property @cached_property
def _uri(self) -> str | None: def _uri(self) -> str | None:
match = x_media_regex.match(self._url) if match := x_media_url.match(self._url):
if match:
return match.group(2).removesuffix('.jpg').removesuffix('.png') return match.group(2).removesuffix('.jpg').removesuffix('.png')
return None return None
@@ -145,10 +121,9 @@ class Tweet:
class ProcessTweet: class ProcessTweet:
__slots__ = ('_httpx_client', '_url', '_tweet') __slots__ = ('_url', '_tweet')
def __init__(self, httpx_client: AsyncClient, url: str): def __init__(self, url: str):
self._httpx_client: AsyncClient = httpx_client
self._url: str = url self._url: str = url
async def __aenter__(self): async def __aenter__(self):
@@ -166,14 +141,14 @@ class ProcessTweet:
pass pass
async def _fetch_tweet(self) -> TweetInfo: async def _fetch_tweet(self) -> TweetInfo:
match = x_url_regex.match(self._url) match = x_url.match(self._url)
assert match, f"Invalid URL: {self._url}" assert match, f"Invalid URL: {self._url}"
auther_id, tweet_id = match.group() auther_id, tweet_id = match.groups()
return await fetch_json(self._httpx_client, vx_api_url.format(auther_id, tweet_id)) return await NetClient.fetch_json(vx_api_url.format(auther_id, tweet_id))
@property @property
def _tweet_text(self) -> str: def _tweet_text(self) -> str:
match = x_tco_regex.search(self._tweet['text']) match = x_tco_url.search(self._tweet['text'])
return self._tweet['text'][:match.start()].strip(" ") if match else self._tweet['text'] return self._tweet['text'][:match.start()].strip(" ") if match else self._tweet['text']
@property @property
@@ -188,85 +163,43 @@ class ProcessTweet:
] ]
class TelegramTweet(Tweet): class TelegramTweet:
_httpx_client: AsyncClient __slots__ = ('_url', '_tweet', '__dict__')
def __init__(self, url: str): def __init__(self, url: str):
self._url: str = url 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): async def __aenter__(self):
self._tweet: dict = await self._fetch_tweet(self._api_param) async with ProcessTweet(self._url) as tweet:
super().__init__(*self._init_properties) self._tweet = tweet
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
@classmethod @cached_property
def init_client(cls) -> None: def url(self) -> str:
cls._httpx_client = create_client() return self._tweet.url
@classmethod @cached_property
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
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: def message_text(self) -> str:
tweet = self._tweet
return message_raw_text.format( return message_raw_text.format(
url=self.url, url=tweet.url,
author_url=self.author_url, author_url=tweet.author_url,
author=html.escape(self.author), author=html.escape(tweet.author),
text=html.escape(self.text) text=html.escape(tweet.text)
) )
@property def inline_query_result(self) -> tuple[TypeInlineQueryResult, ...]:
def inline_query_generator(self) -> Generator[ return tuple(self.inline_query_generator())
InlineQueryResultPhoto | InlineQueryResultVideo | InlineQueryResultMpeg4Gif, None, None
]: def message_media_result(self) -> tuple[TypeMessageMediaResult, ...]:
for tweet_media in self.media: 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)) logger.info(str(tweet_media))
if tweet_media.type == "image": if tweet_media.type == "image":
yield InlineQueryResultPhoto( yield InlineQueryResultPhoto(
@@ -281,7 +214,7 @@ class TelegramTweet(Tweet):
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.text, title=tweet.text,
caption=self.message_text caption=self.message_text
) )
elif tweet_media.type == "gif": elif tweet_media.type == "gif":
@@ -292,27 +225,26 @@ class TelegramTweet(Tweet):
caption=self.message_text caption=self.message_text
) )
@property def message_media_generator(self) -> Generator[TypeMessageMediaResult, None, None]:
def message_media_generator(self) -> Generator[InputMediaPhoto | InputMediaVideo | tuple[str, bool], None, None]: tweet = self._tweet
for tweet_media in self.media: for tweet_media in tweet.media:
logger.info(str(tweet_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,
has_spoiler=self.sensitive has_spoiler=tweet.sensitive
) )
elif tweet_media.type == "video": elif tweet_media.type == "video":
yield InputMediaVideo( yield InputMediaVideo(
media=tweet_media.url, media=tweet_media.url,
has_spoiler=self.sensitive, has_spoiler=tweet.sensitive,
thumbnail=tweet_media.thumb thumbnail=tweet_media.thumb
) )
elif tweet_media.type == "gif": elif tweet_media.type == "gif":
if len(self.media) == 1: if len(tweet.media) == 1:
self._is_single_gif = True yield tweet_media.url, tweet.sensitive
yield tweet_media.url, self.sensitive
yield InputMediaVideo( yield InputMediaVideo(
media=tweet_media.url, media=tweet_media.url,
has_spoiler=self.sensitive, has_spoiler=tweet.sensitive,
thumbnail=tweet_media.thumb thumbnail=tweet_media.thumb
) )
+17
View File
@@ -0,0 +1,17 @@
from typing import 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