From 31c5a603b224419d03aeee04de168198d78a4438 Mon Sep 17 00:00:00 2001 From: Mohsen Falak Date: Sun, 22 May 2022 17:16:31 +0430 Subject: [PATCH] the whole code --- .gitignore | 7 ++ Procfile | 1 + README.md | 142 ++++++++++++++++++++++++++++++++++++++++ app.json | 24 +++++++ bot.py | 31 +++++++++ main.py | 3 + plugins/call_back.py | 36 +++++++++++ plugins/commands.py | 150 +++++++++++++++++++++++++++++++++++++++++++ presets.py | 47 ++++++++++++++ requirements.txt | 2 + runtime.txt | 1 + sample_config.py | 31 +++++++++ support/buttons.py | 24 +++++++ user.py | 25 ++++++++ 14 files changed, 524 insertions(+) create mode 100644 .gitignore create mode 100644 Procfile create mode 100644 README.md create mode 100644 app.json create mode 100644 bot.py create mode 100644 main.py create mode 100644 plugins/call_back.py create mode 100644 plugins/commands.py create mode 100644 presets.py create mode 100644 requirements.txt create mode 100644 runtime.txt create mode 100644 sample_config.py create mode 100644 support/buttons.py create mode 100644 user.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7730a05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.vscode +__pycache__ +lib +config.py +*.session +log.txt +.idea diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..c445cb1 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +worker: python3 main.py \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d0e08e --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +
+ +[![Contributors][contributors-shield]][contributors-url] +[![Forks][forks-shield]][forks-url] +[![Stargazers][stars-shield]][stars-url] +[![Issues][issues-shield]][issues-url] +[![GPL3 License][license-shield]][license-url] + + +
+
+ + Logo + + +

uniquify bot

+ +

+ Uniquify bot is a Telegram bot that deletes duplicate files (Video, Audio, Photo, Voice, and Document) from target chat. +
+ Report Bug + ยท + Request Feature +

+
+ +
+ Table of Contents +
    +
  1. + About The Project + +
  2. +
  3. + Getting Started + +
  4. +
  5. Contributing
  6. +
  7. License
  8. +
  9. Contact
  10. +
+
+ + + +## About The Project + +uniquify bot will help you to find and delete duplicate files in your channel or group. +this bot uses `file_unique_id` to find duplicate files, so we can assure you that there is the same file as the deleted file in your group or channel. + +

(back to top)

+ + + +### Built With + +* [Python](https://www.python.org/) +* [pyrogram](https://pyrogram.org/) + +

(back to top)

+ + +## Getting Started + +To get a local copy up and running follow these simple example steps. + +### Installation + +1. Get your `api_id` and `api_hash` in [https://my.telegram.org](https://my.telegram.org) +2. Create a new bot in [https://t.me/botfather](https://t.me/botfather) +3. Clone the repo + ```sh + git clone https://github.com/OxMohsen/uniquify-bot.git + ``` +4. navigate into the new folder + ```sh + cd uniquify-bot + ``` +5. Install python packages + ```sh + pip3 install -r requirements.txt + ``` +5. rename the `sample-config.py` to `config.py` +6. fill the `config.py` with your data +7. run the bot + ```sh + python3 main.py + ``` + +if you using heroku, you can deploy the bot with the following button.
+ + + + +

(back to top)

+ +## Contributing + +Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". +Don't forget to give the project a star! Thanks again! + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +

(back to top)

+ + +## License + +Distributed under the GPLv3 License. See `LICENSE` for more information. + +

(back to top)

+ + +## Contact + +Your Name - [@OxMohsen](https://twitter.com/OxMohsen) - oxmohsen@oxmohsen.ir + +Project Link: [https://github.com/OxMohsen/uniquify-bot](https://github.com/OxMohsen/uniquify-bot) + +

(back to top)

+ + +[contributors-shield]: https://img.shields.io/github/contributors/OxMohsen/uniquify-bot.svg?style=for-the-badge +[contributors-url]: https://github.com/OxMohsen/uniquify-bot/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/OxMohsen/uniquify-bot.svg?style=for-the-badge +[forks-url]: https://github.com/OxMohsen/uniquify-bot/network/members +[stars-shield]: https://img.shields.io/github/stars/OxMohsen/uniquify-bot.svg?style=for-the-badge +[stars-url]: https://github.com/OxMohsen/uniquify-bot/stargazers +[issues-shield]: https://img.shields.io/github/issues/OxMohsen/uniquify-bot.svg?style=for-the-badge +[issues-url]: https://github.com/OxMohsen/uniquify-bot/issues +[license-shield]: https://img.shields.io/github/license/OxMohsen/uniquify-bot.svg?style=for-the-badge +[license-url]: https://github.com/OxMohsen/uniquify-bot/blob/master/LICENSE \ No newline at end of file diff --git a/app.json b/app.json new file mode 100644 index 0000000..2e5eac7 --- /dev/null +++ b/app.json @@ -0,0 +1,24 @@ +{ + "name": "uniquify-bot", + "description": "Telegram bot to remove duplicate media from a chat", + "keywords": ["telegram", "duplicate", "media", "removing", "bot"], + "logo": "https://img.apksum.com/4c/com.duplicate.files.remover.duplicatefinderfiles/1.0/icon.png", + "website": "https://github.com/OxMohsen/uniquify-bot", + "repository": "https://github.com/OxMohsen/uniquify-bot", + "env": { + "APP_ID": {"description": "Get this value from https://my.telegram.org", "required": true}, + "API_HASH": {"description": "Get this value from https://my.telegram.org" , "required": true}, + "TG_BOT_TOKEN": {"description": "Get bot token from @BotFather bot","required": true}, + "TG_USER_SESSION": {"description": "String session from an admin user.","required": true}, + "AUTH_USERS": {"description": "User ids of authorized users separated by space", "required": true} + }, + "buildpacks": [ + {"url": "heroku/python"} + ], + "formation": { + "worker": { + "quantity": 1, + "size": "free" + } + } +} diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..d0fe9c8 --- /dev/null +++ b/bot.py @@ -0,0 +1,31 @@ +from pyrogram import Client, enums + +from config import LOGGER, Config +from user import User + + +class Bot(Client): + USER: User = None + USER_ID: int = None + + def __init__(self): + super().__init__( + "oxmohsen_bot", + api_hash=Config.API_HASH, + api_id=Config.APP_ID, + bot_token=Config.TG_BOT_TOKEN, + sleep_threshold=0, + plugins={"root": "plugins"}, + ) + self.LOGGER = LOGGER + + async def start(self): + await super().start() + usr_bot_me = await self.get_me() + self.set_parse_mode(enums.ParseMode.HTML) + self.LOGGER(__name__).info(f"Bot {usr_bot_me.first_name} (@{usr_bot_me.username}) started!") + self.USER, self.USER_ID = await User().start() + + async def stop(self, *args): + await super().stop() + self.LOGGER(__name__).info("Bot stopped. Bye.") diff --git a/main.py b/main.py new file mode 100644 index 0000000..1ff905f --- /dev/null +++ b/main.py @@ -0,0 +1,3 @@ +from bot import Bot + +Bot().run() diff --git a/plugins/call_back.py b/plugins/call_back.py new file mode 100644 index 0000000..9bb2867 --- /dev/null +++ b/plugins/call_back.py @@ -0,0 +1,36 @@ +from bot import Bot +from presets import Presets +from pyrogram import Client, filters +from pyrogram.types import CallbackQuery +from support.buttons import reply_markup_back, reply_markup_start + +from plugins.commands import purge_status + + +@Client.on_callback_query(filters.regex(r"^help_btn$")) +async def help_button(b: Bot, cb: CallbackQuery): + await cb.answer() + await cb.message.edit_text(Presets.HELP_MESSAGE, reply_markup=reply_markup_back) + + +@Client.on_callback_query(filters.regex(r"^back_btn$")) +async def back_button(b: Bot, cb: CallbackQuery): + await cb.answer() + await cb.message.edit_text( + Presets.WELCOME_MSG.format(cb.from_user.mention), + reply_markup=reply_markup_start, + ) + + +@Client.on_callback_query(filters.regex(r"^close_btn$")) +async def close_button(b: Bot, cb: CallbackQuery): + await cb.message.delete() + + +@Client.on_callback_query(filters.regex(r"^cancel_btn$")) +async def cancel_button(b: Bot, cb: CallbackQuery): + id = int(cb.from_user.id) + try: + purge_status.pop(id) + except Exception: + pass diff --git a/plugins/commands.py b/plugins/commands.py new file mode 100644 index 0000000..79fe7dd --- /dev/null +++ b/plugins/commands.py @@ -0,0 +1,150 @@ +import asyncio + +from bot import Bot +from config import Config +from presets import Presets +from pyrogram import filters, enums +from pyrogram.errors import FloodWait +from pyrogram.types import Message +from support.buttons import reply_markup_cancel, reply_markup_close, reply_markup_start + +purge_status = {} +chat = {} +main_delay = {} + + +@Bot.on_message(filters.private & filters.command(["start", "help"])) +async def start_bot(c: Bot, m: Message): + await m.reply_text( + Presets.WELCOME_MSG.format(m.from_user.mention), + parse_mode=enums.ParseMode.HTML, + disable_web_page_preview=True, + reply_markup=reply_markup_start, + ) + + +@Bot.on_message(filters.private & filters.command("chat")) +async def config_chat(c: Bot, m: Message): + id = int(m.from_user.id) + msg = await m.reply_text(Presets.WAIT_MSG, m.id) + if id not in Config.AUTH_USERS: + await msg.edit_text(Presets.NOT_AUTH_TXT, reply_markup=reply_markup_close) + return + chat_id = str() + status = [] + await asyncio.sleep(1) + if (" " in m.text) and (m.text.split(" ")[1][2:].isdigit()): + chat_str = m.text.split(" ")[1] + if str(chat_str).startswith("-100"): + chat_id = chat_str + else: + chat_id = "-100" + chat_str + me = await c.USER.get_me() + try: + status = await c.USER.get_chat_member(int(chat_id), me.id) + except Exception: + await msg.edit(Presets.NOT_IN_CHAT, reply_markup=reply_markup_close) + return + if status.privileges.can_delete_messages: + chat[id] = int(chat_id) + await msg.edit(Presets.CHAT_ID_CNF.format(chat_id)) + else: + await msg.edit( + Presets.INCORRECT_PERMISSION, reply_markup=reply_markup_close + ) + else: + await m.delete() + await msg.edit(Presets.INVALID_CHAT, reply_markup=reply_markup_close) + + +@Bot.on_message(filters.private & filters.command("delay")) +async def purge_delay(c: Bot, m: Message): + id = int(m.from_user.id) + msg = await m.reply_text(Presets.WAIT_MSG, m.id) + if id not in Config.AUTH_USERS: + await msg.edit_text(Presets.NOT_AUTH_TXT, reply_markup=reply_markup_close) + return + if (" " in m.text) and (m.text.split(" ")[1].isdigit()): + value = int(m.text.split(" ")[1]) + main_delay[id] = value + await msg.edit_text( + Presets.DELAY_CNF.format(value), reply_markup=reply_markup_close + ) + else: + await m.delete() + await msg.edit(Presets.INVALID_DELAY, reply_markup=reply_markup_close) + + +@Bot.on_message(filters.private & filters.command("purge")) +async def delete_duplicates(c: Bot, m: Message): + id_index = [] + duplicates = delay = count = int() + id = int(m.from_user.id) + purge_status[id] = id + msg1 = await m.reply_text(Presets.PROCESSING_MSG) + await asyncio.sleep(1) + msg2 = await m.reply_text(Presets.CHECKING_MSG, reply_markup=reply_markup_cancel) + if id in main_delay: + delay = main_delay[id] + if id in chat: + await msg2.edit_text(Presets.CANCEL_TEXT, reply_markup=reply_markup_cancel) + async for message in c.USER.get_chat_history(chat[id]): + if id not in purge_status: + try: + chat.pop(id) + main_delay.pop(id) + except Exception: + pass + if not duplicates: + await msg1.delete() + await msg2.edit_text( + Presets.CANCELLED_MSG, reply_markup=reply_markup_close + ) + return + if message and (not message.empty) and (id in purge_status): + for file_type in tuple(Presets.FILE_TYPES): + media = getattr(message, file_type, None) + if media is not None: + uid = str(media.file_unique_id) + if uid in id_index: + try: + await c.USER.delete_messages(chat[id], message.id) + except FloodWait as e: + await asyncio.sleep(e.x) + except Exception: + pass + duplicates += 1 + try: + await msg1.edit_text( + Presets.DELETING_MSGS.format(duplicates, message.id) + ) + except FloodWait as e: + await asyncio.sleep(e.x) + except Exception: + pass + await asyncio.sleep(delay) + else: + id_index.append(uid) + count += 1 + if count >= 99: + count = int() + await asyncio.sleep(3) + else: + pass + if not duplicates: + await msg1.delete() + await msg2.edit_text(Presets.NO_DUPLICATES, reply_markup=reply_markup_close) + else: + await msg2.edit_text( + Presets.PROCESS_FINISHED_TEXT, reply_markup=reply_markup_close + ) + try: + chat.pop(id) + purge_status.pop(id) + main_delay.pop(id) + except Exception: + pass + else: + await m.delete() + await msg2.delete() + await msg1.edit(Presets.PURGE_ERROR, reply_markup=reply_markup_close) diff --git a/presets.py b/presets.py new file mode 100644 index 0000000..8417779 --- /dev/null +++ b/presets.py @@ -0,0 +1,47 @@ +class Presets(object): + FILE_TYPES = ["photo", "animation", "document", "video", "audio"] + WELCOME_MSG = "Hello {} ๐Ÿ‘‹,\nI can remove duplicate media files from your chat. For more, click on the help button." + HELP_MESSAGE = """ +By using this bot you can delete duplicate media in target chat. + +The bot is only an interface to do the process, while the user is doing the deleting job. The bot doesn't need to be in the chat. + +The user must be an admin with the Delete Messages privilege in the target chat. + +Supported media are document, video, photo, animation, and audio file. + +Duplicate Media counter and current message-id will be displayed in the UI with the process cancel button. + +The list of Admin commands is: +/chat -100xxxxxxxxxx - Set the target chat +/delay 10 - 10 second delay +/purge - Delete duplicate media + """ + WAIT_MSG = "Please wait..." + NOT_AUTH_TXT = "You are not Authorized !" + CHECKING_MSG = "Looking for Duplicates.." + CANCELLED_MSG = "Purging Cancelled by user" + NO_DUPLICATES = ( + "Congrats ๐ŸŽ‰\nThere are no duplicate media in the target chat." + ) + DELAY_CNF = ( + "Success โœ…\nA Delay of {} Seconds will be applied in the process." + ) + INVALID_DELAY = ( + "Error โŽ\nInput must be in the format of\n>> /delay 10" + ) + CHAT_ID_CNF = "Success โœ…\nChat id {} saved !\n\nYou can now execute /purge command." + NOT_IN_CHAT = "Error โŽ\nThe user is not a member of the target chat. Join the target chat as an admin, and try again later." + INCORRECT_PERMISSION = "Error โŽ\nthe user is not an admin or doesn't have the privilege to delete messages in the target chat." + INVALID_CHAT = "Invalid Input โŽ\n/chat -10025486542156" + DELETING_MSGS = """ +Messages deleted : {} +\xad \xad +Message id covered: {} + """ + CANCEL_TEXT = "\xad Click to cancel \xad" + PROCESS_FINISHED_TEXT = "Success โœ…\nAll duplicate media were deleted." + PROCESSING_MSG = "Please wait...\nThis will take some time to find the duplicates. Have a cup of coffee by the time I'll finish it off!" + PURGE_ERROR = ( + "Error โŽ\nConfigure the target chat first by using the /chat command." + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bd65fa6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyrogram +tgcrypto diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000..fadb070 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.10.0 diff --git a/sample_config.py b/sample_config.py new file mode 100644 index 0000000..884ff2d --- /dev/null +++ b/sample_config.py @@ -0,0 +1,31 @@ +import os +import logging + + +logging.basicConfig( + level=logging.INFO, + format="[%(asctime)s - %(levelname)s] - %(name)s - %(message)s", + datefmt="%d-%b-%y %H:%M:%S", +) +logging.getLogger("pyrogram").setLevel(logging.WARNING) + + +class Config(object): + # Get a bot token from @botfather + TG_BOT_TOKEN = os.environ.get("TG_BOT_TOKEN", "") + + # Get from my.telegram.org + APP_ID = int(os.environ.get("APP_ID", "")) + + # Get from my.telegram.org + API_HASH = os.environ.get("API_HASH", "") + + # Authorized users to use this bot + AUTH_USERS = set(int(x) for x in os.environ.get("AUTH_USERS", "").split()) + + # Generate a user session string + TG_USER_SESSION = os.environ.get("TG_USER_SESSION", "oxmohsen") + + +def LOGGER(name: str) -> logging.Logger: + return logging.getLogger(name) diff --git a/support/buttons.py b/support/buttons.py new file mode 100644 index 0000000..658ac8a --- /dev/null +++ b/support/buttons.py @@ -0,0 +1,24 @@ +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup + +cancel_button = [[InlineKeyboardButton("Cancel", callback_data="cancel_btn")]] + +start_button = [ + [ + InlineKeyboardButton("Developer", url="t.me/OxMohsen"), + InlineKeyboardButton("Help", callback_data="help_btn"), + ] +] + +back_button = [[InlineKeyboardButton("โฌ…๏ธ Back", callback_data="back_btn")]] + +close_button = [ + [ + InlineKeyboardButton("Developer", url="t.me/OxMohsen"), + InlineKeyboardButton("Close", callback_data="close_btn"), + ] +] + +reply_markup_close = InlineKeyboardMarkup(close_button) +reply_markup_back = InlineKeyboardMarkup(back_button) +reply_markup_cancel = InlineKeyboardMarkup(cancel_button) +reply_markup_start = InlineKeyboardMarkup(start_button) diff --git a/user.py b/user.py new file mode 100644 index 0000000..bdacbb0 --- /dev/null +++ b/user.py @@ -0,0 +1,25 @@ +from pyrogram import Client, enums + +from config import LOGGER, Config + + +class User(Client): + def __init__(self): + super().__init__( + Config.TG_USER_SESSION, + api_hash=Config.API_HASH, + api_id=Config.APP_ID, + workers=8, + ) + self.LOGGER = LOGGER + + async def start(self): + await super().start() + usr_bot_me = await self.get_me() + self.set_parse_mode(enums.ParseMode.HTML) + self.LOGGER(__name__).info(f"User {usr_bot_me.first_name} (@{usr_bot_me.username}) started!") + return self, usr_bot_me.id + + async def stop(self, *args): + await super().stop() + self.LOGGER(__name__).info("User stopped. Bye.")