upload files

This commit is contained in:
2024-03-22 16:19:13 +08:00
commit f1bdb09efd
10 changed files with 498 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
**/__pycache__
**/*venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
*.db
.python-version
LICENSE
README.md
pers_data
cert/
+6
View File
@@ -0,0 +1,6 @@
**/.idea
__pycache__/
cert/
pers_data
docker-compose.yml
x.py
+23
View File
@@ -0,0 +1,23 @@
FROM python:3.12-slim-bullseye
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN python -m pip install --no-cache-dir --upgrade -r requirements.txt
WORKDIR /app
COPY . /app
RUN set -eux; \
apt-get update; \
apt-get install -y gosu; \
rm -rf /var/lib/apt/lists/*; \
# verify that the binary works
gosu nobody true
RUN chmod a+x docker-entrypoint.sh
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["python", "main.py"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 TheFunny
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+32
View File
@@ -0,0 +1,32 @@
# import json
import logging
import re
import os
# with open("config.json") as config_file:
# config = json.load(config_file)
# admin = config["telegram"]["admin"]
# token = config["telegram"]["token"]
BOT_TOKEN = os.getenv("BOT_TOKEN")
ADMIN = os.getenv("ADMIN").split(",")
WEBHOOK = os.getenv("WEBHOOK", False)
if WEBHOOK:
WEBHOOK_LISTEN = os.getenv("WEBHOOK_LISTEN", "0.0.0.0")
WEBHOOK_PORT = int(os.getenv("WEBHOOK_PORT", 8443))
WEBHOOK_URL = os.getenv("WEBHOOK_URL")
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)
logging.basicConfig(
level=os.getenv("LOG_LEVEL", "WARNING"),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
+22
View File
@@ -0,0 +1,22 @@
version: '3.7'
services:
tgxmb:
build: .
restart: always
ports:
- "8443:8443"
environment:
LOCAL_USER_ID: '1000'
BOT_TOKEN: ''
BOT_ADMIN: ''
WEBHOOK: false
WEBHOOK_LISTEN: '127.0.0.1'
WEBHOOK_PORT: 8443
WEBHOOK_URL: 'https://example.com'
WEBHOOK_KEY: './cert/private.key'
WEBHOOK_CERT: './cert/cert.pem'
WEBHOOK_SECRET_TOKEN: 'secret-token'
# LOG_LEVEL: 'WARNING'
volumes:
- ./cert:/app/cert
container_name: tgxmb
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
set -e
if [ "$(id -u)" -eq '0' ]
then
USER_ID=${LOCAL_USER_ID:-9001}
useradd --shell /bin/bash -u ${USER_ID} -o -c "" -m user > /dev/null 2>&1
usermod -a -G root user > /dev/null 2>&1
chown -R `id -u user`:`id -u user` /app > /dev/null 2>&1
export HOME=/home/user
exec gosu user "$0" "$@"
fi
exec "$@"
+144
View File
@@ -0,0 +1,144 @@
from aiohttp import ClientSession
from telegram import Update, Chat, ChatMemberAdministrator
from telegram.constants import ParseMode, ChatAction
from telegram.ext import (
Application,
ApplicationBuilder,
ContextTypes,
Defaults,
filters,
InlineQueryHandler,
PicklePersistence,
MessageHandler, CommandHandler
)
import common
from tweet import TGTweet
async def inline_query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.inline_query.query
if query == "":
return
async with TGTweet(context.bot_data['client'], query) as tweet:
result = list(tweet.inline_query_generator)
await update.inline_query.answer(result)
async def reply_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
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:
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
)
if 'forward_channel_id' in context.user_data:
try:
await update.effective_chat.copy_messages(
chat_id=context.user_data['forward_channel_id'],
message_ids=[m.id for m in message_sent],
)
except Exception as e:
await update.effective_message.reply_text(str(e))
async def cmd_set_forward_channel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.effective_chat.send_action(ChatAction.TYPING)
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 = context.bot.get_chat(channel)
except Exception as e:
await update.effective_message.reply_text(str(e))
return
if channel.type != "CHANNEL":
await update.effective_message.reply_text("That is not a channel.")
return
channel_admin = await channel.get_administrators()
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.")
async def cmd_remove_forward_channel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.effective_chat.send_action(ChatAction.TYPING)
if 'forward_channel_id' in context.user_data:
del context.user_data['forward_channel_id']
await update.effective_message.reply_text("Remove successfully.")
return
await update.effective_message.reply_text("No channel to remove.")
async def post_init(application: Application) -> None:
# commands = [
# BotCommand('start', CMD_START),
# ]
# await application.bot.set_my_commands(commands)
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()
async def post_stop(application: Application) -> None:
await application.bot.send_message(common.ADMIN[0], "Shutting down...")
async def post_shutdown(application: Application) -> None:
await application.bot_data['client'].close()
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'
application = (ApplicationBuilder()
.token(common.BOT_TOKEN)
.defaults(defaults)
.persistence(persistence)
.post_init(post_init)
.post_stop(post_stop)
.post_shutdown(post_shutdown)
# .proxy(proxy_url)
# .get_updates_proxy(proxy_url)
.build()
)
# user_filter = filters.User()
# user_filter.add_user_ids(common.admin)
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),
]
application.add_handlers(handlers)
if common.WEBHOOK:
application.run_webhook(
listen=common.WEBHOOK_LISTEN,
port=common.WEBHOOK_PORT,
secret_token=common.WEBHOOK_SECRET_TOKEN,
key=common.WEBHOOK_KEY,
cert=common.WEBHOOK_CERT,
webhook_url=common.WEBHOOK_URL
)
else:
application.run_polling()
if __name__ == '__main__':
main()
+2
View File
@@ -0,0 +1,2 @@
python-telegram-bot[webhooks]~=21.0.1
aiohttp[speedups]~=3.9.3
+201
View File
@@ -0,0 +1,201 @@
from typing import Generator
from aiohttp import ClientSession
from telegram import (
InlineQueryResultPhoto,
InlineQueryResultVideo,
InputMediaPhoto,
InputMediaVideo
)
from common import x_url_regex, x_media_regex, x_tco_regex
twimg_url = 'https://pbs.twimg.com/'
vx_api_url = 'https://api.vxtwitter.com/status/'
message_raw_text = """{url}
<a href="{author_url}">{author}</a>: {text}
"""
async def fetch_json(session: ClientSession, url: str) -> dict:
async with session.get(url) as response:
assert response.status == 200
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
@property
def _uri(self) -> str | None:
match = x_media_regex.match(self._url)
if match:
return match.group(2).removesuffix('.jpg')
return None
@property
def url(self) -> str:
match self._type:
case "image":
return f"{twimg_url}{self._uri}?format=jpg&name=orig"
case "video":
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
@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):
def __init__(self, session: ClientSession, url: str):
self._session: ClientSession = session
self._url: str = url
self._id: str = self._tweet_id
assert self._id
async def __aenter__(self):
self._tweet: dict = await self._fetch_tweet(self._id)
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)
@property
def _tweet_id(self) -> str | None:
match = x_url_regex.match(self._url)
if match:
return match.group(1).strip('/')
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:
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
@property
def message_text(self) -> str:
return message_raw_text.format(
url=self.url,
author_url=self.author_url,
author=self.author,
text=self.text
)
@property
def inline_query_generator(self) -> Generator[InlineQueryResultPhoto | InlineQueryResultVideo, None, None]:
for i, tweet_media in enumerate(self.media):
if tweet_media.type == "image":
yield InlineQueryResultPhoto(
id=self.id,
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,
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
)
@property
def pm_media_generator(self) -> Generator[InputMediaPhoto | InputMediaVideo, None, None]:
for tweet_media in self.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
)