Compare commits

...
31 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
4 changed files with 139 additions and 74 deletions
+14 -4
View File
@@ -2,8 +2,15 @@ import logging
import os import os
import re 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("BOT_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,13 +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) 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)
+78 -40
View File
@@ -1,23 +1,21 @@
import html
from functools import wraps from functools import wraps
from typing import TYPE_CHECKING
from aiohttp import ClientSession from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram import Update, Chat, InlineKeyboardMarkup, InlineKeyboardButton, ForceReply, Message from telegram.constants import ChatAction, ChatType, ParseMode
from telegram.constants import ParseMode, ChatAction, ChatType from telegram.ext import (ApplicationBuilder, CallbackQueryHandler, CommandHandler, Defaults,
from telegram.ext import ( InlineQueryHandler, MessageHandler, PicklePersistence, filters)
Application,
ApplicationBuilder,
ContextTypes,
Defaults,
filters,
InlineQueryHandler,
PicklePersistence,
MessageHandler,
CommandHandler, CallbackQueryHandler
)
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 send_action(action):
def decorator(func): def decorator(func):
@@ -35,7 +33,7 @@ async def inline_query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
query = update.inline_query.query query = update.inline_query.query
if query == "": if query == "":
return return
common.logger.info(f"Query: {query}") logger.info(f"Query: {query}")
async with TGTweet(query) as tweet: 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)
@@ -44,19 +42,28 @@ async def inline_query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
@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
common.logger.info(f"Receiving url: {url}") logger.info(f"Receiving url: {url}")
async with TGTweet(url) as tweet: async with TGTweet(url) as tweet:
media = list(tweet.pm_media_generator) media = list(tweet.pm_media_generator)
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(
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 url = tweet.url
if context.user_data.get('edit_before_forward', False): if context.user_data.get('edit_before_forward', False):
message_reply = await update.effective_message.reply_text( message_reply = await update.effective_message.reply_text(
"Reply to edit message.", "Reply to edit message. [URL]",
reply_markup=ForceReply(selective=True, input_field_placeholder="{URL}"), reply_markup=InlineKeyboardMarkup.from_button(
InlineKeyboardButton("↩️ Confirm", callback_data="forward")
),
reply_to_message_id=update.message.message_id, reply_to_message_id=update.message.message_id,
) )
context.user_data['message_reply'] = message_reply context.user_data['message_reply'] = message_reply
@@ -70,11 +77,13 @@ async def url_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
async def forward_message( async def forward_message(
update: Update, update: Update,
context: ContextTypes.DEFAULT_TYPE, context: ContextTypes.DEFAULT_TYPE,
message_sent: tuple[Message, ...], message_to_send: tuple[Message, ...],
) -> None: ) -> None:
try: try:
for i, m in enumerate(message_sent): await update.effective_chat.copy_messages(
await m.copy(context.user_data['forward_channel_id']) context.user_data['forward_channel_id'],
[m.id for m in message_to_send]
)
except Exception as e: except Exception as e:
await update.effective_message.reply_text(str(e)) await update.effective_message.reply_text(str(e))
@@ -84,27 +93,32 @@ async def edit_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
return return
if update.message.reply_to_message != context.user_data['message_reply']: if update.message.reply_to_message != context.user_data['message_reply']:
return return
update_text = update.message.text template = context.user_data.get('template', None)
match = common.message_url_regex.search(update_text) message_url = '<a href="{0}">{1}</a>'
if match: url = context.user_data['message_url']
match = match.span() if template:
update_text = update_text[:match[0]] + '<a href="{0}">{1}</a>'.format( update_text = template.replace("[]", message_url.format(
context.user_data['message_url'], update_text[match[0] + 1:match[1] - 1] url,
) + update_text[match[1]:] 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'] message_to_send = context.user_data['message_to_send']
await message_to_send[0].edit_caption( await message_to_send[0].edit_caption(update_text)
update_text,
reply_markup=InlineKeyboardMarkup.from_button(
InlineKeyboardButton("↩️ Confirm", callback_data="forward")
)
)
async def query_forward_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def query_forward_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
message_to_send = context.user_data['message_to_send'] message_to_send = context.user_data['message_to_send']
await forward_message(update, context, message_to_send) await forward_message(update, context, message_to_send)
await update.callback_query.answer('✅ Forwarded') await update.callback_query.answer('✅ Forwarded')
await update.callback_query.edit_message_reply_markup() await update.callback_query.delete_message()
del context.user_data['message_reply'] del context.user_data['message_reply']
del context.user_data['message_to_send'] del context.user_data['message_to_send']
del context.user_data['message_url'] del context.user_data['message_url']
@@ -153,12 +167,33 @@ async def cmd_edit_before_forward(update: Update, context: ContextTypes.DEFAULT_
ebf_status = context.user_data.get('edit_before_forward', False) ebf_status = context.user_data.get('edit_before_forward', False)
if ebf_status: if ebf_status:
context.user_data['edit_before_forward'] = False 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.") await update.effective_message.reply_text("Disable edit before forward.")
return return
context.user_data['edit_before_forward'] = True context.user_data['edit_before_forward'] = True
await update.effective_message.reply_text("Enable edit before forward.") 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),
@@ -167,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)
TGTweet.set_session(ClientSession()) TGTweet.init_client()
async def post_stop(application: Application) -> None: async def post_stop(application: Application) -> None:
@@ -175,7 +210,7 @@ async def post_stop(application: Application) -> None:
async def post_shutdown(application: Application) -> None: async def post_shutdown(application: Application) -> None:
await TGTweet.close_session() await TGTweet.close_client()
def main(): def main():
@@ -188,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)
.concurrent_updates(True)
.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, url_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("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, edit_message),
CallbackQueryHandler(query_forward_message, pattern="forward"), 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'
+44 -28
View File
@@ -1,16 +1,14 @@
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,
InlineQueryResultMpeg4Gif, from common import get_logger, x_media_regex, x_tco_regex, x_url_regex
InputMediaPhoto,
InputMediaVideo, logger = get_logger(__name__)
InputMediaAnimation
)
from common import x_url_regex, x_media_regex, x_tco_regex, logger
twimg_url = 'https://pbs.twimg.com/' twimg_url = 'https://pbs.twimg.com/'
vx_api_url = 'https://api.vxtwitter.com/{0}/status/{1}' vx_api_url = 'https://api.vxtwitter.com/{0}/status/{1}'
@@ -20,11 +18,19 @@ message_raw_text = """{url}
""" """
async def fetch_json(session: ClientSession, url: str) -> dict: 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}") logger.info(f"Fetching {url}")
async with session.get(url) as response: response = await _client.get(url)
assert response.status == 200, f"Failed to fetch {url}, status code {response.status}" assert response.status_code == response.is_success, f"Failed to fetch {url}, status code {response.status_code}"
return await response.json() return response.json()
class TweetMedia: class TweetMedia:
@@ -119,11 +125,12 @@ class Tweet:
class TGTweet(Tweet): class TGTweet(Tweet):
_session: ClientSession _httpx_client: AsyncClient
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._api_param: tuple[str] = self._tweet_id
self._is_single_gif: bool = False
assert self._api_param assert self._api_param
async def __aenter__(self): async def __aenter__(self):
@@ -135,15 +142,15 @@ class TGTweet(Tweet):
pass pass
@classmethod @classmethod
def set_session(cls, session: ClientSession) -> None: def init_client(cls) -> None:
cls._session = session cls._httpx_client = create_client()
@classmethod @classmethod
async def close_session(cls) -> None: async def close_client(cls) -> None:
await cls._session.close() await close_client(cls._httpx_client)
async def _fetch_tweet(self, api_param: tuple[str]) -> dict: async def _fetch_tweet(self, api_param: tuple[str]) -> dict:
return await fetch_json(self._session, vx_api_url.format(*api_param)) return await fetch_json(self._httpx_client, vx_api_url.format(*api_param))
@property @property
def _tweet_id(self) -> tuple[str] | None: def _tweet_id(self) -> tuple[str] | None:
@@ -178,17 +185,23 @@ class TGTweet(Tweet):
sensitive = self._tweet['possibly_sensitive'] sensitive = self._tweet['possibly_sensitive']
return 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)) logger.info(str(tweet_media))
if tweet_media.type == "image": if tweet_media.type == "image":
@@ -196,7 +209,7 @@ class TGTweet(Tweet):
id=str(i), id=str(i),
photo_url=tweet_media.url, photo_url=tweet_media.url,
thumbnail_url=tweet_media.thumb, thumbnail_url=tweet_media.thumb,
caption=self.message_text if not i else None caption=self.message_text
) )
elif tweet_media.type == "video": elif tweet_media.type == "video":
yield InlineQueryResultVideo( yield InlineQueryResultVideo(
@@ -205,18 +218,18 @@ class TGTweet(Tweet):
mime_type="video/mp4", mime_type="video/mp4",
thumbnail_url=tweet_media.thumb, thumbnail_url=tweet_media.thumb,
title=self.text, title=self.text,
caption=self.message_text if not i else None caption=self.message_text
) )
elif tweet_media.type == "gif": elif tweet_media.type == "gif":
yield InlineQueryResultMpeg4Gif( yield InlineQueryResultMpeg4Gif(
id=str(i), id=str(i),
mpeg4_url=tweet_media.url, mpeg4_url=tweet_media.url,
thumbnail_url=tweet_media.thumb, thumbnail_url=tweet_media.thumb,
caption=self.message_text if not i else None 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)) logger.info(str(tweet_media))
if tweet_media.type == "image": if tweet_media.type == "image":
@@ -231,7 +244,10 @@ class TGTweet(Tweet):
thumbnail=tweet_media.thumb thumbnail=tweet_media.thumb
) )
elif tweet_media.type == "gif": elif tweet_media.type == "gif":
yield InputMediaAnimation( if len(self.media) == 1:
self._is_single_gif = True
yield tweet_media.url, self.sensitive
yield InputMediaVideo(
media=tweet_media.url, media=tweet_media.url,
has_spoiler=self.sensitive, has_spoiler=self.sensitive,
thumbnail=tweet_media.thumb thumbnail=tweet_media.thumb