Compare commits

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