let's try pyrogram 2.x again

This commit is contained in:
Benny 2023-12-16 18:37:38 +01:00
parent acacfe58c9
commit f7243a4a93
No known key found for this signature in database
GPG key ID: 6CD0DBDA5235D481
16 changed files with 156 additions and 238 deletions

View file

@ -1,4 +1,5 @@
env env
venv
db_data db_data
.ash_history .ash_history
.DS_Store .DS_Store

3
.gitignore vendored
View file

@ -170,3 +170,6 @@ reinforcement/*
/ytdlbot/ytdl-main.session /ytdlbot/ytdl-main.session
/ytdlbot/ytdl-celery.session-journal /ytdlbot/ytdl-celery.session-journal
/ytdlbot/ytdl-celery.session /ytdlbot/ytdl-celery.session
/ytdlbot/main.session
/ytdlbot/tasks.session
/ytdlbot/tasks.session-journal

0
.gitmodules vendored
View file

View file

@ -1,9 +0,0 @@
# This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
# and commit this file to your remote git repository to share the goodness with others.
# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart
tasks:
- init: pip3 install -r requirements.txt
- init: sudo apt update && sudo apt install ffmpeg -y

View file

@ -1,16 +1,13 @@
FROM python:3.11-alpine as builder FROM python:3.11 as builder
RUN apk update && apk add --no-cache tzdata alpine-sdk libffi-dev ca-certificates
ADD requirements.txt /tmp/ ADD requirements.txt /tmp/
RUN pip3 install --user -r /tmp/requirements.txt && rm /tmp/requirements.txt RUN pip3 install --user -r /tmp/requirements.txt && rm /tmp/requirements.txt
FROM python:3.11-alpine FROM python:3.11-slim
WORKDIR /ytdlbot/ytdlbot WORKDIR /ytdlbot/ytdlbot
ENV TZ=Europe/Stockholm ENV TZ=Europe/London
COPY apk.txt /tmp/ RUN apt update && apt install -y --no-install-recommends --no-install-suggests ffmpeg vnstat git aria2
RUN apk update && xargs apk add < /tmp/apk.txt
COPY --from=builder /root/.local /usr/local COPY --from=builder /root/.local /usr/local
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

View file

@ -74,7 +74,7 @@ This bot can be deployed on any platform that supports Python.
To deploy this bot, follow these steps: To deploy this bot, follow these steps:
1. Install bot dependencies 1. Install bot dependencies
* Install Python 3.8 or a later version, FFmpeg. * Install Python 3.10 or a later version, FFmpeg.
* (optional)Aria2 and add it to the PATH. * (optional)Aria2 and add it to the PATH.
2. Clone the code from the repository and cd into it. 2. Clone the code from the repository and cd into it.
@ -153,7 +153,6 @@ You can configure all the following environment variables:
* AUTHORIZED_USER: only authorized users can use the bot * AUTHORIZED_USER: only authorized users can use the bot
* REQUIRED_MEMBERSHIP: group or channel username, user must join this group to use the bot * REQUIRED_MEMBERSHIP: group or channel username, user must join this group to use the bot
* ENABLE_CELERY: celery mode, default: disable * ENABLE_CELERY: celery mode, default: disable
* ENABLE_QUEUE: celery queue
* BROKER: celery broker, should be redis://redis:6379/0 * BROKER: celery broker, should be redis://redis:6379/0
* MYSQL_HOST:MySQL host * MYSQL_HOST:MySQL host
* MYSQL_USER: MySQL username * MYSQL_USER: MySQL username
@ -291,11 +290,11 @@ https://twitter.com/BennyThinks/status/1475836588542341124
## Test instagram ## Test instagram
https://www.instagram.com/p/ClBSqo3PkJw/ * single image: https://www.instagram.com/p/CXpxSyOrWCA/
* single video: https://www.instagram.com/p/Cah_7gnDVUW/
https://www.instagram.com/p/CaiAHoWDnrM/ * reels: https://www.instagram.com/p/C0ozGsjtY0W/
* image carousel: https://www.instagram.com/p/C0ozPQ5o536/
https://www.instagram.com/p/CZtUDyyv1u1/ * video and image carousel: https://www.instagram.com/p/C0ozhsVo-m8/
# Donation # Donation

View file

@ -1 +0,0 @@
ffmpeg vnstat git aria2

View file

@ -1,4 +1,4 @@
pyrogram==1.4.16 pyrogram==2.0.106
tgcrypto==1.2.5 tgcrypto==1.2.5
yt-dlp==2023.11.16 yt-dlp==2023.11.16
APScheduler==3.10.4 APScheduler==3.10.4
@ -11,7 +11,7 @@ flower==2.0.1
psutil==5.9.6 psutil==5.9.6
influxdb==5.3.1 influxdb==5.3.1
beautifulsoup4==4.12.2 beautifulsoup4==4.12.2
fakeredis==2.20.0 fakeredis==2.20.1
supervisor==4.2.5 supervisor==4.2.5
tgbot-ping==1.0.7 tgbot-ping==1.0.7
redis==5.0.1 redis==5.0.1

View file

@ -12,14 +12,12 @@ from pyrogram import Client
from config import APP_HASH, APP_ID, PYRO_WORKERS, TOKEN, IPv6 from config import APP_HASH, APP_ID, PYRO_WORKERS, TOKEN, IPv6
def create_app(session: str, workers: int = PYRO_WORKERS) -> Client: def create_app(name: str, workers: int = PYRO_WORKERS) -> Client:
_app = Client( return Client(
session, name,
APP_ID, APP_ID,
APP_HASH, APP_HASH,
bot_token=TOKEN, bot_token=TOKEN,
workers=workers, workers=workers,
ipv6=IPv6, ipv6=IPv6,
) )
return _app

View file

@ -12,8 +12,8 @@ import os
from blinker import signal from blinker import signal
# general settings # general settings
WORKERS: int = int(os.getenv("WORKERS", 100)) WORKERS: int = int(os.getenv("WORKERS", 10))
PYRO_WORKERS: int = int(os.getenv("PYRO_WORKERS", min(64, (os.cpu_count() + 4) * 10))) PYRO_WORKERS: int = int(os.getenv("PYRO_WORKERS", min(32, (os.cpu_count() or 0) + 4)))
APP_ID: int = int(os.getenv("APP_ID", 198214)) APP_ID: int = int(os.getenv("APP_ID", 198214))
APP_HASH = os.getenv("APP_HASH", "1234b90") APP_HASH = os.getenv("APP_HASH", "1234b90")
TOKEN = os.getenv("TOKEN", "1234") TOKEN = os.getenv("TOKEN", "1234")
@ -36,7 +36,6 @@ REQUIRED_MEMBERSHIP: str = os.getenv("REQUIRED_MEMBERSHIP", "")
# celery related # celery related
ENABLE_CELERY = os.getenv("ENABLE_CELERY", False) ENABLE_CELERY = os.getenv("ENABLE_CELERY", False)
ENABLE_QUEUE = os.getenv("ENABLE_QUEUE", False)
BROKER = os.getenv("BROKER", f"redis://{REDIS}:6379/4") BROKER = os.getenv("BROKER", f"redis://{REDIS}:6379/4")
MYSQL_HOST = os.getenv("MYSQL_HOST", "mysql") MYSQL_HOST = os.getenv("MYSQL_HOST", "mysql")
@ -49,7 +48,6 @@ ARCHIVE_ID = os.getenv("ARCHIVE_ID")
IPv6 = os.getenv("IPv6", False) IPv6 = os.getenv("IPv6", False)
ENABLE_FFMPEG = os.getenv("ENABLE_FFMPEG", False) ENABLE_FFMPEG = os.getenv("ENABLE_FFMPEG", False)
PLAYLIST_SUPPORT = os.getenv("PLAYLIST_SUPPORT", False) PLAYLIST_SUPPORT = os.getenv("PLAYLIST_SUPPORT", False)
M3U8_SUPPORT = os.getenv("M3U8_SUPPORT", False) M3U8_SUPPORT = os.getenv("M3U8_SUPPORT", False)
ENABLE_ARIA2 = os.getenv("ENABLE_ARIA2", False) ENABLE_ARIA2 = os.getenv("ENABLE_ARIA2", False)

View file

@ -75,7 +75,7 @@ class Redis:
self.r = redis.StrictRedis(host=REDIS, db=0, decode_responses=True) self.r = redis.StrictRedis(host=REDIS, db=0, decode_responses=True)
self.r.ping() self.r.ping()
except Exception: except Exception:
logging.warning("Redis connection failed, using fake redis instead.") logging.debug("Redis connection failed, using fake redis instead.")
self.r = fakeredis.FakeStrictRedis(host=REDIS, db=0, decode_responses=True) self.r = fakeredis.FakeStrictRedis(host=REDIS, db=0, decode_responses=True)
db_banner = "=" * 20 + "DB data" + "=" * 20 db_banner = "=" * 20 + "DB data" + "=" * 20
@ -256,7 +256,7 @@ class MySQL:
host=MYSQL_HOST, user=MYSQL_USER, passwd=MYSQL_PASS, db="ytdl", charset="utf8mb4" host=MYSQL_HOST, user=MYSQL_USER, passwd=MYSQL_PASS, db="ytdl", charset="utf8mb4"
) )
except Exception: except Exception:
logging.warning("MySQL connection failed, using fake mysql instead.") logging.debug("MySQL connection failed, using fake mysql instead.")
self.con = FakeMySQL() self.con = FakeMySQL()
self.con.ping(reconnect=True) self.con.ping(reconnect=True)

View file

@ -24,15 +24,10 @@ import ffpb
import filetype import filetype
import requests import requests
import yt_dlp as ytdl import yt_dlp as ytdl
from pyrogram import types
from tqdm import tqdm from tqdm import tqdm
from config import ( from config import AUDIO_FORMAT, ENABLE_ARIA2, ENABLE_FFMPEG, TG_MAX_SIZE, IPv6
AUDIO_FORMAT,
ENABLE_ARIA2,
ENABLE_FFMPEG,
TG_MAX_SIZE,
IPv6,
)
from limit import Payment from limit import Payment
from utils import adjust_formats, apply_log_formatter, current_time, sizeof_fmt from utils import adjust_formats, apply_log_formatter, current_time, sizeof_fmt
@ -40,8 +35,8 @@ r = fakeredis.FakeStrictRedis()
apply_log_formatter() apply_log_formatter()
def edit_text(bot_msg, text: str): def edit_text(bot_msg: types.Message, text: str):
key = f"{bot_msg.chat.id}-{bot_msg.message_id}" key = f"{bot_msg.chat.id}-{bot_msg.id}"
# if the key exists, we shouldn't send edit message # if the key exists, we shouldn't send edit message
if not r.exists(key): if not r.exists(key):
time.sleep(random.random()) time.sleep(random.random())
@ -87,7 +82,7 @@ def remove_bash_color(text):
def download_hook(d: dict, bot_msg): def download_hook(d: dict, bot_msg):
# since we're using celery, server location may be located in different continent. # since we're using celery, server location may be located in different region.
# Therefore, we can't trigger the hook very often. # Therefore, we can't trigger the hook very often.
# the key is user_id + download_link # the key is user_id + download_link
original_url = d["info_dict"]["original_url"] original_url = d["info_dict"]["original_url"]

View file

@ -155,8 +155,8 @@ class TronTrx:
cur.execute("select user_id, payment_id from payment where payment_id like 'tron,0,T%'") cur.execute("select user_id, payment_id from payment where payment_id like 'tron,0,T%'")
data = cur.fetchall() data = cur.fetchall()
logging.info("Checking unpaid payment...%s", data)
for row in data: for row in data:
logging.info("Checking user payment %s", row)
user_id = row[0] user_id = row[0]
addr, index = row[1].split(",")[2:] addr, index = row[1].split(",")[2:]
try: try:

View file

@ -7,11 +7,10 @@
__author__ = "Benny <benny.think@gmail.com>" __author__ = "Benny <benny.think@gmail.com>"
import asyncio
import logging import logging
import math
import os import os
import pathlib import pathlib
import random
import re import re
import shutil import shutil
import subprocess import subprocess
@ -20,7 +19,7 @@ import threading
import time import time
import traceback import traceback
import typing import typing
from hashlib import md5 from typing import Any
from urllib.parse import quote_plus from urllib.parse import quote_plus
import filetype import filetype
@ -30,9 +29,7 @@ import requests
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from celery import Celery from celery import Celery
from celery.worker.control import Panel from celery.worker.control import Panel
from pyrogram import Client, idle, types from pyrogram import Client, enums, idle, types
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
from channel import Channel from channel import Channel
from client_init import create_app from client_init import create_app
@ -40,7 +37,6 @@ from config import (
ARCHIVE_ID, ARCHIVE_ID,
BROKER, BROKER,
ENABLE_CELERY, ENABLE_CELERY,
ENABLE_QUEUE,
ENABLE_VIP, ENABLE_VIP,
OWNER, OWNER,
RATE_LIMIT, RATE_LIMIT,
@ -66,37 +62,33 @@ apply_log_formatter()
bot_text = BotText() bot_text = BotText()
logging.getLogger("apscheduler.executors.default").propagate = False logging.getLogger("apscheduler.executors.default").propagate = False
# celery -A tasks worker --loglevel=info --pool=solo
# app = Celery('celery', broker=BROKER, accept_content=['pickle'], task_serializer='pickle')
app = Celery("tasks", broker=BROKER) app = Celery("tasks", broker=BROKER)
bot = create_app("tasks")
channel = Channel() channel = Channel()
session = "ytdl-celery"
celery_client = create_app(session)
def get_messages(chat_id: int, message_id: int):
def get_messages(chat_id, message_id):
try: try:
return celery_client.get_messages(chat_id, message_id) return bot.get_messages(chat_id, message_id)
except ConnectionError as e: except ConnectionError as e:
logging.critical("WTH!!! %s", e) logging.critical("CRITICAL ERROR: %s", e)
celery_client.start() bot.start()
return celery_client.get_messages(chat_id, message_id) return bot.get_messages(chat_id, message_id)
@app.task(rate_limit=f"{RATE_LIMIT}/m") @app.task(rate_limit=f"{RATE_LIMIT}/m")
def ytdl_download_task(chat_id, message_id, url: str): def ytdl_download_task(chat_id: int, message_id: int, url: str):
logging.info("YouTube celery tasks started for %s", url) logging.info("YouTube celery tasks started for %s", url)
bot_msg = get_messages(chat_id, message_id) bot_msg = get_messages(chat_id, message_id)
ytdl_normal_download(celery_client, bot_msg, url) ytdl_normal_download(bot_msg, url)
logging.info("YouTube celery tasks ended.") logging.info("YouTube celery tasks ended.")
@app.task() @app.task()
def audio_task(chat_id, message_id): def audio_task(chat_id: int, message_id: int):
logging.info("Audio celery tasks started for %s-%s", chat_id, message_id) logging.info("Audio celery tasks started for %s-%s", chat_id, message_id)
bot_msg = get_messages(chat_id, message_id) bot_msg = get_messages(chat_id, message_id)
normal_audio(celery_client, bot_msg) normal_audio(bot_msg)
logging.info("Audio celery tasks ended.") logging.info("Audio celery tasks ended.")
@ -113,14 +105,14 @@ def get_unique_clink(original_url: str, user_id: int):
@app.task() @app.task()
def direct_download_task(chat_id, message_id, url): def direct_download_task(chat_id: int, message_id: int, url: str):
logging.info("Direct download celery tasks started for %s", url) logging.info("Direct download celery tasks started for %s", url)
bot_msg = get_messages(chat_id, message_id) bot_msg = get_messages(chat_id, message_id)
direct_normal_download(celery_client, bot_msg, url) direct_normal_download(bot, bot_msg, url)
logging.info("Direct download celery tasks ended.") logging.info("Direct download celery tasks ended.")
def forward_video(client, bot_msg, url: str): def forward_video(bot_msg: types.Message, url: str):
redis = Redis() redis = Redis()
chat_id = bot_msg.chat.id chat_id = bot_msg.chat.id
unique = get_unique_clink(url, chat_id) unique = get_unique_clink(url, chat_id)
@ -129,28 +121,27 @@ def forward_video(client, bot_msg, url: str):
redis.update_metrics("cache_miss") redis.update_metrics("cache_miss")
return False return False
res_msg: "Message" = upload_processor(client, bot_msg, url, cached_fid) res_msg = upload_processor(bot_msg, url, cached_fid)
obj = res_msg.document or res_msg.video or res_msg.audio or res_msg.animation or res_msg.photo obj = res_msg.document or res_msg.video or res_msg.audio or res_msg.animation or res_msg.photo
caption, _ = gen_cap(bot_msg, url, obj) caption, _ = gen_cap(bot_msg, url, obj)
res_msg.edit_text(caption, reply_markup=gen_video_markup()) res_msg.edit_text(caption, reply_markup=gen_video_markup())
bot_msg.edit_text(f"Download success!✅✅✅") bot_msg.edit_text(f"Download success!✅")
redis.update_metrics("cache_hit") redis.update_metrics("cache_hit")
return True return True
def ytdl_download_entrance(client: Client, bot_msg: types.Message, url: str, mode=None): def ytdl_download_entrance(bot_msg: types.Message, url: str, mode=None):
payment = Payment() payment = Payment()
chat_id = bot_msg.chat.id chat_id = bot_msg.chat.id
try: try:
if forward_video(client, bot_msg, url): if forward_video(bot_msg, url):
return return
mode = mode or payment.get_user_settings(chat_id)[-1] mode = mode or payment.get_user_settings(chat_id)[-1]
if ENABLE_CELERY and mode in [None, "Celery"]: if ENABLE_CELERY and mode in [None, "Celery"]:
async_task(ytdl_download_task, chat_id, bot_msg.message_id, url) ytdl_download_task.delay(chat_id, bot_msg.id, url)
# ytdl_download_task.delay(chat_id, bot_msg.message_id, url)
else: else:
ytdl_normal_download(client, bot_msg, url) ytdl_normal_download(bot_msg, url)
except Exception as e: except Exception as e:
logging.error("Failed to download %s, error: %s", url, e) logging.error("Failed to download %s, error: %s", url, e)
bot_msg.edit_text(f"Download failed!❌\n\n`{traceback.format_exc()[0:4000]}`", disable_web_page_preview=True) bot_msg.edit_text(f"Download failed!❌\n\n`{traceback.format_exc()[0:4000]}`", disable_web_page_preview=True)
@ -159,23 +150,22 @@ def ytdl_download_entrance(client: Client, bot_msg: types.Message, url: str, mod
def direct_download_entrance(client: Client, bot_msg: typing.Union[types.Message, typing.Coroutine], url: str): def direct_download_entrance(client: Client, bot_msg: typing.Union[types.Message, typing.Coroutine], url: str):
if ENABLE_CELERY: if ENABLE_CELERY:
direct_normal_download(client, bot_msg, url) direct_normal_download(client, bot_msg, url)
# direct_download_task.delay(bot_msg.chat.id, bot_msg.message_id, url) # direct_download_task.delay(bot_msg.chat.id, bot_msg.id, url)
else: else:
direct_normal_download(client, bot_msg, url) direct_normal_download(client, bot_msg, url)
def audio_entrance(client, bot_msg): def audio_entrance(bot_msg: types.Message):
if ENABLE_CELERY: if ENABLE_CELERY:
async_task(audio_task, bot_msg.chat.id, bot_msg.message_id) audio_task.delay(bot_msg.chat.id, bot_msg.id)
# audio_task.delay(bot_msg.chat.id, bot_msg.message_id)
else: else:
normal_audio(client, bot_msg) normal_audio(bot_msg)
def direct_normal_download(client: Client, bot_msg: typing.Union[types.Message, typing.Coroutine], url: str): def direct_normal_download(client: Client, bot_msg: typing.Union[types.Message, typing.Coroutine], url: str):
chat_id = bot_msg.chat.id chat_id = bot_msg.chat.id
headers = { headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36" "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.3987.149 Safari/537.36"
} }
length = 0 length = 0
@ -206,7 +196,7 @@ def direct_normal_download(client: Client, bot_msg: typing.Union[types.Message,
logging.info("Downloaded file %s", filename) logging.info("Downloaded file %s", filename)
st_size = os.stat(filepath).st_size st_size = os.stat(filepath).st_size
client.send_chat_action(chat_id, "upload_document") client.send_chat_action(chat_id, enums.ChatAction.UPLOAD_DOCUMENT)
client.send_document( client.send_document(
bot_msg.chat.id, bot_msg.chat.id,
filepath, filepath,
@ -217,7 +207,7 @@ def direct_normal_download(client: Client, bot_msg: typing.Union[types.Message,
bot_msg.edit_text("Download success!✅") bot_msg.edit_text("Download success!✅")
def normal_audio(client: Client, bot_msg: typing.Union[types.Message, typing.Coroutine]): def normal_audio(bot_msg: typing.Union[types.Message, typing.Coroutine]):
chat_id = bot_msg.chat.id chat_id = bot_msg.chat.id
# fn = getattr(bot_msg.video, "file_name", None) or getattr(bot_msg.document, "file_name", None) # fn = getattr(bot_msg.video, "file_name", None) or getattr(bot_msg.document, "file_name", None)
status_msg: typing.Union[types.Message, typing.Coroutine] = bot_msg.reply_text( status_msg: typing.Union[types.Message, typing.Coroutine] = bot_msg.reply_text(
@ -225,59 +215,36 @@ def normal_audio(client: Client, bot_msg: typing.Union[types.Message, typing.Cor
) )
orig_url: str = re.findall(r"https?://.*", bot_msg.caption)[0] orig_url: str = re.findall(r"https?://.*", bot_msg.caption)[0]
with tempfile.TemporaryDirectory(prefix="ytdl-", dir=TMPFILE_PATH) as tmp: with tempfile.TemporaryDirectory(prefix="ytdl-", dir=TMPFILE_PATH) as tmp:
client.send_chat_action(chat_id, "record_audio") bot.send_chat_action(chat_id, enums.ChatAction.RECORD_AUDIO)
# just try to download the audio using yt-dlp # just try to download the audio using yt-dlp
filepath = ytdl_download(orig_url, tmp, status_msg, hijack="bestaudio[ext=m4a]") filepath = ytdl_download(orig_url, tmp, status_msg, hijack="bestaudio[ext=m4a]")
status_msg.edit_text("Sending audio now...") status_msg.edit_text("Sending audio now...")
client.send_chat_action(chat_id, "upload_audio") bot.send_chat_action(chat_id, enums.ChatAction.UPLOAD_AUDIO)
for f in filepath: for f in filepath:
client.send_audio(chat_id, f) bot.send_audio(chat_id, f)
status_msg.edit_text("✅ Conversion complete.") status_msg.edit_text("✅ Conversion complete.")
Redis().update_metrics("audio_success") Redis().update_metrics("audio_success")
def get_dl_source(): def ytdl_normal_download(bot_msg: types.Message | typing.Any, url: str):
worker_name = os.getenv("WORKER_NAME")
if worker_name:
return f"Downloaded by {worker_name}"
return ""
def upload_transfer_sh(bm, paths: list) -> str:
d = {p.name: (md5(p.name.encode("utf8")).hexdigest() + p.suffix, p.open("rb")) for p in paths}
monitor = MultipartEncoderMonitor(MultipartEncoder(fields=d), lambda x: upload_hook(x.bytes_read, x.len, bm))
headers = {"Content-Type": monitor.content_type}
try:
req = requests.post("https://transfer.sh", data=monitor, headers=headers)
bm.edit_text(f"Download success!✅")
return re.sub(r"https://", "\nhttps://", req.text)
except requests.exceptions.RequestException as e:
return f"Upload failed!❌\n\n```{e}```"
def flood_owner_message(client, ex):
client.send_message(OWNER, f"CRITICAL INFO: {ex}")
def ytdl_normal_download(client: Client, bot_msg: typing.Union[types.Message, typing.Coroutine], url: str):
chat_id = bot_msg.chat.id chat_id = bot_msg.chat.id
temp_dir = tempfile.TemporaryDirectory(prefix="ytdl-", dir=TMPFILE_PATH) temp_dir = tempfile.TemporaryDirectory(prefix="ytdl-", dir=TMPFILE_PATH)
video_paths = ytdl_download(url, temp_dir.name, bot_msg) video_paths = ytdl_download(url, temp_dir.name, bot_msg)
logging.info("Download complete.") logging.info("Download complete.")
client.send_chat_action(chat_id, "upload_document") bot.send_chat_action(chat_id, enums.ChatAction.UPLOAD_DOCUMENT)
bot_msg.edit_text("Download complete. Sending now...") bot_msg.edit_text("Download complete. Sending now...")
try: try:
upload_processor(client, bot_msg, url, video_paths) upload_processor(bot_msg, url, video_paths)
except pyrogram.errors.Flood as e: except pyrogram.errors.Flood as e:
logging.critical("FloodWait from Telegram: %s", e) logging.critical("FloodWait from Telegram: %s", e)
client.send_message( bot.send_message(
chat_id, chat_id,
f"I'm being rate limited by Telegram. Your video will come after {e.x} seconds. Please wait patiently.", f"I'm being rate limited by Telegram. Your video will come after {e} seconds. Please wait patiently.",
) )
flood_owner_message(client, e) bot.send_message(OWNER, f"CRITICAL INFO: {e}")
time.sleep(e.x) time.sleep(e.value)
upload_processor(client, bot_msg, url, video_paths) upload_processor(bot_msg, url, video_paths)
bot_msg.edit_text("Download success!✅") bot_msg.edit_text("Download success!✅")
@ -306,7 +273,7 @@ def generate_input_media(file_paths: list, cap: str) -> list:
return input_media return input_media
def upload_processor(client, bot_msg, url, vp_or_fid: typing.Union[str, list]): def upload_processor(bot_msg: types.Message, url: str, vp_or_fid: str | list):
redis = Redis() redis = Redis()
# raise pyrogram.errors.exceptions.FloodWait(13) # raise pyrogram.errors.exceptions.FloodWait(13)
# if is str, it's a file id; else it's a list of paths # if is str, it's a file id; else it's a list of paths
@ -316,7 +283,7 @@ def upload_processor(client, bot_msg, url, vp_or_fid: typing.Union[str, list]):
if isinstance(vp_or_fid, list) and len(vp_or_fid) > 1: if isinstance(vp_or_fid, list) and len(vp_or_fid) > 1:
# just generate the first for simplicity, send as media group(2-20) # just generate the first for simplicity, send as media group(2-20)
cap, meta = gen_cap(bot_msg, url, vp_or_fid[0]) cap, meta = gen_cap(bot_msg, url, vp_or_fid[0])
res_msg = client.send_media_group(chat_id, generate_input_media(vp_or_fid, cap)) res_msg: list["types.Message"] | Any = bot.send_media_group(chat_id, generate_input_media(vp_or_fid, cap))
# TODO no cache for now # TODO no cache for now
return res_msg[0] return res_msg[0]
elif isinstance(vp_or_fid, list) and len(vp_or_fid) == 1: elif isinstance(vp_or_fid, list) and len(vp_or_fid) == 1:
@ -335,7 +302,7 @@ def upload_processor(client, bot_msg, url, vp_or_fid: typing.Union[str, list]):
logging.info("Sending as document") logging.info("Sending as document")
try: try:
# send as document could be sent as video even if it's a document # send as document could be sent as video even if it's a document
res_msg = client.send_document( res_msg = bot.send_document(
chat_id, chat_id,
vp_or_fid, vp_or_fid,
caption=cap, caption=cap,
@ -347,7 +314,7 @@ def upload_processor(client, bot_msg, url, vp_or_fid: typing.Union[str, list]):
) )
except ValueError: except ValueError:
logging.error("Retry to send as video") logging.error("Retry to send as video")
res_msg = client.send_video( res_msg = bot.send_video(
chat_id, chat_id,
vp_or_fid, vp_or_fid,
supports_streaming=True, supports_streaming=True,
@ -359,7 +326,7 @@ def upload_processor(client, bot_msg, url, vp_or_fid: typing.Union[str, list]):
) )
elif settings[2] == "audio": elif settings[2] == "audio":
logging.info("Sending as audio") logging.info("Sending as audio")
res_msg = client.send_audio( res_msg = bot.send_audio(
chat_id, chat_id,
vp_or_fid, vp_or_fid,
caption=cap, caption=cap,
@ -370,7 +337,7 @@ def upload_processor(client, bot_msg, url, vp_or_fid: typing.Union[str, list]):
# settings==video # settings==video
logging.info("Sending as video") logging.info("Sending as video")
try: try:
res_msg = client.send_video( res_msg = bot.send_video(
chat_id, chat_id,
vp_or_fid, vp_or_fid,
supports_streaming=True, supports_streaming=True,
@ -384,7 +351,7 @@ def upload_processor(client, bot_msg, url, vp_or_fid: typing.Union[str, list]):
# try to send as annimation, photo # try to send as annimation, photo
try: try:
logging.warning("Retry to send as animation") logging.warning("Retry to send as animation")
res_msg = client.send_animation( res_msg = bot.send_animation(
chat_id, chat_id,
vp_or_fid, vp_or_fid,
caption=cap, caption=cap,
@ -396,7 +363,7 @@ def upload_processor(client, bot_msg, url, vp_or_fid: typing.Union[str, list]):
except Exception: except Exception:
# this is likely a photo # this is likely a photo
logging.warning("Retry to send as photo") logging.warning("Retry to send as photo")
res_msg = client.send_photo( res_msg = bot.send_photo(
chat_id, chat_id,
vp_or_fid, vp_or_fid,
caption=cap, caption=cap,
@ -409,7 +376,7 @@ def upload_processor(client, bot_msg, url, vp_or_fid: typing.Union[str, list]):
redis.add_send_cache(unique, getattr(obj, "file_id", None)) redis.add_send_cache(unique, getattr(obj, "file_id", None))
redis.update_metrics("video_success") redis.update_metrics("video_success")
if ARCHIVE_ID and isinstance(vp_or_fid, pathlib.Path): if ARCHIVE_ID and isinstance(vp_or_fid, pathlib.Path):
client.forward_messages(bot_msg.chat.id, ARCHIVE_ID, res_msg.message_id) bot.forward_messages(bot_msg.chat.id, ARCHIVE_ID, res_msg.id)
return res_msg return res_msg
@ -441,7 +408,11 @@ def gen_cap(bm, url, video_path):
remain = f"Download token count: free {free}, pay {pay}" remain = f"Download token count: free {free}, pay {pay}"
else: else:
remain = "" remain = ""
worker = get_dl_source()
if worker_name := os.getenv("WORKER_NAME"):
worker = f"Downloaded by {worker_name}"
else:
worker = ""
cap = ( cap = (
f"{user_info}\n{file_name}\n\n{url}\n\nInfo: {meta['width']}x{meta['height']} {file_size}\t" f"{user_info}\n{file_name}\n\n{url}\n\nInfo: {meta['width']}x{meta['height']} {file_size}\t"
f"{meta['duration']}s\n{remain}\n{worker}\n{bot_text.custom_text}" f"{meta['duration']}s\n{remain}\n{worker}\n{bot_text.custom_text}"
@ -450,10 +421,10 @@ def gen_cap(bm, url, video_path):
def gen_video_markup(): def gen_video_markup():
markup = InlineKeyboardMarkup( markup = types.InlineKeyboardMarkup(
[ [
[ # First row [ # First row
InlineKeyboardButton( # Generates a callback query when pressed types.InlineKeyboardButton( # Generates a callback query when pressed
"convert to audio", callback_data="convert" "convert to audio", callback_data="convert"
) )
] ]
@ -472,7 +443,6 @@ def hot_patch(*args):
app_path = pathlib.Path().cwd().parent app_path = pathlib.Path().cwd().parent
logging.info("Hot patching on path %s...", app_path) logging.info("Hot patching on path %s...", app_path)
apk_install = "xargs apk add < apk.txt"
pip_install = "pip install -r requirements.txt" pip_install = "pip install -r requirements.txt"
unset = "git config --unset http.https://github.com/.extraheader" unset = "git config --unset http.https://github.com/.extraheader"
pull_unshallow = "git pull origin --unshallow" pull_unshallow = "git pull origin --unshallow"
@ -484,52 +454,32 @@ def hot_patch(*args):
subprocess.call(pull, shell=True, cwd=app_path) subprocess.call(pull, shell=True, cwd=app_path)
logging.info("Code is updated, applying hot patch now...") logging.info("Code is updated, applying hot patch now...")
subprocess.call(apk_install, shell=True, cwd=app_path)
subprocess.call(pip_install, shell=True, cwd=app_path) subprocess.call(pip_install, shell=True, cwd=app_path)
psutil.Process().kill() psutil.Process().kill()
def async_task(task_name, *args):
if not ENABLE_QUEUE:
task_name.delay(*args)
return
t0 = time.time()
inspect = app.control.inspect()
worker_stats = inspect.stats()
route_queues = []
padding = math.ceil(sum([i["pool"]["max-concurrency"] for i in worker_stats.values()]) / len(worker_stats))
for worker_name, stats in worker_stats.items():
route = worker_name.split("@")[1]
concurrency = stats["pool"]["max-concurrency"]
route_queues.extend([route] * (concurrency + padding))
destination = random.choice(route_queues)
logging.info("Selecting worker %s from %s in %.2fs", destination, route_queues, time.time() - t0)
task_name.apply_async(args=args, queue=destination)
def run_celery():
worker_name = os.getenv("WORKER_NAME", "")
argv = ["-A", "tasks", "worker", "--loglevel=info", "--pool=threads", f"--concurrency={WORKERS}", "-n", worker_name]
if ENABLE_QUEUE:
argv.extend(["-Q", worker_name])
app.worker_main(argv)
def purge_tasks(): def purge_tasks():
count = app.control.purge() count = app.control.purge()
return f"purged {count} tasks." return f"purged {count} tasks."
def run_celery():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
worker_name = os.getenv("WORKER_NAME", "")
argv = ["-A", "tasks", "worker", "--loglevel=info", "--pool=threads", f"--concurrency={WORKERS}", "-n", worker_name]
app.worker_main(argv)
if __name__ == "__main__": if __name__ == "__main__":
# celery_client.start() bot.start()
print("Bootstrapping Celery worker now.....") print("Bootstrapping Celery worker now.....")
time.sleep(5) time.sleep(3)
threading.Thread(target=run_celery, daemon=True).start() threading.Thread(target=run_celery, daemon=True).start()
scheduler = BackgroundScheduler(timezone="Asia/Shanghai") scheduler = BackgroundScheduler(timezone="Europe/London")
scheduler.add_job(auto_restart, "interval", seconds=900) scheduler.add_job(auto_restart, "interval", seconds=900)
scheduler.start() scheduler.start()
idle() idle()
celery_client.stop() bot.stop()

View file

@ -166,7 +166,6 @@ class Detector:
"types.UpdatesTooLong", "types.UpdatesTooLong",
"Got shutdown from remote", "Got shutdown from remote",
"Code is updated", "Code is updated",
'Retrying "messages.GetMessages"',
"OSError: Connection lost", "OSError: Connection lost",
"[Errno -3] Try again", "[Errno -3] Try again",
"MISCONF", "MISCONF",
@ -179,7 +178,7 @@ class Detector:
def next_salt_detector(self): def next_salt_detector(self):
text = "Next salt in" text = "Next salt in"
if self.logs.count(text) >= 4: if self.logs.count(text) >= 5:
logging.critical("Next salt crash: %s", self.func_name()) logging.critical("Next salt crash: %s", self.func_name())
return True return True
@ -187,17 +186,17 @@ class Detector:
text = "The msg_id is too low" text = "The msg_id is too low"
if text in self.logs: if text in self.logs:
logging.critical("msg id crash: %s ", self.func_name()) logging.critical("msg id crash: %s ", self.func_name())
for item in pathlib.Path(__file__).parent.glob("ytdl-*"): for item in pathlib.Path(__file__).parent.glob("*.session"):
logging.warning("Removing session file: %s", item)
item.unlink(missing_ok=True) item.unlink(missing_ok=True)
time.sleep(3) time.sleep(3)
return True return True
# def idle_detector(self): def sqlite_locked_detector(self):
# mtime = os.stat("/var/log/ytdl.log").st_mtime text = "sqlite3.OperationalError: database is locked"
# cur_ts = time.time() if text in self.logs:
# if cur_ts - mtime > 7200: logging.critical("database is locked: %s ", self.func_name())
# logging.warning("Potential crash detected by %s, it's time to commit suicide...", self.func_name()) return True
# return True
def auto_restart(): def auto_restart():

View file

@ -15,18 +15,17 @@ import re
import tempfile import tempfile
import time import time
import traceback import traceback
import typing
from io import BytesIO from io import BytesIO
from typing import Any
import pyrogram.errors import pyrogram.errors
import qrcode import qrcode
import yt_dlp import yt_dlp
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from pyrogram import Client, filters, types from pyrogram import Client, enums, filters, types
from pyrogram.errors.exceptions.bad_request_400 import UserNotParticipant from pyrogram.errors.exceptions.bad_request_400 import UserNotParticipant
from pyrogram.raw import functions from pyrogram.raw import functions
from pyrogram.raw import types as raw_types from pyrogram.raw import types as raw_types
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from tgbot_ping import get_runtime from tgbot_ping import get_runtime
from youtubesearchpython import VideosSearch from youtubesearchpython import VideosSearch
@ -59,12 +58,11 @@ from tasks import (
) )
from utils import auto_restart, clean_tempfile, customize_logger, get_revision from utils import auto_restart, clean_tempfile, customize_logger, get_revision
logging.info("Authorized users are %s", AUTHORIZED_USER)
customize_logger(["pyrogram.client", "pyrogram.session.session", "pyrogram.connection.connection"]) customize_logger(["pyrogram.client", "pyrogram.session.session", "pyrogram.connection.connection"])
logging.getLogger("apscheduler.executors.default").propagate = False logging.getLogger("apscheduler.executors.default").propagate = False
app = create_app(":memory:") app = create_app("main")
logging.info("Authorized users are %s", AUTHORIZED_USER)
channel = Channel() channel = Channel()
@ -73,7 +71,7 @@ def private_use(func):
chat_id = getattr(message.from_user, "id", None) chat_id = getattr(message.from_user, "id", None)
# message type check # message type check
if message.chat.type != "private" and not message.text.lower().startswith("/ytdl"): if message.chat.type != enums.ChatType.PRIVATE and not message.text.lower().startswith("/ytdl"):
logging.debug("%s, it's annoying me...🙄️ ", message.text) logging.debug("%s, it's annoying me...🙄️ ", message.text)
return return
@ -89,14 +87,11 @@ def private_use(func):
if REQUIRED_MEMBERSHIP: if REQUIRED_MEMBERSHIP:
try: try:
member: typing.Union[types.ChatMember, typing.Coroutine] = app.get_chat_member( member: types.ChatMember | Any = app.get_chat_member(REQUIRED_MEMBERSHIP, chat_id)
REQUIRED_MEMBERSHIP, chat_id
)
if member.status not in [ if member.status not in [
"creator", enums.ChatMemberStatus.ADMINISTRATOR,
"administrator", enums.ChatMemberStatus.MEMBER,
"member", enums.ChatMemberStatus.OWNER,
"owner",
]: ]:
raise UserNotParticipant() raise UserNotParticipant()
else: else:
@ -116,11 +111,11 @@ def start_handler(client: Client, message: types.Message):
payment = Payment() payment = Payment()
from_id = message.from_user.id from_id = message.from_user.id
logging.info("%s welcome to youtube-dl bot!", message.from_user.id) logging.info("%s welcome to youtube-dl bot!", message.from_user.id)
client.send_chat_action(from_id, "typing") client.send_chat_action(from_id, enums.ChatAction.TYPING)
is_old_user = payment.check_old_user(from_id) is_old_user = payment.check_old_user(from_id)
if is_old_user: if is_old_user:
info = "" info = ""
elif ENABLE_VIP: if ENABLE_VIP:
free_token, pay_token, reset = payment.get_token(from_id) free_token, pay_token, reset = payment.get_token(from_id)
info = f"Free token: {free_token}, Pay token: {pay_token}, Reset: {reset}" info = f"Free token: {free_token}, Pay token: {pay_token}, Reset: {reset}"
else: else:
@ -132,21 +127,21 @@ def start_handler(client: Client, message: types.Message):
@app.on_message(filters.command(["help"])) @app.on_message(filters.command(["help"]))
def help_handler(client: Client, message: types.Message): def help_handler(client: Client, message: types.Message):
chat_id = message.chat.id chat_id = message.chat.id
client.send_chat_action(chat_id, "typing") client.send_chat_action(chat_id, enums.ChatAction.TYPING)
client.send_message(chat_id, BotText.help, disable_web_page_preview=True) client.send_message(chat_id, BotText.help, disable_web_page_preview=True)
@app.on_message(filters.command(["about"])) @app.on_message(filters.command(["about"]))
def about_handler(client: Client, message: types.Message): def about_handler(client: Client, message: types.Message):
chat_id = message.chat.id chat_id = message.chat.id
client.send_chat_action(chat_id, "typing") client.send_chat_action(chat_id, enums.ChatAction.TYPING)
client.send_message(chat_id, BotText.about) client.send_message(chat_id, BotText.about)
@app.on_message(filters.command(["sub"])) @app.on_message(filters.command(["sub"]))
def subscribe_handler(client: Client, message: types.Message): def subscribe_handler(client: Client, message: types.Message):
chat_id = message.chat.id chat_id = message.chat.id
client.send_chat_action(chat_id, "typing") client.send_chat_action(chat_id, enums.ChatAction.TYPING)
if message.text == "/sub": if message.text == "/sub":
result = channel.get_user_subscription(chat_id) result = channel.get_user_subscription(chat_id)
else: else:
@ -161,7 +156,7 @@ def subscribe_handler(client: Client, message: types.Message):
@app.on_message(filters.command(["unsub"])) @app.on_message(filters.command(["unsub"]))
def unsubscribe_handler(client: Client, message: types.Message): def unsubscribe_handler(client: Client, message: types.Message):
chat_id = message.chat.id chat_id = message.chat.id
client.send_chat_action(chat_id, "typing") client.send_chat_action(chat_id, enums.ChatAction.TYPING)
text = message.text.split(" ") text = message.text.split(" ")
if len(text) == 1: if len(text) == 1:
client.send_message(chat_id, "/unsub channel_id", disable_web_page_preview=True) client.send_message(chat_id, "/unsub channel_id", disable_web_page_preview=True)
@ -181,7 +176,7 @@ def patch_handler(client: Client, message: types.Message):
chat_id = message.chat.id chat_id = message.chat.id
if username == OWNER: if username == OWNER:
celery_app.control.broadcast("hot_patch") celery_app.control.broadcast("hot_patch")
client.send_chat_action(chat_id, "typing") client.send_chat_action(chat_id, enums.ChatAction.TYPING)
client.send_message(chat_id, "Oorah!") client.send_message(chat_id, "Oorah!")
hot_patch() hot_patch()
@ -206,7 +201,7 @@ def purge_handler(client: Client, message: types.Message):
def ping_handler(client: Client, message: types.Message): def ping_handler(client: Client, message: types.Message):
redis = Redis() redis = Redis()
chat_id = message.chat.id chat_id = message.chat.id
client.send_chat_action(chat_id, "typing") client.send_chat_action(chat_id, enums.ChatAction.TYPING)
if os.uname().sysname == "Darwin" or ".heroku" in os.getenv("PYTHONHOME", ""): if os.uname().sysname == "Darwin" or ".heroku" in os.getenv("PYTHONHOME", ""):
bot_info = "ping unavailable." bot_info = "ping unavailable."
else: else:
@ -233,7 +228,7 @@ def sub_count_handler(client: Client, message: types.Message):
def direct_handler(client: Client, message: types.Message): def direct_handler(client: Client, message: types.Message):
redis = Redis() redis = Redis()
chat_id = message.from_user.id chat_id = message.from_user.id
client.send_chat_action(chat_id, "typing") client.send_chat_action(chat_id, enums.ChatAction.TYPING)
url = re.sub(r"/direct\s*", "", message.text) url = re.sub(r"/direct\s*", "", message.text)
logging.info("direct start %s", url) logging.info("direct start %s", url)
if not re.findall(r"^https?://", url.lower()): if not re.findall(r"^https?://", url.lower()):
@ -250,27 +245,27 @@ def direct_handler(client: Client, message: types.Message):
def settings_handler(client: Client, message: types.Message): def settings_handler(client: Client, message: types.Message):
chat_id = message.chat.id chat_id = message.chat.id
payment = Payment() payment = Payment()
client.send_chat_action(chat_id, "typing") client.send_chat_action(chat_id, enums.ChatAction.TYPING)
data = MySQL().get_user_settings(chat_id) data = MySQL().get_user_settings(chat_id)
set_mode = data[-1] set_mode = data[-1]
text = {"Local": "Celery", "Celery": "Local"}.get(set_mode, "Local") text = {"Local": "Celery", "Celery": "Local"}.get(set_mode, "Local")
mode_text = f"Download mode: **{set_mode}**" mode_text = f"Download mode: **{set_mode}**"
if message.chat.username == OWNER or payment.get_pay_token(chat_id): if message.chat.username == OWNER or payment.get_pay_token(chat_id):
extra = [InlineKeyboardButton(f"Change download mode to {text}", callback_data=text)] extra = [types.InlineKeyboardButton(f"Change download mode to {text}", callback_data=text)]
else: else:
extra = [] extra = []
markup = InlineKeyboardMarkup( markup = types.InlineKeyboardMarkup(
[ [
[ # First row [ # First row
InlineKeyboardButton("send as document", callback_data="document"), types.InlineKeyboardButton("send as document", callback_data="document"),
InlineKeyboardButton("send as video", callback_data="video"), types.InlineKeyboardButton("send as video", callback_data="video"),
InlineKeyboardButton("send as audio", callback_data="audio"), types.InlineKeyboardButton("send as audio", callback_data="audio"),
], ],
[ # second row [ # second row
InlineKeyboardButton("High Quality", callback_data="high"), types.InlineKeyboardButton("High Quality", callback_data="high"),
InlineKeyboardButton("Medium Quality", callback_data="medium"), types.InlineKeyboardButton("Medium Quality", callback_data="medium"),
InlineKeyboardButton("Low Quality", callback_data="low"), types.InlineKeyboardButton("Low Quality", callback_data="low"),
], ],
extra, extra,
] ]
@ -288,7 +283,7 @@ def settings_handler(client: Client, message: types.Message):
def buy_handler(client: Client, message: types.Message): def buy_handler(client: Client, message: types.Message):
# process as chat.id, not from_user.id # process as chat.id, not from_user.id
chat_id = message.chat.id chat_id = message.chat.id
client.send_chat_action(chat_id, "typing") client.send_chat_action(chat_id, enums.ChatAction.TYPING)
# currency USD # currency USD
token_count = message.text.replace("/buy", "").strip() token_count = message.text.replace("/buy", "").strip()
if token_count.isdigit(): if token_count.isdigit():
@ -296,11 +291,11 @@ def buy_handler(client: Client, message: types.Message):
else: else:
price = 100 price = 100
markup = InlineKeyboardMarkup( markup = types.InlineKeyboardMarkup(
[ [
[ [
InlineKeyboardButton("Bot Payments", callback_data=f"bot-payments-{price}"), types.InlineKeyboardButton("Bot Payments", callback_data=f"bot-payments-{price}"),
InlineKeyboardButton("TRON(TRX)", callback_data="tron-trx"), types.InlineKeyboardButton("TRON(TRX)", callback_data="tron-trx"),
], ],
] ]
) )
@ -311,7 +306,7 @@ def buy_handler(client: Client, message: types.Message):
def tronpayment_btn_calback(client: Client, callback_query: types.CallbackQuery): def tronpayment_btn_calback(client: Client, callback_query: types.CallbackQuery):
callback_query.answer("Generating QR code...") callback_query.answer("Generating QR code...")
chat_id = callback_query.message.chat.id chat_id = callback_query.message.chat.id
client.send_chat_action(chat_id, "typing") client.send_chat_action(chat_id, enums.ChatAction.TYPING)
addr = TronTrx().get_payment_address(chat_id) addr = TronTrx().get_payment_address(chat_id)
with BytesIO() as bio: with BytesIO() as bio:
@ -324,13 +319,13 @@ def tronpayment_btn_calback(client: Client, callback_query: types.CallbackQuery)
def bot_payment_btn_calback(client: Client, callback_query: types.CallbackQuery): def bot_payment_btn_calback(client: Client, callback_query: types.CallbackQuery):
callback_query.answer("Generating invoice...") callback_query.answer("Generating invoice...")
chat_id = callback_query.message.chat.id chat_id = callback_query.message.chat.id
client.send_chat_action(chat_id, "typing") client.send_chat_action(chat_id, enums.ChatAction.TYPING)
data = callback_query.data data = callback_query.data
price = int(data.split("-")[-1]) price = int(data.split("-")[-1])
payload = f"{chat_id}-buy" payload = f"{chat_id}-buy"
invoice = generate_invoice(price, f"Buy {TOKEN_PRICE} download tokens", "Pay by card", payload) invoice = generate_invoice(price, f"Buy {TOKEN_PRICE} download tokens", "Pay by card", payload)
app.send( app.invoke(
functions.messages.SendMedia( functions.messages.SendMedia(
peer=(raw_types.InputPeerUser(user_id=chat_id, access_hash=0)), peer=(raw_types.InputPeerUser(user_id=chat_id, access_hash=0)),
media=invoice, media=invoice,
@ -384,10 +379,9 @@ def link_checker(url: str) -> str:
def search_ytb(kw: str): def search_ytb(kw: str):
videosSearch = VideosSearch(kw, limit=10) videos_search = VideosSearch(kw, limit=10)
text = "" text = ""
results = videosSearch.result()["result"] results = videos_search.result()["result"]
for item in results: for item in results:
title = item.get("title") title = item.get("title")
link = item.get("link") link = item.get("link")
@ -402,7 +396,7 @@ def download_handler(client: Client, message: types.Message):
redis = Redis() redis = Redis()
payment = Payment() payment = Payment()
chat_id = message.from_user.id chat_id = message.from_user.id
client.send_chat_action(chat_id, "typing") client.send_chat_action(chat_id, enums.ChatAction.TYPING)
redis.user_count(chat_id) redis.user_count(chat_id)
if message.document: if message.document:
with tempfile.NamedTemporaryFile(mode="r+") as tf: with tempfile.NamedTemporaryFile(mode="r+") as tf:
@ -431,9 +425,7 @@ def download_handler(client: Client, message: types.Message):
if ENABLE_VIP and not payment.check_old_user(chat_id): if ENABLE_VIP and not payment.check_old_user(chat_id):
free, pay, reset = payment.get_token(chat_id) free, pay, reset = payment.get_token(chat_id)
if free + pay <= 0: if free + pay <= 0:
message.reply_text( message.reply_text(f"You don't have enough token. Please wait until {reset} or /buy .", quote=True)
f"You don't have enough token. Please wait until {reset} or /buy more token.", quote=True
)
redis.update_metrics("reject_token") redis.update_metrics("reject_token")
return return
else: else:
@ -444,22 +436,22 @@ def download_handler(client: Client, message: types.Message):
text = BotText.get_receive_link_text() text = BotText.get_receive_link_text()
try: try:
# raise pyrogram.errors.exceptions.FloodWait(10) # raise pyrogram.errors.exceptions.FloodWait(10)
bot_msg: typing.Union[types.Message, typing.Coroutine] = message.reply_text(text, quote=True) bot_msg: types.Message | Any = message.reply_text(text, quote=True)
except pyrogram.errors.Flood as e: except pyrogram.errors.Flood as e:
f = BytesIO() f = BytesIO()
f.write(str(e).encode()) f.write(str(e).encode())
f.write(b"Your job will be done soon. Just wait! Don't rush.") f.write(b"Your job will be done soon. Just wait! Don't rush.")
f.name = "Please don't flood me.txt" f.name = "Please don't flood me.txt"
bot_msg = message.reply_document( bot_msg = message.reply_document(
f, caption=f"Flood wait! Please wait {e.x} seconds...." f"Your job will start automatically", quote=True f, caption=f"Flood wait! Please wait {e} seconds...." f"Your job will start automatically", quote=True
) )
f.close() f.close()
client.send_message(OWNER, f"Flood wait! 🙁 {e.x} seconds....") client.send_message(OWNER, f"Flood wait! 🙁 {e} seconds....")
time.sleep(e.x) time.sleep(e.value)
client.send_chat_action(chat_id, "upload_video") client.send_chat_action(chat_id, enums.ChatAction.UPLOAD_VIDEO)
bot_msg.chat = message.chat bot_msg.chat = message.chat
ytdl_download_entrance(client, bot_msg, url) ytdl_download_entrance(bot_msg, url)
@app.on_callback_query(filters.regex(r"document|video|audio")) @app.on_callback_query(filters.regex(r"document|video|audio"))
@ -484,14 +476,13 @@ def download_resolution_callback(client: Client, callback_query: types.CallbackQ
def audio_callback(client: Client, callback_query: types.CallbackQuery): def audio_callback(client: Client, callback_query: types.CallbackQuery):
redis = Redis() redis = Redis()
if not ENABLE_FFMPEG: if not ENABLE_FFMPEG:
callback_query.answer("Audio conversion is disabled now.") callback_query.answer("Request rejected.")
callback_query.message.reply_text("Audio conversion is disabled now.") callback_query.message.reply_text("Audio conversion is disabled now.")
return return
callback_query.answer(f"Converting to audio...please wait patiently") callback_query.answer(f"Converting to audio...please wait patiently")
redis.update_metrics("audio_request") redis.update_metrics("audio_request")
vmsg = callback_query.message audio_entrance(callback_query.message)
audio_entrance(client, vmsg)
@app.on_callback_query(filters.regex(r"Local|Celery")) @app.on_callback_query(filters.regex(r"Local|Celery"))
@ -509,14 +500,12 @@ def periodic_sub_check():
logging.info(f"periodic update:{video_url} - {uids}") logging.info(f"periodic update:{video_url} - {uids}")
for uid in uids: for uid in uids:
try: try:
bot_msg: typing.Union[types.Message, typing.Coroutine] = app.send_message( bot_msg: types.Message | Any = app.send_message(uid, f"{video_url} is out. Watch it on YouTube")
uid, f"{video_url} is out. Watch it on YouTube"
)
# ytdl_download_entrance(app, bot_msg, video_url, mode="direct") # ytdl_download_entrance(app, bot_msg, video_url, mode="direct")
except (exceptions.bad_request_400.PeerIdInvalid, exceptions.bad_request_400.UserIsBlocked) as e: except (exceptions.bad_request_400.PeerIdInvalid, exceptions.bad_request_400.UserIsBlocked) as e:
logging.warning("User is blocked or deleted. %s", e) logging.warning("User is blocked or deleted. %s", e)
channel.deactivate_user_subscription(uid) channel.deactivate_user_subscription(uid)
except Exception as e: except Exception:
logging.error("Unknown error when sending message to user. %s", traceback.format_exc()) logging.error("Unknown error when sending message to user. %s", traceback.format_exc())
finally: finally:
time.sleep(random.random() * 3) time.sleep(random.random() * 3)
@ -527,7 +516,7 @@ def raw_update(client: Client, update, users, chats):
payment = Payment() payment = Payment()
action = getattr(getattr(update, "message", None), "action", None) action = getattr(getattr(update, "message", None), "action", None)
if update.QUALNAME == "types.UpdateBotPrecheckoutQuery": if update.QUALNAME == "types.UpdateBotPrecheckoutQuery":
client.send( client.invoke(
functions.messages.SetBotPrecheckoutResults( functions.messages.SetBotPrecheckoutResults(
query_id=update.query_id, query_id=update.query_id,
success=True, success=True,
@ -549,14 +538,13 @@ def trx_notify(_, **kwargs):
if __name__ == "__main__": if __name__ == "__main__":
redis = Redis()
MySQL() MySQL()
TRX_SIGNAL.connect(trx_notify) TRX_SIGNAL.connect(trx_notify)
scheduler = BackgroundScheduler(timezone="Europe/London", job_defaults={"max_instances": 6}) scheduler = BackgroundScheduler(timezone="Europe/London", job_defaults={"max_instances": 6})
scheduler.add_job(auto_restart, "interval", seconds=600) scheduler.add_job(auto_restart, "interval", seconds=600)
scheduler.add_job(clean_tempfile, "interval", seconds=120) scheduler.add_job(clean_tempfile, "interval", seconds=120)
if not IS_BACKUP_BOT: if not IS_BACKUP_BOT:
scheduler.add_job(redis.reset_today, "cron", hour=0, minute=0) scheduler.add_job(Redis().reset_today, "cron", hour=0, minute=0)
scheduler.add_job(InfluxDB().collect_data, "interval", seconds=120) scheduler.add_job(InfluxDB().collect_data, "interval", seconds=120)
scheduler.add_job(TronTrx().check_payment, "interval", seconds=60, max_instances=1) scheduler.add_job(TronTrx().check_payment, "interval", seconds=60, max_instances=1)
# default quota allocation of 10,000 units per day # default quota allocation of 10,000 units per day