diff --git a/README.md b/README.md index e5c71959..f3627212 100644 --- a/README.md +++ b/README.md @@ -1,107 +1,10 @@ -# [ScreenShot Bot](https://tx.me/ScreenShotTGBot) -> Telegram Bot For Screenshot Generation. +### Screenshot-Bot +___________________ -## Description +### [š”»š•–š•žš•  š”¹š• š•„](https://telegram.dog/SCREEN_SHOT_ROBOT) -An attempt to implement the screenshot generation of telegram files without downloading the entire file. Live version can be found here [@ScreenShot Bot](https://tx.me/ScreenShotTGBot). -> Screenshot Generation with Custom Watermark---Sample Video Generation---Trim video. +### [š•˜š•šš•„š•™š•¦š•“šŸ™š•„š•˜](https://github.com/github1tg) -## Installation Guide +## You can also tap the Deploy To Heroku button below to deploy straight to Heroku! -### You can also tap the Deploy To Heroku button below to deploy straight to Heroku! - -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://www.heroku.com/deploy?template=https://github.com/TGExplore/Screenshot-Bot) - -### Watch our YouTube video for more details - [Telegram Screenshot | Trim | Sample Video Generator Bot](https://youtu.be/Fsc-ZUvdO20) - -### Prerequisites -* FFmpeg. -* Python3 (3.6 or higher). - -### Local setup -* Clone to local machine. -``` -$ git clone https://github.com/odysseusmax/animated-lamp.git -$ cd animated-lamp -```` - -* Create and activate virtual environment. -``` -$ pip3 install virtualenv -$ virtualenv venv -$ source venv/bin/activate -``` - -* Install dependencies. -``` -$ pip3 install -U -r requirements.txt -``` - -### Environment Variables - -Properly setup the environment variables or populate `config.py` with the values (some of the values are sensitive data, so keep them safe). - -* `API_ID`(required) - Get your telegram API_ID from [https://my.telegram.org/](https://my.telegram.org/). -* `API_HASH`(required) - Get your telegram API_HASH from [https://my.telegram.org/](https://my.telegram.org/). -* `BOT_TOKEN`(required) - Obtain your bot token from [Bot Father](https://t.me/BotFather "Bot Father"). -* `SESSION_NAME`(required) - Name you want to call your bot's session, Eg: bot username. -* `LOG_CHANNEL`(required) - Log channel's id. -* `DATABASE_URL`(required) - Mongodb database URI. -* `AUTH_USERS`(required) - Authorised user(s) id separated by space. -* `HOST`(required) - Public URL of streaming service ([Source](https://github.com/TGExplore/musical-waddle)). -* `MAX_PROCESSES_PER_USER`(optional) - Number of parallel processes each user can have, defaults to 2. -* `MAX_TRIM_DURATION`(optional) - Maximum allowed seconds for trimming. Defaults to 600. -* `TRACK_CHANNEL`(optional) - User activity tracking channel's id. Only needed if you want to track and block any user. Disabled by default. -* `SLOW_SPEED_DELAY`(optional) - Delay required between each request. Defaults to 15s. - -### Run bot -`$ python3 -m bot` - -Now go to your bot and do a `/start`. - -## Supported commands and functions - -### Commands - -* `/start` - Command to start bot or check whether bot is alive. -* `/settings` - Command to configure bot's behavior' -* `/set_watermark` - Command to add custom watermark text to screenshots. Usage: `/set_watermark watermark_text`. - -* `/status` - Admin/Auth users only command. Returns number of total users. -* `/ban_user` - Admin/Auth users only command. Command to ban any user. Usage: `/ban_user user_id ban_duration ban_reason`. `user_id` - telegram id of the user, `ban_duration` - ban duration in days, `ban_reason` - reason for ban. All 3 parameters are required. -* `/unban_user` - Admin/Auth users only command. Command to ban any banned user. Usage: `/unban_user user_id`. `user_id` - telegram id of the user. The parameter is required. -* `/banned_users` - Admin/Auth users only command. Command to view all banned users. Usage: `/banned_users`. This takes no parameters. -* `/broadcast` - Admin/Auth user only command. Command to broadcast some message to all users. Usage: reply `/broadcast` to the message you want to broadcast. - -### Functions -* `Screenshot Generation` - Generates screenshots from telegram video files or streaming links. Number of screenshots range from 2-10. -* `Manual Screenshot` - Generates screenshots of specific time. Number of screenshots range from 1-10. -* `Sample Video Generation` - Generates sample video from telegram video files or streaming links. Video duration range from 30s to 150s. Configurable in `/settings`. -* `Video Trimming` - Trims any telegram video files or streaming links. Video duration depends on the environment. By default upto 10 mins (600s). - -### Settings -In bot settings. -* `Upload Mode` - Screenshot upload mode. Either `as image file` or `as document file`. Defaults to `as image file`. -* `Watermark` - Watermark text to be embedded to screenshots. Texts upto 30 characters supported. Disabled by default. -* `Watermark Color` - Font color to be used for watermark. Any of `white`, `black`, `red`, `blue`, `green`, `yellow`, `orange`, `purple`, `brown`, `gold`, `silver`, `pink`. Defaults to `white`. -* `Watermark Font Size` - Font size to be used for watermarks. Any of `small(30)`, `medium(40)`, `large(50)`. Defaults to `medium`. -* `Sample Video Duration` - Sample video's duration. Any of `30s`, `60s`, `90s`, `120s`, `150s`. Defaults to `30s`. -* `Screenshot Genetation Mode` - Either `random` or `equally spaced`. Defaults to `equally spaced`. - - -## Contributions -Contributions are welcome. - -## Contact -You can contact me [@InFoTelGroup](https://tx.me/InFoTelGroup). - -## Thanks - -Thanks to [@odysseusmax](https://tx.me/odysseusmax) for his [animated-lamp](https://github.com/odysseusmax/animated-lamp) Bot. - -Thanks to [Dan](https://github.com/delivrance "Dan") for his [Pyrogram](https://github.com/pyrogram/pyrogram "Pyrogram") library. - -Thanks to [Tulir Asokan](https://github.com/tulir "Tulir Asokan") for his [TgFileStream](https://github.com/tulir/tgfilestream "TgFileStream") Bot. - -## License -Code released under [The GNU General Public License](LICENSE). +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://www.heroku.com/deploy?template=https://github.com/github1tg/Screenshot-Bot) diff --git a/app.json b/app.json index 9c4cdded..63476692 100644 --- a/app.json +++ b/app.json @@ -9,7 +9,7 @@ "Trim Video", "without downloading the entire file" ], - "repository": "https://github.com/TGExplore/Screenshot-Bot", + "repository": "https://github.com/github1tg/Screenshot-Bot", "env": { "API_ID": { "description": "Your Telegram API ID. Get this value from my.telegram.org!", diff --git a/bot/README.md b/bot/README.md new file mode 100644 index 00000000..85a0ba9a --- /dev/null +++ b/bot/README.md @@ -0,0 +1,3 @@ +### [š”»š•–š•žš•  š”¹š• š•„](https://telegram.dog/SCREEN_SHOT_ROBOT) + +### [š•˜š•šš•„š•™š•¦š•“šŸ™š•„š•˜](https://github.com/github1tg) diff --git a/bot/__main__.py b/bot/__main__.py index 63c673c8..1dbfd2cf 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -1,4 +1,13 @@ +import logging + from .screenshotbot import ScreenShotBot +from .config import Config if __name__ == "__main__": + + logging.basicConfig(level=logging.DEBUG if Config.DEBUG else logging.INFO) + logging.getLogger("pyrogram").setLevel( + logging.INFO if Config.DEBUG else logging.WARNING + ) + ScreenShotBot().run() diff --git a/bot/config.py b/bot/config.py index e7cb43a3..59fa64bf 100644 --- a/bot/config.py +++ b/bot/config.py @@ -1,24 +1,49 @@ import os -from pathlib import Path + class Config: - - API_ID = int(os.environ.get('API_ID')) - API_HASH = os.environ.get('API_HASH') - BOT_TOKEN = os.environ.get('BOT_TOKEN') - SESSION_NAME = os.environ.get('SESSION_NAME') - LOG_CHANNEL = int(os.environ.get('LOG_CHANNEL')) - DATABASE_URL = os.environ.get('DATABASE_URL') - AUTH_USERS = [int(i) for i in os.environ.get('AUTH_USERS', '').split(' ')] - MAX_PROCESSES_PER_USER = int(os.environ.get('MAX_PROCESSES_PER_USER', 2)) - MAX_TRIM_DURATION = int(os.environ.get('MAX_TRIM_DURATION', 600)) - TRACK_CHANNEL = int(os.environ.get('TRACK_CHANNEL', False)) - SLOW_SPEED_DELAY = int(os.environ.get('SLOW_SPEED_DELAY', 15)) - HOST = os.environ.get('HOST', '') - - SCRST_OP_FLDR = Path('screenshots/') - SMPL_OP_FLDR = Path('samples/') - THUMB_OP_FLDR = Path('thumbnails/') - COLORS = ['white', 'black', 'red', 'blue', 'green', 'yellow', 'orange', 'purple', 'brown', 'gold', 'silver', 'pink'] - FONT_SIZES_NAME = ['Small', 'Medium', 'Large'] + + API_ID = int(os.environ.get("API_ID")) + API_HASH = os.environ.get("API_HASH") + BOT_TOKEN = os.environ.get("BOT_TOKEN") + SESSION_NAME = os.environ.get("SESSION_NAME", ":memory:") + LOG_CHANNEL = int(os.environ.get("LOG_CHANNEL")) + DATABASE_URL = os.environ.get("DATABASE_URL") + AUTH_USERS = [int(i) for i in os.environ.get("AUTH_USERS", "").split(" ")] + MAX_PROCESSES_PER_USER = int(os.environ.get("MAX_PROCESSES_PER_USER", 2)) + MAX_TRIM_DURATION = int(os.environ.get("MAX_TRIM_DURATION", 600)) + TRACK_CHANNEL = int(os.environ.get("TRACK_CHANNEL", False)) + SLOW_SPEED_DELAY = int(os.environ.get("SLOW_SPEED_DELAY", 5)) + HOST = os.environ.get("HOST", "") + TIMEOUT = int(os.environ.get("TIMEOUT", 60 * 30)) + DEBUG = bool(os.environ.get("DEBUG")) + WORKER_COUNT = int(os.environ.get("WORKER_COUNT", 20)) + IAM_HEADER = os.environ.get("IAM_HEADER", "") + + COLORS = [ + "white", + "black", + "red", + "blue", + "green", + "yellow", + "orange", + "purple", + "brown", + "gold", + "silver", + "pink", + ] + FONT_SIZES_NAME = ["Small", "Medium", "Large"] FONT_SIZES = [30, 40, 50] + POSITIONS = [ + "Top Left", + "Top Center", + "Top Right", + "Center Left", + "Centered", + "Center Right", + "Bottom Left", + "Bottom Center", + "Bottom Right", + ] diff --git a/bot/database/README.md b/bot/database/README.md new file mode 100644 index 00000000..85a0ba9a --- /dev/null +++ b/bot/database/README.md @@ -0,0 +1,3 @@ +### [š”»š•–š•žš•  š”¹š• š•„](https://telegram.dog/SCREEN_SHOT_ROBOT) + +### [š•˜š•šš•„š•™š•¦š•“šŸ™š•„š•˜](https://github.com/github1tg) diff --git a/bot/database/database.py b/bot/database/database.py index 736231c3..a6f06ae2 100644 --- a/bot/database/database.py +++ b/bot/database/database.py @@ -2,21 +2,34 @@ import motor.motor_asyncio +from bot.config import Config -class Database: - - def __init__(self, uri, database_name): - self._client = motor.motor_asyncio.AsyncIOMotorClient(uri) - self.db = self._client[database_name] + +class Singleton(type): + __instances__ = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls.__instances__: + cls.__instances__[cls] = super(Singleton, cls).__call__(*args, **kwargs) + + return cls.__instances__[cls] + + +class Database(metaclass=Singleton): + def __init__(self): + self._client = motor.motor_asyncio.AsyncIOMotorClient(Config.DATABASE_URL) + self.db = self._client[Config.SESSION_NAME] self.col = self.db.users - - + + self.cache = {} + def new_user(self, id): return dict( - id = id, - join_date = datetime.date.today().isoformat(), + id=id, + join_date=datetime.date.today().isoformat(), + last_used_on=datetime.date.today().isoformat(), as_file=False, - watermark_text='', + watermark_text="", sample_duration=30, as_round=False, watermark_color=0, @@ -26,129 +39,157 @@ def new_user(self, id): is_banned=False, ban_duration=0, banned_on=datetime.date.max.isoformat(), - ban_reason='' - ) + ban_reason="", + ), ) - - + + async def get_user(self, id): + user = self.cache.get(id) + if user is not None: + return user + + user = await self.col.find_one({"id": int(id)}) + self.cache[id] = user + return user + async def add_user(self, id): user = self.new_user(id) await self.col.insert_one(user) - - + async def is_user_exist(self, id): - user = await self.col.find_one({'id':int(id)}) + user = await self.get_user(id) return True if user else False - - + async def total_users_count(self): count = await self.col.count_documents({}) return count - - + async def is_as_file(self, id): - user = await self.col.find_one({'id':int(id)}) - return user.get('as_file', False) - - + user = await self.get_user(id) + return user.get("as_file", False) + async def is_as_round(self, id): - user = await self.col.find_one({'id':int(id)}) - return user.get('as_round', False) - - + user = await self.get_user(id) + return user.get("as_round", False) + async def update_as_file(self, id, as_file): - await self.col.update_one({'id': id}, {'$set': {'as_file': as_file}}) - - + self.cache[id]["as_file"] = as_file + await self.col.update_one({"id": id}, {"$set": {"as_file": as_file}}) + async def update_as_round(self, id, as_round): - await self.col.update_one({'id': id}, {'$set': {'as_round': as_round}}) - - - async def update_watermark_text(self, id, watermark_text=''): - await self.col.update_one({'id': id}, {'$set': {'watermark_text': watermark_text}}) - - + self.cache[id]["as_round"] = as_round + await self.col.update_one({"id": id}, {"$set": {"as_round": as_round}}) + + async def update_watermark_text(self, id, watermark_text=""): + self.cache[id]["watermark_text"] = watermark_text + await self.col.update_one( + {"id": id}, {"$set": {"watermark_text": watermark_text}} + ) + async def update_sample_duration(self, id, sample_duration): - await self.col.update_one({'id': id}, {'$set': {'sample_duration': sample_duration}}) - - + self.cache[id]["sample_duration"] = sample_duration + await self.col.update_one( + {"id": id}, {"$set": {"sample_duration": sample_duration}} + ) + async def update_watermark_color(self, id, watermark_color): - await self.col.update_one({'id': id}, {'$set': {'watermark_color': watermark_color}}) - - + self.cache[id]["watermark_color"] = watermark_color + await self.col.update_one( + {"id": id}, {"$set": {"watermark_color": watermark_color}} + ) + async def update_screenshot_mode(self, id, screenshot_mode): - await self.col.update_one({'id': id}, {'$set': {'screenshot_mode': screenshot_mode}}) - - + self.cache[id]["screenshot_mode"] = screenshot_mode + await self.col.update_one( + {"id": id}, {"$set": {"screenshot_mode": screenshot_mode}} + ) + async def update_font_size(self, id, font_size): - await self.col.update_one({'id': id}, {'$set': {'font_size': font_size}}) - - + self.cache[id]["font_size"] = font_size + await self.col.update_one({"id": id}, {"$set": {"font_size": font_size}}) + + async def update_watermark_position(self, id, watermark_position): + self.cache[id]["watermark_position"] = watermark_position + await self.col.update_one( + {"id": id}, {"$set": {"watermark_position": watermark_position}} + ) + + async def update_last_used_on(self, id): + self.cache[id]["last_used_on"] = datetime.date.today().isoformat() + await self.col.update_one( + {"id": id}, {"$set": {"last_used_on": datetime.date.today().isoformat()}} + ) + async def remove_ban(self, id): + await self.get_user(id) ban_status = dict( is_banned=False, ban_duration=0, banned_on=datetime.date.max.isoformat(), - ban_reason='' + ban_reason="", ) - await self.col.update_one({'id': id}, {'$set': {'ban_status': ban_status}}) - - + self.cache[id]["ban_status"] = ban_status + await self.col.update_one({"id": id}, {"$set": {"ban_status": ban_status}}) + async def ban_user(self, user_id, ban_duration, ban_reason): + await self.get_user(user_id) ban_status = dict( is_banned=True, ban_duration=ban_duration, banned_on=datetime.date.today().isoformat(), - ban_reason=ban_reason + ban_reason=ban_reason, ) - await self.col.update_one({'id': user_id}, {'$set': {'ban_status': ban_status}}) - - + self.cache[user_id]["ban_status"] = ban_status + await self.col.update_one({"id": user_id}, {"$set": {"ban_status": ban_status}}) + async def get_watermark_text(self, id): - user = await self.col.find_one({'id':int(id)}) - return user.get('watermark_text', '') - - + user = await self.get_user(id) + return user.get("watermark_text", "") + async def get_sample_duration(self, id): - user = await self.col.find_one({'id':int(id)}) - return user.get('sample_duration', 30) - - + user = await self.get_user(id) + return user.get("sample_duration", 30) + async def get_watermark_color(self, id): - user = await self.col.find_one({'id':int(id)}) - return user.get('watermark_color', 0) - - + user = await self.get_user(id) + return user.get("watermark_color", 0) + + async def get_watermark_position(self, id): + user = await self.get_user(id) + return user.get("watermark_position", 6) + async def get_screenshot_mode(self, id): - user = await self.col.find_one({'id':int(id)}) - return user.get('screenshot_mode', 0) - - + user = await self.get_user(id) + return user.get("screenshot_mode", 0) + async def get_font_size(self, id): - user = await self.col.find_one({'id':int(id)}) - return user.get('font_size', 1) - - + user = await self.get_user(id) + return user.get("font_size", 1) + async def get_ban_status(self, id): default = dict( is_banned=False, ban_duration=0, banned_on=datetime.date.max.isoformat(), - ban_reason='' + ban_reason="", ) - user = await self.col.find_one({'id':int(id)}) - return user.get('ban_status', default) - - + user = await self.get_user(id) + return user.get("ban_status", default) + async def get_all_banned_users(self): - banned_users = self.col.find({'ban_status.is_banned': True}) + banned_users = self.col.find({"ban_status.is_banned": True}) return banned_users - async def get_all_users(self): all_users = self.col.find({}) return all_users - - + async def delete_user(self, user_id): - await self.col.delete_many({'id': int(user_id)}) + user_id = int(user_id) + if self.cache.get(user_id): + self.cache.pop(user_id) + await self.col.delete_many({"id": user_id}) + + async def get_last_used_on(self, id): + user = await self.get_user(id) + return user.get("last_used_on", datetime.date.today().isoformat()) diff --git a/bot/messages.py b/bot/messages.py new file mode 100644 index 00000000..8742ed27 --- /dev/null +++ b/bot/messages.py @@ -0,0 +1,85 @@ +class Messages: + ADDED_TO_QUEUE = ( + "ššˆšš˜ššžšš› šš›ššŽšššššžššŽššœšš šš‘ššŠššœ šš‹ššŽššŽšš— ššŠššššššŽšš šššš˜ šššš‘ššŽ šššššžššŽššžššŽ. š™øšš šš¢šš˜ššž šš‘ššŠššŸššŽ šš–šš˜šš›ššŽ šššš‘ššŠšš— {per_user_process_count} " + "šš˜šš—šššš˜šš’šš—šš šš™šš›šš˜ššŒššŽššœššœššŽššœ, šššš‘ššŽšš— šššš‘šš’ššœ šš™šš›šš˜ššŒššŽššœššœ šš šš’šš•šš• šš˜šš—šš•šš¢ ššœššššŠšš›šš ššŠššššššŽšš› šš˜šš—ššŽ šš˜šš šššš‘ššŽšš– šššš’šš—šš’ššœšš‘ššŽššœ." + ) + MEDIA_MESSAGE_DELETED = "šš†šš‘šš¢ šššš’šš šš¢šš˜ššž ššššŽšš•ššŽššššŽ šššš‘ššŽ šššš’šš•ššŽ 😔, š™½šš˜šš  šš’ ššŒššŠšš—šš—šš˜šš šš‘ššŽšš•šš™ šš¢šš˜ššž šŸ¤”." + CANNOT_OPEN_FILE = "😢 š–²š—ˆš—‹š—‹š—’! š–Ø š–¼š–ŗš—‡š—‡š—ˆš— š—ˆš—‰š–¾š—‡ š—š—š–¾ š–æš—‚š—…š–¾." + PROCESS_TIMEOUT = ( + "😢 šš‚šš˜šš›šš›šš¢! šš™šš›šš˜ššŒššŽššœššœ ššššŠšš’šš•ššŽšš ššššžššŽ šššš˜ šššš’šš–ššŽšš˜ššžšš. ššˆšš˜ššžšš› šš™šš›šš˜ššŒššŽššœššœ šš ššŠššœ " + "ššššŠšš”šš’šš—šš šššš˜šš˜ šš•šš˜šš—šš šššš˜ ššŒšš˜šš–šš™šš•ššŽššššŽ, šš‘ššŽšš—ššŒššŽ ššŒššŠšš—ššŒššŽšš•šš•ššŽšš." + ) + TRACK_USER_ACTIVITY = "User id: `{chat_id}`" + PROCESSING_REQUEST = "š™æšš›šš˜ššŒššŽššœššœšš’šš—šš šš¢šš˜ššžšš› šš›ššŽšššššžššŽššœšš, š™æšš•ššŽššŠššœššŽ šš ššŠšš’šš! 🄱" + SCREENSHOT_AT = "ScreenShot at {time}" + SCREENSHOT_PROCESS_FAILED = "😢 šš‚šš˜šš›šš›šš¢! šš‚ššŒšš›ššŽššŽšš—ššœšš‘šš˜šš ššššŽšš—ššŽšš›ššŠšššš’šš˜šš— ššššŠšš’šš•ššŽšš šš™šš˜ššœššœšš’šš‹šš•šš¢ ššššžššŽ šššš˜ ššœšš˜šš–ššŽ šš’šš—šššš›ššŠššœšššš›ššžššŒššššžšš›ššŽ ššššŠšš’šš•ššžšš›ššŽ 😢." + SCREENSHOT_PROCESS_SUCCESS = ( + "🤷 š•š• š•¦ š•£š•–š•¢š•¦š•–š•¤š•„š•–š•• {count} š•¤š•”š•£š•–š•–š•Ÿš•¤š•™š• š•„š•¤ š•’š•Ÿš•• " + "{total_count} š•¤š•”š•£š•–š•–š•Ÿš•¤š•™š• š•„š•¤ š•˜š•–š•Ÿš•–š•£š•’š•„š•–š••, " + "ā„•š• š•Ø š•¤š•„š•’š•£š•„š•šš•Ÿš•˜ š•„š•  š•¦š•”š•š• š•’š••!" + ) + PROCESS_UPLOAD_CONFIRM = ( + "š™Žš™Ŗš™˜š™˜š™šš™Øš™Øš™›š™Ŗš™”š™”š™® š™˜š™¤š™¢š™„š™”š™šš™©š™šš™™ š™„š™§š™¤š™˜š™šš™Øš™Ø š™žš™£ {total_process_duration}\n\n" + "š™ƒš™¤š™¬ š™žš™Ø š™©š™š™žš™Ø š™—š™¤š™©..šŸ¤”šŸ„“ [š˜¾š™”š™žš™˜š™  š™š™šš™§š™š.](tg://resolve?domain=MD_BOTZ&post=21)." + ) + WRONG_FORMAT = "š™æšš•ššŽššŠššœššŽ šššš˜šš•šš•šš˜šš  šššš‘ššŽ ššœšš™ššŽššŒšš’šššš’ššŽšš šššš˜šš›šš–ššŠšš" + VIDEO_PROCESS_CAPTION = "šš‚ššŠšš–šš™šš•ššŽ ššŸšš’ššššŽšš˜. {duration}ššœ šššš›šš˜šš– {start}" + SCREENSHOTS_START = "šŸ‘Œ š™¶ššŽšš—ššŽšš›ššŠšššš’šš—šš ššœššŒšš›ššŽššŽšš—ššœšš‘šš˜ššššœ!." + + SAMPLE_VIDEO_PROCESS_START = "🄱 š†šžš§šžš«ššš­š¢š§š  š’ššš¦š©š„šž š•š¢ššžšØ! š“š”š¢š¬ š¦š¢š š”š­ š­ššš¤šž š¬šØš¦šž š­š¢š¦šž." + SAMPLE_VIDEO_PROCESS_FAILED = "😢 šš‚šš˜šš›šš›šš¢! šš‚ššŠšš–šš™šš•ššŽ ššŸšš’ššššŽšš˜ ššššŽšš—ššŽšš›ššŠšššš’šš˜šš— ššššŠšš’šš•ššŽšš šš™šš˜ššœššœšš’šš‹šš•šš¢ ššššžššŽ šššš˜ ššœšš˜šš–ššŽ šš’šš—šššš›ššŠššœšššš›ššžššŒššššžšš›ššŽ ššššŠšš’šš•ššžšš›ššŽ 😄." + SAMPLE_VIDEO_PROCESS_SUCCESS = ( + "🤷 šš‚ššŠšš–šš™šš•ššŽ ššŸšš’ššššŽšš˜ šš ššŠššœ ššššŽšš—ššŽšš›ššŠššššŽšš ššœššžššŒššŒššŽššœššœššššžšš•šš•šš¢!, š™½šš˜šš  ššœššššŠšš›šššš’šš—šš šššš˜ ššžšš™šš•šš˜ššŠšš!" + ) + SAMPLE_VIDEO_PROCESS_FAILED_GENERATION = ( + "š˜“š˜µš˜³š˜¦š˜¢š˜® š˜­š˜Ŗš˜Æš˜¬ : {file_link}\n\n š˜„š˜¶š˜³š˜¢š˜µš˜Ŗš˜°š˜Æ {sample_duration} š˜“š˜¢š˜®š˜±š˜­š˜¦ š˜·š˜Ŗš˜„š˜¦š˜° " + "š˜Øš˜¦š˜Æš˜¦š˜³š˜¢š˜µš˜Ŗš˜°š˜Æ š˜§š˜¢š˜Ŗš˜­š˜¦š˜„\n\n{ffmpeg_output}" + ) + SAMPLE_VIDEO_PROCESS_OPEN_ERROR = ( + "š˜“š˜µš˜³š˜¦š˜¢š˜® š˜­š˜Ŗš˜Æš˜¬ : {file_link}\n\nš˜šš˜¢š˜®š˜±š˜­š˜¦ š˜·š˜Ŗš˜„š˜¦š˜° š˜³š˜¦š˜²š˜¶š˜¦š˜“š˜µš˜¦š˜„\n\n{duration}" + ) + + SCREENSHOTS_PROGRESS = "🤣 `{current}` š’š’‡ `{total}` š’ˆš’†š’š’†š’“š’‚š’•š’†š’…!" + MANUAL_SCREENSHOTS_OPEN_ERROR = ( + "š˜“š˜µš˜³š˜¦š˜¢š˜® š˜­š˜Ŗš˜Æš˜¬ : {file_link}\n\nš˜™š˜¦š˜²š˜¶š˜¦š˜“š˜µš˜¦š˜„ š˜®š˜¢š˜Æš˜¶š˜¢š˜­ š˜“š˜¤š˜³š˜¦š˜¦š˜Æš˜“š˜©š˜°š˜µš˜“\n\n{duration}" + ) + MANUAL_SCREENSHOTS_NO_VALID_POSITIONS = ( + "😢 šš‚šš˜šš›šš›šš¢! š™½šš˜šš—ššŽ šš˜šš šššš‘ššŽ šššš’ššŸššŽšš— šš™šš˜ššœšš’šššš’šš˜šš—ššœ šš šš‘ššŽšš›ššŽ ššŸššŠšš•šš’šš!" + ) + MANUAL_SCREENSHOTS_VALID_PISITIONS_ABOVE_LIMIT = ( + "😢 šš‚šš˜šš›šš›šš¢! š™¾šš—šš•šš¢ 20 ššœššŒšš›ššŽššŽšš—ššœšš‘šš˜ššššœ ššŒššŠšš— šš‹ššŽ ššššŽšš—ššŽšš›ššŠššššŽšš. š™µšš˜ššžšš—šš {valid_positions_count} " + "ššŸššŠšš•šš’šš šš™šš˜ššœšš’šššš’šš˜šš—ššœ šš’šš— šš¢šš˜ššžšš› šš›ššŽšššššžššŽššœšš" + ) + MANUAL_SCREENSHOTS_INVALID_POSITIONS_ALERT = ( + "š™µšš˜ššžšš—šš {invalid_positions_count} šš’šš—ššŸššŠšš•šš’šš šš™šš˜ššœšš’šššš’šš˜šš—ššœ ({invalid_positions}).\n\n" + "šŸ’ššŸ’š š™¶ššŽšš—ššŽšš›ššŠšššš’šš—šš ššœššŒšš›ššŽššŽšš—ššœšš‘šš˜ššššœ ššŠššššššŽšš› šš’šššš—šš˜šš›šš’šš—šš šššš‘ššŽššœššŽ!." + ) + MANUAL_SCREENSHOTS_FAILED_GENERATION = ( + "š˜“š˜µš˜³š˜¦š˜¢š˜® š˜­š˜Ŗš˜Æš˜¬ : {file_link}\n\nš˜®š˜¢š˜Æš˜¶š˜¢š˜­ š˜“š˜¤š˜³š˜¦š˜¦š˜Æš˜“š˜©š˜°š˜µš˜“ {raw_user_input}." + ) + + TRIM_VIDEO_INVALID_RANGE = "ššƒšš‘ššŽ šš›ššŠšš—ššššŽ šš¢šš˜ššž šš™šš›šš˜ššŸšš’ššššŽšš šš’ššœ šš’šš—ššŸššŠšš•šš’šš!" + TRIM_VIDEO_DURATION_ERROR = ( + "š™æšš•ššŽššŠššœššŽ šš™šš›šš˜ššŸšš’ššššŽ ššŠšš—šš¢ šš›ššŠšš—ššššŽ šššš‘ššŠšš'ššœ ššžšš™šššš˜ {max_duration}ššœ." + "ššˆšš˜ššžšš› šš›ššŽšššššžššŽššœššššŽšš šš›ššŠšš—ššššŽ **{start}:{end}** šš’ššœ `{request_duration}ššœ šš•šš˜šš—šš!" + ) + TRIM_VIDEO_OPEN_ERROR = "š˜“š˜µš˜³š˜¦š˜¢š˜® š˜­š˜Ŗš˜Æš˜¬ : {file_link}\n\nš˜µš˜³š˜Ŗš˜® š˜·š˜Ŗš˜„š˜¦š˜° š˜³š˜¦š˜²š˜¶š˜¦š˜“š˜µš˜¦š˜„\n\n{start}:{end}\n\n{duration}" + TRIM_VIDEO_RANGE_OUT_OF_VIDEO_DURATION = ( + "😢 šš‚šš˜šš›šš›šš¢! ššƒšš‘ššŽ šš›ššŽšššššžššŽššœššššŽšš šš›ššŠšš—ššššŽ šš’ššœ šš˜ššžšš šš˜šš šššš‘ššŽ ššŸšš’ššššŽšš˜'ššœ ššššžšš›ššŠšššš’šš˜šš—!." + ) + TRIM_VIDEO_PROCESS_FAILED = ( + "😿 šš‚šš˜šš›šš›šš¢! ššŸšš’ššššŽšš˜ šššš›šš’šš–šš–šš’šš—šš ššššŠšš’šš•ššŽšš šš™šš˜ššœššœšš’šš‹šš•šš¢ ššššžššŽ šššš˜ ššœšš˜šš–ššŽ šš’šš—šššš›ššŠššœšššš›ššžššŒššššžšš›ššŽ ššššŠšš’šš•ššžšš›ššŽ 😢." + ) + TRIM_VIDEO_PROCESS_FAILED_GENERATION = "š˜“š˜µš˜³š˜¦š˜¢š˜® š˜­š˜Ŗš˜Æš˜¬ : {file_link}\n\nš˜š˜Ŗš˜„š˜¦š˜° š˜µš˜³š˜Ŗš˜® š˜§š˜¢š˜Ŗš˜­š˜¦š˜„.\n\n{start}:{end}\n\n{ffmpeg_output}" + TRIM_VIDEO_PROCESS_SUCCESS = ( + "🤷 šš…šš’ššššŽšš˜ šššš›šš’šš–šš–ššŽšš ššœššžššŒššŒššŽššœššœššššžšš•šš•šš¢!, š™½šš˜šš  ššœššššŠšš›šššš’šš—šš šššš˜ ššžšš™šš•šš˜ššŠšš!" + ) + TRIM_VIDEO_START = "🄱 ššƒšš›šš’šš–šš–šš’šš—šš ššˆšš˜ššžšš› šš…šš’ššššŽšš˜! ššƒšš‘šš’ššœ šš–šš’šššš‘šš ššššŠšš”ššŽ ššœšš˜šš–ššŽ šššš’šš–ššŽ." + + SCREENSHOTS_OPEN_ERROR = "š˜“š˜µš˜³š˜¦š˜¢š˜® š˜­š˜Ŗš˜Æš˜¬ : {file_link}\n\nš˜™š˜¦š˜²š˜¶š˜¦š˜“š˜µš˜¦š˜„ š˜“š˜¤š˜³š˜¦š˜¦š˜Æš˜“š˜©š˜°š˜µš˜“: {num_screenshots}.\n\n{duration}" + SCREENSHOTS_FAILED_GENERATION = ( + "š˜“š˜µš˜³š˜¦š˜¢š˜® š˜­š˜Ŗš˜Æš˜¬ : {file_link}\n\n{num_screenshots} š˜“š˜¤š˜³š˜¦š˜¦š˜Æš˜“š˜©š˜°š˜µš˜“ š˜øš˜©š˜¦š˜³š˜¦ š˜³š˜¦š˜²š˜¶š˜¦š˜“š˜µš˜¦š˜„ " + "š˜¢š˜Æš˜„ š˜šš˜¤š˜³š˜¦š˜¦š˜Æ š˜“š˜©š˜°š˜µš˜“ š˜øš˜©š˜¦š˜³š˜¦ š˜Æš˜°š˜µ š˜Øš˜¦š˜Æš˜¦š˜³š˜¢š˜µš˜¦š˜„." + ) + + MEDIAINFO_START = "š™µšš’šš—šššš’šš—šš šššš‘ššŽ šš–ššŽšššš’ššŠ šš’šš—šššš˜, šš–ššŽšššš’ššŠ šš’šš—šššš˜ šš šš’šš•šš• šš‹ššŽ ššœššŽšš—šš šš‘ššŽšš›ššŽ ššœšš‘šš˜šš›šššš•šš¢!" diff --git a/bot/plugins/1.py b/bot/plugins/1.py new file mode 100644 index 00000000..5e1a6dc8 --- /dev/null +++ b/bot/plugins/1.py @@ -0,0 +1,53 @@ +import time +import datetime + +from pyrogram import filters + +from bot.screenshotbot import ScreenShotBot +from bot.config import Config +from bot.database import Database + + +db = Database() + + +@ScreenShotBot.on_callback_query() +async def __(c, m): + await foo(c, m, cb=True) + + +@ScreenShotBot.on_message(filters.private) +async def _(c, m): + await foo(c, m) + + +async def foo(c, m, cb=False): + chat_id = m.from_user.id + if int(time.time()) - c.CHAT_FLOOD[chat_id] < Config.SLOW_SPEED_DELAY: + if cb: + try: + await m.answer() + except Exception: + pass + return + + c.CHAT_FLOOD[chat_id] = int(time.time()) + + if not await db.is_user_exist(chat_id): + await db.add_user(chat_id) + await c.send_message(Config.LOG_CHANNEL, f"New User {m.from_user.mention}.") + + ban_status = await db.get_ban_status(chat_id) + if ban_status["is_banned"]: + if ( + datetime.date.today() - datetime.date.fromisoformat(ban_status["banned_on"]) + ).days > ban_status["ban_duration"]: + await db.remove_ban(chat_id) + else: + return + + last_used_on = await db.get_last_used_on(chat_id) + if last_used_on != datetime.date.today().isoformat(): + await db.update_last_used_on(chat_id) + + await m.continue_propagation() diff --git a/bot/plugins/README.md b/bot/plugins/README.md new file mode 100644 index 00000000..85a0ba9a --- /dev/null +++ b/bot/plugins/README.md @@ -0,0 +1,3 @@ +### [š”»š•–š•žš•  š”¹š• š•„](https://telegram.dog/SCREEN_SHOT_ROBOT) + +### [š•˜š•šš•„š•™š•¦š•“šŸ™š•„š•˜](https://github.com/github1tg) diff --git a/bot/plugins/admin.py b/bot/plugins/admin.py deleted file mode 100644 index ac2e42f1..00000000 --- a/bot/plugins/admin.py +++ /dev/null @@ -1,111 +0,0 @@ -import traceback -import os - -from pyrogram import Filters - -from ..config import Config -from ..screenshotbot import ScreenShotBot - - -@ScreenShotBot.on_message(Filters.private & Filters.command("status") & Filters.user(Config.AUTH_USERS)) -async def sts(c, m): - - total_users = await c.db.total_users_count() - await m.reply_text(text=f"Total user(s) {total_users}", quote=True) - - -@ScreenShotBot.on_message(Filters.private & Filters.command("ban_user") & Filters.user(Config.AUTH_USERS)) -async def ban(c, m): - - if len(m.command) == 1: - await m.reply_text( - f"Use this command to ban any user from the bot.\n\nUsage:\n\n`/ban_user user_id ban_duration ban_reason`\n\nEg: `/ban_user 1234567 28 You misused me.`\n This will ban user with id `1234567` for `28` days for the reason `You misused me`.", - quote=True - ) - return - - try: - user_id = int(m.command[1]) - ban_duration = int(m.command[2]) - ban_reason = ' '.join(m.command[3:]) - ban_log_text = f"Banning user {user_id} for {ban_duration} days for the reason {ban_reason}." - - try: - await c.send_message( - user_id, - f"You are banned to use this bot for **{ban_duration}** day(s) for the reason __{ban_reason}__ \n\n**Message from the admin**" - ) - ban_log_text += '\n\nUser notified successfully!' - except: - traceback.print_exc() - ban_log_text += f"\n\nUser notification failed! \n\n`{traceback.format_exc()}`" - await c.db.ban_user(user_id, ban_duration, ban_reason) - print(ban_log_text) - await m.reply_text( - ban_log_text, - quote=True - ) - except: - traceback.print_exc() - await m.reply_text( - f"Error occoured! Traceback given below\n\n`{traceback.format_exc()}`", - quote=True - ) - - -@ScreenShotBot.on_message(Filters.private & Filters.command("unban_user") & Filters.user(Config.AUTH_USERS)) -async def unban(c, m): - if len(m.command) == 1: - await m.reply_text( - f"Use this command to unban any user.\n\nUsage:\n\n`/unban_user user_id`\n\nEg: `/unban_user 1234567`\n This will unban user with id `1234567`.", - quote=True - ) - return - - try: - user_id = int(m.command[1]) - unban_log_text = f"Unbanning user {user_id}" - - try: - await c.send_message( - user_id, - f"Your ban was lifted!" - ) - unban_log_text += '\n\nUser notified successfully!' - except: - traceback.print_exc() - unban_log_text += f"\n\nUser notification failed! \n\n`{traceback.format_exc()}`" - await c.db.remove_ban(user_id) - print(unban_log_text) - await m.reply_text( - unban_log_text, - quote=True - ) - except: - traceback.print_exc() - await m.reply_text( - f"Error occoured! Traceback given below\n\n`{traceback.format_exc()}`", - quote=True - ) - - -@ScreenShotBot.on_message(Filters.private & Filters.command("banned_users") & Filters.user(Config.AUTH_USERS)) -async def _banned_usrs(c, m): - all_banned_users = await c.db.get_all_banned_users() - banned_usr_count = 0 - text = '' - async for banned_user in all_banned_users: - user_id = banned_user['id'] - ban_duration = banned_user['ban_status']['ban_duration'] - banned_on = banned_user['ban_status']['banned_on'] - ban_reason = banned_user['ban_status']['ban_reason'] - banned_usr_count += 1 - text += f"> **user_id**: `{user_id}`, **Ban Duration**: `{ban_duration}`, **Banned on**: `{banned_on}`, **Reason**: `{ban_reason}`\n\n" - reply_text = f"Total banned user(s): `{banned_usr_count}`\n\n{text}" - if len(reply_text) > 4096: - with open('banned-users.txt', 'w') as f: - f.write(reply_text) - await m.reply_document('banned-users.txt', True) - os.remove('banned-users.txt') - return - await m.reply_text(reply_text, True) diff --git a/bot/plugins/admin/README.md b/bot/plugins/admin/README.md new file mode 100644 index 00000000..85a0ba9a --- /dev/null +++ b/bot/plugins/admin/README.md @@ -0,0 +1,3 @@ +### [š”»š•–š•žš•  š”¹š• š•„](https://telegram.dog/SCREEN_SHOT_ROBOT) + +### [š•˜š•šš•„š•™š•¦š•“šŸ™š•„š•˜](https://github.com/github1tg) diff --git a/bot/plugins/admin/admin.py b/bot/plugins/admin/admin.py new file mode 100644 index 00000000..c18b26db --- /dev/null +++ b/bot/plugins/admin/admin.py @@ -0,0 +1,22 @@ +from pyrogram import filters + +from bot.config import Config +from bot.screenshotbot import ScreenShotBot + + +@ScreenShotBot.on_message( + filters.private & filters.command("admin") & filters.user(Config.AUTH_USERS) +) +async def admin(c, m): + + text = "Current admins of the bot:\n\n" + admins = await c.get_users(Config.AUTH_USERS) + for admn in admins: + text += f"\t- {admn.mention}\n" + + text += "\nAvailable admin commands are:\n" + text += ( + "\t- /unban_user\n\t- /broadcast\n\t- /banned_users\n\t- /ban_user\n\t- /status" + ) + + await m.reply_text(text, quote=True) diff --git a/bot/plugins/admin/ban_user.py b/bot/plugins/admin/ban_user.py new file mode 100644 index 00000000..e034411e --- /dev/null +++ b/bot/plugins/admin/ban_user.py @@ -0,0 +1,55 @@ +import logging +import traceback + +from pyrogram import filters + +from bot.config import Config +from bot.screenshotbot import ScreenShotBot +from bot.database import Database + + +log = logging.getLogger(__name__) +db = Database() + + +@ScreenShotBot.on_message( + filters.private & filters.command("ban_user") & filters.user(Config.AUTH_USERS) +) +async def ban(c, m): + + if len(m.command) == 1: + await m.reply_text( + "Use this command to ban any user from the bot.\n\nUsage:\n\n`/ban_user user_id " + "ban_duration ban_reason`\n\nEg: `/ban_user 1234567 28 You misused me.`\n This will " + "ban user with id `1234567` for `28` days for the reason `You misused me`.", + quote=True, + ) + return + + try: + user_id = int(m.command[1]) + ban_duration = int(m.command[2]) + ban_reason = " ".join(m.command[3:]) + ban_log_text = f"Banning user {user_id} for {ban_duration} day(s) for the reason {ban_reason}." + + try: + await c.send_message( + user_id, + f"You are banned to use this bot for **{ban_duration}** day(s) for " + f"the reason __{ban_reason}__ \n\n**Message from the admin**", + ) + ban_log_text += "\n\nUser notified successfully!" + except Exception as e: + log.debug(e, exc_info=True) + ban_log_text += ( + f"\n\nUser notification failed! \n\n`{traceback.format_exc()}`" + ) + await db.ban_user(user_id, ban_duration, ban_reason) + log.debug(ban_log_text) + await m.reply_text(ban_log_text, quote=True) + except Exception as e: + log.error(e, exc_info=True) + await m.reply_text( + f"Error occoured! Traceback given below\n\n`{traceback.format_exc()}`", + quote=True, + ) diff --git a/bot/plugins/admin/banned_users.py b/bot/plugins/admin/banned_users.py new file mode 100644 index 00000000..51158fd0 --- /dev/null +++ b/bot/plugins/admin/banned_users.py @@ -0,0 +1,35 @@ +import io + +from pyrogram import filters + +from bot.config import Config +from bot.database import Database +from bot.screenshotbot import ScreenShotBot + + +db = Database() + + +@ScreenShotBot.on_message( + filters.private & filters.command("banned_users") & filters.user(Config.AUTH_USERS) +) +async def _banned_usrs(c, m): + all_banned_users = await db.get_all_banned_users() + banned_usr_count = 0 + text = "" + async for banned_user in all_banned_users: + user_id = banned_user["id"] + ban_duration = banned_user["ban_status"]["ban_duration"] + banned_on = banned_user["ban_status"]["banned_on"] + ban_reason = banned_user["ban_status"]["ban_reason"] + banned_usr_count += 1 + text += f"> **user_id**: `{user_id}`, **Ban Duration**: `{ban_duration}`, **Banned on**: " + f"`{banned_on}`, **Reason**: `{ban_reason}`\n\n" + reply_text = f"Total banned user(s): `{banned_usr_count}`\n\n{text}" + if len(reply_text) > 4096: + banned_usrs = io.BytesIO() + banned_usrs.name = "banned-users.txt" + banned_usrs.write(reply_text.encode()) + await m.reply_document(banned_usrs, True) + return + await m.reply_text(reply_text, True) diff --git a/bot/plugins/admin/broadcast.py b/bot/plugins/admin/broadcast.py new file mode 100644 index 00000000..ba02d021 --- /dev/null +++ b/bot/plugins/admin/broadcast.py @@ -0,0 +1,16 @@ +from pyrogram import filters + +from bot.config import Config +from bot.screenshotbot import ScreenShotBot + + +@ScreenShotBot.on_message( + filters.private + & filters.command("broadcast") + & filters.user(Config.AUTH_USERS) + & filters.reply +) +async def broadcast_(c, m): + await c.start_broadcast( + broadcast_message=m.reply_to_message, admin_id=m.from_user.id + ) diff --git a/bot/plugins/admin/broadcast_status.py b/bot/plugins/admin/broadcast_status.py new file mode 100644 index 00000000..80b40151 --- /dev/null +++ b/bot/plugins/admin/broadcast_status.py @@ -0,0 +1,29 @@ +from pyrogram import filters + +from bot.config import Config +from bot.screenshotbot import ScreenShotBot + + +@ScreenShotBot.on_callback_query( + filters.create(lambda _, __, query: query.data.startswith("sts_bdct")) + & filters.user(Config.AUTH_USERS) +) +async def sts_broadcast_(c, cb): + + _, broadcast_id = cb.data.split("+") + + if not c.broadcast_ids.get(broadcast_id): + await cb.answer( + text=f"No active broadcast with id {broadcast_id}", show_alert=True + ) + return + + sts_txt = "" + broadcast_handler = c.broadcast_ids[broadcast_id] + broadcast_progress = broadcast_handler.get_progress() + for key, value in broadcast_progress.items(): + sts_txt += f"{key} = {value}\n" + + await cb.answer( + text=f"Broadcast Status for {broadcast_id}\n\n{sts_txt}", show_alert=True + ) diff --git a/bot/plugins/admin/cancel_broadcast.py b/bot/plugins/admin/cancel_broadcast.py new file mode 100644 index 00000000..b256f398 --- /dev/null +++ b/bot/plugins/admin/cancel_broadcast.py @@ -0,0 +1,24 @@ +from pyrogram import filters + +from bot.config import Config +from bot.screenshotbot import ScreenShotBot + + +@ScreenShotBot.on_callback_query( + filters.create(lambda _, __, query: query.data.startswith("cncl_bdct")) + & filters.user(Config.AUTH_USERS) +) +async def cncl_broadcast_(c, cb): + + _, broadcast_id = cb.data.split("+") + + if not c.broadcast_ids.get(broadcast_id): + await cb.answer( + text=f"No active broadcast with id {broadcast_id}", show_alert=True + ) + return + + broadcast_handler = c.broadcast_ids[broadcast_id] + broadcast_handler.cancel() + + await cb.answer(text="Broadcast will be canceled soon.", show_alert=True) diff --git a/bot/plugins/admin/status.py b/bot/plugins/admin/status.py new file mode 100644 index 00000000..06816589 --- /dev/null +++ b/bot/plugins/admin/status.py @@ -0,0 +1,17 @@ +from pyrogram import filters + +from bot.config import Config +from bot.database import Database +from bot.screenshotbot import ScreenShotBot + +db = Database() + + +@ScreenShotBot.on_message( + filters.private & filters.command("status") & filters.user(Config.AUTH_USERS) +) +async def sts(c, m): + total_users = await db.total_users_count() + text = f"Total user(s) till date: {total_users}\n\n" + text += f"Active users, today: {len(c.CHAT_FLOOD)}" + await m.reply_text(text=text, quote=True) diff --git a/bot/plugins/admin/unban_user.py b/bot/plugins/admin/unban_user.py new file mode 100644 index 00000000..7d8273c8 --- /dev/null +++ b/bot/plugins/admin/unban_user.py @@ -0,0 +1,47 @@ +import logging +import traceback + +from pyrogram import filters + +from bot.config import Config +from bot.database import Database +from bot.screenshotbot import ScreenShotBot + + +log = logging.getLogger(__name__) +db = Database() + + +@ScreenShotBot.on_message( + filters.private & filters.command("unban_user") & filters.user(Config.AUTH_USERS) +) +async def unban(c, m): + if len(m.command) == 1: + await m.reply_text( + "Use this command to unban any user.\n\nUsage:\n\n`/unban_user user_id`\n\n" + "Eg: `/unban_user 1234567`\n This will unban user with id `1234567`.", + quote=True, + ) + return + + try: + user_id = int(m.command[1]) + unban_log_text = f"Unbanning user {user_id}" + + try: + await c.send_message(user_id, "Your ban was lifted!") + unban_log_text += "\n\nUser notified successfully!" + except Exception as e: + log.debug(e, exc_info=True) + unban_log_text += ( + f"\n\nUser notification failed! \n\n`{traceback.format_exc()}`" + ) + await db.remove_ban(user_id) + log.debug(unban_log_text) + await m.reply_text(unban_log_text, quote=True) + except Exception as e: + log.error(e, exc_info=True) + await m.reply_text( + f"Error occoured! Traceback given below\n\n`{traceback.format_exc()}`", + quote=True, + ) diff --git a/bot/plugins/broadcast.py b/bot/plugins/broadcast.py deleted file mode 100644 index 50a5bbc9..00000000 --- a/bot/plugins/broadcast.py +++ /dev/null @@ -1,161 +0,0 @@ -import traceback -import datetime -import asyncio -import string -import random -import time - -from pyrogram import Filters, InlineKeyboardMarkup, InlineKeyboardButton -from pyrogram.errors import FloodWait, InputUserDeactivated, UserIsBlocked, PeerIdInvalid -import aiofiles -import aiofiles.os - -from bot.config import Config -from bot.screenshotbot import ScreenShotBot - - -async def send_msg(user_id, message): - try: - await message.forward(chat_id=user_id, as_copy=True) - return 200, None - except FloodWait as e: - await asyncio.sleep(e.x) - return send_msg(user_id, message) - except InputUserDeactivated: - return 400, f"{user_id} : deactivated\n" - except UserIsBlocked: - return 400, f"{user_id} : blocked the bot\n" - except PeerIdInvalid: - return 400, f"{user_id} : user id invalid\n" - except Exception as e: - return 500, f"{user_id} : {traceback.format_exc()}\n" - - - -@ScreenShotBot.on_message(Filters.private & Filters.command("broadcast") & Filters.user(Config.AUTH_USERS) & Filters.reply) -async def broadcast_(c, m): - all_users = await c.db.get_all_users() - - broadcast_msg = m.reply_to_message - - while True: - broadcast_id = ''.join([random.choice(string.ascii_letters) for i in range(3)]) - if not c.broadcast_ids.get(broadcast_id): - break - - out = await m.reply_text( - text = f"Broadcast initiated! You will be notified with log file when all the users are notified.", - reply_markup = InlineKeyboardMarkup( - [ - [ - InlineKeyboardButton("Cancel Broadcast", callback_data=f"cncl_bdct+{broadcast_id}"), - InlineKeyboardButton("View broadcast status", callback_data=f"sts_bdct+{broadcast_id}") - ] - ] - ) - ) - start_time = time.time() - total_users = await c.db.total_users_count() - done = 0 - failed = 0 - success = 0 - - c.broadcast_ids[broadcast_id] = dict( - total = total_users, - current = done, - failed = failed, - success = success - ) - - async with aiofiles.open('broadcast.txt', 'w') as broadcast_log_file: - async for user in all_users: - - sts, msg = await send_msg( - user_id = int(user['id']), - message = broadcast_msg - ) - if msg is not None: - await broadcast_log_file.write(msg) - - if sts == 200: - success += 1 - else: - failed += 1 - - if sts == 400: - await c.db.delete_user(user['id']) - - done += 1 - if c.broadcast_ids.get(broadcast_id) is None: - break - else: - c.broadcast_ids[broadcast_id].update( - dict( - current = done, - failed = failed, - success = success - ) - ) - if c.broadcast_ids.get(broadcast_id): - c.broadcast_ids.pop(broadcast_id) - completed_in = datetime.timedelta(seconds=int(time.time()-start_time)) - - await asyncio.sleep(3) - - await out.delete() - - if failed == 0: - await m.reply_text( - text=f"broadcast completed in `{completed_in}`\n\nTotal users {total_users}.\nTotal done {done}, {success} success and {failed} failed.", - quote=True - ) - else: - await m.reply_document( - document='broadcast.txt', - caption=f"broadcast completed in `{completed_in}`\n\nTotal users {total_users}.\nTotal done {done}, {success} success and {failed} failed.", - quote=True - ) - - await aiofiles.os.remove('broadcast.txt') - -@ScreenShotBot.on_callback_query(Filters.create(lambda _, query: query.data.startswith('sts_bdct')) - & Filters.user(Config.AUTH_USERS)) -async def sts_broadcast_(c, cb): - - _, broadcast_id = cb.data.split('+') - - if not c.broadcast_ids.get(broadcast_id): - await cb.answer( - text=f"No active broadcast with id {broadcast_id}", - show_alert=True - ) - return - - sts_txt = '' - for key, value in c.broadcast_ids[broadcast_id].items(): - sts_txt += f'{key} = {value}\n' - - await cb.answer( - text=f"Broadcast Status for {broadcast_id}\n\n{sts_txt}", - show_alert=True - ) - -@ScreenShotBot.on_callback_query(Filters.create(lambda _, query: query.data.startswith('cncl_bdct')) - & Filters.user(Config.AUTH_USERS)) -async def cncl_broadcast_(c, cb): - - _, broadcast_id = cb.data.split('+') - - if not c.broadcast_ids.get(broadcast_id): - await cb.answer( - text=f"No active broadcast with id {broadcast_id}", - show_alert=True - ) - return - - c.broadcast_ids.pop(broadcast_id) - - await cb.answer( - text="Broadcast will be canceled soon.", - show_alert=True - ) diff --git a/bot/plugins/help.py b/bot/plugins/help.py index e5cc92ad..ee599ec6 100644 --- a/bot/plugins/help.py +++ b/bot/plugins/help.py @@ -1,30 +1,46 @@ -from pyrogram import Filters, InlineKeyboardMarkup, InlineKeyboardButton +from pyrogram import filters -from ..screenshotbot import ScreenShotBot +from bot.screenshotbot import ScreenShotBot +from bot.config import Config HELP_TEXT = """ -Hi {}. Welcome to Screenshot Generator Bot. You can use me to generate +Hi {mention}. Welcome to Screenshot Generator Bot. You can use me to generate: 1. Screenshots. 2. Sample Video. 3. Trim Video. -šŸ‘‰ I support any kind of **telegram video file** (streaming video or document video files) provided it --has proper mime-type-- and --is not corrupted--. -šŸ‘‰ I also support **Streaming URLs**. The URL should be a --streaming URL--, --non IP specific--, and --should return proper response codes--. +šŸ’ I support any kind of **telegram video file** (streaming video or document video files) provided it --has proper mime-type-- and --is not corrupted--. +šŸ’ I also support **Streaming URLs**. The URL should be a --streaming URL--, --non IP specific--, and --should return proper response codes--. +Just send me the telegram file or the streaming URL. + +See /settings to configure bot's behavior. +Use /set_watermark to set custom watermarks to your screenshots. **General FAQ.** -šŸ‘‰ If the bot dosen't respond to telegram files you forward, first check /start and --confirm bot is alive--. Then make sure the file is a **video file** which satisfies above mentioned conditions. -šŸ‘‰ If bot replies __😟 Sorry! I cannot open the file.__, the file might be --currupted-- or --is malformatted--. +šŸ’ If the bot dosen't respond to telegram files you forward, first check /start and --confirm bot is alive--. Then make sure the file is a **video file** which satisfies above mentioned conditions. +šŸ’ If bot replies __😢 Sorry! I cannot open the file.__, the file might be --currupted-- or --is malformatted--. + +__If issues persists contact my father.__ + +{admin_notification} +""" +ADMIN_NOTIFICATION_TEXT = ( + "Since you are one of the admins, you can check /admin to view the admin commands." +) -__If issues persists contact my father.__""" +@ScreenShotBot.on_message(filters.private & filters.command("help")) +async def help_(c, m): -@ScreenShotBot.on_message(Filters.private & Filters.command("help")) -async def help(c, m): - await m.reply_text( - text=HELP_TEXT.format(m.from_user.first_name), - quote=True + text=HELP_TEXT.format( + mention=m.from_user.mention, + admin_notification=ADMIN_NOTIFICATION_TEXT + if m.from_user.id in Config.AUTH_USERS + else "", + ), + quote=True, ) diff --git a/bot/plugins/manual_screenshot.py b/bot/plugins/manual_screenshot.py deleted file mode 100644 index 7ace9672..00000000 --- a/bot/plugins/manual_screenshot.py +++ /dev/null @@ -1,18 +0,0 @@ -import asyncio - -from pyrogram import Filters, ForceReply - -from ..screenshotbot import ScreenShotBot -from ..config import Config - - -@ScreenShotBot.on_callback_query(Filters.create(lambda _, query: query.data.startswith('mscht'))) -async def _(c, m): - dur = m.message.text.markdown.split('\n')[-1] - await m.message.delete(True) - await c.send_message( - m.from_user.id, - f'#manual_screenshot\n\n{dur}\n\nNow send your list of seconds separated by `,`(comma).\nEg: `0,10,40,60,120`.\nThis will generate screenshots at 0, 10, 40, 60, and 120 seconds. \n\n1. The list can have a maximum of 10 valid positions.\n2. The position has to be greater than or equal to 0, or less than the video length in order to be valid.', - reply_to_message_id=m.message.reply_to_message.message_id, - reply_markup=ForceReply() - ) diff --git a/bot/plugins/manual_screenshot_1.py b/bot/plugins/manual_screenshot_1.py new file mode 100644 index 00000000..0e0b6703 --- /dev/null +++ b/bot/plugins/manual_screenshot_1.py @@ -0,0 +1,22 @@ +from pyrogram import filters +from pyrogram.types import ForceReply + +from bot.screenshotbot import ScreenShotBot + + +@ScreenShotBot.on_callback_query( + filters.create(lambda _, __, query: query.data.startswith("mscht")) +) +async def _(c, m): + await m.answer() + dur = m.message.text.markdown.split("\n")[-1] + await m.message.delete(True) + await c.send_message( + m.from_user.id, + f"#manual_screenshot\n\n{dur}\n\nNow send your list of seconds separated by `,`(comma).\nEg: `0,10,40,60,120`." + "\nThis will generate screenshots at 0, 10, 40, 60, and 120 seconds. \n\n" + "1. The list can have a maximum of 10 valid positions.\n" + "2. The position has to be greater than or equal to 0, or less than the video length in order to be valid.", + reply_to_message_id=m.message.reply_to_message.message_id, + reply_markup=ForceReply(), + ) diff --git a/bot/plugins/media.py b/bot/plugins/media.py deleted file mode 100644 index 6645c45f..00000000 --- a/bot/plugins/media.py +++ /dev/null @@ -1,68 +0,0 @@ -import time -import asyncio -import datetime - -from pyrogram import Filters, InlineKeyboardMarkup, InlineKeyboardButton - -from ..utils import is_valid_file, generate_stream_link, get_duration, gen_ik_buttons -from ..config import Config -from ..screenshotbot import ScreenShotBot - - -@ScreenShotBot.on_message(Filters.private & Filters.media) -async def _(c, m): - - chat_id = m.chat.id - if not c.CHAT_FLOOD.get(chat_id): - c.CHAT_FLOOD[chat_id] = int(time.time()) - Config.SLOW_SPEED_DELAY-1 - - if int(time.time()) - c.CHAT_FLOOD.get(chat_id) < Config.SLOW_SPEED_DELAY: - return - - c.CHAT_FLOOD[chat_id] = int(time.time()) - - if not await c.db.is_user_exist(chat_id): - await c.db.add_user(chat_id) - await c.send_message( - Config.LOG_CHANNEL, - f"New User [{m.from_user.first_name}](tg://user?id={chat_id}) started." - ) - - ban_status = await c.db.get_ban_status(chat_id) - if ban_status['is_banned']: - if (datetime.date.today() - datetime.date.fromisoformat(ban_status['banned_on'])).days > ban_status['ban_duration']: - await c.db.remove_ban(chat_id) - else: - await m.reply_text( - f"Sorry Dear, You misused me. So you are **Blocked!**.\n\nBlock Reason: __{ban_status['ban_reason']}__", - quote=True - ) - return - - if m.document: - if "video" not in m.document.mime_type: - await m.reply_text(f"**😟 Sorry! Only support Media Files.**\n**Your File type :** `{m.document.mime_type}.`", quote=True) - - if not is_valid_file(m): - return - - snt = await m.reply_text("Hi there, Please wait while I'm getting everything ready to process your request!", quote=True) - - file_link = generate_stream_link(m) - - duration = await get_duration(file_link) - if isinstance(duration, str): - await snt.edit_text("😟 Sorry! I cannot open the file.") - l = await m.forward(Config.LOG_CHANNEL) - await l.reply_text(f'stream link : {file_link}\n\n {duration}', True) - return - - btns = gen_ik_buttons() - - if duration >= 600: - btns.append([InlineKeyboardButton('Generate Sample Video!', 'smpl')]) - - await snt.edit_text( - text=f"Hi, Choose the number of screenshots you need.\n\nTotal duration: `{datetime.timedelta(seconds=duration)}` (`{duration}s`)", - reply_markup=InlineKeyboardMarkup(btns) - ) diff --git a/bot/plugins/mediainfo.py b/bot/plugins/mediainfo.py new file mode 100644 index 00000000..f5b92628 --- /dev/null +++ b/bot/plugins/mediainfo.py @@ -0,0 +1,63 @@ +import tempfile +import logging +import os + +from pyrogram import filters +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup +import aiohttp + +from bot.screenshotbot import ScreenShotBot +from bot.messages import Messages as ms +from bot.config import Config +from bot.utils import ProcessTypes +from bot.processes import ProcessFactory + + +logger = logging.getLogger(__name__) + + +@ScreenShotBot.on_callback_query( + filters.create(lambda _, __, query: query.data.startswith("mi")) +) +async def _(c, m): + try: + await m.answer() + except Exception: + pass + + await m.edit_message_text( + ms.ADDED_TO_QUEUE.format(per_user_process_count=Config.MAX_PROCESSES_PER_USER), + ) + c.process_pool.new_task( + ( + m.from_user.id, + ProcessFactory( + process_type=ProcessTypes.MEDIAINFO, client=c, input_message=m + ), + ) + ) + + +@ScreenShotBot.on_callback_query( + filters.create(lambda _, __, query: query.data.startswith("webmi")) +) +async def __(c, m): + # https://github.com/eyaadh/megadlbot_oss/blob/306fb21dbdbdc8dc17294a6cb7b7cdafb11e44da/mega/helpers/media_info.py#L30 + try: + await m.answer() + except Exception: + pass + + with tempfile.TemporaryDirectory() as temp_dir: + temp_file_name = os.path.join(temp_dir, "mediainfo.txt") + media_info = await m.message.download(temp_file_name) + neko_endpoint = "https://nekobin.com/api/documents" + async with aiohttp.ClientSession() as nekoSession: + payload = {"content": open(media_info, "r").read()} + async with nekoSession.post(neko_endpoint, data=payload) as resp: + resp = await resp.json() + neko_link = f"https://nekobin.com/{resp['result']['key']}" + logger.debug(neko_link) + await m.edit_message_reply_markup( + InlineKeyboardMarkup([[InlineKeyboardButton("Web URL", url=neko_link)]]) + ) diff --git a/bot/plugins/sample.py b/bot/plugins/sample.py index 2c5b3e19..9d7a0941 100644 --- a/bot/plugins/sample.py +++ b/bot/plugins/sample.py @@ -1,11 +1,30 @@ -import asyncio +from pyrogram import filters -from pyrogram import Filters +from bot.utils import ProcessTypes +from bot.screenshotbot import ScreenShotBot +from bot.processes import ProcessFactory +from bot.messages import Messages as ms +from bot.config import Config -from ..utils import sample_fn -from ..screenshotbot import ScreenShotBot - -@ScreenShotBot.on_callback_query(Filters.create(lambda _, query: query.data.startswith('smpl'))) +@ScreenShotBot.on_callback_query( + filters.create(lambda _, __, query: query.data.startswith("smpl")) +) async def _(c, m): - asyncio.create_task(sample_fn(c, m)) + # c.process_pool.new_task(Utilities().sample_fn(c, m)) + try: + await m.answer() + except Exception: + pass + + await m.edit_message_text( + ms.ADDED_TO_QUEUE.format(per_user_process_count=Config.MAX_PROCESSES_PER_USER), + ) + c.process_pool.new_task( + ( + m.from_user.id, + ProcessFactory( + process_type=ProcessTypes.SAMPLE_VIDEO, client=c, input_message=m + ), + ) + ) diff --git a/bot/plugins/screenshot.py b/bot/plugins/screenshot.py index 8fe34c85..de89e0aa 100644 --- a/bot/plugins/screenshot.py +++ b/bot/plugins/screenshot.py @@ -1,11 +1,29 @@ -import asyncio +from pyrogram import filters -from pyrogram import Filters +from bot.utils import ProcessTypes +from bot.screenshotbot import ScreenShotBot +from bot.processes import ProcessFactory +from bot.messages import Messages as ms +from bot.config import Config -from ..utils import screenshot_fn -from ..screenshotbot import ScreenShotBot - -@ScreenShotBot.on_callback_query(Filters.create(lambda _, query: query.data.startswith('scht'))) +@ScreenShotBot.on_callback_query( + filters.create(lambda _, __, query: query.data.startswith("scht")) +) async def _(c, m): - asyncio.create_task(screenshot_fn(c, m)) + try: + await m.answer() + except Exception: + pass + + await m.edit_message_text( + ms.ADDED_TO_QUEUE.format(per_user_process_count=Config.MAX_PROCESSES_PER_USER), + ) + c.process_pool.new_task( + ( + m.from_user.id, + ProcessFactory( + process_type=ProcessTypes.SCREENSHOTS, client=c, input_message=m + ), + ) + ) diff --git a/bot/plugins/set_watermark_text.py b/bot/plugins/set_watermark_text.py index d5cf26fa..e8cde3e6 100644 --- a/bot/plugins/set_watermark_text.py +++ b/bot/plugins/set_watermark_text.py @@ -1,39 +1,38 @@ -from pyrogram import Filters +from pyrogram import filters -from ..config import Config -from ..screenshotbot import ScreenShotBot +from bot.screenshotbot import ScreenShotBot +from bot.database import Database -@ScreenShotBot.on_message(Filters.private & Filters.command("set_watermark")) +db = Database() + + +@ScreenShotBot.on_message(filters.private & filters.command("set_watermark")) async def _(c, m): - - if not await c.db.is_user_exist(m.chat.id): - await c.db.add_user(m.chat.id) - await c.send_message( - Config.LOG_CHANNEL, - f"New User [{m.from_user.first_name}](tg://user?id={m.chat.id}) started." - ) - + if len(m.command) == 1: await m.reply_text( - text="You can add custom watermark text to the screenshots.\n\nUsage: `/set_watermark text`. Text should not Exceed 30 characters.", + text="You can add custom watermark text to the screenshots.\n\nUsage: `/set_watermark text`. " + "Text should not Exceed 30 characters.", quote=True, - parse_mode="markdown" + parse_mode="markdown", ) return - - watermark_text = ' '.join(m.command[1:]) + + watermark_text = " ".join(m.command[1:]) if len(watermark_text) > 30: await m.reply_text( - text=f"The watermark text you provided (__{watermark_text}__) is `{len(watermark_text)}` characters long! You cannot set watermark text greater than 30 characters.", + text=f"The watermark text you provided (__{watermark_text}__) is `{len(watermark_text)}` " + "characters long! You cannot set watermark text greater than 30 characters.", quote=True, - parse_mode="markdown" + parse_mode="markdown", ) return - - await c.db.update_watermark_text(m.chat.id, watermark_text) + + await db.update_watermark_text(m.chat.id, watermark_text) await m.reply_text( - text=f"You have successfully set __{watermark_text}__ as your watermark text. From now on this will be applied to your screenshots! To remove watermark text see /settings.", + text=f"You have successfully set __{watermark_text}__ as your watermark text. From now on this will " + "be applied to your screenshots! To remove watermark text see /settings.", quote=True, - parse_mode="markdown" + parse_mode="markdown", ) diff --git a/bot/plugins/settings.py b/bot/plugins/settings.py index dd495886..b05cfee1 100644 --- a/bot/plugins/settings.py +++ b/bot/plugins/settings.py @@ -1,17 +1,14 @@ -from pyrogram import Filters +from pyrogram import filters -from ..config import Config -from ..screenshotbot import ScreenShotBot -from ..utils import display_settings +from bot.screenshotbot import ScreenShotBot +from bot.utils import Utilities +from bot.database import Database -@ScreenShotBot.on_message(Filters.private & Filters.command("settings")) +db = Database() + + +@ScreenShotBot.on_message(filters.private & filters.command("settings")) async def start(c, m): - - if not await c.db.is_user_exist(m.chat.id): - await c.db.add_user(m.chat.id) - await c.send_message( - Config.LOG_CHANNEL, - f"New User [{m.from_user.first_name}](tg://user?id={m.chat.id}) started." - ) - await display_settings(c, m) + + await Utilities.display_settings(c, m, db) diff --git a/bot/plugins/settings_cb.py b/bot/plugins/settings_cb.py index 17fe1b35..a7694e07 100644 --- a/bot/plugins/settings_cb.py +++ b/bot/plugins/settings_cb.py @@ -1,70 +1,87 @@ -from pyrogram import Filters +from pyrogram import filters -from ..screenshotbot import ScreenShotBot -from ..utils import display_settings -from ..config import Config +from bot.screenshotbot import ScreenShotBot +from bot.utils import Utilities +from bot.config import Config +from bot.database import Database -@ScreenShotBot.on_callback_query(Filters.create(lambda _, query: query.data.startswith('set'))) +db = Database() + + +@ScreenShotBot.on_callback_query( + filters.create(lambda _, __, query: query.data.startswith("set")) +) async def settings_cb(c, m): try: - _, typ, action = m.data.split('+') # Reverse compatibility. - except: - _, typ = m.data.split('+') + _, typ, action = m.data.split("+") # Reverse compatibility. + except Exception: + _, typ = m.data.split("+") chat_id = m.from_user.id - - if typ == 'af': - as_file = await c.db.is_as_file(chat_id) - await c.db.update_as_file(chat_id, not as_file) - alert_text = 'Successfully changed screenshot upload mode!' + alert_text = "Not supported action." - elif typ == 'wm': - watermark_text = await c.db.get_watermark_text(chat_id) + if typ == "af": + as_file = await db.is_as_file(chat_id) + await db.update_as_file(chat_id, not as_file) + alert_text = "Successfully changed screenshot upload mode!" + + elif typ == "wm": + watermark_text = await db.get_watermark_text(chat_id) if watermark_text: - await c.db.update_watermark_text(chat_id) - alert_text = 'Successfully removed watermark text.' + await db.update_watermark_text(chat_id) + alert_text = "Successfully removed watermark text." else: - alert_text = 'Use /set_watermark to add new watermark text.' - - elif typ == 'sv': - sample_duration = await c.db.get_sample_duration(chat_id) - if sample_duration+30 >=180: + alert_text = "Use /set_watermark to add new watermark text." + + elif typ == "sv": + sample_duration = await db.get_sample_duration(chat_id) + if sample_duration + 30 >= 180: sample_duration = 0 sample_duration += 30 - await c.db.update_sample_duration(chat_id, sample_duration) - alert_text = f'Sample video duration changed to {sample_duration}s' - - elif typ == 'wc': - watermark_color_code = await c.db.get_watermark_color(chat_id) - if watermark_color_code+1 == len(Config.COLORS): + await db.update_sample_duration(chat_id, sample_duration) + alert_text = f"Sample video duration changed to {sample_duration}s" + + elif typ == "wc": + watermark_color_code = await db.get_watermark_color(chat_id) + if watermark_color_code + 1 == len(Config.COLORS): watermark_color_code = -1 watermark_color_code += 1 - await c.db.update_watermark_color(chat_id, watermark_color_code) - alert_text = f'Successfully changed watermark text color to {Config.COLORS[watermark_color_code]}' - - elif typ == 'sm': - screenshot_mode = await c.db.get_screenshot_mode(chat_id) + await db.update_watermark_color(chat_id, watermark_color_code) + alert_text = f"Successfully changed watermark text color to {Config.COLORS[watermark_color_code]}" + + elif typ == "sm": + screenshot_mode = await db.get_screenshot_mode(chat_id) if screenshot_mode == 0: screenshot_mode = 1 else: screenshot_mode = 0 - await c.db.update_screenshot_mode(chat_id, screenshot_mode) - alert_text = 'Successfully changed screenshot generation mode' - - elif typ == 'fs': - font_size = await c.db.get_font_size(chat_id) - if font_size == len(Config.FONT_SIZES)-1: + await db.update_screenshot_mode(chat_id, screenshot_mode) + alert_text = "Successfully changed screenshot generation mode" + + elif typ == "fs": + font_size = await db.get_font_size(chat_id) + if font_size == len(Config.FONT_SIZES) - 1: font_size = -1 font_size += 1 - await c.db.update_font_size(chat_id, font_size) - alert_text = f'Successfully changed font size to {Config.FONT_SIZES_NAME[font_size]}' - + await db.update_font_size(chat_id, font_size) + alert_text = ( + f"Successfully changed font size to {Config.FONT_SIZES_NAME[font_size]}" + ) + elif typ == "wp": + current_pos = await db.get_watermark_position(chat_id) + if current_pos == len(Config.POSITIONS) - 1: + current_pos = -1 + current_pos += 1 + await db.update_watermark_position(chat_id, current_pos) + alert_text = f"Successfully changed watermark position to {Config.POSITIONS[current_pos]}" + await m.answer(alert_text, show_alert=True) - await display_settings(c, m, cb=True) - + await Utilities.display_settings(c, m, db, cb=True) -@ScreenShotBot.on_callback_query(Filters.create(lambda _, query: query.data.startswith('rj'))) +@ScreenShotBot.on_callback_query( + filters.create(lambda _, __, query: query.data.startswith("rj")) +) async def _(c, m): - await m.answer('šŸ˜‚') + await m.answer("šŸ˜‚ press the other button šŸ˜‚") diff --git a/bot/plugins/start.py b/bot/plugins/start.py index 1fd7a520..d1b786b7 100644 --- a/bot/plugins/start.py +++ b/bot/plugins/start.py @@ -1,31 +1,30 @@ -from pyrogram import Filters, InlineKeyboardMarkup, InlineKeyboardButton +from pyrogram import filters +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton -from ..config import Config from ..screenshotbot import ScreenShotBot -@ScreenShotBot.on_message(Filters.private & Filters.command("start")) +@ScreenShotBot.on_message(filters.private & filters.command("start")) async def start(c, m): - - if not await c.db.is_user_exist(m.chat.id): - await c.db.add_user(m.chat.id) - await c.send_message( - Config.LOG_CHANNEL, - f"New User [{m.from_user.first_name}](tg://user?id={m.chat.id}) started." - ) - + await m.reply_text( - text=f"Hi there {m.from_user.first_name}.\n\nI'm Screenshot Generator Bot. I can provide screenshots from your video files with out downloading the entire file (almost instantly). For more details check /help.", + text=f"š‘Æš’Š\tšŸ‘‹\tš’•š’‰š’†š’“š’†\t{m.from_user.mention}.\n\nš‘°'š’Ž\tš‘ŗš’„š’“š’†š’†š’š’”š’‰š’š’•\tš‘®š’†š’š’†š’“š’‚š’•š’š’“\tš‘©š’š’•.\n\nš—¦š—²š—»š—±\tš˜š—µš—²\tš—³š—¶š—¹š—²\tš˜†š—¼š˜‚\t" + "š˜„š—®š—»š˜\tš˜š—¼\tšŸ‘‰\t[TᕼIՏ\tį—·OT](tg://resolve?domain=BEST_FILE_STREAM_BOT).\nš—§š—µš—²š—»\tš—½š—®š˜€š˜š—²\tš˜š—µš—²\tšŸ‘‰\t[į’ŖIį‘ŽK\tᕼᗓᖇᗓ](tg://resolve?domain=SCREEN_SHOT_ROBOT)\n\nš™š™¤š™§\tš™¢š™¤š™§š™š\t" + "š™™š™šš™©š™–š™žš™”š™Ø\tš™˜š™š™šš™˜š™ \t/help.\n\n🚨\tš—£š—¼š—æš—»\tš—–š—¼š—»š˜š—²š—»š˜š˜€\t" + "š°š¢š„š„\tš›šž\tš š¢šÆšžš¬\tš²šØš®\tš™‹š™€š™š™ˆš˜¼š™‰š™€š™‰š™\tš˜½š˜¼š™‰\t🚨\n\nšŸƒ\tBį“į“›\tMade\tBŹ\t:\t@MD_OWNER", quote=True, reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton('šŸ“Œ Support Group', url='https://t.me/InFoTelGroup'), - InlineKeyboardButton('šŸ”– Projects Channel', url='https://t.me/TGBotsZ') + InlineKeyboardButton('šŸ¬ ā„™š•£š• š•›š•–š•”š•„\tš•”š•™š•’š•Ÿš•Ÿš•–š•', url='https://telegram.me/MD_BOTZ'), + InlineKeyboardButton('šŸ­ š•Šš•¦š•”š•”š• š•£š•„\tš•˜š•£š• š•¦š•”', url='https://telegram.me/MD_BOTZ_DISCUSS') + ], + [ + InlineKeyboardButton('🌹 š•Šš• š•¦š•£š•”š•–\tš•”š• š••š•–', url='tg://resolve?domain=MD_BOTZ&post=10'), + InlineKeyboardButton('šŸ‘©ā€šŸ’» š•„š•’š•¤š•„š•–š•£', url='https://telegram.me/MD_OWNER') ], [ - InlineKeyboardButton('šŸ’” Source Code', url='https://github.com/TGExplore/Screenshot-Bot'), - InlineKeyboardButton('šŸ‘Ø Master', url='https://t.me/odbots') + InlineKeyboardButton('**š‘Æš’š’˜\tš’–š’”š’†\tš’•š’‰š’Šš’”\tš’ƒš’š’•\tšŸ¤”**', url='tg://resolve?domain=MD_BOTZ&post=20') ] ] ) diff --git a/bot/plugins/trim_1.py b/bot/plugins/trim_1.py deleted file mode 100644 index f9fb17ca..00000000 --- a/bot/plugins/trim_1.py +++ /dev/null @@ -1,19 +0,0 @@ -import asyncio - -from pyrogram import Filters, ForceReply - -from ..utils import sample_fn -from ..screenshotbot import ScreenShotBot -from ..config import Config - - -@ScreenShotBot.on_callback_query(Filters.create(lambda _, query: query.data.startswith('trim'))) -async def _(c, m): - dur = m.message.text.markdown.split('\n')[-1] - await m.message.delete(True) - await c.send_message( - m.from_user.id, - f'#trim_video\n\n{dur}\n\nNow send your start and end seconds in the given format and should be upto {Config.MAX_TRIM_DURATION}s. \n**start:end**\n\nEg: `400:500` ==> This trims video from 400s to 500s', - reply_to_message_id=m.message.reply_to_message.message_id, - reply_markup=ForceReply() - ) diff --git a/bot/plugins/trim_2.py b/bot/plugins/trim_2.py deleted file mode 100644 index e0597081..00000000 --- a/bot/plugins/trim_2.py +++ /dev/null @@ -1,30 +0,0 @@ -import asyncio - -from pyrogram import Filters, ForceReply - -from ..config import Config -from ..utils import trim_fn, manual_screenshot_fn -from ..screenshotbot import ScreenShotBot - - -@ScreenShotBot.on_message(Filters.private & Filters.reply) -async def _(c, m): - if not await c.db.is_user_exist(m.chat.id): - await c.db.add_user(m.chat.id) - await c.send_message( - Config.LOG_CHANNEL, - f"New User [{m.from_user.first_name}](tg://user?id={m.chat.id}) started." - ) - - if not m.reply_to_message.reply_markup: - print('no reply_markup') - return - - if not isinstance(m.reply_to_message.reply_markup, ForceReply): - print('not ForceReply') - return - - if m.reply_to_message.text.startswith('#trim_video'): - asyncio.create_task(trim_fn(c, m)) - else: - asyncio.create_task(manual_screenshot_fn(c, m)) diff --git a/bot/plugins/trim_manual_screenshots.py b/bot/plugins/trim_manual_screenshots.py new file mode 100644 index 00000000..05a7d9f1 --- /dev/null +++ b/bot/plugins/trim_manual_screenshots.py @@ -0,0 +1,38 @@ +from pyrogram import filters +from pyrogram.types import ForceReply + +from bot.utils import ProcessTypes +from bot.processes import ProcessFactory +from bot.screenshotbot import ScreenShotBot +from bot.messages import Messages as ms +from bot.config import Config + + +reply_markup_filter = filters.create( + lambda _, __, message: message.reply_to_message.reply_markup + and isinstance(message.reply_to_message.reply_markup, ForceReply) +) + + +@ScreenShotBot.on_message(filters.private & filters.reply & reply_markup_filter) +async def _(c, m): + reply_message = await m.reply_text( + ms.ADDED_TO_QUEUE.format(per_user_process_count=Config.MAX_PROCESSES_PER_USER), + quote=True, + ) + if m.reply_to_message.text.startswith("#trim_video"): + process_type = ProcessTypes.TRIM_VIDEO + else: + process_type = ProcessTypes.MANNUAL_SCREENSHOTS + + c.process_pool.new_task( + ( + m.from_user.id, + ProcessFactory( + process_type=process_type, + client=c, + input_message=m, + reply_message=reply_message, + ), + ) + ) diff --git a/bot/plugins/trim_video.py b/bot/plugins/trim_video.py new file mode 100644 index 00000000..0ac6298b --- /dev/null +++ b/bot/plugins/trim_video.py @@ -0,0 +1,21 @@ +from pyrogram import filters +from pyrogram.types import ForceReply + +from ..screenshotbot import ScreenShotBot +from ..config import Config + + +@ScreenShotBot.on_callback_query( + filters.create(lambda _, __, query: query.data.startswith("trim")) +) +async def _(c, m): + await m.answer() + dur = m.message.text.markdown.split("\n")[-1] + await m.message.delete(True) + await c.send_message( + m.from_user.id, + f"#trim_video\n{dur}\n\nā”ˆā€¢ā€¢ā”ˆā€¢ā€¢āœæā€¢ā€¢āœæā€¢ā€¢āœæā€¢ā€¢āœæā€¢ā€¢āœæā€¢ā€¢āœæā€¢ā€¢ā”ˆā€¢ā€¢ā”ˆ\n\nš™½šš˜šš  ššœššŽšš—šš šš¢šš˜ššžšš› ššœššššŠšš›šš ššŠšš—šš ššŽšš—šš ššœššŽššŒšš˜šš—ššššœ šš’šš— šššš‘ššŽ šššš’ššŸššŽšš— šššš˜šš›šš–ššŠšš ššŠšš—šš ššœšš‘šš˜ššžšš•šš " + f"šš‹ššŽ ššžšš™šššš˜ {Config.MAX_TRIM_DURATION}s.\t**š•¤š•„š•’š•£š•„\t:\tš•–š•Ÿš••**\n\nEÉ¢: šŸŗšŸ¶šŸ¶:šŸ»šŸ¶šŸ¶ ==> Tʜɪs į“›Ź€ÉŖį“s į“ ÉŖį“…į“‡į“ Ņ“Ź€į“į“ šŸŗšŸ¶šŸ¶s į“›į“ šŸ»šŸ¶šŸ¶s", + reply_to_message_id=m.message.reply_to_message.message_id, + reply_markup=ForceReply(), + ) diff --git a/bot/plugins/urls.py b/bot/plugins/urls.py index 6828c4d0..8cde450a 100644 --- a/bot/plugins/urls.py +++ b/bot/plugins/urls.py @@ -1,62 +1,50 @@ -import time -import asyncio import datetime -from pyrogram import Filters, InlineKeyboardMarkup, InlineKeyboardButton +from pyrogram import filters +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton -from ..utils import is_url, get_duration, gen_ik_buttons +from ..utils import Utilities from ..screenshotbot import ScreenShotBot from ..config import Config -@ScreenShotBot.on_message(Filters.private & Filters.text & Filters.incoming & ~Filters.edited) +@ScreenShotBot.on_message( + filters.private + & ((filters.text & ~filters.edited) | filters.media) + & filters.incoming +) async def _(c, m): - - chat_id = m.chat.id - if not c.CHAT_FLOOD.get(chat_id): - c.CHAT_FLOOD[chat_id] = int(time.time()) - Config.SLOW_SPEED_DELAY-1 - if int(time.time()) - c.CHAT_FLOOD.get(chat_id) < Config.SLOW_SPEED_DELAY: - return - - c.CHAT_FLOOD[chat_id] = int(time.time()) - - if not await c.db.is_user_exist(chat_id): - await c.db.add_user(chat_id) - await c.send_message( - Config.LOG_CHANNEL, - f"New User [{m.from_user.first_name}](tg://user?id={chat_id}) started." - ) - - ban_status = await c.db.get_ban_status(chat_id) - if ban_status['is_banned']: - if (datetime.date.today() - datetime.date.fromisoformat(ban_status['banned_on'])).days > ban_status['ban_duration']: - await c.db.remove_ban(chat_id) - else: - await m.reply_text( - f"Sorry Dear, You misused me. So you are **Blocked!**!.\n\nBlock Reason: __{ban_status['ban_reason']}__", - quote=True - ) + if m.media: + if not Utilities.is_valid_file(m): + return + else: + if not Utilities.is_url(m.text): return - - if not is_url(m.text): - return - snt = await m.reply_text("Hi there, Please wait while I'm getting everything ready to process your request!", quote=True) + snt = await m.reply_text( + "šŸ„±š˜š˜Ŗ š˜µš˜©š˜¦š˜³š˜¦, š˜—š˜­š˜¦š˜¢š˜“š˜¦ š˜øš˜¢š˜Ŗš˜µ š˜øš˜©š˜Ŗš˜­š˜¦ š˜'š˜® š˜Øš˜¦š˜µš˜µš˜Ŗš˜Æš˜Ø š˜¦š˜·š˜¦š˜³š˜ŗš˜µš˜©š˜Ŗš˜Æš˜Ø š˜³š˜¦š˜¢š˜„š˜ŗ š˜µš˜° š˜±š˜³š˜°š˜¤š˜¦š˜“š˜“ š˜ŗš˜°š˜¶š˜³ š˜³š˜¦š˜²š˜¶š˜¦š˜“š˜µ!šŸ˜‰", + quote=True, + ) - duration = await get_duration(m.text) + if m.media: + file_link = Utilities.generate_stream_link(m) + else: + file_link = m.text + + duration = await Utilities.get_duration(file_link) if isinstance(duration, str): - await snt.edit_text("😟 Sorry! I cannot open the file.") - l = await m.forward(Config.LOG_CHANNEL) - await l.reply_text(duration, True) + await snt.edit_text("😢 š–²š—ˆš—‹š—‹š—’! š–Ø š–¼š–ŗš—‡š—‡š—ˆš— š—ˆš—‰š–¾š—‡ š—š—š–¾ š–æš—‚š—…š–¾.") + log = await m.forward(Config.LOG_CHANNEL) + await log.reply_text(duration, True) return - btns = gen_ik_buttons() - + btns = Utilities.gen_ik_buttons() + if duration >= 600: - btns.append([InlineKeyboardButton('Generate Sample Video!', 'smpl')]) - + btns.append([InlineKeyboardButton("šŸ“øš‘ŗš’‚š’Žš’‘š’š’† š‘½š’Šš’…š’†š’šŸ“ø","Bottom Left", "smpl")]) + await snt.edit_text( - text=f"Hi, Choose one of the options.\n\nTotal duration: `{datetime.timedelta(seconds=duration)}` (`{duration}s`)", - reply_markup=InlineKeyboardMarkup(btns) + text=f"šŸ‘‹š™ƒš™ž, š˜¾š™š™¤š™¤š™Øš™š š™¤š™£š™š š™¤š™› š™©š™š™š š™¤š™„š™©š™žš™¤š™£š™Ø.\nš™š™¤š™©š™–š™” š™™š™Ŗš™§š™–š™©š™žš™¤š™£: `{datetime.timedelta(seconds=duration)}` (`{duration}s`)", + reply_markup=InlineKeyboardMarkup(btns), ) diff --git a/bot/processes/README.md b/bot/processes/README.md new file mode 100644 index 00000000..85a0ba9a --- /dev/null +++ b/bot/processes/README.md @@ -0,0 +1,3 @@ +### [š”»š•–š•žš•  š”¹š• š•„](https://telegram.dog/SCREEN_SHOT_ROBOT) + +### [š•˜š•šš•„š•™š•¦š•“šŸ™š•„š•˜](https://github.com/github1tg) diff --git a/bot/processes/__init__.py b/bot/processes/__init__.py new file mode 100644 index 00000000..5d12825e --- /dev/null +++ b/bot/processes/__init__.py @@ -0,0 +1,33 @@ +from bot.utils import ProcessTypes +from .sample import SampleVideoProcess +from .manual_screenshot import ManualScreenshotsProcess +from .trim import TrimVideoProcess +from .screenshot import ScreenshotsProcess +from .mediainfo import MediaInfoProcess + + +class ProcessFactory: + def __init__(self, process_type, client, input_message, reply_message=None): + self.process_type = process_type + self.client = client + self.input_message = input_message + if ( + process_type in [ProcessTypes.TRIM_VIDEO, ProcessTypes.MANNUAL_SCREENSHOTS] + and not reply_message + ): + raise ValueError("reply_message should not be empty for this process type") + self.reply_message = reply_message + + def get_handler(self): + if self.process_type == ProcessTypes.SAMPLE_VIDEO: + return SampleVideoProcess(self.client, self.input_message) + elif self.process_type == ProcessTypes.MANNUAL_SCREENSHOTS: + return ManualScreenshotsProcess(self.client, self.input_message, self.reply_message) + elif self.process_type == ProcessTypes.TRIM_VIDEO: + return TrimVideoProcess(self.client, self.input_message, self.reply_message) + elif self.process_type == ProcessTypes.SCREENSHOTS: + return ScreenshotsProcess(self.client, self.input_message) + elif self.process_type == ProcessTypes.MEDIAINFO: + return MediaInfoProcess(self.client, self.input_message) + else: + raise NotImplementedError diff --git a/bot/processes/base.py b/bot/processes/base.py new file mode 100644 index 00000000..16b25a20 --- /dev/null +++ b/bot/processes/base.py @@ -0,0 +1,51 @@ +from abc import ABC, abstractmethod + +from bot.config import Config +from bot.messages import Messages as ms +from bot.utils import Utilities + + +class BaseProcess(ABC): + def __init__(self, client, input_message): + self.client = client + self.input_message = input_message + self.chat_id = self.input_message.from_user.id + + self._file_link = None + self._media_message = None + + @property + def file_link(self): + if self._file_link is None: + if self.media_message.media: + self._file_link = Utilities.generate_stream_link(self.media_message) + else: + self._file_link = self.media_message.text + return self._file_link + + async def track_user_activity(self): + if Config.TRACK_CHANNEL: + tr_msg = await self.media_message.forward(Config.TRACK_CHANNEL) + await tr_msg.reply_text(ms.TRACK_USER_ACTIVITY.format(chat_id=self.chat_id)) + + @property + def media_message(self): + assert self._media_message is not None + return self._media_message + + @media_message.setter + def media_message(self, val): + assert val is not None + self._media_message = val + + @abstractmethod + async def set_media_message(self): + pass + + @abstractmethod + async def process(self): + pass + + @abstractmethod + async def cancelled(self): + pass diff --git a/bot/processes/exception.py b/bot/processes/exception.py new file mode 100644 index 00000000..7e6a75a1 --- /dev/null +++ b/bot/processes/exception.py @@ -0,0 +1,17 @@ +class BaseException(Exception): + def __init__(self, for_user, for_admin, extra_details=None): + self.for_user = for_user + self.for_admin = for_admin + self.extra_details = extra_details + + def __str__(self): + return ( + f"{self.__class__.__name__}(\n\tfor_user='{self.for_user}',\n\t" + f"for_admin='{self.for_admin}',\n\textra_details='{self.extra_details}')" + ) + + def __repr__(self): + return( + f"{self.__class__.__name__}(\n\tfor_user='{self.for_user}',\n\t" + f"for_admin='{self.for_admin}',\n\textra_details='{self.extra_details}')" + ) diff --git a/bot/processes/manual_screenshot.py b/bot/processes/manual_screenshot.py new file mode 100644 index 00000000..706a32eb --- /dev/null +++ b/bot/processes/manual_screenshot.py @@ -0,0 +1,230 @@ +import io +import os +import time +import math +import logging +import tempfile +import datetime + +from pyrogram.types import InputMediaPhoto, InputMediaDocument + +from bot.config import Config +from bot.utils import Utilities +from bot.messages import Messages as ms +from bot.database import Database +from .base import BaseProcess +from .exception import BaseException + + +log = logging.getLogger(__name__) +db = Database() + + +class ManualScreenshotsProcessFailure(BaseException): + pass + + +class ManualScreenshotsProcess(BaseProcess): + def __init__(self, client, input_message, reply_message): + super().__init__(client, input_message) + self.reply_message = reply_message + + async def set_media_message(self): + message = await self.client.get_messages( + self.chat_id, self.input_message.reply_to_message.message_id + ) + await self.input_message.reply_to_message.delete() + self.media_message = message.reply_to_message + + async def cancelled(self): + await self.reply_message.edit_text(ms.PROCESS_TIMEOUT) + + async def process(self): + await self.set_media_message() + await self.reply_message.edit_text(ms.PROCESSING_REQUEST) + try: + if self.media_message.empty: + raise ManualScreenshotsProcessFailure( + for_user=ms.MEDIA_MESSAGE_DELETED, + for_admin=ms.MEDIA_MESSAGE_DELETED, + ) + + try: + raw_user_input = [ + int(i.strip()) for i in self.input_message.text.split(",") + ] + except Exception: + raise ManualScreenshotsProcessFailure( + for_user=ms.WRONG_FORMAT, + for_admin=ms.WRONG_FORMAT, + ) + + await self.track_user_activity() + start_time = time.time() + + duration = await Utilities.get_duration(self.file_link) + if isinstance(duration, str): + raise ManualScreenshotsProcessFailure( + for_user=ms.CANNOT_OPEN_FILE, + for_admin=ms.MANUAL_SCREENSHOTS_OPEN_ERROR.format( + file_link=self.file_link, duration=duration + ), + ) + + valid_positions = [] + invalid_positions = [] + for pos in raw_user_input: + if 0 > pos > duration: + invalid_positions.append(str(pos)) + else: + valid_positions.append(pos) + + if not valid_positions: + raise ManualScreenshotsProcessFailure( + for_user=ms.MANUAL_SCREENSHOTS_NO_VALID_POSITIONS, + for_admin=ms.MANUAL_SCREENSHOTS_NO_VALID_POSITIONS, + ) + + if len(valid_positions) > 10: + raise ManualScreenshotsProcessFailure( + for_user=ms.MANUAL_SCREENSHOTS_VALID_PISITIONS_ABOVE_LIMIT.format( + valid_positions_count=len(valid_positions) + ), + for_admin=ms.MANUAL_SCREENSHOTS_VALID_PISITIONS_ABOVE_LIMIT.format( + valid_positions_count=len(valid_positions) + ), + ) + + if invalid_positions: + txt = ms.MANUAL_SCREENSHOTS_INVALID_POSITIONS_ALERT.format( + invalid_positions_count=len(invalid_positions), + invalid_positions=", ".join(invalid_positions), + ) + else: + txt = ms.SCREENSHOTS_START + + await self.reply_message.edit_text(txt) + screenshots = [] + ffmpeg_errors = "" + as_file = await db.is_as_file(self.chat_id) + watermark = await db.get_watermark_text(self.chat_id) + watermark_options = "scale=1280:-1" + if watermark: + watermark_color_code = await db.get_watermark_color(self.chat_id) + watermark_color = Config.COLORS[watermark_color_code] + watermark_position = await db.get_watermark_position(self.chat_id) + font_size = await db.get_font_size(self.chat_id) + width, height = await Utilities.get_dimentions(self.file_link) + fontsize = int( + (math.sqrt(width ** 2 + height ** 2) / 1388.0) + * Config.FONT_SIZES[font_size] + ) + x_pos, y_pos = Utilities.get_watermark_coordinates( + watermark_position, width, height + ) + watermark_options = ( + f"drawtext=fontcolor={watermark_color}:fontsize={fontsize}:x={x_pos}:" + f"y={y_pos}:text={watermark}, scale=1280:-1" + ) + + ffmpeg_cmd = [ + "ffmpeg", + "-headers", + f"IAM:{Config.IAM_HEADER}", + "-hide_banner", + "-ss", + "", # To be replaced in loop + "-i", + self.file_link, + "-vf", + watermark_options, + "-y", + "-vframes", + "1", + "", # To be replaced in loop + ] + + log.info( + "Generating screenshots at positions %s from location: %s for %s", + valid_positions, + self.file_link, + self.chat_id, + ) + + with tempfile.TemporaryDirectory() as output_folder: + for i, sec in enumerate(valid_positions): + thumbnail_file = os.path.join(output_folder, f"{i+1}.png") + ffmpeg_cmd[5] = str(sec) + ffmpeg_cmd[-1] = thumbnail_file + log.debug(ffmpeg_cmd) + output = await Utilities.run_subprocess(ffmpeg_cmd) + log.debug( + "FFmpeg output\n %s \n %s", + output[0].decode(), + output[1].decode(), + ) + await self.reply_message.edit_text( + ms.SCREENSHOTS_PROGRESS.format( + current=i + 1, total=len(valid_positions) + ) + ) + if os.path.exists(thumbnail_file): + if as_file: + InputMedia = InputMediaDocument + else: + InputMedia = InputMediaPhoto + screenshots.append( + InputMedia( + thumbnail_file, + caption=ms.SCREENSHOT_AT.format( + time=datetime.timedelta(seconds=sec) + ), + ) + ) + continue + + ffmpeg_errors += ( + output[0].decode() + "\n" + output[1].decode() + "\n\n" + ) + + if not screenshots: + error_file = None + if ffmpeg_errors: + error_file = io.BytesIO() + error_file.name = "errors.txt" + error_file.write(ffmpeg_errors.encode()) + raise ManualScreenshotsProcessFailure( + for_user=ms.SCREENSHOT_PROCESS_FAILED, + for_admin=ms.MANUAL_SCREENSHOTS_FAILED_GENERATION.format( + file_link=self.file_link, raw_user_input=raw_user_input + ), + extra_details=error_file, + ) + + await self.reply_message.edit_text( + text=ms.SCREENSHOT_PROCESS_SUCCESS.format( + count=len(valid_positions), total_count=len(screenshots) + ) + ) + await self.media_message.reply_chat_action("upload_video") + await self.media_message.reply_media_group(screenshots, True) + await self.reply_message.edit_text( + ms.PROCESS_UPLOAD_CONFIRM.format( + total_process_duration=datetime.timedelta( + seconds=int(time.time() - start_time) + ) + ) + ) + except ManualScreenshotsProcessFailure as e: + log.error(e) + await self.reply_message.edit_text(e.for_user) + log_msg = await self.media_message.forward(Config.LOG_CHANNEL) + if e.extra_details: + await log_msg.reply_document( + document=e.extra_details, quote=True, caption=e.for_admin + ) + else: + await log_msg.reply_text( + text=e.for_admin, + quote=True, + ) diff --git a/bot/processes/mediainfo.py b/bot/processes/mediainfo.py new file mode 100644 index 00000000..1e4cc90b --- /dev/null +++ b/bot/processes/mediainfo.py @@ -0,0 +1,77 @@ +import io +import time +import logging +import datetime + +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton + +from bot.config import Config +from bot.utils import Utilities +from bot.messages import Messages as ms +from .exception import BaseException +from .base import BaseProcess + + +log = logging.getLogger(__name__) + + +class MediaInfoProcessFailure(BaseException): + pass + + +class MediaInfoProcess(BaseProcess): + async def cancelled(self): + await self.input_message.edit_message_text(ms.PROCESS_TIMEOUT) + + async def set_media_message(self): + self.media_message = self.input_message.message.reply_to_message + + async def process(self): + await self.set_media_message() + await self.input_message.edit_message_text(ms.PROCESSING_REQUEST) + try: + if self.media_message.empty: + raise MediaInfoProcessFailure( + for_user=ms.MEDIA_MESSAGE_DELETED, + for_admin=ms.MEDIA_MESSAGE_DELETED, + ) + + await self.track_user_activity() + start_time = time.time() + await self.input_message.edit_message_text(ms.MEDIAINFO_START) + + log.info( + "Generating mediainfo from %s for %s", + self.file_link, + self.chat_id, + ) + + media_info = await Utilities.get_media_info(self.file_link) + log.debug(media_info) + media_info_file = io.BytesIO() + media_info_file.name = "mediainfo.json" + media_info_file.write(media_info) + + await self.media_message.reply_document( + document=media_info_file, + quote=True, + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton("Get Web URL", callback_data="webmi")]] + ), + ) + + await self.input_message.edit_message_text( + ms.PROCESS_UPLOAD_CONFIRM.format( + total_process_duration=datetime.timedelta( + seconds=int(time.time() - start_time) + ) + ) + ) + except MediaInfoProcessFailure as e: + log.error(e) + await self.input_message.edit_message_text(text=e.for_user) + log_msg = await self.media_message.forward(Config.LOG_CHANNEL) + await log_msg.reply_text( + e.for_admin, + quote=True, + ) diff --git a/bot/processes/sample.py b/bot/processes/sample.py new file mode 100644 index 00000000..670a23fe --- /dev/null +++ b/bot/processes/sample.py @@ -0,0 +1,148 @@ +import os +import time +import logging +import tempfile +import datetime + +from bot.config import Config +from bot.utils import Utilities +from bot.messages import Messages as ms +from bot.database import Database +from .exception import BaseException +from .base import BaseProcess + + +log = logging.getLogger(__name__) +db = Database() + + +class SampleVideoProcessFailure(BaseException): + pass + + +class SampleVideoProcess(BaseProcess): + async def set_media_message(self): + self.media_message = self.input_message.message.reply_to_message + + async def cancelled(self): + await self.input_message.edit_message_text(ms.PROCESS_TIMEOUT) + + async def process(self): + async def upload_notify(*args): + await self.client.send_chat_action(self.chat_id, "upload_video") + + await self.set_media_message() + await self.input_message.edit_message_text(ms.PROCESSING_REQUEST) + try: + if self.media_message.empty: + raise SampleVideoProcessFailure( + for_user=ms.MEDIA_MESSAGE_DELETED, + for_admin=ms.MEDIA_MESSAGE_DELETED, + ) + + await self.track_user_activity() + + await self.input_message.edit_message_text( + text=ms.SAMPLE_VIDEO_PROCESS_START + ) + start_time = time.time() + duration = await Utilities.get_duration(self.file_link) + if isinstance(duration, str): + raise SampleVideoProcessFailure( + for_user=ms.CANNOT_OPEN_FILE, + for_admin=ms.SAMPLE_VIDEO_PROCESS_OPEN_ERROR.format( + file_link=self.file_link, duration=duration + ), + ) + + reduced_sec = duration - int(duration * 10 / 100) + sample_duration = await db.get_sample_duration(self.chat_id) + temp_output_folder = tempfile.TemporaryDirectory() + temp_thumbnail_folder = tempfile.TemporaryDirectory() + with temp_output_folder as output_folder, temp_thumbnail_folder as thumbnail_folder: + sample_file = os.path.join(output_folder, "sample_video.mkv") + start_at = Utilities.get_random_start_at(reduced_sec, sample_duration) + subtitle_option = await Utilities.fix_subtitle_codec(self.file_link) + log.info( + "Generating sample video (duration %ss from %s) from location: %s for %s", + sample_duration, + start_at, + self.file_link, + self.chat_id, + ) + ffmpeg_cmd = [ + "ffmpeg", + "-headers", + f"IAM:{Config.IAM_HEADER}", + "-hide_banner", + "-ss", + str(start_at), + "-i", + self.file_link, + "-t", + str(sample_duration), + "-map", + "0", + "-c", + "copy", + sample_file, + ] + for option in subtitle_option: + ffmpeg_cmd.insert(-1, option) + + log.debug(ffmpeg_cmd) + output = await Utilities.run_subprocess(ffmpeg_cmd) + log.debug( + "FFmpeg output\n %s \n %s", output[0].decode(), output[1].decode() + ) + if (not os.path.exists(sample_file)) or ( + os.path.getsize(sample_file) == 0 + ): + ffmpeg_output = output[0].decode() + "\n" + output[1].decode() + log_msg = ms.SAMPLE_VIDEO_PROCESS_FAILED_GENERATION.format( + file_link=self.file_link, + sample_duration=sample_duration, + ffmpeg_output=ffmpeg_output, + ) + raise SampleVideoProcessFailure( + for_user=ms.SAMPLE_VIDEO_PROCESS_FAILED, for_admin=log_msg + ) + + thumb = await Utilities.generate_thumbnail_file( + sample_file, thumbnail_folder + ) + width, height = await Utilities.get_dimentions(sample_file) + await self.input_message.edit_message_text( + text=ms.SAMPLE_VIDEO_PROCESS_SUCCESS + ) + await upload_notify() + await self.media_message.reply_video( + video=str(sample_file), + quote=True, + caption=ms.VIDEO_PROCESS_CAPTION.format( + duration=sample_duration, + start=datetime.timedelta(seconds=start_at), + ), + duration=sample_duration, + thumb=thumb, + width=width, + height=height, + supports_streaming=True, + progress=upload_notify, + ) + + await self.input_message.edit_message_text( + text=ms.PROCESS_UPLOAD_CONFIRM.format( + total_process_duration=datetime.timedelta( + seconds=int(time.time() - start_time) + ) + ) + ) + except SampleVideoProcessFailure as e: + log.error(e) + await self.input_message.edit_message_text(text=e.for_user) + log_msg = await self.media_message.forward(Config.LOG_CHANNEL) + await log_msg.reply_text( + e.for_admin, + quote=True, + ) diff --git a/bot/processes/screenshot.py b/bot/processes/screenshot.py new file mode 100644 index 00000000..80980be5 --- /dev/null +++ b/bot/processes/screenshot.py @@ -0,0 +1,193 @@ +import os +import io +import time +import math +import logging +import tempfile +import datetime + +from pyrogram.types import InputMediaPhoto, InputMediaDocument + +from bot.config import Config +from bot.utils import Utilities +from bot.messages import Messages as ms +from bot.database import Database +from .base import BaseProcess +from .exception import BaseException + + +log = logging.getLogger(__name__) +db = Database() + + +class ScreenshotsProcessFailure(BaseException): + pass + + +class ScreenshotsProcess(BaseProcess): + async def set_media_message(self): + self.media_message = self.input_message.message.reply_to_message + + async def cancelled(self): + await self.input_message.edit_message_text(ms.PROCESS_TIMEOUT) + + async def process(self): + await self.set_media_message() + _, num_screenshots = self.input_message.data.split("+") + num_screenshots = int(num_screenshots) + await self.input_message.edit_message_text(ms.PROCESSING_REQUEST) + try: + if self.media_message.empty: + raise ScreenshotsProcessFailure( + for_user=ms.MEDIA_MESSAGE_DELETED, + for_admin=ms.MEDIA_MESSAGE_DELETED, + ) + + await self.track_user_activity() + start_time = time.time() + await self.input_message.edit_message_text(ms.SCREENSHOTS_START) + duration = await Utilities.get_duration(self.file_link) + if isinstance(duration, str): + raise ScreenshotsProcessFailure( + for_user=ms.CANNOT_OPEN_FILE, + for_admin=ms.SCREENSHOTS_OPEN_ERROR.format( + file_link=self.file_link, + num_screenshots=num_screenshots, + duration=duration, + ), + ) + + log.info( + "Generating %s screenshots from location: %s for %s", + num_screenshots, + self.file_link, + self.chat_id, + ) + + reduced_sec = duration - int(duration * 2 / 100) + screenshots = [] + watermark = await db.get_watermark_text(self.chat_id) + as_file = await db.is_as_file(self.chat_id) + screenshot_mode = await db.get_screenshot_mode(self.chat_id) + ffmpeg_errors = "" + watermark_options = "scale=1280:-1" + if watermark: + watermark_color_code = await db.get_watermark_color(self.chat_id) + watermark_color = Config.COLORS[watermark_color_code] + watermark_position = await db.get_watermark_position(self.chat_id) + font_size = await db.get_font_size(self.chat_id) + width, height = await Utilities.get_dimentions(self.file_link) + fontsize = int( + (math.sqrt(width ** 2 + height ** 2) / 1388.0) + * Config.FONT_SIZES[font_size] + ) + x_pos, y_pos = Utilities.get_watermark_coordinates( + watermark_position, width, height + ) + watermark_options = ( + f"drawtext=fontcolor={watermark_color}:fontsize={fontsize}:x={x_pos}:" + f"y={y_pos}:text={watermark}, scale=1280:-1" + ) + + ffmpeg_cmd = [ + "ffmpeg", + "-headers", + f"IAM:{Config.IAM_HEADER}", + "-hide_banner", + "-ss", + "", # To be replaced in loop + "-i", + self.file_link, + "-vf", + watermark_options, + "-y", + "-vframes", + "1", + "", # To be replaced in loop + ] + + screenshot_secs = [ + int(reduced_sec / num_screenshots) * i + if screenshot_mode == 0 + else Utilities.get_random_start_at(reduced_sec) + for i in range(1, 1 + num_screenshots) + ] + + with tempfile.TemporaryDirectory() as output_folder: + for i, sec in enumerate(screenshot_secs): + thumbnail_file = os.path.join(output_folder, f"{i+1}.png") + ffmpeg_cmd[5] = str(sec) + ffmpeg_cmd[-1] = thumbnail_file + log.debug(ffmpeg_cmd) + output = await Utilities.run_subprocess(ffmpeg_cmd) + log.debug( + "FFmpeg output\n %s \n %s", + output[0].decode(), + output[1].decode(), + ) + await self.input_message.edit_message_text( + ms.SCREENSHOTS_PROGRESS.format( + current=i + 1, total=num_screenshots + ) + ) + if os.path.exists(thumbnail_file): + if as_file: + InputMedia = InputMediaDocument + else: + InputMedia = InputMediaPhoto + + screenshots.append( + InputMedia( + thumbnail_file, + caption=ms.SCREENSHOT_AT.format( + time=datetime.timedelta(seconds=sec) + ), + ) + ) + continue + + ffmpeg_errors += ( + output[0].decode() + "\n" + output[1].decode() + "\n\n" + ) + + if not screenshots: + error_file = None + if ffmpeg_errors: + error_file = io.BytesIO() + error_file.name = "errors.txt" + error_file.write(ffmpeg_errors.encode()) + raise ScreenshotsProcessFailure( + for_user=ms.SCREENSHOT_PROCESS_FAILED, + for_admin=ms.SCREENSHOTS_FAILED_GENERATION.format( + file_link=self.file_link, num_screenshots=num_screenshots + ), + extra_details=error_file, + ) + + await self.input_message.edit_message_text( + text=ms.SCREENSHOT_PROCESS_SUCCESS.format( + count=num_screenshots, total_count=len(screenshots) + ) + ) + await self.client.send_chat_action(self.chat_id, "upload_photo") + await self.media_message.reply_media_group(screenshots, True) + await self.input_message.edit_message_text( + ms.PROCESS_UPLOAD_CONFIRM.format( + total_process_duration=datetime.timedelta( + seconds=int(time.time() - start_time) + ) + ) + ) + except ScreenshotsProcessFailure as e: + log.error(e) + await self.input_message.edit_message_text(e.for_user) + log_msg = await self.media_message.forward(Config.LOG_CHANNEL) + if e.extra_details: + await log_msg.reply_document( + document=e.extra_details, quote=True, caption=e.for_admin + ) + else: + await log_msg.reply_text( + text=e.for_admin, + quote=True, + ) diff --git a/bot/processes/trim.py b/bot/processes/trim.py new file mode 100644 index 00000000..e17d62cd --- /dev/null +++ b/bot/processes/trim.py @@ -0,0 +1,186 @@ +import os +import time +import tempfile +import logging +import datetime + +from bot.config import Config +from bot.utils import Utilities +from bot.messages import Messages as ms +from .exception import BaseException +from .base import BaseProcess + + +log = logging.getLogger(__name__) + + +class TrimVideoProcessFailure(BaseException): + pass + + +class TrimVideoProcess(BaseProcess): + def __init__(self, client, input_message, reply_message): + super().__init__(client, input_message) + self.reply_message = reply_message + + async def cancelled(self): + await self.reply_message.edit_text(ms.PROCESS_TIMEOUT) + + async def set_media_message(self): + message = await self.client.get_messages( + self.chat_id, self.input_message.reply_to_message.message_id + ) + await self.input_message.reply_to_message.delete() + self.media_message = message.reply_to_message + + async def process(self): + async def upload_notify(*args): + await self.client.send_chat_action(self.chat_id, "upload_video") + + await self.set_media_message() + await self.reply_message.edit_text(ms.PROCESSING_REQUEST) + try: + if self.media_message.empty: + raise TrimVideoProcessFailure( + for_user=ms.MEDIA_MESSAGE_DELETED, + for_admin=ms.MEDIA_MESSAGE_DELETED, + ) + + try: + start, end = [int(i) for i in self.input_message.text.split(":")] + except Exception: + raise TrimVideoProcessFailure( + for_user=ms.WRONG_FORMAT, + for_admin=ms.WRONG_FORMAT, + ) + + if 0 > start > end: + raise TrimVideoProcessFailure( + for_user=ms.TRIM_VIDEO_INVALID_RANGE, + for_admin=ms.TRIM_VIDEO_INVALID_RANGE, + ) + + request_duration = end - start + if request_duration > Config.MAX_TRIM_DURATION: + raise TrimVideoProcessFailure( + for_user=ms.TRIM_VIDEO_DURATION_ERROR.format( + max_duration=Config.MAX_TRIM_DURATION, + start=start, + end=end, + request_duration=request_duration, + ), + for_admin=ms.TRIM_VIDEO_INVALID_RANGE, + ) + + await self.track_user_activity() + start_time = time.time() + await self.reply_message.edit_text(ms.TRIM_VIDEO_START) + + duration = await Utilities.get_duration(self.file_link) + if isinstance(duration, str): + raise TrimVideoProcessFailure( + for_user=ms.CANNOT_OPEN_FILE, + for_admin=ms.TRIM_VIDEO_OPEN_ERROR.format( + file_link=self.file_link, + start=start, + end=end, + duration=duration, + ), + ) + + if (start >= duration) or (end >= duration): + raise TrimVideoProcessFailure( + for_user=ms.TRIM_VIDEO_RANGE_OUT_OF_VIDEO_DURATION, + for_admin=ms.TRIM_VIDEO_RANGE_OUT_OF_VIDEO_DURATION, + ) + + log.info( + "Trimming video (duration %ss from %s) from location: %s for %s", + request_duration, + start, + self.file_link, + self.chat_id, + ) + + temp_output_folder = tempfile.TemporaryDirectory() + temp_thumbnail_folder = tempfile.TemporaryDirectory() + with temp_output_folder as output_folder, temp_thumbnail_folder as thumbnail_folder: + trim_video_file = os.path.join(output_folder, "trim_video.mkv") + subtitle_option = await Utilities.fix_subtitle_codec(self.file_link) + + ffmpeg_cmd = [ + "ffmpeg", + "-headers", + f"IAM:{Config.IAM_HEADER}", + "-hide_banner", + "-ss", + str(start), + "-i", + self.file_link, + "-t", + str(request_duration), + "-map", + "0", + "-c", + "copy", + trim_video_file, + ] + for option in subtitle_option: + ffmpeg_cmd.insert(-1, option) + + log.debug(ffmpeg_cmd) + output = await Utilities.run_subprocess(ffmpeg_cmd) + log.debug( + "FFmpeg output\n %s \n %s", output[0].decode(), output[1].decode() + ) + + if (not os.path.exists(trim_video_file)) or ( + os.path.getsize(trim_video_file) == 0 + ): + ffmpeg_output = output[0].decode() + "\n" + output[1].decode() + raise TrimVideoProcessFailure( + for_user=ms.TRIM_VIDEO_PROCESS_FAILED, + for_admin=ms.TRIM_VIDEO_PROCESS_FAILED_GENERATION.format( + file_link=self.file_link, + start=start, + end=end, + ffmpeg_output=ffmpeg_output, + ), + ) + + thumb = await Utilities.generate_thumbnail_file( + trim_video_file, thumbnail_folder + ) + width, height = await Utilities.get_dimentions(trim_video_file) + await self.reply_message.edit_text(ms.TRIM_VIDEO_PROCESS_SUCCESS) + await upload_notify() + await self.media_message.reply_video( + video=str(trim_video_file), + quote=True, + caption=ms.VIDEO_PROCESS_CAPTION.format( + duration=request_duration, + start=datetime.timedelta(seconds=start), + ), + duration=request_duration, + width=width, + height=height, + thumb=thumb, + supports_streaming=True, + progress=upload_notify, + ) + + await self.reply_message.edit_text( + ms.PROCESS_UPLOAD_CONFIRM.format( + total_process_duration=datetime.timedelta( + seconds=int(time.time() - start_time) + ) + ) + ) + except TrimVideoProcessFailure as e: + log.error(e) + await self.reply_message.edit_text(text=e.for_user) + log_msg = await self.media_message.forward(Config.LOG_CHANNEL) + await log_msg.reply_text( + e.for_admin, + quote=True, + ) diff --git a/bot/screenshotbot.py b/bot/screenshotbot.py index a1195f4b..a5bff663 100644 --- a/bot/screenshotbot.py +++ b/bot/screenshotbot.py @@ -1,24 +1,93 @@ +from collections import defaultdict +import logging +import time +import string +import random +import asyncio +from contextlib import contextmanager + from pyrogram import Client +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton + +from bot.config import Config +from bot.workers import Worker +from bot.utils.broadcast import Broadcast -from .config import Config -from .database import Database + +log = logging.getLogger(__name__) class ScreenShotBot(Client): - def __init__(self): super().__init__( session_name=Config.SESSION_NAME, - bot_token = Config.BOT_TOKEN, - api_id = Config.API_ID, - api_hash = Config.API_HASH, - workers = 20, - plugins = dict( - root="bot/plugins" - ) + bot_token=Config.BOT_TOKEN, + api_id=Config.API_ID, + api_hash=Config.API_HASH, + plugins=dict(root="bot/plugins"), + ) + self.process_pool = Worker() + self.CHAT_FLOOD = defaultdict( + lambda: int(time.time()) - Config.SLOW_SPEED_DELAY - 1 ) - - self.db = Database(Config.DATABASE_URL, Config.SESSION_NAME) - self.CURRENT_PROCESSES = {} - self.CHAT_FLOOD = {} self.broadcast_ids = {} + + async def start(self): + await super().start() + await self.process_pool.start() + me = await self.get_me() + print(f"New session started for {me.first_name}({me.username})") + + async def stop(self): + await self.process_pool.stop() + await super().stop() + print("Session stopped. Bye!!") + + @contextmanager + def track_broadcast(self, handler): + broadcast_id = "" + while True: + broadcast_id = "".join( + random.choice(string.ascii_letters) for _ in range(3) + ) + if broadcast_id not in self.broadcast_ids: + break + + self.broadcast_ids[broadcast_id] = handler + try: + yield broadcast_id + finally: + self.broadcast_ids.pop(broadcast_id) + + async def start_broadcast(self, broadcast_message, admin_id): + asyncio.create_task(self._start_broadcast(broadcast_message, admin_id)) + + async def _start_broadcast(self, broadcast_message, admin_id): + try: + broadcast_handler = Broadcast( + client=self, broadcast_message=broadcast_message + ) + with self.track_broadcast(broadcast_handler) as broadcast_id: + reply_message = await self.send_message( + chat_id=admin_id, + text="Broadcast started. Use the buttons to check the progress or to cancel the broadcast.", + reply_to_message_id=broadcast_message.message_id, + reply_markup=InlineKeyboardMarkup( + [ + InlineKeyboardButton( + text="Check Progress", + callback_data=f"sts_bdct+{broadcast_id}", + ), + InlineKeyboardButton( + text="Cancel!", + callback_data=f"cncl_bdct+{broadcast_id}", + ), + ] + ), + ) + + await broadcast_handler.start() + + await reply_message.edit_text("Broadcast completed") + except Exception as e: + log.error(e, exc_info=True) diff --git a/bot/utils/README.md b/bot/utils/README.md new file mode 100644 index 00000000..85a0ba9a --- /dev/null +++ b/bot/utils/README.md @@ -0,0 +1,3 @@ +### [š”»š•–š•žš•  š”¹š• š•„](https://telegram.dog/SCREEN_SHOT_ROBOT) + +### [š•˜š•šš•„š•™š•¦š•“šŸ™š•„š•˜](https://github.com/github1tg) diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index 16281fe0..8444092e 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -1 +1 @@ -from .utils import * +from .utils import Utilities, ProcessTypes diff --git a/bot/utils/broadcast.py b/bot/utils/broadcast.py new file mode 100644 index 00000000..949406b3 --- /dev/null +++ b/bot/utils/broadcast.py @@ -0,0 +1,107 @@ +import traceback +import datetime +import logging +import asyncio +import time +import io + +from pyrogram.errors import ( + FloodWait, + InputUserDeactivated, + UserIsBlocked, + PeerIdInvalid, +) + +from bot.database import Database +from bot.config import Config + + +log = logging.getLogger(__name__) +db = Database() + + +class Broadcast: + def __init__(self, client, broadcast_message): + self.client = client + self.broadcast_message = broadcast_message + + self.cancelled = False + self.progress = dict(total=0, current=0, failed=0, success=0) + + def get_progress(self): + return self.progress + + def cancel(self): + self.cancelled = True + + async def _send_msg(self, user_id): + try: + await self.broadcast_message.copy(chat_id=user_id) + return 200, None + except FloodWait as e: + await asyncio.sleep(e.x + 1) + return self._send_msg(user_id) + except InputUserDeactivated as e: + log.error(e) + return 400, f"{user_id} : deactivated\n" + except UserIsBlocked as e: + log.error(e) + return 400, f"{user_id} : blocked the bot\n" + except PeerIdInvalid as e: + log.error(e) + return 400, f"{user_id} : user id invalid\n" + except Exception as e: + log.error(e, exc_info=True) + return 500, f"{user_id} : {traceback.format_exc()}\n" + + async def start(self): + all_users = await db.get_all_users() + + start_time = time.time() + total_users = await db.total_users_count() + done = 0 + failed = 0 + success = 0 + + log_file = io.BytesIO() + log_file.name = f"{datetime.datetime.utcnow()}_broadcast.txt" + broadcast_log = "" + async for user in all_users: + await asyncio.sleep(0.5) + sts, msg = await self._send_msg(user_id=int(user["id"])) + if msg is not None: + broadcast_log += msg + + if sts == 200: + success += 1 + else: + failed += 1 + + if sts == 400: + await db.delete_user(user["id"]) + + done += 1 + self.progress.update(dict(current=done, failed=failed, success=success)) + if self.cancelled: + break + + log_file.write(broadcast_log.encode()) + completed_in = datetime.timedelta(seconds=int(time.time() - start_time)) + await asyncio.sleep(3) + update_text = ( + f"#broadcast completed in `{completed_in}`\n\nTotal users {total_users}.\n" + f"Total done {done}, {success} success and {failed} failed.\n" + "Status: {}".format("Completed" if not self.cancelled else "Cancelled") + ) + + if failed == 0: + await self.client.send_message( + chat_id=Config.LOG_CHANNEL, + text=update_text, + ) + else: + await self.client.send_document( + chat_id=Config.LOG_CHANNEL, + document=log_file, + caption=update_text, + ) diff --git a/bot/utils/utils.py b/bot/utils/utils.py index bb86225b..dd767dbb 100644 --- a/bot/utils/utils.py +++ b/bot/utils/utils.py @@ -1,713 +1,302 @@ import os -import re -import uuid -import time -import math -import shlex import random import asyncio -import datetime -import traceback +import logging +from urllib.parse import urljoin -from pyrogram import InputMediaPhoto, InlineKeyboardMarkup, InlineKeyboardButton -from pyrogram.errors import FloodWait +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton -from ..config import Config +from bot.config import Config +log = logging.getLogger(__name__) -def is_valid_file(msg): - if not msg.media: + +class ProcessTypes: + SAMPLE_VIDEO = 1 + TRIM_VIDEO = 2 + MANNUAL_SCREENSHOTS = 3 + SCREENSHOTS = 4 + MEDIAINFO = 5 + + +class Utilities: + @staticmethod + def is_valid_file(msg): + if not msg.media: + return False + if msg.video: + return True + if (msg.document) and any( + mime in msg.document.mime_type + for mime in ["video", "application/octet-stream"] + ): + return True return False - if msg.video: - return True - if (msg.document) and any(mime in msg.document.mime_type for mime in ['video', "application/octet-stream"]): - return True - return False - -def is_url(text): - return text.startswith('http') - - -def get_random_start_at(seconds, dur=0): - return random.randint(0, seconds-dur) - - -async def run_subprocess(cmd): - process = await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - return await process.communicate() - - -async def generate_thumbnail_file(file_path, uid): - output_folder = Config.THUMB_OP_FLDR.joinpath(uid) - if not output_folder.exists(): - os.makedirs(output_folder) - - thumb_file = output_folder.joinpath('thumb.jpg') - ffmpeg_cmd = f"ffmpeg -ss 0 -i '{file_path}' -vframes 1 -vf \"scale=320:-1\" -y '{thumb_file}'" - output = await run_subprocess(ffmpeg_cmd) - if not thumb_file.exists(): - return None - return thumb_file - - -def pack_id(msg): - file_id = 0 - chat_id_offset = 2 - pack_bits = 32 - msg_id_offset = pack_bits + chat_id_offset - - file_id |= msg.chat.id << chat_id_offset - file_id |= msg.message_id << msg_id_offset - return file_id - - -def generate_stream_link(media_msg): - file_id = pack_id(media_msg) - return f"{Config.HOST}/stream/{file_id}" - - -async def get_dimentions(input_file_link): - ffprobe_cmd = f"ffprobe -v error -show_entries stream=width,height -of csv=p=0:s=x -select_streams v:0 {shlex.quote(input_file_link)}" - output = await run_subprocess(ffprobe_cmd) - #print(output) - try: - width, height = [int(i.strip()) for i in output[0].decode().split('x')] - except Exception as e: - print(e) - width, height = 1280, 534 - return width, height - - -async def get_duration(input_file_link): - ffmpeg_dur_cmd = f"ffprobe -v error -show_entries format=duration -of csv=p=0:s=x -select_streams v:0 {shlex.quote(input_file_link)}" - #print(ffmpeg_dur_cmd) - out, err = await run_subprocess(ffmpeg_dur_cmd) - out = out.decode().strip() - if not out: - return err.decode() - duration = round(float(out)) - if duration: - return duration - return 'No duration!' - - -async def fix_subtitle_codec(file_link): - fixable_codecs = ['mov_text'] - - ffmpeg_dur_cmd = f"ffprobe -v error -select_streams s -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 {shlex.quote(file_link)}" - - out, err = await run_subprocess(ffmpeg_dur_cmd) - out = out.decode().strip() - if not out: - return '' - - fix_cmd = '' - codecs = [i.strip() for i in out.split('\n')] - for indx, codec in enumerate(codecs): - if any(fixable_codec in codec for fixable_codec in fixable_codecs): - fix_cmd += f'-c:s:{indx} srt ' - - return fix_cmd - - -async def edit_message_text(m, **kwargs): - while True: - try: - return await m.edit_message_text(**kwargs) - except FloodWait as e: - await asyncio.sleep(e.x) - except: - break - - -async def display_settings(c, m, cb=False): - chat_id = m.from_user.id if cb else m.chat.id - - as_file = await c.db.is_as_file(chat_id) - as_round = await c.db.is_as_round(chat_id) - watermark_text = await c.db.get_watermark_text(chat_id) - sample_duration = await c.db.get_sample_duration(chat_id) - watermark_color_code = await c.db.get_watermark_color(chat_id) - screenshot_mode = await c.db.get_screenshot_mode(chat_id) - font_size = await c.db.get_font_size(chat_id) - - sv_btn = [ - InlineKeyboardButton("Sample Video Duration", 'rj'), - InlineKeyboardButton(f"{sample_duration}s", 'set+sv') - ] - wc_btn = [ - InlineKeyboardButton("Watermark Color", 'rj'), - InlineKeyboardButton(f"{Config.COLORS[watermark_color_code]}", 'set+wc') - ] - fs_btn = [ - InlineKeyboardButton("Watermark Font Size", 'rj'), - InlineKeyboardButton(f"{Config.FONT_SIZES_NAME[font_size]}", 'set+fs') - ] - as_file_btn = [InlineKeyboardButton("Upload Mode", 'rj')] - wm_btn = [InlineKeyboardButton("Watermark", 'rj')] - sm_btn = [InlineKeyboardButton("Screenshot Generation Mode", 'rj')] - - - if as_file: - as_file_btn.append(InlineKeyboardButton("šŸ“ Uploading as Document.", 'set+af')) - else: - as_file_btn.append(InlineKeyboardButton("šŸ–¼ļø Uploading as Image.", 'set+af')) - - if watermark_text: - wm_btn.append(InlineKeyboardButton(f"{watermark_text}", 'set+wm')) - else: - wm_btn.append(InlineKeyboardButton("No watermark exists!", 'set+wm')) - - if screenshot_mode == 0: - sm_btn.append(InlineKeyboardButton("Equally spaced screenshots", 'set+sm')) - else: - sm_btn.append(InlineKeyboardButton("Random screenshots", 'set+sm')) - - settings_btn = [as_file_btn, wm_btn, wc_btn, fs_btn, sv_btn, sm_btn] - - if cb: + + @staticmethod + def is_url(text): + return text.startswith("http") + + @staticmethod + def get_random_start_at(seconds, dur=0): + return random.randint(0, seconds - dur) + + @staticmethod + async def run_subprocess(cmd): + process = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + return await process.communicate() + + @staticmethod + async def generate_thumbnail_file(file_path, output_folder): + os.makedirs(output_folder, exist_ok=True) + thumb_file = os.path.join(output_folder, "thumb.jpg") + ffmpeg_cmd = [ + "ffmpeg", + "-ss", + "0", + "-i", + file_path, + "-vframes", + "1", + "-vf", + "scale=320:-1", + "-y", + str(thumb_file), + ] + output = await Utilities.run_subprocess(ffmpeg_cmd) + log.debug(output) + if not os.path.exists(thumb_file): + return None + return thumb_file + + @staticmethod + def generate_stream_link(media_msg): + file_id = media_msg.message_id + chat_id = media_msg.chat.id + return urljoin(Config.HOST, f"file/{chat_id}/{file_id}") + + @staticmethod + async def get_media_info(file_link): + ffprobe_cmd = [ + "ffprobe", + "-headers", + f"IAM:{Config.IAM_HEADER}", + "-i", + file_link, + "-v", + "quiet", + "-of", + "json", + "-show_streams", + "-show_format", + "-show_chapters", + "-show_programs", + ] + data, err = await Utilities.run_subprocess(ffprobe_cmd) + return data + + @staticmethod + async def get_dimentions(file_link): + ffprobe_cmd = [ + "ffprobe", + "-headers", + f"IAM:{Config.IAM_HEADER}", + "-i", + file_link, + "-v", + "error", + "-show_entries", + "stream=width,height", + "-of", + "csv=p=0:s=x", + "-select_streams", + "v:0", + ] + + output = await Utilities.run_subprocess(ffprobe_cmd) + log.debug(output) try: - await m.edit_message_reply_markup( - InlineKeyboardMarkup(settings_btn) - ) - except: - pass - return - - await m.reply_text( - text = f"Here You can configure my behavior.", - quote=True, - reply_markup=InlineKeyboardMarkup(settings_btn) - ) - - -async def screenshot_fn(c, m): - - chat_id = m.from_user.id - if c.CURRENT_PROCESSES.get(chat_id, 0) == Config.MAX_PROCESSES_PER_USER: - await m.answer('You have reached the maximum parallel processes! Try again after one of them finishes.', show_alert=True) - return - if not c.CURRENT_PROCESSES.get(chat_id): - c.CURRENT_PROCESSES[chat_id] = 0 - c.CURRENT_PROCESSES[chat_id] += 1 - - _, num_screenshots = m.data.split('+') - num_screenshots = int(num_screenshots) - media_msg = m.message.reply_to_message - #print(media_msg) - if media_msg.empty: - await edit_message_text(m, text='Why did you delete the file 😠, Now i cannot help you šŸ˜’.') - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - uid = str(uuid.uuid4()) - output_folder = Config.SCRST_OP_FLDR.joinpath(uid) - if not output_folder.exists(): - os.makedirs(output_folder) - - if Config.TRACK_CHANNEL: - tr_msg = await media_msg.forward(Config.TRACK_CHANNEL) - await tr_msg.reply_text(f"User id: `{chat_id}`") - - if media_msg.media: - typ = 1 - else: - typ = 2 - - try: - start_time = time.time() - - await edit_message_text(m, text='Processing your request, Please wait! 😓') - - if typ == 2: - file_link = media_msg.text + width, height = [int(i.strip()) for i in output[0].decode().split("x")] + except Exception as e: + log.debug(e, exc_info=True) + width, height = 1280, 534 + return width, height + + @staticmethod + async def get_duration(file_link): + ffmpeg_dur_cmd = [ + "ffprobe", + "-headers", + f"IAM:{Config.IAM_HEADER}", + "-i", + file_link, + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "csv=p=0:s=x", + "-select_streams", + "v:0", + ] + out, err = await Utilities.run_subprocess(ffmpeg_dur_cmd) + log.debug(f"{out} \n {err}") + out = out.decode().strip() + if not out: + return err.decode() + duration = round(float(out)) + if duration: + return duration + return "No duration!" + + @staticmethod + async def fix_subtitle_codec(file_link): + fixable_codecs = ["mov_text"] + + ffmpeg_dur_cmd = [ + "ffprobe", + "-headers", + f"IAM:{Config.IAM_HEADER}", + "-i", + file_link, + "-v", + "error", + "-select_streams", + "s", + "-show_entries", + "stream=codec_name", + "-of", + "default=noprint_wrappers=1:nokey=1", + ] + + out, err = await Utilities.run_subprocess(ffmpeg_dur_cmd) + log.debug(f"{out} \n {err}") + out = out.decode().strip() + if not out: + return [] + + fix_cmd = [] + codecs = [i.strip() for i in out.split("\n")] + for indx, codec in enumerate(codecs): + if any(fixable_codec in codec for fixable_codec in fixable_codecs): + fix_cmd += [f"-c:s:{indx}", "srt"] + + return fix_cmd + + @staticmethod + def get_watermark_coordinates(pos, width, height): + def gcd(m, n): + return m if not n else gcd(n, m % n) + + def ratio(x, y): + d = gcd(x, y) + return x / d, y / d + + a_ratio = ratio(width, height) + x_fact = 2 + x_pad = round((width * x_fact) / 100) + y_pad = round((x_pad * a_ratio[1]) / a_ratio[0]) + + # https://superuser.com/questions/939357/how-to-position-drawtext-text + + if pos == 0: + return x_pad, y_pad # top left + elif pos == 1: + return "(w-text_w)/2", f"{y_pad}" # top center + elif pos == 2: + return f"w-tw-{x_pad}", f"{y_pad}" # top right + elif pos == 3: + return x_pad, "(h-text_h)/2" # center left + elif pos == 4: + return "(w-text_w)/2", "(h-text_h)/2" # centered + elif pos == 5: + return f"w-tw-{x_pad}", "(h-text_h)/2" # center right + elif pos == 6: + return x_pad, f"h-th-{y_pad}" # bottom left + elif pos == 7: + return "(w-text_w)/2", f"h-th-{y_pad}" # bottom center else: - file_link = generate_stream_link(media_msg) - - await edit_message_text(m, text='šŸ˜€ Generating screenshots!') - - duration = await get_duration(file_link) - if isinstance(duration, str): - await edit_message_text(m, text="😟 Sorry! I cannot open the file.") - l = await media_msg.forward(Config.LOG_CHANNEL) - await l.reply_text(f'stream link : {file_link}\n\nRequested screenshots: {num_screenshots} \n\n{duration}', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return + return f"w-tw-{x_pad}", f"h-th-{y_pad}" # bottom right + + @staticmethod + async def display_settings(c, m, db, cb=False): + chat_id = m.from_user.id if cb else m.chat.id + + as_file = await db.is_as_file(chat_id) + watermark_text = await db.get_watermark_text(chat_id) + sample_duration = await db.get_sample_duration(chat_id) + watermark_color_code = await db.get_watermark_color(chat_id) + watermark_position = await db.get_watermark_position(chat_id) + screenshot_mode = await db.get_screenshot_mode(chat_id) + font_size = await db.get_font_size(chat_id) + + sv_btn = [ + InlineKeyboardButton("Sample Video Duration", "rj"), + InlineKeyboardButton(f"{sample_duration}s", "set+sv"), + ] + wc_btn = [ + InlineKeyboardButton("Watermark Color", "rj"), + InlineKeyboardButton(f"{Config.COLORS[watermark_color_code]}", "set+wc"), + ] + fs_btn = [ + InlineKeyboardButton("Watermark Font Size", "rj"), + InlineKeyboardButton(f"{Config.FONT_SIZES_NAME[font_size]}", "set+fs"), + ] + wp_btn = [ + InlineKeyboardButton("Watermark Position", "rj"), + InlineKeyboardButton(f"{Config.POSITIONS[watermark_position]}", "set+wp"), + ] + as_file_btn = [InlineKeyboardButton("Upload Mode", "rj")] + wm_btn = [InlineKeyboardButton("Watermark", "rj")] + sm_btn = [InlineKeyboardButton("Screenshot Generation Mode", "rj")] - reduced_sec = duration - int(duration*2 / 100) - print(f"Total seconds: {duration}, Reduced seconds: {reduced_sec}") - screenshots = [] - watermark = await c.db.get_watermark_text(chat_id) - watermark_color_code = await c.db.get_watermark_color(chat_id) - watermark_color = Config.COLORS[watermark_color_code] - as_file = await c.db.is_as_file(chat_id) - screenshot_mode = await c.db.get_screenshot_mode(chat_id) - font_size = await c.db.get_font_size(chat_id) - ffmpeg_errors = '' - - if screenshot_mode == 0: - screenshot_secs = [int(reduced_sec/num_screenshots)*i for i in range(1, 1+num_screenshots)] - else: - screenshot_secs = [get_random_start_at(reduced_sec) for i in range(1, 1+num_screenshots)] - - width, height = await get_dimentions(file_link) - fontsize = int((math.sqrt( width**2 + height**2 ) / 1388.0) * Config.FONT_SIZES[font_size]) - - for i, sec in enumerate(screenshot_secs): - thumbnail_template = output_folder.joinpath(f'{i+1}.png') - #print(sec) - ffmpeg_cmd = f"ffmpeg -hide_banner -ss {sec} -i {shlex.quote(file_link)} -vf \"drawtext=fontcolor={watermark_color}:fontsize={fontsize}:x=20:y=H-th-10:text='{shlex.quote(watermark)}', scale=1280:-1\" -y -vframes 1 '{thumbnail_template}'" - output = await run_subprocess(ffmpeg_cmd) - await edit_message_text(m, text=f'šŸ˜€ `{i+1}` of `{num_screenshots}` generated!') - if thumbnail_template.exists(): - if as_file: - screenshots.append({ - 'document':str(thumbnail_template), - 'caption':f"ScreenShot at {datetime.timedelta(seconds=sec)}" - }) - else: - screenshots.append( - InputMediaPhoto( - str(thumbnail_template), - caption=f"ScreenShot at {datetime.timedelta(seconds=sec)}" - ) - ) - continue - ffmpeg_errors += output[1].decode() + '\n\n' - - #print(screenshots) - if not screenshots: - await edit_message_text(m, text='😟 Sorry! Screenshot generation failed possibly due to some infrastructure failure 😄.') - - l = await media_msg.forward(Config.LOG_CHANNEL) - if ffmpeg_errors: - error_file = f"{uid}-errors.txt" - with open(error_file, 'w') as f: - f.write(ffmpeg_errors) - await l.reply_document(error_file, caption=f"stream link : {file_link}\n\n{num_screenshots} screenshots where requested and Screen shots where not generated.") - os.remove(error_file) - else: - await l.reply_text(f'stream link : {file_link}\n\n{num_screenshots} screenshots where requested and Screen shots where not generated.', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - await edit_message_text(m, text=f'šŸ¤“ You requested {num_screenshots} screenshots and {len(screenshots)} screenshots generated, Now starting to upload!') - - await media_msg.reply_chat_action("upload_photo") - if as_file: - aws = [media_msg.reply_document(quote=True, **photo) for photo in screenshots] - await asyncio.gather(*aws) - else: - await media_msg.reply_media_group(screenshots, True) - - await edit_message_text(m, text=f'Successfully completed process in {datetime.timedelta(seconds=int(time.time()-start_time))}\n\n\n\nĀ©ļø @odbots @TGBotsZ @InFoTelGroup') - c.CURRENT_PROCESSES[chat_id] -= 1 - - except: - traceback.print_exc() - await edit_message_text(m, text='😟 Sorry! Screenshot generation failed possibly due to some infrastructure failure 😄.') - - l = await media_msg.forward(Config.LOG_CHANNEL) - await l.reply_text(f'{num_screenshots} screenshots where requested and some error occoured\n\n{traceback.format_exc()}', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - - -async def sample_fn(c, m): - chat_id = m.from_user.id - if c.CURRENT_PROCESSES.get(chat_id, 0) == Config.MAX_PROCESSES_PER_USER: - await m.answer('You have reached the maximum parallel processes! Try again after one of them finishes.', show_alert=True) - return - if not c.CURRENT_PROCESSES.get(chat_id): - c.CURRENT_PROCESSES[chat_id] = 0 - c.CURRENT_PROCESSES[chat_id] += 1 - - media_msg = m.message.reply_to_message - if media_msg.empty: - await edit_message_text(m, text='Why did you delete the file 😠, Now i cannot help you šŸ˜’.') - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - uid = str(uuid.uuid4()) - output_folder = Config.SMPL_OP_FLDR.joinpath(uid) - if not output_folder.exists(): - os.makedirs(output_folder) - - if Config.TRACK_CHANNEL: - tr_msg = await media_msg.forward(Config.TRACK_CHANNEL) - await tr_msg.reply_text(f"User id: `{chat_id}`") - - if media_msg.media: - typ = 1 - else: - typ = 2 - - try: - start_time = time.time() - - await edit_message_text(m, text='Processing your request, Please wait! 😓') - - if typ == 2: - file_link = media_msg.text - else: - file_link = generate_stream_link(media_msg) - - await edit_message_text(m, text='šŸ˜€ Generating Sample Video! This might take some time.') - - duration = await get_duration(file_link) - if isinstance(duration, str): - await edit_message_text(m, text="😟 Sorry! I cannot open the file.") - l = await media_msg.forward(Config.LOG_CHANNEL) - await l.reply_text(f'stream link : {file_link}\n\nSample video requested\n\n{duration}', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - reduced_sec = duration - int(duration*10 / 100) - print(f"Total seconds: {duration}, Reduced seconds: {reduced_sec}") - sample_duration = await c.db.get_sample_duration(chat_id) - - start_at = get_random_start_at(reduced_sec, sample_duration) - - sample_file = output_folder.joinpath(f'sample_video.mkv') - subtitle_option = await fix_subtitle_codec(file_link) - - ffmpeg_cmd = f"ffmpeg -hide_banner -ss {start_at} -i {shlex.quote(file_link)} -t {sample_duration} -map 0 -c copy {subtitle_option} {sample_file}" - output = await run_subprocess(ffmpeg_cmd) - #print(output[1].decode()) - - if not sample_file.exists(): - await edit_message_text(m, text='😟 Sorry! Sample video generation failed possibly due to some infrastructure failure 😄.') - - l = await media_msg.forward(Config.LOG_CHANNEL) - await l.reply_text(f'stream link : {file_link}\n\n duration {sample_duration} sample video generation failed\n\n{output[1].decode()}', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - thumb = await generate_thumbnail_file(sample_file, uid) - - await edit_message_text(m, text=f'šŸ¤“ Sample video was generated successfully!, Now starting to upload!') - - await media_msg.reply_chat_action("upload_video") - - await media_msg.reply_video( - video=sample_file, - quote=True, - caption=f"Sample video. {sample_duration}s from {datetime.timedelta(seconds=start_at)}", - duration=sample_duration, - thumb=thumb, - supports_streaming=True + as_file_btn.append( + InlineKeyboardButton("šŸ—‚ļø Uploading as Document šŸ—‚ļø.", "set+af") ) - - await edit_message_text(m, text=f'Successfully completed process in {datetime.timedelta(seconds=int(time.time()-start_time))}\n\n\n\nĀ©ļø @odbots @TGBotsZ @InFoTelGroup') - c.CURRENT_PROCESSES[chat_id] -= 1 - - except: - traceback.print_exc() - await edit_message_text(m, text='😟 Sorry! Sample video generation failed possibly due to some infrastructure failure 😄.') - - l = await media_msg.forward(Config.LOG_CHANNEL) - await l.reply_text(f'sample video requested and some error occoured\n\n{traceback.format_exc()}', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - - -async def trim_fn(c, m): - chat_id = m.chat.id - if c.CURRENT_PROCESSES.get(chat_id, 0) == Config.MAX_PROCESSES_PER_USER: - await m.reply_text('You have reached the maximum parallel processes! Try again after one of them finishes.', True) - return - if not c.CURRENT_PROCESSES.get(chat_id): - c.CURRENT_PROCESSES[chat_id] = 0 - c.CURRENT_PROCESSES[chat_id] += 1 - - message = await c.get_messages( - chat_id, - m.reply_to_message.message_id - ) - await m.reply_to_message.delete() - media_msg = message.reply_to_message - - if media_msg.empty: - await m.reply_text('Why did you delete the file 😠, Now i cannot help you šŸ˜’.', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - try: - start, end = [int(i) for i in m.text.split(':')] - except: - await m.reply_text('Please follow the specified format', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - if (start >= end) or (start < 0): - await m.reply_text('Invalid range!', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - request_duration = end-start - - if request_duration > Config.MAX_TRIM_DURATION: - await m.reply_text(f'Please provide any range that\'s upto {Config.MAX_TRIM_DURATION}s. Your requested range **{start}:{end}** is `{request_duration}s` long!', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - uid = str(uuid.uuid4()) - output_folder = Config.SMPL_OP_FLDR.joinpath(uid) - if not output_folder.exists(): - os.makedirs(output_folder) - - if Config.TRACK_CHANNEL: - tr_msg = await media_msg.forward(Config.TRACK_CHANNEL) - await tr_msg.reply_text(f"User id: `{chat_id}`") - - if media_msg.media: - typ = 1 - else: - typ = 2 - - snt = await m.reply_text('Processing your request, Please wait! 😓', True) - - try: - start_time = time.time() - - if typ == 2: - file_link = media_msg.text else: - file_link = generate_stream_link(media_msg) - - await snt.edit_text('šŸ˜€ Trimming Your Video! This might take some time.') - - duration = await get_duration(file_link) - if isinstance(duration, str): - await snt.edit_text("😟 Sorry! I cannot open the file.") - l = await media_msg.forward(Config.LOG_CHANNEL) - await l.reply_text(f'stream link : {file_link}\n\ntrim video requested\n\n{start}:{end}', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - if (start>=duration) or (end>=duration): - await snt.edit_text("😟 Sorry! The requested range is out of the video's duration!.") - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - sample_file = output_folder.joinpath(f'trim_video.mkv') - subtitle_option = await fix_subtitle_codec(file_link) - - ffmpeg_cmd = f"ffmpeg -hide_banner -ss {start} -i {shlex.quote(file_link)} -t {request_duration} -map 0 -c copy {subtitle_option} {sample_file}" - output = await run_subprocess(ffmpeg_cmd) - #print(output[1].decode()) - - if not sample_file.exists(): - await snt.edit_text('😟 Sorry! video trimming failed possibly due to some infrastructure failure 😄.') - - l = await media_msg.forward(Config.LOG_CHANNEL) - await l.reply_text(f'stream link : {file_link}\n\nVideo trimm failed. **{start}:{end}**\n\n{output[1].decode()}', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - thumb = await generate_thumbnail_file(sample_file, uid) - - await snt.edit_text('šŸ¤“ Video trimmed successfully!, Now starting to upload!') - - await m.reply_chat_action("upload_video") - - await m.reply_video( - video=sample_file, - quote=True, - caption=f"Trimmed video from {datetime.timedelta(seconds=start)} to {datetime.timedelta(seconds=end)}", - duration=request_duration, - thumb=thumb, - supports_streaming=True - ) - - await snt.edit_text(f'Successfully completed process in {datetime.timedelta(seconds=int(time.time()-start_time))}\n\n\n\nĀ©ļø @odbots @TGBotsZ @InFoTelGroup') - c.CURRENT_PROCESSES[chat_id] -= 1 - - except: - traceback.print_exc() - await snt.edit_text('😟 Sorry! Video trimming failed possibly due to some infrastructure failure 😄.') - - l = await media_msg.forward(Config.LOG_CHANNEL) - await l.reply_text(f'sample video requested and some error occoured\n\n{traceback.format_exc()}', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - -async def manual_screenshot_fn(c, m): - - chat_id = m.chat.id - if c.CURRENT_PROCESSES.get(chat_id, 0) == Config.MAX_PROCESSES_PER_USER: - await m.reply_text('You have reached the maximum parallel processes! Try again after one of them finishes.', True) - return - if not c.CURRENT_PROCESSES.get(chat_id): - c.CURRENT_PROCESSES[chat_id] = 0 - c.CURRENT_PROCESSES[chat_id] += 1 - - message = await c.get_messages( - chat_id, - m.reply_to_message.message_id - ) - await m.reply_to_message.delete() - media_msg = message.reply_to_message - - if media_msg.empty: - await m.reply_text('Why did you delete the file 😠, Now i cannot help you šŸ˜’.', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - try: - raw_user_input = [int(i.strip()) for i in m.text.split(',')] - except: - await m.reply_text('Please follow the specified format', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - uid = str(uuid.uuid4()) - output_folder = Config.SCRST_OP_FLDR.joinpath(uid) - if not output_folder.exists(): - os.makedirs(output_folder) - - if Config.TRACK_CHANNEL: - tr_msg = await media_msg.forward(Config.TRACK_CHANNEL) - await tr_msg.reply_text(f"User id: `{chat_id}`") - - if media_msg.media: - typ = 1 - else: - typ = 2 - - snt = await m.reply_text('Processing your request, Please wait! 😓', True) - - try: - start_time = time.time() - - if typ == 2: - file_link = media_msg.text + as_file_btn.append(InlineKeyboardButton("šŸ“ø Uploading as Image šŸ“ø.", "set+af")) + + if watermark_text: + wm_btn.append(InlineKeyboardButton(f"{watermark_text}", "set+wm")) else: - file_link = generate_stream_link(media_msg) - - duration = await get_duration(file_link) - if isinstance(duration, str): - await snt.edit_text("😟 Sorry! I cannot open the file.") - - l = await media_msg.forward(Config.LOG_CHANNEL) - await l.reply_text(f'stream link : {file_link}\n\nRequested manual screenshots\n\n{duration}', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - valid_positions = [] - invalid_positions = [] - for pos in raw_user_input: - if pos<0 or pos>duration: - invalid_positions.append(str(pos)) - else: - valid_positions.append(pos) - - if not valid_positions: - await snt.edit_text("😟 Sorry! None of the given positions where valid!") - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - if len(valid_positions) > 10: - await snt.edit_text(f"😟 Sorry! Only 10 screenshots can be generated. Found {len(valid_positions)} valid positions in your request") - c.CURRENT_PROCESSES[chat_id] -= 1 - return - - if invalid_positions: - invalid_position_str = ', '.join(invalid_positions) - txt = f"Found {len(invalid_positions)} invalid positions ({invalid_position_str}).\n\nšŸ˜€ Generating screenshots after ignoring these!." + wm_btn.append(InlineKeyboardButton("No watermark exists!", "set+wm")) + + if screenshot_mode == 0: + sm_btn.append(InlineKeyboardButton("Equally spaced screenshots", "set+sm")) else: - txt = 'šŸ˜€ Generating screenshots!.' - - await snt.edit_text(txt) - - screenshots = [] - watermark = await c.db.get_watermark_text(chat_id) - watermark_color_code = await c.db.get_watermark_color(chat_id) - watermark_color = Config.COLORS[watermark_color_code] - as_file = await c.db.is_as_file(chat_id) - font_size = await c.db.get_font_size(chat_id) - ffmpeg_errors = '' - - width, height = await get_dimentions(file_link) - fontsize = int((math.sqrt( width**2 + height**2 ) / 1388.0) * Config.FONT_SIZES[font_size]) - - for i, sec in enumerate(valid_positions): - thumbnail_template = output_folder.joinpath(f'{i+1}.png') - #print(sec) - ffmpeg_cmd = f"ffmpeg -hide_banner -ss {sec} -i {shlex.quote(file_link)} -vf \"drawtext=fontcolor={watermark_color}:fontsize={fontsize}:x=20:y=H-th-10:text='{shlex.quote(watermark)}', scale=1280:-1\" -y -vframes 1 '{thumbnail_template}'" - output = await run_subprocess(ffmpeg_cmd) - await snt.edit_text(f'šŸ˜€ `{i+1}` of `{len(valid_positions)}` generated!') - if thumbnail_template.exists(): - if as_file: - screenshots.append({ - 'document':str(thumbnail_template), - 'caption':f"ScreenShot at {datetime.timedelta(seconds=sec)}" - }) - else: - screenshots.append( - InputMediaPhoto( - str(thumbnail_template), - caption=f"ScreenShot at {datetime.timedelta(seconds=sec)}" - ) - ) - continue - ffmpeg_errors += output[0].decode() + '\n' + output[1].decode() + '\n\n' - - #print(screenshots) - if not screenshots: - await snt.edit_text('😟 Sorry! Screenshot generation failed possibly due to some infrastructure failure 😄.') - - l = await media_msg.forward(Config.LOG_CHANNEL) - if ffmpeg_errors: - error_file = f"{uid}-errors.txt" - with open(error_file, 'w') as f: - f.write(ffmpeg_errors) - await l.reply_document(error_file, caption=f"stream link : {file_link}\n\nmanual screenshots {raw_user_input}.") - os.remove(error_file) - else: - await l.reply_text(f'stream link : {file_link}\n\nmanual screenshots {raw_user_input}.', True) - c.CURRENT_PROCESSES[chat_id] -= 1 + sm_btn.append(InlineKeyboardButton("Random screenshots", "set+sm")) + + settings_btn = [as_file_btn, wm_btn, wc_btn, fs_btn, wp_btn, sv_btn, sm_btn] + + if cb: + try: + await m.edit_message_reply_markup(InlineKeyboardMarkup(settings_btn)) + except Exception: + pass return - - await snt.edit_text(text=f'šŸ¤“ You requested {len(valid_positions)} screenshots and {len(screenshots)} screenshots generated, Now starting to upload!') - - await media_msg.reply_chat_action("upload_photo") - - if as_file: - aws = [media_msg.reply_document(quote=True, **photo) for photo in screenshots] - await asyncio.gather(*aws) - else: - await media_msg.reply_media_group(screenshots, True) - - await snt.edit_text(f'Successfully completed process in {datetime.timedelta(seconds=int(time.time()-start_time))}\n\n\n\nĀ©ļø @odbots @TGBotsZ @InFoTelGroup') - c.CURRENT_PROCESSES[chat_id] -= 1 - - except: - traceback.print_exc() - await snt.edit_text('😟 Sorry! Screenshot generation failed possibly due to some infrastructure failure 😄.') - - l = await media_msg.forward(Config.LOG_CHANNEL) - await l.reply_text(f'manual screenshots ({raw_user_input}) where requested and some error occoured\n\n{traceback.format_exc()}', True) - c.CURRENT_PROCESSES[chat_id] -= 1 - -def gen_ik_buttons(): - btns = [] - i_keyboard = [] - for i in range(2, 11): - i_keyboard.append( - InlineKeyboardButton( - f"{i}", - f"scht+{i}" - ) + + await m.reply_text( + text="Here You can configure my behavior.\n\nPress the button to change the settings.", + quote=True, + reply_markup=InlineKeyboardMarkup(settings_btn), ) - if (i>2) and (i%2) == 1: - btns.append(i_keyboard) - i_keyboard = [] - if i==10: - btns.append(i_keyboard) - btns.append([InlineKeyboardButton('Manual Screenshots!', 'mscht')]) - btns.append([InlineKeyboardButton('Trim Video!', 'trim')]) - return btns + + @staticmethod + def gen_ik_buttons(): + btns = [] + i_keyboard = [] + for i in range(1, 21): + i_keyboard.append(InlineKeyboardButton(f"{i}", f"scht+{i}")) + if (i > 1) and (i % 5) == 0: + btns.append(i_keyboard) + i_keyboard = [] + if i == 21: + btns.append(i_keyboard) + btns.append([InlineKeyboardButton("āœ‚ļøš‘“š’‚š’š’–š’‚š’ š‘ŗš’„š’“š’†š’†š’š’”š’‰š’š’•š’”āœ‚ļø", "mscht")]) + btns.append([InlineKeyboardButton("āš™ļøš‘»š’“š’Šš’Ž š‘½š’Šš’…š’†š’āš™ļø", "trim")]) + btns.append([InlineKeyboardButton("šŸ“š‘“š’†š’…š’Šš’‚ š‘°š’š’‡š’š’“š’Žš’‚š’•š’Šš’š’šŸ“", "mi")]) + return btns diff --git a/bot/workers/README.md b/bot/workers/README.md new file mode 100644 index 00000000..85a0ba9a --- /dev/null +++ b/bot/workers/README.md @@ -0,0 +1,3 @@ +### [š”»š•–š•žš•  š”¹š• š•„](https://telegram.dog/SCREEN_SHOT_ROBOT) + +### [š•˜š•šš•„š•™š•¦š•“šŸ™š•„š•˜](https://github.com/github1tg) diff --git a/bot/workers/__init__.py b/bot/workers/__init__.py new file mode 100644 index 00000000..dbda4828 --- /dev/null +++ b/bot/workers/__init__.py @@ -0,0 +1 @@ +from .worker import Worker \ No newline at end of file diff --git a/bot/workers/worker.py b/bot/workers/worker.py new file mode 100644 index 00000000..a74c9ef3 --- /dev/null +++ b/bot/workers/worker.py @@ -0,0 +1,73 @@ +import asyncio +import logging +from collections import defaultdict +from contextlib import asynccontextmanager + +from async_timeout import timeout + +from bot.config import Config + + +logger = logging.getLogger(__name__) + + +class TooMuchProcess(Exception): + pass + + +class Worker: + def __init__(self): + self.worker_count = Config.WORKER_COUNT + self.user_process_count = defaultdict(lambda: 0) + self.queue = asyncio.Queue() + + async def start(self): + for _ in range(self.worker_count): + asyncio.create_task(self._worker()) + logger.debug("Started %s workers", self.worker_count) + + async def stop(self): + logger.debug("Stopping workers") + for _ in range(self.worker_count): + self.new_task(None) + await self.queue.join() + logger.debug("Stopped workers") + + def new_task(self, task): + self.queue.put_nowait(task) + + @asynccontextmanager + async def count_user_process(self, chat_id): + if self.user_process_count[chat_id] >= Config.MAX_PROCESSES_PER_USER: + raise TooMuchProcess + + self.user_process_count[chat_id] += 1 + try: + yield + finally: + self.user_process_count[chat_id] -= 1 + + async def _worker(self): + while True: + task = await self.queue.get() + try: + if task is None: + break + + chat_id, process_factory = task + handler = process_factory.get_handler() + try: + async with self.count_user_process(chat_id), timeout( + Config.TIMEOUT + ): + await handler.process() + except (asyncio.TimeoutError, asyncio.CancelledError): + await handler.cancelled() + except TooMuchProcess: + await asyncio.sleep(10) + self.new_task((chat_id, process_factory)) + + except Exception as e: + logger.error(e, exc_info=True) + finally: + self.queue.task_done() diff --git a/requirements.txt b/requirements.txt index 30cb6c15..4ded562f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ -https://github.com/pyrogram/pyrogram/archive/asyncio.zip -https://github.com/Tinche/aiofiles/archive/master.zip +pyrogram tgcrypto motor dnspython +async-timeout +aiohttp diff --git a/runtime.txt b/runtime.txt index 724c203e..1a181794 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.8.2 +python-3.9.1