├── rclone.conf ├── .dockerignore ├── sticker └── criduck.tgs ├── commands.sh ├── folder.sh ├── .env_sample ├── Dockerfile ├── config └── aria2.conf ├── requirements.txt ├── aria.py ├── LICENSE ├── docker-compose.yaml ├── .gitignore ├── handlers.py ├── README.md ├── bot.py └── services.py /rclone.conf: -------------------------------------------------------------------------------- 1 | [gdrive] 2 | type = drive 3 | scope = drive 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | credentials.json 2 | config 3 | .devcontainer 4 | .vscode 5 | .env -------------------------------------------------------------------------------- /sticker/criduck.tgs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coko8023/kuebikobot/master/sticker/criduck.tgs -------------------------------------------------------------------------------- /commands.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | gdrive upload --service-account credentials.json $1 -p ${DRIVE_FOLDER} --share --delete -------------------------------------------------------------------------------- /folder.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rclone copy downloads/"${1}" gdrive:"${DRIVE_FOLDER_NAME}" 4 | rclone link gdrive:"${DRIVE_FOLDER_NAME}"/"${1}" 5 | rm -rf downloads/"$1" -------------------------------------------------------------------------------- /.env_sample: -------------------------------------------------------------------------------- 1 | BOT_API_KEY= 2 | RPC_SECRET= 3 | BOT_LOG_CHAT= 4 | RCLONE_DRIVE_SERVICE_ACCOUNT_CREDENTIALS='' -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN mkdir -p downloads 6 | COPY requirements.txt ./ 7 | 8 | RUN pip install -r requirements.txt 9 | 10 | SHELL ["/bin/bash", "-c"] 11 | 12 | RUN curl https://rclone.org/install.sh | bash 13 | 14 | COPY rclone.conf /root/.config/rclone/rclone.conf 15 | 16 | COPY . . 17 | 18 | RUN chmod +x commands.sh && chmod +x folder.sh 19 | 20 | CMD ["python","bot.py"] 21 | 22 | -------------------------------------------------------------------------------- /config/aria2.conf: -------------------------------------------------------------------------------- 1 | #directory to save download file 2 | dir=/downloads 3 | save-session=~/.aria2/aria2.session 4 | input-file=~/.aria2/aria2.session 5 | #Allow to run in the background 6 | daemon=true 7 | #Replace rpc-secret with your password 8 | enable-rpc=true 9 | #Allow all source to connect 10 | rpc-allow-origin-all=true 11 | #Default port is 6800 12 | #rpc-listen-port=6800 13 | rpc-listen-all=true 14 | max-concurrent-downloads=5 15 | continue=true 16 | max-connection-per-server=5 17 | min-split-size=10M 18 | split=10 19 | max-overall-download-limit=0 20 | max-download-limit=0 21 | max-overall-upload-limit=0 22 | max-upload-limit=0 23 | file-allocation=none 24 | check-certificate=false 25 | save-session-interval=60 26 | seed-time=0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiocontextvars==0.2.2 2 | aria2p==0.8.1 3 | cachetools==4.0.0 4 | certifi==2019.11.28 5 | cffi==1.14.0 6 | chardet==3.0.4 7 | console-progressbar==1.1.2 8 | contextvars==2.4 9 | cryptography==3.3.2 10 | decorator==4.4.2 11 | future==0.18.2 12 | google-api-core==1.16.0 13 | google-api-python-client==1.8.0 14 | google-auth==1.12.0 15 | google-auth-httplib2==0.0.3 16 | googleapis-common-protos==1.51.0 17 | httplib2==0.19.0 18 | idna==2.9 19 | immutables==0.11 20 | loguru==0.4.1 21 | oauth2client==4.1.3 22 | 23 | protobuf==3.11.3 24 | pyasn1==0.4.8 25 | pyasn1-modules==0.2.8 26 | pycparser==2.20 27 | PyDrive==1.3.1 28 | python-telegram-bot==12.5 29 | pytz==2019.3 30 | PyYAML==5.4 31 | requests==2.23.0 32 | rsa==4.7 33 | six==1.14.0 34 | tornado==6.0.4 35 | uritemplate==3.0.1 36 | urllib3==1.26.5 37 | websocket-client==0.57.0 38 | gunicorn 39 | -------------------------------------------------------------------------------- /aria.py: -------------------------------------------------------------------------------- 1 | import aria2p 2 | import socket 3 | import os 4 | 5 | from aria2p.downloads import Download 6 | 7 | # Resolve IP of aria2 by hostname 8 | host = socket.gethostbyname('aria2-pro') 9 | 10 | 11 | # Instance of Aria2 api 12 | # This line connects the bot to the aria2 rpc server 13 | 14 | aria2: aria2p.API = aria2p.API( 15 | aria2p.Client( 16 | host=f"http://{host}", 17 | secret=os.environ.get("RPC_SECRET") 18 | ) 19 | ) 20 | 21 | 22 | def addDownload(link: str) -> None: 23 | """Adds download link to aria and starts the download 24 | 25 | Args: 26 | link: Download url link 27 | """ 28 | link = link.replace('/mirror', '') 29 | link = link.strip() 30 | download: Download = aria2.add_magnet(link) 31 | while download.is_active: 32 | print("downloading") 33 | if(download.is_complete): 34 | print("Download complete") 35 | 36 | 37 | downloads = aria2.get_downloads() 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sahil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | aria2-pro: 4 | container_name: aria2-pro 5 | image: p3terx/aria2-pro 6 | environment: 7 | - RPC_SECRET=${RPC_SECRET} 8 | - RPC_PORT=6800 9 | 10 | expose: 11 | - "6800" 12 | - "6888" 13 | volumes: 14 | - aria-downloads:/downloads 15 | 16 | healthcheck: 17 | test: ["CMD", "nc", "-zv", "http://localhost:6800"] 18 | interval: 30s 19 | timeout: 10s 20 | retries: 3 21 | start_period: 30s 22 | 23 | restart: unless-stopped 24 | # Since Aria2 will continue to generate logs, limit the log size to 1M to prevent your hard disk from running out of space. 25 | logging: 26 | driver: json-file 27 | options: 28 | max-size: 1m 29 | 30 | pythonbot: 31 | build: 32 | context: . 33 | dockerfile: Dockerfile 34 | 35 | environment: 36 | - BOT_API_KEY=${BOT_API_KEY} 37 | - BOT_LOG_CHAT=${BOT_LOG_CHAT} 38 | - RPC_SECRET=${RPC_SECRET} 39 | - GDRIVE_CONFIG_DIR=/usr/src/app 40 | - RCLONE_DRIVE_SHARED_WITH_ME=true 41 | - DRIVE_FOLDER_NAME=Mirrors 42 | - RCLONE_DRIVE_SERVICE_ACCOUNT_CREDENTIALS=${RCLONE_DRIVE_SERVICE_ACCOUNT_CREDENTIALS} 43 | 44 | volumes: 45 | - aria-downloads:/usr/src/app/downloads 46 | 47 | depends_on: 48 | - aria2-pro 49 | 50 | restart: unless-stopped 51 | 52 | volumes: 53 | aria-downloads: 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | .idea/workspace.xml 127 | .idea/pythonbot.iml 128 | .idea/misc.xml 129 | .idea/pythonbot.iml 130 | 131 | credentials.json 132 | 133 | .devcontainer 134 | rclone_backup.config 135 | 136 | .vscode -------------------------------------------------------------------------------- /handlers.py: -------------------------------------------------------------------------------- 1 | from telegram.ext import updater 2 | import subprocess 3 | import re 4 | import aria 5 | import os 6 | import sys 7 | from threading import Thread 8 | 9 | # Import BOT_LOG_CHAT from environment variable 10 | BOT_LOG_CHAT = os.environ.get("BOT_LOG_CHAT") 11 | 12 | 13 | def button(update, context): 14 | query = update.callback_query 15 | query.answer() 16 | data = query.data 17 | if data.startswith("pause:"): 18 | gid = data.replace('pause:', '') 19 | download = aria.aria2.get_download(gid) 20 | try: 21 | aria.aria2.pause([download]) 22 | print("Download Paused(btn)") 23 | except Exception as e: 24 | print(e) 25 | 26 | if data.startswith("resume:"): 27 | gid = data.replace('resume:', '') 28 | download = aria.aria2.get_download(gid) 29 | try: 30 | download.update() 31 | download.resume() 32 | print("Download Resumed(btn)") 33 | except Exception as e: 34 | print(e) 35 | 36 | if data.startswith("cancel:"): 37 | gid = data.replace('cancel:', '') 38 | download = aria.aria2.get_download(gid) 39 | try: 40 | download.update() 41 | download.remove(force=True, files=True) 42 | print("Download Cancelled(btn)") 43 | except Exception as e: 44 | print(e) 45 | 46 | 47 | def uploader(updater, update, message, download, ftype): 48 | updateText = f"Download Completed \n'{download.name}'\nSize : {(float(download.total_length)/ 1024 ** 2):.2f} MBs" 49 | updater.bot.edit_message_text( 50 | chat_id=message.chat.id, message_id=message.message_id, text=updateText) 51 | current = update.message.reply_text( 52 | f"{download.name} is complete, \nUploading now") 53 | op = "" 54 | if ftype == 'folder': 55 | op = subprocess.run( 56 | ['bash', 'folder.sh', f"{download.name}"], check=True, stdout=subprocess.PIPE).stdout.decode('utf-8') 57 | if ftype == 'file': 58 | op = subprocess.run( 59 | ['bash', 'folder.sh', f"downloads/{download.name}"], check=True, stdout=subprocess.PIPE).stdout.decode('utf-8') 60 | print(op) 61 | link = re.findall( 62 | 'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', op) 63 | print(link) 64 | print("Download instance finished") 65 | updater.bot.send_message(chat_id=BOT_LOG_CHAT, text=f'Upload complete') 66 | updater.bot.edit_message_text( 67 | chat_id=message.chat.id, message_id=current.message_id, text=str(link[0])) 68 | 69 | 70 | def stop_and_restart(updater): 71 | """Stop and restart the bot""" 72 | updater.stop() 73 | os.execl(sys.executable, sys.executable, *sys.argv) 74 | 75 | 76 | def restart(updater, update, context): 77 | update.message.reply_text('Me die! Halp plOx!!...') 78 | Thread(target=stop_and_restart, args=[updater]).start() 79 | 80 | 81 | def update_and_restart(updater, update, context): 82 | try: 83 | update.message.reply_text( 84 | 'I just got an update...\nUnlike your Android phone') 85 | subprocess.run(['git', 'pull']) 86 | Thread(target=stop_and_restart, args=[updater]).start() 87 | except: 88 | print("ERROR") 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Bot logo 4 |

5 | 6 |

kuebiko bot

7 | 8 |
9 | 10 | [![Status](https://img.shields.io/badge/status-active-success.svg)]() 11 | [![Platform](https://img.shields.io/badge/platform-heroku-lightgrey)]() 12 | [![GitHub Issues](https://img.shields.io/github/issues/sahilkr24/kuebikobot)](https://github.com/SahilKr24/kuebikobot/issues) 13 | [![GitHub Pull Requests](https://img.shields.io/github/issues-pr/SahilKr24/kuebikobot)](https://github.com/SahilKr24/kuebikobot/pulls) 14 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) 15 | 16 |
17 | 18 | --- 19 | 20 |

🤖 A telegram bot that deploys to heroku and downloads links and torrents and uploads to google drive and returns public share link. 21 |
22 |

23 | 24 | ## 📝 Table of Contents 25 | 26 | - [About](#about) 27 | - [Demo / Working](#demo) 28 | - [How it works](#working) 29 | - [Usage](#usage) 30 | - [Getting Started](#getting_started) 31 | - [Deploying your own bot](#deployment) 32 | - [Built Using](#built_using) 33 | - [Authors](#authors) 34 | 35 | ## 🧐 About 36 | 37 | This bot is written in python and imports aria2 for downloading files and magnet links and uploads to google drive via drive-cli then returns direct download/share link with visibility set to public for easy sharing. 38 | 39 | This bot is asynchronous and also has pause and cancel buttons of easy management of downloads. 40 | 41 | ## 🎥 Demo / Working 42 | 43 | ![Working](https://media.giphy.com/media/20NLMBm0BkUOwNljwv/giphy.gif) 44 | 45 | Will be updated shortly! 46 | 47 | ## 💭 How it works 48 | 49 | The bot first extracts the link from the the command it's called from and then adds it to aria2 cli via webhooks, after that it will show progress every 2 seconds in form of message updates. 50 | 51 | Once the download completes it will then proceed to upload the file/folder using predefined scripts that will then return the shared link in form of a reply to the original message. 52 | 53 | The entire bot is written in Python 3.7 54 | 55 | ## 🎈 Usage 56 | 57 | To use the bot, type: 58 | 59 | ``` 60 | /help 61 | ``` 62 | or 63 | 64 | ``` 65 | /start (personal message only) 66 | ``` 67 | 68 | All commands, i.e. "/help" **are not** case sensitive. 69 | 70 | The bot will then give you the help context menu. 71 | 72 | ### Start: 73 | 74 | > /help 75 | **Response:** 76 | 77 | **/mirror** :for http(s),ftp and file downloads 78 | 79 | **/magnet** :for torrent magnet links 80 | 81 | **/cancel** :cancel all in progress downloads 82 | 83 | **/list** :get a list of downloads 84 | 85 | use these commands along with your link or magnets. 86 | 87 | ### Example: 88 | 89 | ``` 90 | /mirror https://releases.ubuntu.com/20.04/ubuntu-20.04.1-desktop-amd64.iso 91 | ``` 92 | **Response:** 93 | 94 | >Mirror, [14.08.20 19:57]
95 | >[In reply to Sahil]
96 | >Downloading
97 | >'ubuntu-20.04.1-desktop-amd64.iso'
98 | >Progress : 17.62/2656.00 MBs
99 | >at 33.53 MBps
100 | >[--------------------] 0.7 %
101 | 102 | 103 | >Mirror, [14.08.20 19:59]
104 | >[In reply to Sahil]
105 | >https://drive.google.com/open?id=<- id of the file ->
106 | 107 | >Mirror, [14.08.20 19:59]
108 | >Upload complete
109 | --- 110 | Beep boop. I am a bot. If there are any issues, contact at my [GitHub](https://github.com/SahilKr24/kuebikobot) 111 | 112 | Want to make a similar bot? Check out: [GitHub](https://github.com/SahilKr24/kuebikobot) 113 | 114 | ## 🏁 Getting Started 115 | 116 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [deployment](#deployment) for notes on how to deploy the project on heroku. 117 | 118 | ### Prerequisites 119 | 120 | All the prerequisites are mentioned in the requirements.txt. Additionally, you'll need to install aria2c on your linux machine if you want to run a local version. 121 | You'll also need to get google credentials for the google drive via API dashboard from google developers console. 122 | 123 | ``` 124 | aria2c 125 | credentials.json 126 | bot id from bot father (telegram) 127 | ``` 128 | 129 | ### Installing 130 | 131 | First, copy the credentials.json in the root directory and update the bot id in bot.py at line 14. 132 | 133 | Switch to venv and install all the requirements from requirements.txt 134 | 135 | ``` 136 | run python3 bot.py 137 | ``` 138 | 139 | When you run for the first time, it will ask you to authorise or refresh token for drive using local web server, follow the link from terminal and open in any web browser. After authenticating copy back the access code to cli. This will create token.json which is importantand should be kept securely. 140 | 141 | That's it. 142 | The bot should send a **bot started** message on the channels it addded to verifying it's active. 143 | 144 | You're bot is now ready to use. Yay! 145 | 146 | ## 🚀 Deploying your own bot 147 | 148 | To deploy your bot on heroku, please follow the above steps and then procced below: 149 | Procfile and and other settings have already been added as per needs. 150 | 151 | - **Heroku**: https://github.com/SahilKr24/kuebikobot 152 | 153 | >Push the repo to your local github and set up a deployment in heroku. 154 | Add the following buildpacks under **Settings > Buildpacks** 155 | >heroku/python
156 | >https://github.com/amivin/aria2-heroku.git 157 | the second buildpack will ensure that aria is installed on the dyno on which the bot will run. 158 | 159 | After that deploy your branch and if everything is correctly configured, the bot will reply with **bot started** message in the channels. 160 | 161 | ## ⛏️ Built Using 162 | 163 | - [python-telegram-bot](https://pypi.org/project/python-telegram-bot/) - This library provides a pure Python interface for the Telegram Bot API. 164 | - [Heroku](https://www.heroku.com/) - SaaS hosting platform 165 | - [aria2p](https://pypi.org/project/aria2p/) - Command-line tool and library to interact with an aria2c daemon process with JSON-RPC. 166 | - [aria2c](https://github.com/aria2/aria2) - aria2 is a lightweight multi-protocol & multi-source, cross platform download utility operated in command-line. 167 | - [pydrive](https://pypi.org/project/PyDrive/) - Google Drive API made easy. 168 | 169 | 170 | ## ✍️ Authors 171 | 172 | - [@Rkrohk](https://github.com/Rkrohk) - Inital Idea & Scripting 173 | - [@SahilKr24](https://github.com/SahilKr24) - Scripting & Dev-Ops 174 | 175 | See also the list of [contributors](https://github.com/SahilKr24/kuebikobot/contributors) who participated in this project. 176 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import os 4 | import telegram 5 | from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, Dispatcher, CallbackQueryHandler 6 | from telegram.ext.dispatcher import run_async 7 | import aria 8 | import time 9 | from services import murror, muggnet 10 | from handlers import button 11 | import handlers 12 | 13 | logging.basicConfig(level=logging.DEBUG, 14 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 15 | logger = logging.getLogger() 16 | logger.setLevel(logging.INFO) 17 | 18 | # Resolve BOT API KEY from environment variable 19 | updater = Updater(os.environ.get("BOT_API_KEY"), use_context=True) 20 | 21 | # Resolve BOT LOG CHAT from environment variable 22 | BOT_LOG_CHAT = os.environ.get("BOT_LOG_CHAT") 23 | 24 | 25 | def id(update: telegram.Update, context): 26 | """Sends a message containing the Chat Id for the current chat 27 | 28 | Args: 29 | update: 30 | A telegram incoming update 31 | 32 | """ 33 | 34 | chatid = update.message.chat.id 35 | update.message.reply_text(chatid) 36 | updater.bot.send_message(chat_id=chatid, text="Here is the id") 37 | 38 | 39 | def cri(update: telegram.Update, context): 40 | """Sends a message containing an emoji of a duck crying 41 | 42 | Args: 43 | update: 44 | Telegram incoming update 45 | 46 | """ 47 | 48 | chatid = update.message.chat.id 49 | criemoji = u'\U0001F62D' 50 | f = open('sticker/criduck.tgs', 'rb') 51 | update.message.reply_text(criemoji + criemoji + criemoji) 52 | updater.bot.send_sticker(chat_id=chatid, sticker=f) 53 | 54 | 55 | def start(update: telegram.Update, context): 56 | """Sends a hello message with help commands 57 | 58 | Args: 59 | update: 60 | Telegram incoming update 61 | """ 62 | 63 | update.message.reply_text( 64 | f"Hello " + update.message.chat.first_name + "\nFor help send /help") 65 | 66 | 67 | def help_func(update: telegram.Update, context): 68 | """Sends response for the "/help" command 69 | 70 | Args: 71 | update: 72 | Telegram incoming update 73 | """ 74 | 75 | update.message.reply_text( 76 | f"/mirror :for http(s),ftp and file downloads\n/magnet :for torrent magnet links\n/cancel :cancel all in progress downloads\n/list :get a list of downloads") 77 | 78 | 79 | def listdownloads(update: telegram.Update, context): 80 | """Fetches downloads from the aria server 81 | 82 | Args: 83 | update: 84 | Telegram incoming update 85 | 86 | Returns: 87 | A list of downloads from the aria server 88 | 89 | """ 90 | 91 | listofdownloads = [dwld for dwld in aria.aria2.get_downloads()] 92 | update.message.reply_text(listofdownloads) 93 | 94 | 95 | def cancel(update: telegram.Update, context): 96 | """Cancels all active downloads and removes the files from the server 97 | 98 | Args: 99 | update: 100 | Telegram incoming update 101 | """ 102 | 103 | downloads = aria.aria2.get_downloads() 104 | 105 | try: 106 | aria.aria2.remove(downloads=downloads, files=True, 107 | clean=True, force=True) 108 | except: 109 | print("No downloads to delete") 110 | update.message.reply_text("Cancelling all downloads") 111 | 112 | 113 | def error(update, context): 114 | """Log Errors caused by Updates.""" 115 | updater.bot.send_message( 116 | chat_id=BOT_LOG_CHAT, text=f'Update {update} caused error {context.error}') 117 | logger.warning('Update "%s" caused error "%s"', update, context.error) 118 | 119 | 120 | def custom_error(custom_message: str): 121 | """Sends an error message to the BOT_LOG_CHAT group 122 | 123 | Args: 124 | custom_message: 125 | The custom error message to be sent to the bot log chat 126 | """ 127 | updater.bot.send_message(chat_id=BOT_LOG_CHAT, text=custom_message) 128 | 129 | 130 | @run_async 131 | def mirror(update: telegram.Update, context): 132 | """Asynchronously starts a download and handles the "/mirror" command 133 | 134 | Args: 135 | update: 136 | Telegram incoming update 137 | """ 138 | 139 | try: 140 | murror(update=update, updater=updater, context=context) 141 | except: 142 | custom_error("Mirror function returned an error") 143 | 144 | 145 | @run_async 146 | def magnet(update, context): 147 | """Asynchronously starts a magnet download and handles "/magnet" command 148 | 149 | Args: 150 | update: 151 | Telegram incoming update 152 | """ 153 | try: 154 | muggnet(update=update, updater=updater, context=context) 155 | except: 156 | custom_error("Magnet function returned an error") 157 | 158 | 159 | def restart(update: telegram.Update, context): 160 | """Restarts the bot 161 | 162 | Args: 163 | update: 164 | Telegram incoming update 165 | """ 166 | try: 167 | handlers.restart(updater, update, context) 168 | except: 169 | custom_error("Nahi maanna? Mat maan!") 170 | 171 | 172 | def resturt(update: telegram.Update, context): 173 | """Pulls an update from github and restarts the bot 174 | 175 | Args: 176 | update: 177 | Telegram incoming update 178 | """ 179 | try: 180 | handlers.update_and_restart(updater, update, context) 181 | except: 182 | custom_error( 183 | "Well, I guess I didn't get an update after all\nYou can feel good about yourself now") 184 | 185 | 186 | def echo(update: telegram.Update, context): 187 | """Replies to a message with the message text. Basically serves as an echo command 188 | 189 | Args: 190 | update: 191 | Telegram incoming update 192 | """ 193 | try: 194 | update.message.reply_text(f"{update.message.text}") 195 | except: 196 | print("ERROR") 197 | 198 | 199 | def main(): 200 | updater.bot.send_message(chat_id=BOT_LOG_CHAT, text="Bot Started") 201 | dp = updater.dispatcher 202 | dp.add_handler(CommandHandler("start", start)) 203 | # handles "/help" command 204 | dp.add_handler(CommandHandler("help", help_func)) 205 | # handles "/mirror " command 206 | dp.add_handler(CommandHandler('mirror', mirror)) 207 | # handles "/magnet " command 208 | dp.add_handler(CommandHandler('magnet', magnet)) 209 | 210 | # custom error handler to send error messages to bot log chat instead of console 211 | dp.add_error_handler(error) 212 | 213 | # handles "/list" command 214 | dp.add_handler(CommandHandler("list", listdownloads)) 215 | 216 | dp.add_handler(CommandHandler("id", id)) # handles "/id" command 217 | dp.add_handler(CommandHandler("cri", cri)) # handles "/cri" command 218 | 219 | # handles "/cancel" command. Cancels all active downloads. 220 | dp.add_handler(CommandHandler("cancel", cancel)) 221 | 222 | # handles button "cancel" and "pause" options 223 | dp.add_handler(CallbackQueryHandler(button)) 224 | dp.add_handler(CommandHandler("restart", restart)) 225 | 226 | # git pulls and then restarts the bot 227 | dp.add_handler(CommandHandler("resturt", resturt)) 228 | dp.add_handler(CommandHandler("echo", echo)) 229 | updater.start_polling() 230 | updater.idle() 231 | 232 | 233 | if __name__ == '__main__': 234 | main() 235 | -------------------------------------------------------------------------------- /services.py: -------------------------------------------------------------------------------- 1 | from aria2p.downloads import Download 2 | import aria 3 | import time 4 | from telegram.ext.dispatcher import run_async 5 | from telegram import InlineKeyboardButton, InlineKeyboardMarkup 6 | from handlers import uploader 7 | 8 | 9 | def build_menu(buttons, 10 | n_cols): 11 | """Builds the "Pause" and "Cancel" buttons """ 12 | menu = [buttons[i:i + n_cols] for i in range(0, len(buttons), n_cols)] 13 | return menu 14 | 15 | 16 | def create_markup(gid): 17 | """Creates Inline Keyboard Buttons and assigns callback data to them""" 18 | 19 | button_list = [ 20 | InlineKeyboardButton("pause", callback_data=f"pause:{gid}"), 21 | InlineKeyboardButton("cancel", callback_data=f"cancel:{gid}") 22 | ] 23 | reply_markup = InlineKeyboardMarkup( 24 | build_menu(buttons=button_list, n_cols=2)) 25 | return reply_markup 26 | 27 | 28 | def create_resume_button(gid): 29 | button_list = [ 30 | # The "gid" argument allows us to send a request to aria2 to stop that particular download 31 | InlineKeyboardButton("resume", callback_data=f"resume:{gid}"), 32 | InlineKeyboardButton("cancel", callback_data=f"cancel:{gid}") 33 | ] 34 | reply_markup = InlineKeyboardMarkup( 35 | build_menu(buttons=button_list, n_cols=2)) 36 | return reply_markup 37 | 38 | 39 | def progessbar(new, tot): 40 | """Builds progressbar 41 | 42 | Args: 43 | new: current progress 44 | tot: total length of the download 45 | 46 | Returns: 47 | progressbar as a string of length 20 48 | """ 49 | length = 20 50 | progress = int(round(length * new / float(tot))) 51 | percent = round(new/float(tot) * 100.0, 1) 52 | bar = '=' * progress + '-' * (length - progress) 53 | return '[%s] %s %s\r' % (bar, percent, '%') 54 | 55 | 56 | @run_async 57 | def murror(update, updater, context): 58 | link = update.message.text 59 | message = update.message.reply_text("Starting...") 60 | link = link.replace('/mirror', '') # Extracting url from message 61 | link = link.strip() # Removing whitespaces 62 | print(link) 63 | download: Download = None 64 | try: 65 | download = aria.aria2.add_uris([link]) # Adding URL to aria2 66 | except Exception as e: 67 | print(e) 68 | if (str(e).endswith("No URI to download.")): 69 | updater.bot.edit_message_text( 70 | chat_id=message.chat.id, message_id=message.message_id, text="No link provided!") 71 | return None 72 | 73 | prevmessage = None 74 | time.sleep(1) 75 | while download.is_active or not download.is_complete: 76 | 77 | try: 78 | download.update() 79 | except Exception as e: 80 | 81 | # Handles the case if a download is manually deleted from aria2 dashboard 82 | if (str(e).endswith("is not found")): 83 | print("Mirror Deleted") 84 | updater.bot.edit_mtessage_text( 85 | chat_id=message.chat.id, message_id=message.message_id, text="Download removed") 86 | break 87 | print(e) 88 | print("Issue in downloading!") 89 | 90 | # Handle download cancel 91 | if download.status == 'removed': 92 | print("Mirror was cancelled") 93 | updater.bot.edit_message_text( 94 | chat_id=message.chat.id, message_id=message.message_id, text="Download removed") 95 | break 96 | 97 | # Handles case if aria2 faces an error while downloading 98 | if download.status == 'error': 99 | print("Mirror had an error") 100 | download.remove(force=True, files=True) 101 | updater.bot.edit_message_text( 102 | chat_id=message.chat.id, message_id=message.message_id, text="Download failed to resume/download!") 103 | break 104 | 105 | print(f"Mirror Status? {download.status}") 106 | 107 | if download.status == "active": 108 | try: 109 | download.update() 110 | 111 | # progressbar 112 | barop = progessbar(download.completed_length, 113 | download.total_length) 114 | print(barop) 115 | updateText = f"Downloading \n'{download.name}'\nProgress : {(float(download.completed_length)/ 1024 ** 2):.2f}/{(float(download.total_length)/ 1024 ** 2):.2f} MBs \nat {(float(download.download_speed)/ 1024 ** 2):.2f} MBps\n{barop}" 116 | if prevmessage != updateText: 117 | updater.bot.edit_message_text( 118 | chat_id=message.chat.id, message_id=message.message_id, text=updateText, reply_markup=create_markup(download.gid)) 119 | prevmessage = updateText 120 | print("downloading") 121 | time.sleep(2) 122 | except Exception as e: 123 | if (str(e).endswith("is not found")): 124 | break 125 | print(e) 126 | print("Issue in downloading!/Flood Control") 127 | time.sleep(2) 128 | elif download.status == "paused": 129 | try: 130 | download.update() 131 | updateText = f"Download Paused \n'{download.name}'\nProgress : {(float(download.completed_length)/ 1024 ** 2):.2f}/{(float(download.total_length)/ 1024 ** 2):.2f} MBs" 132 | 133 | # To get past telegram flood control, we make sure that the text in every message edit is new 134 | if prevmessage != updateText: 135 | updater.bot.edit_message_text(chat_id=message.chat.id, message_id=message.message_id, 136 | text=updateText, reply_markup=create_resume_button(download.gid)) 137 | prevmessage = updateText 138 | print("paused") 139 | time.sleep(2) 140 | except Exception as e: 141 | print(e) 142 | print("Download Paused Flood!") 143 | time.sleep(2) 144 | time.sleep(2) 145 | 146 | if download.status == "complete": 147 | if download.is_complete: 148 | print(download.name) 149 | try: 150 | uploader(updater, update, message, download, 'folder') 151 | except Exception as e: 152 | print(e) 153 | print("Upload Issue!") 154 | return None 155 | 156 | 157 | @run_async 158 | def muggnet(update, updater, context): 159 | link = update.message.text 160 | message = update.message.reply_text("Starting...") 161 | link = link.replace('/magnet', '') 162 | link = link.strip() 163 | download: Download = None 164 | 165 | try: 166 | download = aria.aria2.add_magnet(link) 167 | except Exception as e: 168 | print(e) 169 | if (str(e).endswith("No URI to download.")): 170 | updater.bot.edit_message_text( 171 | chat_id=message.chat.id, message_id=message.message_id, text="No link provided!") 172 | return None 173 | 174 | prevmessagemag = None 175 | 176 | while download.is_active: 177 | try: 178 | download.update() 179 | print("Downloading metadata") 180 | updateText = f"Downloading \n'{download.name}'\nProgress : {(float(download.completed_length)/ 1024):.2f}/{(float(download.total_length)/ 1024):.2f} KBs" 181 | if prevmessagemag != updateText: 182 | updater.bot.edit_message_text( 183 | chat_id=message.chat.id, message_id=message.message_id, text=updateText) 184 | prevmessagemag = updateText 185 | time.sleep(2) 186 | except: 187 | print("Metadata download problem/Flood Control Measures!") 188 | try: 189 | download.update() 190 | except Exception as e: 191 | if (str(e).endswith("is not found")): 192 | print("Metadata Cancelled/Failed") 193 | updater.bot.edit_message_text( 194 | chat_id=message.chat.id, message_id=message.message_id, text="Metadata couldn't be downloaded") 195 | return None 196 | time.sleep(2) 197 | 198 | time.sleep(2) 199 | match = str(download.followed_by_ids[0]) 200 | downloads = aria.aria2.get_downloads() 201 | currdownload = None 202 | for download in downloads: 203 | if download.gid == match: 204 | currdownload = download 205 | break 206 | print("Download complete") 207 | prevmessage = None 208 | while currdownload.is_active or not currdownload.is_complete: 209 | 210 | try: 211 | currdownload.update() 212 | except Exception as e: 213 | if (str(e).endswith("is not found")): 214 | print("Magnet Deleted") 215 | updater.bot.edit_message_text( 216 | chat_id=message.chat.id, message_id=message.message_id, text="Magnet download was removed") 217 | break 218 | print(e) 219 | print("Issue in downloading!") 220 | 221 | if currdownload.status == 'removed': 222 | print("Magnet was cancelled") 223 | updater.bot.edit_message_text( 224 | chat_id=message.chat.id, message_id=message.message_id, text="Magnet download was cancelled") 225 | break 226 | 227 | if currdownload.status == 'error': 228 | print("Mirror had an error") 229 | currdownload.remove(force=True, files=True) 230 | updater.bot.edit_message_text(chat_id=message.chat.id, message_id=message.message_id, 231 | text="Magnet failed to resume/download!\nRun /cancel once and try again.") 232 | break 233 | 234 | print(f"Magnet Status? {currdownload.status}") 235 | 236 | if currdownload.status == "active": 237 | try: 238 | currdownload.update() 239 | barop = progessbar( 240 | currdownload.completed_length, currdownload.total_length) 241 | print(barop) 242 | updateText = f"Downloading \n'{currdownload.name}'\nProgress : {(float(currdownload.completed_length)/ 1024 ** 2):.2f}/{(float(currdownload.total_length)/ 1024 ** 2):.2f} MBs \nat {(float(currdownload.download_speed)/ 1024 ** 2):.2f} MBps\n{barop}" 243 | if prevmessage != updateText: 244 | updater.bot.edit_message_text( 245 | chat_id=message.chat.id, message_id=message.message_id, text=updateText, reply_markup=create_markup(currdownload.gid)) 246 | prevmessage = updateText 247 | time.sleep(2) 248 | except Exception as e: 249 | if (str(e).endswith("is not found")): 250 | break 251 | print(e) 252 | print("Issue in downloading!") 253 | time.sleep(2) 254 | elif currdownload.status == "paused": 255 | try: 256 | currdownload.update() 257 | updateText = f"Download Paused \n'{currdownload.name}'\nProgress : {(float(currdownload.completed_length)/ 1024 ** 2):.2f}/{(float(currdownload.total_length)/ 1024 ** 2):.2f} MBs" 258 | if prevmessage != updateText: 259 | updater.bot.edit_message_text(chat_id=message.chat.id, message_id=message.message_id, 260 | text=updateText, reply_markup=create_resume_button(currdownload.gid)) 261 | prevmessage = updateText 262 | time.sleep(2) 263 | except Exception as e: 264 | print(e) 265 | print("Download Paused Flood") 266 | time.sleep(2) 267 | time.sleep(2) 268 | 269 | time.sleep(1) 270 | if currdownload.is_complete: 271 | print(currdownload.name) 272 | try: 273 | uploader(updater, update, message, currdownload, 'folder') 274 | except Exception as e: 275 | print(e) 276 | print("Upload Issue!") 277 | return None 278 | --------------------------------------------------------------------------------