diff --git a/.env.sample b/.env.sample new file mode 100644 index 00000000..ba4f0dd3 --- /dev/null +++ b/.env.sample @@ -0,0 +1,16 @@ +APP_ID=app-id-from-my.telegram.org +API_HASH=api-hash-from-my.telegram.org +BOT_TOKEN=bot-token-from-@BotFather + +MEGA_EMAIL=yourname@gmail.com +MEGA_PASSWORD=mypassword + +AUTH_USERS=* +USE_ENV=False +LOG_CHAT=-1001234567890 +MONGO_URI=mongodb+srv://username:password@host/dbname?retryWrites=true&w=majority +CYPHER_KEY=vJmDXO4xria6SYVcOfVYd3k3YM8WiiGBfRjbQ8MBsvI= + +DOWNLOAD_LOCATION=${PWD}/NexaBots +CHUNK_SIZE=524288 +TG_MAX_SIZE=2040108421 \ No newline at end of file diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml new file mode 100644 index 00000000..104c0a04 --- /dev/null +++ b/.github/workflows/notify.yml @@ -0,0 +1,11 @@ +name: Notify on Telegram +on: [push, fork, pull_request, release] +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Notify the commit on Telegram + uses: Itz-fork/github-telegram-notify@main + with: + bot_token: '${{ secrets.BOT_TOKEN }}' + chat_id: '${{ secrets.CHAT_ID }}' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8088c738..c6cc057d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,10 +3,10 @@ FROM fedora:latest RUN dnf upgrade -y RUN dnf install \ https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm -y -RUN dnf install git python3-pip ffmpeg megatools -y +RUN dnf install gcc python3-devel git python3-pip ffmpeg megatools -y RUN pip3 install -U pip RUN mkdir /app/ WORKDIR /app/ -RUN git clone https://github.com/Itz-fork/Mega.nz-Bot.git /app +RUN git clone -b nightly https://github.com/Itz-fork/Mega.nz-Bot.git /app RUN pip3 install -U -r requirements.txt -CMD [ "bash", "startup.sh" ] +CMD ["python3", "-m", "megadl"] \ No newline at end of file diff --git a/LICENSE b/LICENSE index 33fee88a..f288702d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,9 @@ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - Copyright (C) 2021 Itz-fork. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies - of this telegram Bot, but changing it is not allowed. + of this license document, but changing it is not allowed. Preamble diff --git a/Procfile b/Procfile index 049ad8b4..7068badb 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -worker: bash startup.sh \ No newline at end of file +worker: python3 -m megadl diff --git a/README.md b/README.md index 8798968f..bdd4d3e3 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,38 @@ -# Mega.nz-Bot -A simple telegram bot written in Python using Pyrogram framework which helps you to download, upload and import [^1] files / folders from [Mega.nz](https://mega.nz/) cloud storage with telegram. +

+ +
+ + +

+ + +# Mega.nz-Bot - Cypher πŸ₯· +A simple telegram bot to download, upload files or folders from [Mega.nz](https://mega.nz/) # Features -- ⚑ Download, Upload & Import files easily -- πŸ“± Mega.nz user account support -- πŸ™…β€β™‚οΈ No login required -- πŸ–‡οΈ Almost all file / folder links are supported [^2] -- πŸ›‘οΈ Can be used as either public or private bot -- πŸ•΅οΈβ€β™‚οΈ Inline Mode [Still In Development Stage: BETA] +- ⚑ Download, Upload files/folders easily +- πŸ™…β€β™‚οΈ No login required[^1] +- πŸ—ƒοΈ Support for Mega.nz user account +- 🀝 Support for both private and public content +- πŸ›‘ Can be used as either private or public bot +- πŸ–‡ Direct download link to mega.nz upload +- 🧐 See what files are in your links # Deploy Deploy your own Bot β™₯️! **Star 🌟 Fork 🍴 and Deploy** -### Config Vars πŸ““, -**Mandatory Vars,** -- `APP_ID` - Your APP_ID. Get it from [my.telegram.org](my.telegram.org) -- `API_HASH` - Your API_ID. Get it from [my.telegram.org](my.telegram.org) -- `AUTH_USERS` - Telegram IDs Of Auth Users, Only they can use this bot (If you didn't set this as public bot). Separate them by a space. (Ex: `123445 2648589`) -- `BOT_TOKEN` - Your Bot Token From [@BotFather](https://t.me/BotFather) - -**Non Mandatory Vars,** -- `IS_PUBLIC_BOT` - Set this to 'True' if you want to set Download Function as Public. Default to 'False' -- `LOGS_CHANNEL` - To get this, follow these steps, - - Make a private channel - - Send a message and copy it's link - - The link'll be something like `https://t.me/c/12345/1`. Simply copy the `12345` part from it and add `-100` to the beginning of it. Now it'll be something like `-10012345`. That's your channel id! -- `MEGA_EMAIL` - Fill this if you want to use your own Mega Account. This is your Mega account Email -- `MEGA_PASSWORD` - Fill this if you want to use your own Mega Account. This is your Mega account Password - -Check out [config.sample file](https://github.com/Itz-fork/Mega.nz-Bot/blob/main/config.sample) if you aren't using heroku πŸ€— - -### With Heroku -> Notice ⚠️: -> Please refer the [Deployment](https://github.com/Itz-fork/X-Bin-Patch#deployment) guide inorder to deploy this bot Heroku. THIS REPO ISN'T FUCKING COMPATIBLE WITH HEROKU! - +### Heroku [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://github.com/Itz-fork/X-Bin-Patch#deployment) -### With VPS/PC +### Local --- **Tip πŸ’‘:** -If you're using a linux distro with `apt`, `pacman` or `dnf` package manager, you can use the official installer script to setup [Mega.nz-Bot](https://github.com/Itz-fork/Mega.nz-Bot). To do so run the following command, +If you're using a linux distro with `apt`, `pacman` or `dnf` as the package manager, you can use the official installer script to setup [Mega.nz-Bot](https://github.com/Itz-fork/Mega.nz-Bot). ```bash -curl -sS https://raw.githubusercontent.com/Itz-fork/Mega.nz-Bot/main/installer.sh | bash +curl -sS https://raw.githubusercontent.com/Itz-fork/Mega.nz-Bot/nightly/installer.sh | bash ``` --- @@ -63,18 +51,18 @@ cd Mega.nz-Bot pip3 install -U -r requirements.txt ``` - Install [megatools](https://megatools.megous.com/), [ffmpeg](https://ffmpeg.org/download.html) according to your system -- Fill config vars with your own values ([How to get config values](https://github.com/Itz-fork/Mega.nz-Bot#config-vars-)), - - If you have GUI system use a normal text editor like notepad, sublime text etc. - - For CLI systems, [use nano](https://gist.github.com/Itz-fork/fd11c08ef7464bdae3663a1f9c77c9e9) and edit the config file using `nano config.py` command. +- Create a `.env` file (see [example](/.env.sample)) +- Fill config vars with your own values ([How to get config values](#config-vars)), - Run the Bot, ``` -bash startup.sh +python3 -m megadl ``` +### Config vars +Please refer to [documentation](https://megabot.hirusha.codes/config-vars) -[^1]: Only download and upload functions support folder links. Currently import function supports only for files -[^2]: Public content only +[^1]: This only applies to public contents and you're still limited by the daily download quota limit of the mega.nz platform # Support [![Support Group](https://img.shields.io/badge/Support_Group-0a0a0a?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/Nexa_bots) \ No newline at end of file diff --git a/app.json b/app.json deleted file mode 100644 index a8ab0711..00000000 --- a/app.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "Mega.nz Bot", - "description": "Simple Telegram Bot to download files from Mega.nz", - "logo": "https://telegra.ph/file/583f46da57641b90c28f9.png", - "keywords": [ - "mega.nz", - "Telegram Bot", - "mega.nz bot" - ], - "website": "https://t.me/NexaBotsUpdates", - "repository": "https://github.com/Itz-fork/Mega.nz-Bot", - "success_url": "https://t.me/NexaBotsUpdates", - "env": { - "APP_ID": { - "description": "Your APP_ID from my.telegram.org", - "required": true - }, - "API_HASH": { - "description": "Your API_HASH from my.telegram.org", - "required": true - }, - "AUTH_USERS": { - "description": "Telegram Ids Of Auth Users, Only they can use this bot. Separate them by space!", - "required": true - }, - "BOT_TOKEN": { - "description": "Your Bot Token From @BotFather", - "required": true - }, - "IS_PUBLIC_BOT": { - "description": "Set this to 'True' If you want to set Download Function as Public. Default to 'False' !", - "required": false, - "value": "False" - }, - "LOGS_CHANNEL": { - "description": "Make a private channel and forward a message from that channel to @ChannelidHEXbot and Get this. Not Mandatory", - "required": false, - "value": "" - }, - "MEGA_EMAIL": { - "description": "Fill this if you want to use your own Mega Account. This is your Mega account Email!", - "required": false - }, - "MEGA_PASSWORD": { - "description": "Fill this if you want to use your own Mega Account. This is your Mega account Password!", - "required": false - } - }, - "addons": [], - "buildpacks": [ - { - "url": "heroku/python" - }, - { - "url": "https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest.git" - } - ], - "formation": { - "worker": { - "quantity": 1, - "size": "free" - } - }, - "stack": "container" -} \ No newline at end of file diff --git a/assests/logo.png b/assests/logo.png new file mode 100644 index 00000000..5ed7604e Binary files /dev/null and b/assests/logo.png differ diff --git a/cache/test.txt b/cache/test.txt deleted file mode 100644 index 540f7504..00000000 --- a/cache/test.txt +++ /dev/null @@ -1,4 +0,0 @@ -Made by Itz-fork - -NOTE: - DON'T DELETE THIS FOLDER! \ No newline at end of file diff --git a/config.py b/config.py deleted file mode 100644 index 37865e0c..00000000 --- a/config.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2022 Itz-fork - -import os - - -class Config(object): - APP_ID = int(os.environ.get("APP_ID", 1234567)) - API_HASH = os.environ.get("API_HASH", "") - BOT_TOKEN = os.environ.get("BOT_TOKEN", "") - AUTH_USERS = set(int(x) for x in os.environ.get("AUTH_USERS", "").split()) - IS_PUBLIC_BOT = os.environ.get("IS_PUBLIC_BOT") in ["True", "true"] - LOGS_CHANNEL = int(os.environ.get("LOGS_CHANNEL")) if os.environ.get("LOGS_CHANNEL") else None - # DON'T CHANGE THESE 2 VARS - DOWNLOAD_LOCATION = "./NexaBots" - TG_MAX_SIZE = 2040108421 - # Mega User Account - MEGA_EMAIL = os.environ.get("MEGA_EMAIL") - MEGA_PASSWORD = os.environ.get("MEGA_PASSWORD") \ No newline at end of file diff --git a/config.sample b/config.sample deleted file mode 100644 index 464a1765..00000000 --- a/config.sample +++ /dev/null @@ -1,13 +0,0 @@ -class Config(object): - APP_ID = YOUR_API_ID - API_HASH = "YOUR_API_HASH" - BOT_TOKEN = "YOUR_BOT_TOKEN" - AUTH_USERS = set(int(x) for x in "TELEGRAM_IDS_OF_AUTH_USERS".split(" ")) - IS_PUBLIC_BOT = False # Set this to True if you want to make your bot public - LOGS_CHANNEL = YOUR_LOG_CHANNEL_ID - # DON'T CHANGE THESE 2 VARS - DOWNLOAD_LOCATION = "./NexaBots" - TG_MAX_SIZE = 2040108421 - # Mega User Account - MEGA_EMAIL = "YOUR_MEGA_NZ_ACCOUNT'S_EMAIL" - MEGA_PASSWORD = "YOUR_MEGA_NZ_ACCOUNT'S_PASSWORD" \ No newline at end of file diff --git a/heroku.yml b/heroku.yml new file mode 100644 index 00000000..59bb5782 --- /dev/null +++ b/heroku.yml @@ -0,0 +1,6 @@ +build: + docker: + worker: Dockerfile + +run: + worker: python3 -m megadl diff --git a/installer.sh b/installer.sh index 860a1e58..18462056 100644 --- a/installer.sh +++ b/installer.sh @@ -1,184 +1,225 @@ -#!/usr/bin/bash +#!/usr/bin/env bash -# Colors +# COLORS White="\033[1;37m" Red="\033[1;31m" +Cyan="\033[1;36m" +Yellow="\033[1;93m" Green="\033[1;32m" +Un_Purple="\033[4;35m" Reset="\033[0m" +# Variables +PKGMN="" +declare -A pkgs_git=([apt]=git-all [pacman]=git [dnf]=git-all) +declare -A pkgs_pip=([apt]=python3-pip [pacman]=python-pip [dnf]=python3-pip) -function show_process_msg() { - echo -e "$White ==> $1 $Reset" + +# Output functions +function show_process() { + echo -e "${White}==> ${1}${Reset}" } -function show_success_msg() { - echo -e "\n${Green}Successfully finished$Reset$White ${1}$Reset${Green}!$Reset" +function show_hint() { + echo -e " Tip πŸ’‘: ${Yellow}${1}${Reset}" } -function show_error_msg() { - echo -e "$Red ERROR: $1 $Reset" - if [[ ! $2 == "n" ]]; then - exit +function show_error() { + echo -e "${Red}ERROR: ${1}${Reset}" + if [ $2 == "noex" ]; then + : + else + exit 1 fi } -echo -e "$White - -Mega.nz-Bot Installer - v1.1 - -" - - -function install_git() { - sudo apt install git-all || - sudo pacman -S git || - sudo dnf install git || - show_error_msg "Your system deosn't match the current list of Oses. Please install 'git' from - https://git-scm.com/book/en/v2/Getting-Started-Installing-Git" +# Identify current package manager +function setup_env() { + # Detect current systems package manager + if $(command -v pacman &> /dev/null) ; then + PKGMN=pacman + elif $(command -v apt &> /dev/null) ; then + PKGMN=apt + elif $(command -v dnf &> /dev/null) ; then + PKGMN=dnf + else show_error "Your system isn't compatible with the installer" + fi } -function install_pip3() { - sudo apt install python3-pip || - sudo pacman -S python-pip || - sudo dnf install python3-pip || - show_error_msg "Your system deosn't match the current list of Oses. Please install 'pip3' from - https://pip.pypa.io/en/stable/installation/" -} -function _aur_megatools() { - git clone https://aur.archlinux.org/megatools.git - cd megatools || show_error_msg "megatools dir doesn't exists rn! Tf did you do?" - makepkg -si - cd .. - rm -rf megatools +# Install packages for the current system +function pkg_installer() { + echo -e "${White} > Installing ${1}${Reset}" + + case $PKGMN in + + pacman) + sudo pacman -S $1 &> /dev/null || show_error "pacman: Unable to install ${1}" + ;; + + apt) + sudo apt install $1 &> /dev/null || show_error "apt: Unable to install ${1}" + ;; + + dnf) + sudo dnf install $1 &> /dev/null || show_error "dnf: Unable to install ${1}" + ;; + + *) + show_error "Your system isn't compatible with the installer" + ;; + + esac } -function install_megatools() { - sudo apt install megatools || - _aur_megatools || - sudo dnf install megatools || - show_error_msg "Your system deosn't match the current list of Oses. Please install 'megatools' from - https://megatools.megous.com/" -} +# CLone git repo +function clone_repo() { + show_process "Cloning Mega.nz-Bot repository" + git clone -b nightly https://github.com/Itz-fork/Mega.nz-Bot.git || show_error "git: Clone failed" -function checkDepends() { - show_process_msg "Checking dependencies" - - is_git=$(command -v git &> /dev/null) - is_ffmpeg=$(command -v ffmpeg &> /dev/null) - is_pip3=$(command -v pip3 &> /dev/null) - is_megatools=$(command -v megatools &> /dev/null) - # Checks if git is installed - if ! $is_git ; then - show_process_msg "Installing git" - install_git - # Checks if ffmpeg is installed - elif ! $is_ffmpeg ; then - show_process_msg "Installing ffmpeg" - curl -sS https://webinstall.dev/ffmpeg | bash || - show_error_msg "Ffmpeg is not installed. Visit - https://ffmpeg.org/download.html" - # Checks if pip3 is installed - elif ! $is_pip3 ; then - show_process_msg "Installing pip3" - install_pip3 - # Checks if megatools is installed - elif ! $is_megatools ; then - show_process_msg "Installing megatools" - install_megatools - fi + show_process "Changing current working directory" + cd Mega.nz-Bot || show_error "fs: 'Mega.nz-Bot' not found" } -function install() { - show_process_msg "Cloning into Mega.nz-Bot repository" - git clone https://github.com/Itz-fork/Mega.nz-Bot || show_error_msg "Unable to clone the Mega.nz-Bot repository" - - show_process_msg "Changing the directory to 'Mega.nz-Bot'" - cd Mega.nz-Bot || show_error_msg "'Mega.nz-Bot' folder not found" +# Check dependencies +function check_deps() { + show_process "Checking dependencies πŸ”" + if ! command -v git &> /dev/null ; then + pkg_installer ${pkgs_git[$PKGMN]} + elif ! command -v pip3 &> /dev/null ; then + pkg_installer ${pkgs_pip[$PKGMN]} + elif ! command -v ffmpeg &> /dev/null ; then + pkg_installer ffmpeg + elif ! command -v megatools &> /dev/null ; then + pkg_installer megatools + fi - show_process_msg "Installing Requirements using pip3" - pip3 install -U -r requirements.txt &> /dev/null || show_error_msg "Unable to install requirements" + show_process "Setting up python virtual environment" + python3 -m venv .venv + source .venv/bin/activate - show_success_msg "Installation" + pip3 install -U -r requirements.txt || + pip install -U -r requirements.txt || + show_error "python: Unable to install requirements" } -function genConfig() { - show_process_msg "Generating config file" +# Generate .env file +function gen_env() { + show_process "Generating env file βš™οΈ" # Mandotory vars read -p "Enter your API ID: " api_id read -p "Enter your API HASH: " api_hash read -p "Enter your BOT TOKEN: " bot_token - read -p "Enter Telegram IDs of Auth users (Seperate by a space): " auth_users - read -p "Enter your LOG CHANNEL ID: " log_channel_id - - # Optional vars + # read mega.nz email and password with default values + def_mail="your@mail.com" + def_pass="strong#password" + mega_email="" + mega_password="" + private_bot=false + while true; do + read -p "Do you want to create private bot with pre-configured mega account? [y/n]" add_mega + case $add_mega in + y|yes|Y|Yes ) private_bot=true; break;; + * ) private_bot=false; break;; + esac + done + if $private_bot ; then + read -p "Enter your Mega.nz Email [$def_mail]: " mega_email + mega_email="MEGA_EMAIL=${mega_email:-def_mail}" + read -p "Enter your Mega.nz Passsword [$def_pass]: " mega_password + mega_password="MEGA_PASSWORD=${mega_password:-def_pass}" + fi + + # read authorized users in to a variable called auth_users + # if auth_users are empty set to "*" otherwise check if private_bot var is set to true + # if the private_bot var is set to true then auth_users will be the input otherwise it will be "*|" + read -p "Enter authorized users (seperate with space) [empty for all users]: " auth_users + auth_users=${auth_users:-"*"} + if $private_bot ; then + auth_users="$auth_users" + else + auth_users="*|$auth_users" + fi + + # add log chat + log_chat="" while true; do - read -p "Do you want to make your bot public? (y/n) " is_pb - case $is_pb in - y|Y) - is_public="True" - break; shift ;; - n|N) - is_public="False" - break; shift ;; - *) - show_error_msg "Invalid option - $is_pb !" "n" + read -p "Do you want to see user activity logs? [y/n]" see_logs + case $see_logs in + y|yes|Y|Yes ) see_logs=true; break;; + * ) see_logs=false; break;; esac done + if $see_logs ; then + read -p "Enter a chat id where bot is an admin [preferably a channel]: " log_chat + log_chat="LOG_CHAT=${log_chat}" + fi + + + # add mongodb url + mongo_url="" while true; do - read -p "Do you want to setup mega account credentials? (y/n) " is_mauth - case $is_mauth in - y|Y) - read -p "Enter your Mega.nz Email: " mega_email - read -p "Enter your Mega.nz Passsword: " mega_password - break; shift;; - n|N) - break; shift ;; - *) - show_error_msg "Invalid option - $is_mauth !" "n";; + read -p "Do you want to setup mongodb? [y/n]" set_mongo + case $set_mongo in + y|yes|Y|Yes ) set_mongo=true; break;; + * ) set_mongo=false; break;; esac done + + if $set_mongo ; then + read -p "Enter the mongodb url [more info: https://t.ly/YxYXq]: " mongo_url + mongo_url="MONGO_URI=${mongo_url}" + fi + + + # generate cypher key + cypher_key=$(python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") + + + touch .env || echo "" > .env + cat <> .env +APP_ID=$api_id +API_HASH=$api_hash +BOT_TOKEN=$bot_token - echo "" > config.py - cat <> config.py -# Copyright 2022 - Itz-fork -# Generated by Mega.nz Installer - - -class Config(object): - APP_ID = $api_id - API_HASH = "$api_hash" - BOT_TOKEN = "$bot_token" - AUTH_USERS = set(int(x) for x in "$auth_users".split(" ")) - IS_PUBLIC_BOT = $is_public - LOGS_CHANNEL = $log_channel_id - # DON'T CHANGE THESE 2 VARS - DOWNLOAD_LOCATION = "./NexaBots" - TG_MAX_SIZE = 2040108421 - # Mega User Account - MEGA_EMAIL = "$mega_email" - MEGA_PASSWORD = "$mega_password" +$mega_email +$mega_password + +USE_ENV=False +AUTH_USERS=$auth_users +$log_chat +$mongo_url +CYPHER_KEY=$cypher_key + +DOWNLOAD_LOCATION="${PWD}"/NexaBots +CHUNK_SIZE=524288 +TG_MAX_SIZE=2040108421 EOF - show_success_msg "Generating config file" + show_hint "If your bot won't work as expected, try reassigning USE_ENV and DOWNLOAD_LOCATION in .env file. Not sure what to do? Contact support at @Nexa_bots" + + show_hint "It is recommended to check the mega.ini file for more configuration options" } -function run_bot() { - show_process_msg "Starting the bot" - python3 -m megadl -} +function run_installer() { + echo -e "${White}${Un_Purple}Welcome to ${Red}Mega.nz-Bot${Reset}${White}${Un_Purple} - ${Cyan}Cypher${Reset}${White}${Un_Purple} Setup!${Reset}" + exit + setup_env + clone_repo + check_deps + gen_env -function main() { - checkDepends - install - genConfig - run_bot + show_hint "You can start the bot with: python3 -m megadl" } -main \ No newline at end of file +run_installer \ No newline at end of file diff --git a/mega.ini b/mega.ini new file mode 100644 index 00000000..b37cedd0 --- /dev/null +++ b/mega.ini @@ -0,0 +1,8 @@ +[Network] +# 10240 = 10MiB/s +SpeedLimit = 10240 +ParallelTransfers = 16 + +[Cache] +# 120 = 2min +Timeout=120 \ No newline at end of file diff --git a/megadl/__init__.py b/megadl/__init__.py index f44accb9..2ce5659f 100644 --- a/megadl/__init__.py +++ b/megadl/__init__.py @@ -1,14 +1,18 @@ -# Copyright (c) 2022 Itz-fork -# Don't kang this else your dad is gae - -from pyrogram import Client -from config import Config - -meganzbot = Client( - name="MegaBot", - bot_token=Config.BOT_TOKEN, - api_id=Config.APP_ID, - api_hash=Config.API_HASH, - plugins=dict(root="megadl/modules"), - sleep_threshold=10 - ) \ No newline at end of file +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: __init__.py + +# start msg +print("Mega.nz Bot - Cypher is starting...") + + +# loading config +from dotenv import load_dotenv +print("--------------------") +print("> Loading config") +load_dotenv() + +# client +from .helpers.cypher import MeganzClient +CypherClient: "MeganzClient" = MeganzClient() \ No newline at end of file diff --git a/megadl/__main__.py b/megadl/__main__.py index 0b5daf2d..a643c5a7 100644 --- a/megadl/__main__.py +++ b/megadl/__main__.py @@ -1,22 +1,17 @@ -# Copyright (c) 2022 Itz-fork -# Don't kang this else your dad is gae +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: __main__.py -import os from pyrogram import idle -from megadl.helpers_nexa.mega_help import check_logs - -from . import meganzbot -from .data import B_START_TEXT, PROCESS_TEXT, START_TEXT -from config import Config +from . import CypherClient +# Run the bot if __name__ == "__main__": - print(B_START_TEXT.format("Your Mega.nz-Bot is Starting! Please Wait...")) - if not os.path.isdir(Config.DOWNLOAD_LOCATION): - os.makedirs(Config.DOWNLOAD_LOCATION) - meganzbot.start() - print(PROCESS_TEXT.format("Checking Log Channel ...")) - check_logs() - print(START_TEXT) - idle() \ No newline at end of file + # Custom pyrogram client + print("> Starting Client") + CypherClient.start() + print("--------------------") + idle() diff --git a/megadl/data.py b/megadl/data.py deleted file mode 100644 index 0051da59..00000000 --- a/megadl/data.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright (c) 2022 Itz-fork -# Don't kang this else your dad is gae - -from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup - - -# Help messages -Messages = { - "dl": """ -**Here is The Help Of Mega.nz Downloader Module** - - - ✘ Send me a Mega.nz file/folder link. (Use `/megapy` command for old `mega.py` download engine) - - ✘ Wait Till It Download and Upload to Telegram. - - -**Made with ❀️ by @NexaBotsUpdates** -""", - - "up": """ -**Here is The Help Of Mega.nz Uploader Module** - - - ✘ First Send or Forward a File to Me. You can also send me a direct link. - - ✘ Then Reply to that file with `/upload` command. - - ✘ Wait till It Download and Upload to Mega.nz - - -**Made with ❀️ by @NexaBotsUpdates** -""", - - "import": """ -"**Here is The Help Of Mega.nz Url Importer Module** - - - ✘ Send or Reply to a Public Mega.nz url with `/import` Command (**Usage:** `/import your_mega_link`) - - ✘ Wait till It Finish - - -**Made with ❀️ by @NexaBotsUpdates** -""", - - "file_info": """ -**Here is The Help Of Get File Info Via Inline Module** - - - ✘ Go to any chat - - ✘ Type: `{uname} details` and after that give a one space and paste your mega.nz link (**Usage:** `{uname} details your_mega_link`) - - -**Made with ❀️ by @NexaBotsUpdates** -""", - - "acc_info": """ -**Here is The Help Of Get Account Info Via Inline Module** - - - ✘ Go to any chat (This will send your mega.nz account data so better do this in a private chat) - - ✘ Type: `{uname} info` (**Usage:** `{uname} info`) - - -**Made with ❀️ by @NexaBotsUpdates** -""" -} - - -# Callback buttons -Buttons = { - "start": [ - [InlineKeyboardButton("Help πŸ“œ", callback_data="helpcallback"), - InlineKeyboardButton("About ⁉️", callback_data="aboutcallback")], - - [InlineKeyboardButton( - "Go Inline", switch_inline_query_current_chat="")] - ], - - "inline": [ - [InlineKeyboardButton("Commands Help οΏ½", callback_data="helpcallback"), - InlineKeyboardButton("Inline Query Help οΏ½", callback_data="inlinehelpcallback")], - - [InlineKeyboardButton( - "Go Inline", switch_inline_query_current_chat="")] - ], - - "help": [ - [InlineKeyboardButton("Downloader πŸ“₯", callback_data="meganzdownloadercb"), - InlineKeyboardButton("Uploader πŸ“€", callback_data="meganzuploadercb")], - - [InlineKeyboardButton( - "Importer πŸ“²", callback_data="meganzimportercb")], - [InlineKeyboardButton( - "Back ⬅️", callback_data="startcallback")] - ], - - "inline_help": [ - [InlineKeyboardButton("Get File Details πŸ“–", callback_data="getfiledetailscb"), - InlineKeyboardButton("Get Account Info πŸ’³", callback_data="getaccoutinfo")] - ], - - "mod_help": [ - [InlineKeyboardButton("Close ❌", callback_data="closeqcb")], - - [InlineKeyboardButton("Back ⬅️", callback_data="helpcallback")] - ], - - "imod_help": [ - [InlineKeyboardButton("Close ❌", callback_data="closeqcb")], - - [InlineKeyboardButton( - "Back ⬅️", callback_data="inlinehelpcallback")] - ], - - "about": [ - [InlineKeyboardButton( - "Source Code πŸ—‚", url="https://github.com/Itz-fork/Mega.nz-Bot")], - - [InlineKeyboardButton("Back ⬅️", callback_data="startcallback"), - InlineKeyboardButton("Close ❌", callback_data="closeqcb")] - ], - - "github": [ - [InlineKeyboardButton( - "Source Code πŸ—‚", url="https://github.com/Itz-fork/Mega.nz-Bot")], - [InlineKeyboardButton( - "Support Group πŸ†˜", url="https://t.me/Nexa_bots")] - ], - - "cancel": [ - [InlineKeyboardButton( - "Cancel ❌", callback_data="cancelvro")] - ] -} - - -async def get_buttons(name): - return InlineKeyboardMarkup(Buttons.get(name)) - -async def get_msg(name): - return Messages.get(name) - - - -############ DEFAULT STRINGS ############ -B_START_TEXT = """ - __ ___ ___ __ - / |/ /__ ___ ____ _ ___ ___ ____ / _ )___ / /_ - / /|_/ / -_) _ `/ _ `/ _ / _ \/_ / /___/ / _ / _ \/ __/ -/_/ /_/\__/\_, /\_,_/ (_) /_//_//__/ /____/\___/\__/ - /___/ - -Process: {} -""" - -PROCESS_TEXT = """ -Process: {} -""" - -LOGGED_AS_USER = """ -Successfully Logged Into Mega.nz User Account -""" - -LOGIN_ERROR_TEXT = """ -Unable to find Mega Email and Password, Loging as an anonymous User... -""" - -ERROR_TEXT = """ - _____ -| ____|_ __ _ __ ___ _ __ -| _| | '__| '__/ _ \| '__| -| |___| | | | | (_) | | -|_____|_| |_| \___/|_| - -Log: {} - -Save the Log file and Send it to @Nexa_bots for Help :) -""" - -START_TEXT=""" - _ __ ____ __ - / | / /__ _ ______ _ / __ )____ / /______ - / |/ / _ \| |/_/ __ `/ / __ / __ \/ __/ ___/ - / /| / __/> %dI" % (len(b) / 4), b) + + +def base64_to_a32(s): + return str_to_a32(base64_url_decode(s)) + + +def base64_url_decode(data): + data += "=="[(2 - len(data) * 3) % 4 :] + data = re.sub(r"[-_,]", lambda x: {"-": "+", "_": "/", ",": ""}[x.group()], data) + return base64.b64decode(data) + + +def a32_to_str(a): + return struct.pack(">%dI" % len(a), *a) + + +def decrypt_attr(attr, key): + attr = aes_cbc_decrypt(attr, a32_to_str(key)) + attr = makestring(attr) + attr = attr.rstrip("\0") + return json.loads(attr[4:]) if attr[:6] == 'MEGA{"' else False + + +def aes_cbc_decrypt_a32(data, key): + return str_to_a32(aes_cbc_decrypt(a32_to_str(data), a32_to_str(key))) + + +def decrypt_key(a, key): + return sum( + (aes_cbc_decrypt_a32(a[i : i + 4], key) for i in range(0, len(a), 4)), () + ) + + +def decrypt_node_key(key_str: str, shared_key: str): + if ":" not in key_str: + return None + encrypted_key = base64_to_a32(key_str.split(":")[1]) + return decrypt_key(encrypted_key, shared_key) diff --git a/megadl/helpers/cypher.py b/megadl/helpers/cypher.py new file mode 100644 index 00000000..606c86b6 --- /dev/null +++ b/megadl/helpers/cypher.py @@ -0,0 +1,304 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Custom pyrogram client useful methods + +import os +import asyncio +import logging +import functools + +from typing import Callable +from asyncio import sleep as xsleep +from cryptography.fernet import Fernet + +from pyrogram.types import Message +from pyrogram import Client, errors +from pyrogram.handlers import MessageHandler + +from .database import CypherDB +from .files import send_as_guessed, fs_cleanup, splitit, listfiles + + +_emsg = """ +##### Mega.nz-Bot Error Handler ##### + +Version: {} +Module: {} +Error: +{} + +""" + + +class MeganzClient(Client): + """ + Custom pyrogram client for Mega.nz-Bot + """ + + version = "cypher-1.0" + dl_loc = None + tmp_loc = None + database = CypherDB() if os.getenv("MONGO_URI") else None + + def __init__(self): + # set DOWNLOAD_LOCATION variable + # if USE_ENV is True it'll use currend dir + NexaBots + # otherwise use location defined in .env file by user + print("> Setting up file locations") + self.cwd = os.getcwd() + if os.getenv("USE_ENV") in ["True", "true"] or not os.getenv( + "DOWNLOAD_LOCATION" + ): + self.dl_loc = f"{self.cwd}/NexaBots" + else: + self.dl_loc = os.getenv("DOWNLOAD_LOCATION") + + self.tmp_loc = f"{self.dl_loc}/temps" + self.mx_size = int(os.getenv("TG_MAX_SIZE", 2040108421)) + + if not os.path.isdir(self.dl_loc): + os.makedirs(self.dl_loc) + + if not os.path.isdir(self.tmp_loc): + os.makedirs(self.tmp_loc) + + # Initializing pyrogram + print("> Initializing client") + super().__init__( + "MegaBotCypher", + bot_token=os.getenv("BOT_TOKEN"), + api_id=os.getenv("APP_ID"), + api_hash=os.getenv("API_HASH"), + plugins=dict(root="megadl/modules"), + sleep_threshold=10, + ) + + # Initializing mongodb + print("> Initializing database") + self.glob_tmp = {} + self.environs = os.environ.copy() + self.cipher = None + + print("> Updating privacy settings") + _auths = os.getenv("AUTH_USERS") + self.auth_users = ( + set() + if not isinstance(_auths, (list, str)) + else set(map(int, os.getenv("AUTH_USERS").split())) + if not _auths.startswith("*") + else {"*"} + if len(_auths.split("|")) > 2 + else set(map(int, _auths.split("|")[1].split())).union({"*"}) + ) + self.log_chat = int(os.getenv("LOG_CHAT")) if os.getenv("LOG_CHAT") else None + self.use_logs = {"dl_from", "up_to"} + self.is_public = True if self.database else False + + if self.is_public: + # check if private key exists as a file + key_loc = f"{os.getcwd()}/cipher.key" + if os.path.isfile(key_loc): + with open(key_loc, "r") as f: + key = f.read().encode() + self.cipher = Fernet(key) + # check if private key exists as an env var + elif os.getenv("CYPHER_KEY"): + self.cipher = Fernet(os.getenv("CYPHER_KEY").encode()) + # otherwise exit with error code 1 + # although we can generate a new key that's not a good idea + # because if deployer lose it, it'll make all the existing data useless + # too much black magic leads to chaos + else: + logging.warning("Unable to find encryption key") + exit(1) + else: + print(" Warning: Mongodb url not found") + + # other stuff + print("> Setting up additional functions") + self.listening = {} + self.mega_running = {} + self.ddl_running = {} + self.add_handler(MessageHandler(self.use_listner)) + + def run_checks(self, func) -> Callable: + """ + Decorator to run middleware + """ + + async def cy_run(client: Client, msg: Message): + can_use = False + uid = msg.from_user.id + try: + if func.__name__ in self.use_logs: + # return if user has already started a process + if uid in self.mega_running or uid in self.ddl_running: + return await msg.reply( + "`You've already started a process. Wait until it's finished before starting another one πŸ₯±`" + ) + # send logs to the log chat if available + if self.log_chat: + _frwded = await msg.forward(chat_id=self.log_chat) + await _frwded.reply( + f"**#UPLOAD_LOG** \n\n**From:** `{uid}` \n**Get history:** `/info {uid}`" + ) + + # Check auth users + if self.database: + status = await self.database.add(uid) + if status["banned"]: + return await msg.reply( + f"**You're banned from using this bot 😬** \n\n**Reason:** `{status['reason']}`" + ) + + if "*" in self.auth_users: + can_use = True + else: + can_use = uid in self.auth_users + + if not can_use: + await msg.reply( + "`You're not authorized to use this bot πŸ™…β€β™‚οΈ` \n\n**Join @NexaBotsUpdates ❀️**" + ) + return msg.stop_propagation() + + return await func(client, msg) + # Floodwait handling + except errors.FloodWait as e: + await xsleep(e.value) + return await func(client, msg) + # pyrogram message not modified error handling + except errors.MessageNotModified: + pass + except FileExistsError: + await self.cyeor(msg, "`File already exists in the server 😬`") + await self.full_cleanup(self.dl_loc, uid) + # Other exceptions + except Exception as e: + await self.cyeor(msg, f"**Oops 🫨, Somethig bad happend!** \n\n`{e}`") + await self.full_cleanup(f"{self.dl_loc}/{uid}", uid) + logging.warning(_emsg.format(self.version, func.__module__, e)) + + return cy_run + + async def cyeor(self, msg, text: str, reply: bool = False, **kwargs): + if isinstance(msg, Message): + if reply: + await msg.reply(text) + else: + await msg.edit(text, **kwargs) + else: + if reply: + await msg.message.reply(text, **kwargs) + else: + await msg.message.edit_text(text, **kwargs) + + async def ask(self, chat_id: int, text: str, *args, **kwargs): + await self.send_message(chat_id, text, *args, **kwargs) + woop = asyncio.get_running_loop() + futr = woop.create_future() + + futr.add_done_callback( + functools.partial(lambda _, uid: self.listening.pop(uid, None), chat_id) + ) + self.listening[chat_id] = {"task": futr} + + # wait for 1 min + try: + return await asyncio.wait_for(futr, 60.0) + except asyncio.TimeoutError: + await self.send_message( + chat_id, "Task was cancelled as you haven't answered for 1 minute πŸ₯±" + ) + self.listening.pop(chat_id, None) + return None + + async def use_listner(self, _, msg: Message): + lstn = self.listening.get(msg.chat.id) + if lstn and not lstn["task"].done(): + lstn["task"].set_result(msg) + return msg.continue_propagation() + + async def full_cleanup(self, path: str = None, user_id: int = None): + """ + Delete the file/folder and the message + """ + try: + if path: + fs_cleanup(path) + self.glob_tmp.pop(user_id, None) + + # idk why I didn't do self..pop(, None), whatever this works + if user_id: + if user_id in self.mega_running: + self.mega_running.pop(user_id) + if user_id in self.ddl_running: + self.ddl_running.pop(user_id) + except: + pass + + async def send_files(self, files: list[str], chat_id: int, msg_id: int, **kwargs): + """ + Send files with automatic floodwait handling and file splitting + """ + if files: + for file in files: + # Split files larger than 2GB + if os.stat(file).st_size > self.mx_size: + await self.edit_message_text( + chat_id, + msg_id, + """ + The file you're trying to upload exceeds telegram limits 😬. + Trying to split the files πŸ”ͺ... + """, + ) + splout = f"{self.tmp_loc}/splitted" + await splitit(file, splout) + for file in listfiles(splout): + await send_as_guessed(self, file, chat_id, msg_id, **kwargs) + fs_cleanup(splout) + else: + await send_as_guessed(self, file, chat_id, msg_id, **kwargs) + else: + await self.edit_message_text( + chat_id, + msg_id, + "Couldn't find files to send. Maybe you've canceled the process πŸ€”?", + ) + + async def send_document(self, *args, **kwargs): + try: + return await super().send_document(*args, **kwargs) + except errors.FloodWait as fw: + await xsleep(fw.value) + return await self.send_document(*args, **kwargs) + + async def send_photo(self, *args, **kwargs): + try: + return await super().send_photo(*args, **kwargs) + except errors.FloodWait as fw: + await xsleep(fw.value) + return await self.send_photo(*args, **kwargs) + + async def send_animation(self, *args, **kwargs): + try: + return await super().send_animation(*args, **kwargs) + except errors.FloodWait as fw: + await xsleep(fw.value) + return await self.send_animation(*args, **kwargs) + + async def send_video(self, *args, **kwargs): + try: + return await super().send_video(*args, **kwargs) + except errors.FloodWait as fw: + await xsleep(fw.value) + return await self.send_video(*args, **kwargs) + + async def send_audio(self, *args, **kwargs): + try: + return await super().send_audio(*args, **kwargs) + except errors.FloodWait as fw: + await xsleep(fw.value) + return await self.send_audio(*args, **kwargs) diff --git a/megadl/helpers/database.py b/megadl/helpers/database.py new file mode 100644 index 00000000..c60719cb --- /dev/null +++ b/megadl/helpers/database.py @@ -0,0 +1,119 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Database functions + +from megadl.lib.aiomongo import AioMongo + + +class CypherDB: + def __init__(self) -> None: + self.mongoc = AioMongo() + self.coll_users = self.mongoc["mega_nz"]["users"] + + # <<<<<<<<<< Active user functions >>>>>>>>>> # + + async def add(self, user_id: int): + await self.mongoc.update_async( + self.coll_users, + {"_id": user_id}, + { + "_id": user_id, + "email": "", + "password": "", + "status": {"banned": False, "reason": ""}, + "total_downloads": 0, + "total_uploads": 0, + "proxy": "", + }, + no_modify=True, + upsert=True, + ) + return await self.mongoc.find_async(self.coll_users, {"_id": user_id}, {"status": 1}) + + async def plus_fl_count( + self, user_id: int, downloads: int | None = None, uploads: int | None = None + ): + if downloads: + await self.mongoc.update_async( + self.coll_users, + {"_id": user_id}, + {"$inc": {"total_downloads": downloads}}, + ) + elif uploads: + await self.mongoc.update_async( + self.coll_users, + {"_id": user_id}, + {"$inc": {"total_uploads": uploads}}, + ) + + async def delete(self, user_id: int): + await self.mongoc.delete_async(self.coll_users, {"_id": user_id}) + + async def is_there(self, user_id: int, use_acc: bool = False): + """ + Returns: + { + "email": "", + "password": "", + "status": {"banned": False, "reason": ""}, + "total_downloads": 0, + "total_uploads": 0, + "proxy": "" + } + """ + uid = {"_id": user_id} + docu = await self.mongoc.find_async(self.coll_users, uid) + if not docu: + return None + docu.pop("_id") + if use_acc: + return docu if not "" in {docu["email"], docu["password"]} else None + else: + return docu + + # <<<<<<<<<< Banned user functions >>>>>>>>>> # + async def ban_user(self, user_id: int, reason: str): + await self.mongoc.update_async( + self.coll_users, + {"_id": user_id}, + {"status": {"banned": True, "reason": reason}}, + ) + + async def unban_user(self, user_id: int): + await self.mongoc.update_async( + self.coll_users, + {"_id": user_id}, + {"status": {"banned": False, "reason": "Got unbanned"}}, + ) + + # <<<<<<<<<< Mega functions >>>>>>>>>> # + + async def mega_login(self, user_id: int, email: str, password: str): + print(user_id, email, password) + await self.mongoc.update_async( + self.coll_users, + {"_id": user_id}, + {"email": email, "password": password}, + upsert=True, + ) + + async def mega_logout(self, user_id: int): + await self.mongoc.delete_async(self.coll_users, {"_id": user_id}) + + async def how_many(self): + return (user["user_id"] async for user in self.coll_users.find({})) + + # <<<<<<<<<< Proxy functions >>>>>>>>>> # + async def update_proxy(self, user_id: int, proxy: str): + await self.mongoc.update_async( + self.coll_users, + {"_id": user_id}, + {"proxy": proxy}, + upsert=True, + ) + + async def get_proxy(self, user_id: int): + return await self.mongoc.find_async( + self.coll_users, {"_id": user_id}, {"proxy": 1} + ) diff --git a/megadl/helpers/files.py b/megadl/helpers/files.py new file mode 100644 index 00000000..7a0c0719 --- /dev/null +++ b/megadl/helpers/files.py @@ -0,0 +1,124 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: File system functions + +from time import time +from shutil import rmtree +from re import search as isit +from datetime import timedelta +from os import path, walk, makedirs, remove + +from filetype import guess +from filesplit.split import Split + +from megadl.helpers.pyros import track_progress +from megadl.helpers.sysfncs import run_partial, run_on_shell + + +# List all the files inside a dir +def listfiles(rpath: str): + """ + Return all files in the path as a list + """ + return [path.join(dpath, f) for (dpath, drnms, fnams) in walk(rpath) for f in fnams] + + +# Clean up function +def fs_cleanup(cpath: str): + if path.isfile(cpath): + remove(cpath) + elif path.isdir(cpath): + rmtree(cpath) + + +# Guess the file type and send it accordingly +async def send_as_guessed(client, file, chat_id, mid, **kwargs): + """ + Upload the file to telegram as the way it should + """ + ftype = await run_partial(guess, file) + strtim = time() + # Send file as a document if the file type could't be guessed + if not ftype: + await client.send_document( + chat_id, + file, + progress=track_progress, + progress_args=(client, [chat_id, mid], strtim), + **kwargs, + ) + else: + fmime = ftype.mime + # GIFs + if isit(r"\bimage/gif\b", fmime): + await client.send_animation( + chat_id, + file, + progress=track_progress, + progress_args=(client, [chat_id, mid], strtim), + **kwargs, + ) + # Images + elif isit(r"\bimage\b", fmime): + await client.send_photo( + chat_id, + file, + progress=track_progress, + progress_args=(client, [chat_id, mid], strtim), + **kwargs, + ) + # Audio + elif isit(r"\baudio\b", fmime): + await client.send_audio( + chat_id, + file, + progress=track_progress, + progress_args=(client, [chat_id, mid], strtim), + **kwargs, + ) + # Video + elif isit(r"\bvideo\b", fmime): + # Get duration of video in seconds + _sh = await run_partial( + run_on_shell, + f"ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 '{file}'", + ) + vid_dur = int(float(_sh)) + # Generate thumbnail for the video + _tmpth = f"{client.tmp_loc}/thumbnails" + _thumb = f"{_tmpth}/{{chat_id}}_{mid}.png" + if not path.isdir(_tmpth): + makedirs(_tmpth) + _sh = await run_partial( + run_on_shell, + f"ffmpeg -y -ss {timedelta(seconds=int(vid_dur/10))} -i '{file}' -vframes 1 '{_thumb}'", + ) + await client.send_video( + chat_id, + file, + duration=vid_dur, + thumb=_thumb, + progress=track_progress, + progress_args=(client, [chat_id, mid], strtim), + **kwargs, + ) + # Document + else: + await client.send_document( + chat_id, + file, + progress=track_progress, + progress_args=(client, [chat_id, mid], strtim), + **kwargs, + ) + + +# Split files +def _usesplit(path_in, path_out): + spl = Split(path_in, path_out) + spl.bysize(2040108421) + + +async def splitit(path_in, path_out): + await run_partial(_usesplit, path_in, path_out) diff --git a/megadl/helpers/pyros.py b/megadl/helpers/pyros.py new file mode 100644 index 00000000..042ccb8b --- /dev/null +++ b/megadl/helpers/pyros.py @@ -0,0 +1,38 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Tools and helper functions related to pyrogram + +from time import time +from math import floor +from humans import human_time, human_bytes + + +# Porogress bar for pyrogram +# Credits: SpEcHiDe's AnyDL-Bot +async def track_progress(current, total, client, ides, start, **kwargs): + now = time() + diff = now - start + if round(diff % 10.00) == 0 or current == total: + percentage = current * 100 / total + speed = current / diff + elapsed_time = round(diff) * 1000 + time_to_completion = round((total - current) / speed) * 1000 + estimated_total_time = elapsed_time + time_to_completion + + elapsed_time = human_time(elapsed_time) + estimated_total_time = human_time(estimated_total_time) + + progress = "[{0}{1}] \n**Process**: {2}%\n".format( + "β–ˆ" * floor(percentage / 5), + "β–‘" * (20 - floor(percentage / 5)), + round(percentage, 2), + ) + + tmp = f"{progress}{human_bytes(current)} of {human_bytes(total)}\n**Speed:** {human_bytes(speed)}/s\n**ETA:** {estimated_total_time if estimated_total_time != '' else '0 s'}\n" + try: + await client.edit_message_text( + ides[0], ides[1], f"{tmp}\n\n**Powered by @NexaBotsUpdates**", **kwargs + ) + except: + pass diff --git a/megadl/helpers/sysfncs.py b/megadl/helpers/sysfncs.py new file mode 100644 index 00000000..58360735 --- /dev/null +++ b/megadl/helpers/sysfncs.py @@ -0,0 +1,74 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Shell, loops and other sys functions + +import asyncio + +from functools import partial +from subprocess import Popen, PIPE +from psutil import Process as PsuPrc +from asyncio import iscoroutinefunction, get_running_loop + + +async def run_partial(func, *args, **kwargs): + """ + Run synchronous functions in a non-blocking coroutine and return the result + + - Arguments: + - func: function - Function to execute + + - Note: + Used for CPU-bound tasks or blocking IO that can't be handled efficiently with asyncio's non-blocking IO. + """ + if iscoroutinefunction(func): + return await func(*args, **kwargs) + else: + loop = get_running_loop() + return await loop.run_in_executor(None, partial(func, *args, **kwargs)) + + +def run_on_shell(cmd: str): + """ + Run shell commands and get it's output + + - Arguments: + - cmd: str - Command to execute + """ + run = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True) + shout = run.stdout.read()[:-1].decode("utf-8") + return shout + + +async def with_sub_shell(cmd: str, **kwargs): + """ + Run shell commands and get it's output + + - Arguments: + - cmd: str - Command to execute + + - Note: + Creates a subprocess using asyncio.create_subprocess_shell + Not suitable for heavy IO bound tasks (use run_on_shell with run_partial for that) + """ + process = await asyncio.create_subprocess_shell( + cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, **kwargs + ) + + stdout, stderr = await process.communicate() + + if stdout: + return stdout[:-1].decode("utf-8") + if stderr: + return stderr[:-1].decode("utf-8") + + +async def kill_family(pid: int): + """ + Murder a process and it's whole family using pid + """ + running = PsuPrc(pid) + for child in running.children(recursive=True): + child.kill() + running.kill() + await asyncio.to_thread(running.wait) diff --git a/megadl/helpers_nexa/account.py b/megadl/helpers_nexa/account.py deleted file mode 100644 index 9c971f8a..00000000 --- a/megadl/helpers_nexa/account.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2022 Itz-fork -# Don't kang this else your dad is gae - -from mega import Mega -from config import Config -from megadl.data import PROCESS_TEXT, LOGIN_ERROR_TEXT, LOGGED_AS_USER - -# Mega user account credentials -mega = Mega() - - -# Mega Account Login -def login_to_mega(): - try: - if Config.MEGA_EMAIL and Config.MEGA_PASSWORD is not None: - # Login as Mega user account - login_to_mega.m = mega.login( - Config.MEGA_EMAIL, Config.MEGA_PASSWORD) - print(LOGGED_AS_USER) - else: - print(LOGIN_ERROR_TEXT) - # Login as anonymous account - login_to_mega.m = mega.login() - except Exception as e: - print(f"Error: \n{e}") - exit() - - -# Mega client -print(PROCESS_TEXT.format("Trying to Log Into Mega.nz ...")) -login_to_mega() -m = login_to_mega.m diff --git a/megadl/helpers_nexa/decorators.py b/megadl/helpers_nexa/decorators.py deleted file mode 100644 index 5224f38b..00000000 --- a/megadl/helpers_nexa/decorators.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2022 Itz-fork -# Don't kang this else your dad is gae - -from pyrogram import Client -from pyrogram.types import Message -from typing import Callable - -from megadl.data import get_buttons -from config import Config - - -def is_public(func: Callable) -> Callable: - async def deco(megabot: Client, message: Message): - try: - if not Config.IS_PUBLIC_BOT: - if message.from_user.id not in Config.AUTH_USERS: - return await message.reply_text( - "**Sorry this bot isn't a Public Bot πŸ₯Ί! But You can make your own bot ☺️, Click on Below Button!**", - reply_markup=await get_buttons("github")) - return await func(megabot, message) - except Exception as e: - await message.reply(f"**Error:** \n`{e}`") - return deco diff --git a/megadl/helpers_nexa/mega_help.py b/megadl/helpers_nexa/mega_help.py deleted file mode 100644 index 31ae923b..00000000 --- a/megadl/helpers_nexa/mega_help.py +++ /dev/null @@ -1,144 +0,0 @@ -# Copyright (c) 2022 Itz-fork -# Don't kang this else your dad is gae - -import math -import time - -from pyrogram.enums import chat_type -from megadl import meganzbot as client -from config import Config -from megadl.data import ERROR_TEXT - - -# Credits: SpEcHiDe's AnyDL-Bot for Progress bar -async def progress_for_pyrogram( - current, - total, - ud_type, - message, - start -): - now = time.time() - diff = now - start - if round(diff % 10.00) == 0 or current == total: - # if round(current / total * 100, 0) % 5 == 0: - percentage = current * 100 / total - speed = current / diff - elapsed_time = round(diff) * 1000 - time_to_completion = round((total - current) / speed) * 1000 - estimated_total_time = elapsed_time + time_to_completion - - elapsed_time = TimeFormatter(milliseconds=elapsed_time) - estimated_total_time = TimeFormatter(milliseconds=estimated_total_time) - - progress = "[{0}{1}] \n**Process**: {2}%\n".format( - ''.join(["β–ˆ" for i in range(math.floor(percentage / 5))]), - ''.join(["β–‘" for i in range(20 - math.floor(percentage / 5))]), - round(percentage, 2)) - - tmp = progress + "{0} of {1}\n**Speed:** {2}/s\n**ETA:** {3}\n".format( - humanbytes(current), - humanbytes(total), - humanbytes(speed), - estimated_total_time if estimated_total_time != '' else "0 s" - ) - try: - await message.edit( - text="{}\n {} \n\n**Powered by @NexaBotsUpdates**".format( - ud_type, - tmp - ) - ) - except: - pass - - -def humanbytes(size): - # https://stackoverflow.com/a/49361727/4723940 - # 2**10 = 1024 - if not size: - return "" - power = 2**10 - n = 0 - Dic_powerN = {0: ' ', 1: 'Ki', 2: 'Mi', 3: 'Gi', 4: 'Ti'} - while size > power: - size /= power - n += 1 - return str(round(size, 2)) + " " + Dic_powerN[n] + 'B' - - -def TimeFormatter(milliseconds: int) -> str: - seconds, milliseconds = divmod(int(milliseconds), 1000) - minutes, seconds = divmod(seconds, 60) - hours, minutes = divmod(minutes, 60) - days, hours = divmod(hours, 24) - tmp = ((str(days) + "d, ") if days else "") + \ - ((str(hours) + "h, ") if hours else "") + \ - ((str(minutes) + "m, ") if minutes else "") + \ - ((str(seconds) + "s, ") if seconds else "") + \ - ((str(milliseconds) + "ms, ") if milliseconds else "") - return tmp[:-2] - - -# Checking log channel -def check_logs(): - if Config.LOGS_CHANNEL: - c = client.get_chat(chat_id=Config.LOGS_CHANNEL) - if c.type != chat_type.ChatType.CHANNEL: - return print(ERROR_TEXT.format("Chat is not a channel")) - elif c.username is not None: - return print(ERROR_TEXT.format("Chat is not private")) - else: - client.send_message(chat_id=Config.LOGS_CHANNEL, - text="`Mega.nz-Bot has Successfully Started!` \n\n**Powered by @NexaBotsUpdates**") - else: - print("No Log Channel ID is Given. Anyway I'm Trying to Start!") - pass - - -# Send Download or Upload logs in log channel -async def send_logs(user_id, mchat_id, up_file=None, mega_url=None, download_logs=False, upload_logs=False, import_logs=False): - if download_logs is True: - try: - if Config.LOGS_CHANNEL: - await client.send_message(chat_id=Config.LOGS_CHANNEL, text=f"**#DOWNLOAD_LOG** \n\n**User ID:** `{user_id}` \n**Chat ID:** `{mchat_id}` \n**Url:** {mega_url}") - else: - print( - f"DOWNLOAD_LOG \nUser ID: {user_id} \n\nChat ID: {mchat_id} \nUrl: {mega_url}") - except Exception as e: - await send_errors(e=e) - elif upload_logs is True: - try: - if Config.LOGS_CHANNEL: - if up_file is not None: - gib_details = await up_file.forward(Config.LOGS_CHANNEL) - await gib_details.reply_text(f"**#UPLOAD_LOG** \n\n**User ID:** `{user_id}` \n**Chat ID:** `{mchat_id}`") - elif mega_url is not None: - await client.send_message(chat_id=Config.LOGS_CHANNEL, text=f"**#UPLOAD_LOG** \n\n**User ID:** `{user_id}` \n**Chat ID:** `{mchat_id}` \n**Url:** {mega_url}") - else: - if up_file is not None: - print( - f"UPLOAD_LOG \nUser ID: {user_id} \n\nChat ID: {mchat_id}") - elif mega_url is not None: - print( - f"UPLOAD_LOG \nUser ID: {user_id} \n\nChat ID: {mchat_id} \nUrl: {mega_url}") - except Exception as e: - await send_errors(e=e) - elif import_logs is True: - try: - if Config.LOGS_CHANNEL: - await client.send_message(chat_id=Config.LOGS_CHANNEL, text=f"**#IMPORT_LOG** \n\n**User ID:** `{user_id}` \n**Chat ID:** `{mchat_id}` \n**Origin Url:** {mega_url}") - else: - print( - f"IMPORT_LOG \nUser ID: {user_id} \n\nChat ID: {mchat_id} \nOrigin Url: {mega_url}") - except Exception as e: - await send_errors(e=e) - -# Send or print errors - - -async def send_errors(e): - if Config.LOGS_CHANNEL: - await client.send_message(Config.LOGS_CHANNEL, f"**#Error** \n`{e}`") - else: - print(ERROR_TEXT.format(e)) diff --git a/megadl/helpers_nexa/megatools.py b/megadl/helpers_nexa/megatools.py deleted file mode 100644 index 63b6dddb..00000000 --- a/megadl/helpers_nexa/megatools.py +++ /dev/null @@ -1,219 +0,0 @@ -# Copyright (c) 2022 Itz-fork -# Don't kang this else your dad is gae - -import os -import subprocess - -from functools import partial -from asyncio import get_running_loop - -from megadl import meganzbot -from config import Config - - -class MegaTools: - """ - Helper class to interact with megatools cli - - Author: https://github.com/Itz-fork - Project: https://github.com/Itz-fork/Mega.nz-Bot - """ - - def __init__(self, cache_first: bool = True) -> None: - self.config = "cache/config.ini" - if cache_first and not os.path.isfile(self.config): - self.genConfig() - else: - print("\nConfig validation wasn't performed as 'cache_first=False'. Program won't work if the config was missing or corrupted!\n") - - async def download(self, url: str, chat_id: int, message_id: int, path: str = "MegaDownloads"): - """ - Download file/folder from given link - - Arguments: - - url: string - Mega.nz link of the content - - chat_id: integer - Telegram chat id of the user - - message_id: integer - Id of the progress message - - path (optional): string - Custom path to where the files need to be downloaded - """ - if await self.__is_account(): - cmd = f"megadl --config {self.config} --path {path} {url}" - else: - cmd = f"megadl --path {path} {url}" - await self.runCmd(cmd, show_updates=True, chat_id=chat_id, msg_id=message_id) - return [val for sublist in [[os.path.join(i[0], j) for j in i[2]] for i in os.walk(path)] for val in sublist] - - async def makeDir(self, path: str): - """ - Make directories - - Arguments: - - - path: string - Name of the directory - """ - cmd = f"megamkdir --config {self.config} /Root/{path}" - await self.runCmd(cmd) - - async def upload(self, file_path: str, chat_id: int, message_id: int, to_path: str = "MegaBot"): - """ - Upload files - - Arguments: - - - file_path: string - Path to the file - - chat_id: integer - Telegram chat id of the user - - message_id: integer - Id of the progress message - - to_path (optional): string - Custom path to where the files need to be uploaded - """ - if not await self.__is_account(): - raise MegaAccountNotFound - if not os.path.isfile(file_path): - raise UploadFailed(self.__genErrorMsg( - "Given path doesn't belong to a file.")) - # Checks if the remote upload path is exists - if not f"/Root/{to_path}" in await self.runCmd(f"megals --config {self.config} /Root/"): - await self.makeDir(to_path) - ucmd = f"megaput --config {self.config} --disable-previews --no-ask-password --path \"/Root/{to_path}\" \"{file_path}\"" - await self.runCmd(ucmd, show_updates=True, chat_id=chat_id, msg_id=message_id) - lcmd = f"megaexport --config {self.config} \"/Root/{to_path}/{os.path.basename(file_path)}\"" - ulink = await self.runCmd(lcmd) - if not ulink: - raise UploadFailed(self.__genErrorMsg( - "Upload failed due to an unknown error.")) - return ulink - - async def runCmd(self, cmd: str, *args, **kwargs): - """ - Run synchronous functions in a non-blocking coroutine - - Arguments - - - cmd: string - Command to execute - """ - loop = get_running_loop() - return await loop.run_in_executor( - None, - partial(self.__shellExec, cmd, *args, **kwargs) - ) - - async def __is_account(self): - return True if Config.MEGA_EMAIL and Config.MEGA_PASSWORD else False - - def genConfig(self, sp_limit: int = 0): - """ - Function to generate 'config.ini' file - - Arguments: - sp_limit: int - Maximum speed limit for both download and upload - """ - if os.path.isfile(self.config): - print("\n'config.ini' file was found. Applying changes!\n") - else: - print("\nNo 'config.ini' file was found. Creating a new config file...\n") - # Creating the config file - conf_temp = f""" -[Login] -Username = {Config.MEGA_EMAIL} -Password = {Config.MEGA_PASSWORD} - -[Cache] -Timeout = 10 - -[Network] -SpeedLimit = {sp_limit} - -[Upload] -CreatePreviews = false -""" - f = open(self.config, "w+") - f.write(conf_temp) - f.close() - - def __shellExec(self, cmd: str, show_updates: bool = False, chat_id: int = None, msg_id: int = None): - try: - run = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, shell=True, encoding="utf-8") - except FileNotFoundError: - raise MegatoolsNotFound - if show_updates: - # Live process info update - while run.poll() is None: - rsh_out = run.stdout.readline() - if rsh_out != "": - try: - meganzbot.edit_message_text( - chat_id, msg_id, f"**Process info:** \n`{rsh_out}`") - except: - pass - else: - sh_out = run.stdout.read()[:-1] - self.__checkErrors(sh_out) - return sh_out - - def __genErrorMsg(self, bs): - return f""" ->>> {bs} - -Please make sure that you've provided the correct username and password. - -You can open a new issue if the problem persists - https://github.com/Itz-fork/Mega.nz-Bot/issues - """ - - def __checkErrors(self, out): - if "not found" in out: - raise MegatoolsNotFound - - elif "Can't create directory" in out: - raise UnableToCreateDirectory(self.__genErrorMsg( - "The program wasn't able to create the directory you requested for.")) - elif "No directories specified" in out: - raise UnableToCreateDirectory( - self.__genErrorMsg("No directory name was given.")) - - elif "Upload failed" in out: - raise UploadFailed(self.__genErrorMsg( - "Uploading was cancelled due to an (un)known error.")) - elif "No files specified for upload" in out: - raise UploadFailed(self.__genErrorMsg( - "Path to the file which need to be uploaded wasn't provided")) - - elif "Can't login to mega.nz" in out: - raise LoginError - - elif "ERROR" in out: - raise UnknownError(self.__genErrorMsg( - "Operation cancelled due to an unknown error.")) - else: - return - - -# Errors - -class MegatoolsNotFound(Exception): - def __init__(self) -> None: - super().__init__("'megatools' cli is not installed in this system. You can download it at - https://megatools.megous.com/") - - -class MegaAccountNotFound(Exception): - def __init__(self) -> None: - super().__init__("Mega.nz email or password is missing") - - -class LoginError(Exception): - def __init__(self) -> None: - super().__init__("Unable to login to your mega.nz account. \n\nYou can open a new issue if the problem persists - https://github.com/Itz-fork/Mega.nz-Bot/issues") - - -class UnableToCreateDirectory(Exception): - def __init__(self, e) -> None: - super().__init__(e) - - -class UploadFailed(Exception): - def __init__(self, e) -> None: - super().__init__(e) - - -class UnknownError(Exception): - def __init__(self, e) -> None: - super().__init__(e) diff --git a/megadl/helpers_nexa/up_helper.py b/megadl/helpers_nexa/up_helper.py deleted file mode 100644 index 64d879f9..00000000 --- a/megadl/helpers_nexa/up_helper.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) 2022 Itz-fork -# Don't kang this else your dad is gae - -import os -import re -import subprocess - -from time import time -from filetype import guess -from megadl.helpers_nexa.mega_help import progress_for_pyrogram -from megadl import meganzbot as Client - -async def run_shell_cmds(command): - run = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) - shell_ouput = run.stdout.read()[:-1].decode("utf-8") - return shell_ouput - -async def get_vid_duration(input_video): - result = await run_shell_cmds(f"ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {input_video}") - return int(float(result)) if result else None - -async def guess_and_send(input_file, chat_id, message, thumb_path="cache"): - start_time = time() - f_mime = guess(f"{input_file}") - try: - mention = (await Client.get_me()).mention - except: - mention = "**Your Mega.nz-Bot!**" - if not f_mime or not f_mime.mime: - return await Client.send_document(chat_id, f"{input_file}", caption=f"`Uploaded by` {mention}", progress=progress_for_pyrogram, progress_args=("**Trying to Upload Now!** \n", message, start_time)) - try: - filemimespotted = f_mime.mime - # For gifs - if re.search(r'\bimage/gif\b', filemimespotted): - await Client.send_animation(chat_id, f"{input_file}", caption=f"`Uploaded by` {mention}", progress=progress_for_pyrogram, progress_args=("**Trying to Upload Now!** \n", message, start_time)) - # For images - elif re.search(r'\bimage\b', filemimespotted): - await Client.send_photo(chat_id, f"{input_file}", caption=f"`Uploaded by` {mention}", progress=progress_for_pyrogram, progress_args=("**Trying to Upload Now!** \n", message, start_time)) - # For videos - elif re.search(r'\bvideo\b', filemimespotted): - viddura = await get_vid_duration(f"{input_file}") - thumbnail_path = f"{thumb_path}/thumbnail_{os.path.basename(input_file)}.jpg" - if os.path.exists(thumbnail_path): - os.remove(thumbnail_path) - await run_shell_cmds(f"ffmpeg -i {input_file} -ss 00:00:01.000 -vframes 1 {thumbnail_path}") - await Client.send_video(chat_id, f"{input_file}", duration=viddura, thumb=thumbnail_path, caption=f"`Uploaded by` {mention}", progress=progress_for_pyrogram, progress_args=("**Trying to Upload Now!** \n", message, start_time)) - # For audio - elif re.search(r'\baudio\b', filemimespotted): - await Client.send_audio(chat_id, f"{input_file}", caption=f"`Uploaded by` {mention}", progress=progress_for_pyrogram, progress_args=("**Trying to Upload Now!** \n", message, start_time)) - else: - await Client.send_document(chat_id, f"{input_file}", caption=f"`Uploaded by` {mention}", progress=progress_for_pyrogram, progress_args=("**Trying to Upload Now!** \n", message, start_time)) - except Exception as e: - print(e) - await Client.send_document(chat_id, f"{input_file}", caption=f"`Uploaded by` {mention}", progress=progress_for_pyrogram, progress_args=("**Trying to Upload Now!** \n", message, start_time)) \ No newline at end of file diff --git a/megadl/lib/README.md b/megadl/lib/README.md new file mode 100644 index 00000000..c8fb8f7d --- /dev/null +++ b/megadl/lib/README.md @@ -0,0 +1,8 @@ +# About +Helper libraries for Mega.nz-Bot + + +# Extensions +- [`megatools`](megatools.py) - Wrapper for megatools cli with extended features +- [`aiomongo`](aiomongo.py) - Async wrapper for pymongo's insert, find, update and delete operations +- [`ddl`](ddl.py) - Downloader for direct download links and gdrive \ No newline at end of file diff --git a/megadl/lib/aiomongo.py b/megadl/lib/aiomongo.py new file mode 100644 index 00000000..810986c2 --- /dev/null +++ b/megadl/lib/aiomongo.py @@ -0,0 +1,89 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Async wrapper for pymongo's insert, find, update and delete operations + +from os import getenv +from typing import Any +from bson.codec_options import TypeRegistry +from pymongo.collection import Collection +from pymongo.mongo_client import MongoClient + +from megadl.helpers.sysfncs import run_partial + + +class AioMongo(MongoClient): + client = None + + def __init__( + self, + port: int | None = None, + document_class: type | None = None, + tz_aware: bool | None = None, + connect: bool | None = None, + type_registry: TypeRegistry | None = None, + **kwargs: Any + ) -> None: + self.atlas_host = getenv("MONGO_URI") + super().__init__( + self.atlas_host, + port, + document_class, + tz_aware, + connect, + type_registry, + **kwargs + ) + + async def insert_async(self, coll: Collection, query: dict, *args, **kwargs): + """ + Perform `insert_one` operation on the given collection + """ + return await run_partial(coll.insert_one, query, *args, **kwargs) + + async def find_async(self, coll: Collection, query: dict, *args, **kwargs): + """ + Perform `find_one` operation on the given collection + """ + return await run_partial(coll.find_one, query, *args, **kwargs) + + async def update_async( + self, + coll: Collection, + query: dict, + value: dict, + no_modify: bool = False, + *args, + **kwargs + ): + """ + Perform `update_one` operation on the given collection + """ + if no_modify: + return await run_partial( + coll.update_one, query, {"$setOnInsert": value}, *args, **kwargs + ) + else: + return await run_partial( + coll.update_one, query, {"$set": value}, *args, **kwargs + ) + + async def delete_async(self, coll: Collection, query: dict, *args, **kwargs): + """ + Perform `delete_one` operation on the given collection + """ + return await run_partial(coll.delete_one, query, *args, **kwargs) + + async def count_documents_async( + self, coll: Collection, query: dict, *args, **kwargs + ): + """ + Perform `count_documents` operation on the given collection + """ + return await run_partial(coll.count_documents, query, *args, **kwargs) + + async def find_many_async(self, coll: Collection, query: dict, *args, **kwargs): + """ + Perform `find` operation on the given collection + """ + return await run_partial(coll.find, query, *args, **kwargs) diff --git a/megadl/lib/ddl.py b/megadl/lib/ddl.py new file mode 100644 index 00000000..9526844f --- /dev/null +++ b/megadl/lib/ddl.py @@ -0,0 +1,109 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Downloader for direct download links and gdrive + +import os +import asyncio + +from time import time +from re import match, sub +from aiohttp import ClientSession +from aiofiles import open as async_open + +from ..helpers.pyros import track_progress + + +class Downloader: + """ + Download files from urls + + Supports + - Direct download links + - Google Drive links (shared files) + + Arguments: + - `client` - Pyrogram client object + """ + + def __init__(self, client) -> None: + self.gdrive_regex = r"https://drive\.google\.com/file/d/(.*?)/.*?\?usp=sharing" + self.tg_client = client + + async def download( + self, url: str, path: str, ides: tuple[int, int, int], **kwargs + ) -> str: + """ + Download a file from direct / gdrive link + + Arguments: + + - `url` - Url to the file + - `path` - Output path + - `ides` - Tuple of ids (chat_id, msg_id, user_id) + """ + if match(self.gdrive_regex, url): + url = await self._parse_gdrive(url) + + dl_task = asyncio.create_task( + self.from_ddl(url=url, path=path, ides=(ides[0], ides[1]), **kwargs) + ) + self.tg_client.ddl_running[ides[2]] = dl_task + return await dl_task + + async def _parse_gdrive(self, url: str): + return sub( + r"https://drive\.google\.com/file/d/(.*?)/.*?\?usp=sharing", + r"https://drive.google.com/uc?export=download&id=\1", + url, + ) + + async def from_ddl( + self, url: str, path: str, ides: tuple[int, int], **kwargs + ) -> str: + """ + Download files from a direct download link + + Arguments: + - `url` - Url of the file + - `path` - Output path + - `ides` - chat id and message id where the action takes place (chat_id, msg_id) + """ + # Create folder if it doesn't exist + wpath = f"{path}/{ides[0]}" + os.makedirs(wpath) + wpath = f"{wpath}/{os.path.basename(url)}" + + async with ClientSession() as session: + _chunksize = int(os.getenv("CHUNK_SIZE")) + async with session.get(url, timeout=None, allow_redirects=True) as resp: + # Raise HttpStatusError on failed requests + if resp.status != 200: + raise HttpStatusError(resp.status) + # Handle content length header + total = resp.content_length + curr = 0 + st = time() + async with async_open(wpath, mode="wb") as file: + async for chunk in resp.content.iter_chunked(_chunksize): + await file.write(chunk) + curr += len(chunk) + # Make sure everything is present before calling track_progress + if None not in {ides, self.tg_client, total}: + await track_progress( + curr, total, self.tg_client, ides, st, **kwargs + ) + + await asyncio.sleep(0) + return wpath + + +# Exceptions raised by the Downloader class +class InvalidUrl(Exception): + def __init__(self) -> None: + super().__init__("The provided string isn't an url!") + + +class HttpStatusError(Exception): + def __init__(self, e) -> None: + super().__init__(f"Request failed with status: {e}") diff --git a/megadl/lib/megatools.py b/megadl/lib/megatools.py new file mode 100644 index 00000000..5b6945d3 --- /dev/null +++ b/megadl/lib/megatools.py @@ -0,0 +1,450 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Wrapper for megatools cli with extended features + +import os +import re +import json +import asyncio + +from humans import human_bytes +from aiohttp import ClientSession +from megadl.helpers.files import listfiles +from megadl.helpers.sysfncs import run_partial, run_on_shell, with_sub_shell +from megadl.helpers.crypt import ( + base64_to_a32, + decrypt_attr, + decrypt_node_key, + base64_url_decode, +) + + +# regexes +class MegaRegexs: + pub_file_folder = re.compile(r"https?:\/\/mega\.nz\/(file|folder|#)?.+") + prv_file_folder = re.compile(r"\/Root\/((.*)|([^\s]*))\.") + + pub_file = re.compile( + r"https://mega.nz/(?:file/([0-z-_]+)#|#!([0-z-_]+)!)([0-z-_]+)" + ) + pub_folder = re.compile( + r"mega.[^/]+/(?:folder/([0-z-_]+)#|(?:(?:#F!([0-z-_]+))[!#]))([0-z-_]+)(?:/folder/([0-z-_]+))*" + ) + + file_id = re.compile(r"(?<=/file/)(.*)(?=#)") + file_key = re.compile(r"(?<=#)(.*)") + old_f_ik = re.compile(r"(?<=!)(.*)") + + user_total = re.compile(r"Total: (.*)") + user_used = re.compile(r"Used: (.*)") + user_free = re.compile(r"Free: (.*)") + + proxy_regex = re.compile(r'^(http|https|socks5|socks5h)://([a-zA-Z0-9\-\.]+):(\d+)$') + + +Regexes = MegaRegexs() + + +class MegaTools: + """ + Helper class for pyrogram bots to interact with megatools cli + + Author: https://github.com/Itz-fork + Project: https://github.com/Itz-fork/Mega.nz-Bot + """ + + def __init__(self, tg_client, pre_conf=None) -> None: + if pre_conf: + self.config = f"--config {tg_client.cwd}/mega.ini {pre_conf}" + elif os.getenv("USE_ENV") in ["True", "true"]: + self.config = "--username $MEGA_EMAIL --password $MEGA_PASSWORD" + elif os.path.isfile(f"{tg_client.cwd}/mega.ini"): + self.config = f"--config {tg_client.cwd}/mega.ini" + else: + self.config = "" + self.client = tg_client + + async def download( + self, + url: str, + user_id: int, + chat_id: int, + message_id: int, + path: str = "MegaDownloads", + **kwargs, + ) -> list: + """ + Download file/folder from given link + + Arguments: + - url: string - Mega.nz link of the content + - user_id: integer - Telegram user id of the user + - chat_id: integer - Telegram chat id of the user + - message_id: integer - Id of the progress message + - path (optional): string - Custom path to where the files need to be downloaded + """ + # Public link download: Supports both file and folders + if Regexes.pub_file_folder.match(url): + cmd = f'megadl {self.config} --path "{path}" {url}' + + # Private link downloads: Supports both file and folders + elif Regexes.prv_file_folder.match(url): + cmd = f'megaget --no-ask-password {self.config} --path "{path}" {url}' + + else: + cmd = f'megacopy --no-ask-password {self.config} -l "{path}" -r "{url}" --download' + await run_partial( + self.__shellExec, + cmd, + user_id=user_id, + chat_id=chat_id, + msg_id=message_id, + **kwargs, + ) + return listfiles(path) + + async def upload( + self, + path: str, + user_id: int, + chat_id: int, + message_id: int, + to_path: str = "MegaBot", + **kwargs, + ) -> str: + """ + Upload files/folders + + Arguments: + + - path: string - Path to the file or folder + - user_id: integer - Telegram user id of the user + - chat_id: integer - Telegram chat id of the process (could be either private or group id) + - message_id: integer - Id of the progress message + - to_path (optional): string - Custom path to where the files need to be uploaded + """ + cmd = "" + # For files + if os.path.isfile(path): + cmd = f'megaput {self.config} --no-ask-password --path "/Root/{to_path}/" "{path}"' + # For folders + elif os.path.isdir(path): + cmd = f'megacopy {self.config} --no-ask-password -l "{path}" -r "/Root/{to_path}"' + else: + raise UploadFailed( + self.__genErrorMsg("Given path doesn't belong to a file or folder.") + ) + # Create remote upload path if it doesn't exist + if not f"/Root/{to_path}" in ( + await run_partial(run_on_shell, f"megals {self.config} /Root/") + ): + await run_partial( + run_on_shell, f'megamkdir {self.config} "/Root/{to_path}"' + ) + # Upload + await run_partial( + self.__shellExec, + cmd, + user_id=user_id, + chat_id=chat_id, + msg_id=message_id, + **kwargs, + ) + # Generate link + ulink = await run_partial( + run_on_shell, + f'megaexport {self.config} "/Root/{to_path}/{os.path.basename(path)}"', + ) + if not ulink: + raise UploadFailed( + self.__genErrorMsg("Upload failed due to an unknown error.") + ) + return ulink + + async def user_fs(self): + """ """ + _sh_dta = await with_sub_shell(f"megadf -h {self.config}") + if _sh_dta: + _total = Regexes.user_total.search(_sh_dta).group(1) + _used = Regexes.user_used.search(_sh_dta).group(1) + _free = Regexes.user_free.search(_sh_dta).group(1) + return _total, _used, _free + else: + raise LoginError + + @staticmethod + async def get_info(url: str) -> list[str]: + """ + Return basic info of a public file/folder + + Arguments: + - url: string - Mega.nz link of the file / folder + + Returns: + - File: + - (file_size, file_name) + + - Folder: + - string of all the files and sub dirs in the url + + - Otherwise: + - (undefined, undefined) + """ + is_file = Regexes.pub_file.search(url) + is_folder = Regexes.pub_folder.search(url) + + if is_file: + file_id = None + file_key = None + data = None + # for mega.nz/file/ + if "/file/" in url: + file_id = Regexes.file_id.search(url)[0] + file_key = Regexes.file_key.search(url)[0] + # for mega.nz/#!...!.... + elif "/#!" in url: + file_id, file_key = Regexes.old_f_ik.search(url)[0].split("!") + else: + return "undefined", "undefined" + + async with ClientSession() as session: + async with session.post( + "https://g.api.mega.co.nz/cs", + json=[{"a": "g", "ad": 1, "p": file_id}], + ) as resp: + data = (await resp.json())[0] + key = base64_to_a32(file_key) + tk = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]) + fsize = human_bytes(data["s"]) + fname = decrypt_attr(base64_url_decode(data["at"]), tk)["n"] + return [fsize, fname] + + elif is_folder: + root_folder, shared_enc_key = tuple( + [x for x in is_folder.groups() if x is not None] + ) + shared_key = base64_to_a32(shared_enc_key) + + # get nodes in a the folder + nodes = None + data = [{"a": "f", "c": 1, "ca": 1, "r": 1}] + # print(nodes) + session = ClientSession() + resp = await session.post( + "https://g.api.mega.co.nz/cs", + params={"id": 0, "n": root_folder}, + data=json.dumps(data), + ) + nodes = (await resp.json())[0]["f"] + + if not nodes: + return "undefined", "undefined" + + # get nodes in a sub dir + async def get_sub_dir_nodes(root_folder, sub_dir): + resp = await session.post( + "https://g.api.mega.co.nz/cs", + params={"id": 0, "n": root_folder}, + data=json.dumps(data), + ) + json_resp = await resp.json() + if ( + isinstance(json_resp, list) + and isinstance(json_resp[0], dict) + and "f" in json_resp[0] + ): + all_nodes = json_resp[0]["f"] + sub_dir_nodes = [node for node in all_nodes if node["p"] == sub_dir] + return sub_dir_nodes + else: + return [] + + # make string + to_return = "" + num = 1 + + async def prepare_string(nodes, shared_key, depth=0): + nonlocal to_return + nonlocal num + for node in nodes: + key = decrypt_node_key(node["k"], shared_key) + file_id = node["h"] + if key: + file_size = node["s"] if "s" in node else "" + if node["t"] == 0: # file + k = ( + key[0] ^ key[4], + key[1] ^ key[5], + key[2] ^ key[6], + key[3] ^ key[7], + ) + attrs = decrypt_attr(base64_url_decode(node["a"]), k) + file_name = attrs["n"] + to_return += f"{' ' * depth}β”œβ”€β”€ {file_name} ({human_bytes(file_size)})\n" + elif node["t"] == 1: # folder + k = key + attrs = decrypt_attr(base64_url_decode(node["a"]), k) + file_name = attrs["n"] + to_return += f"{' ' * depth}β”œβ”€ {file_name}\n" + await prepare_string( + await get_sub_dir_nodes(root_folder, file_id), + shared_key, + depth + 1, + ) + if len(nodes) == num: + break + num += 1 + + await prepare_string(nodes, shared_key) + await session.close() + return to_return + + else: + return "undefined", "undefined" + + async def __shellExec( + self, cmd: str, user_id: int, chat_id: int = None, msg_id: int = None, **kwargs + ): + print(cmd) + run = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=self.client.environs, + ) + self.client.mega_running[user_id] = run.pid + + async def read_stream(stream, handler): + while True: + line = await stream.readline() + if line: + await handler( + line.decode("utf-8") if isinstance(line, bytes) else line + ) + else: + break + + async def handle_stdout(out): + try: + await self.client.edit_message_text( + chat_id, msg_id, f"**Process info:** \n`{out}`", **kwargs + ) + except Exception as e: + print(e) + + async def handle_stderr(err): + if run.returncode is None: + await self.__checkErrors(err) + + stdout = read_stream(run.stdout, handle_stdout) + stderr = read_stream(run.stderr, handle_stderr) + + try: + await asyncio.gather(stdout, stderr) + except asyncio.CancelledError: + asyncio.create_task(self.__terminate_sub(run)) + + await run.wait() + + async def __terminate_sub(run): + run.terminate() + await run.wait() + + def __genErrorMsg(self, bs): + return f""" +>>> {bs} + +Please make sure that you've provided the correct username and password. + +You can open a new issue if the problem persists - https://github.com/Itz-fork/Mega.nz-Bot/issues + """ + + async def __checkErrors(self, out): + if "command not found" in out: + raise MegatoolsNotFound() + + elif "Remote directory not found" in out: + raise RemoteContentNotFound() + + elif "File already exists" in out: + raise FileAlreadyExists() + + elif "already exists at" in out: + pass + + elif "Can't create directory" in out: + raise UnableToCreateDirectory( + self.__genErrorMsg( + "The program wasn't able to create the directory you requested for." + ) + ) + elif "No directories specified" in out: + raise UnableToCreateDirectory( + self.__genErrorMsg("No directory name was given.") + ) + + elif "Upload failed" in out: + raise UploadFailed( + self.__genErrorMsg("Uploading was cancelled due to an (un)known error.") + ) + elif "No files specified for upload" in out: + raise UploadFailed( + self.__genErrorMsg( + "Path to the file which need to be uploaded wasn't provided" + ) + ) + + elif "Can't login to mega.nz" in out: + raise LoginError + + elif "ERROR" in out: + raise UnknownError( + self.__genErrorMsg("Operation cancelled due to an unknown error.") + ) + else: + return + + +# Errors +class MegatoolsNotFound(Exception): + def __init__(self) -> None: + super().__init__( + "'megatools' cli is not installed in this system. You can download it at - https://megatools.megous.com/" + ) + + +class RemoteContentNotFound(Exception): + def __init__(self) -> None: + super().__init__( + "The file or folder you're trying to download doesn't exist in your account" + ) + + +class FileAlreadyExists(Exception): + def __init__(self) -> None: + super().__init__( + "File you're trying to upload already exists in your account under 'MegaBot' folder" + ) + + +class LoginError(Exception): + def __init__(self) -> None: + super().__init__( + "Unable to login to your mega.nz account. \n\nYou can open a new issue if the problem persists - https://github.com/Itz-fork/Mega.nz-Bot/issues" + ) + + +class UnableToCreateDirectory(Exception): + def __init__(self, e) -> None: + super().__init__(e) + + +class UploadFailed(Exception): + def __init__(self, e) -> None: + super().__init__(e) + + +class UnknownError(Exception): + def __init__(self, e) -> None: + super().__init__(e) diff --git a/megadl/lib/notes.md b/megadl/lib/notes.md new file mode 100644 index 00000000..b0bef29c --- /dev/null +++ b/megadl/lib/notes.md @@ -0,0 +1,219 @@ +# Megatools dev notes +Developer notes to work with mega.nz api + + +### Requesting nodes of a shared node +**Request:** Please refer to [`megatools.get_info`](/megatools.py?L=160) +**Response:** +```json +[ + { + "h": "sysQhDbK", + "p": "V6V01DBB", + "u": "TnKrSNAJt4U", + "t": 1, + "a": "bnsrEUjW2A5gNe1qXiiQ_oySfftOThPhiowtaoU5T48", + "k": "sysQhDbK:QXelBbZDeCYXN-M_Pu7VkQ", + "ts": 1700099874, + }, + ... +] +``` +**Meaning:** +- "h": This is the handle ID of the file or folder. It's a unique identifier used by Mega.nz to refer to this specific file or folder. +- "p": This is the parent handle ID, which is the handle ID of the parent folder that this file or folder resides in. +- "u": This is the owner's user handle ID. It's a unique identifier for the user who owns this file or folder. +- "t": This is the type of the node. 0 means it's a file, 1 means it's a folder. +- "a": This is the attributes of the file or folder, which are encrypted. It usually contains the name of the file or folder after decryption. +- "k": This is the encrypted key of the file or folder. It's used to decrypt the file or folder's data. +- "s": This is the size of the file in bytes. It's not present if the node is a folder. +- "ts": This is the timestamp when the file or folder was last modified. It's in Unix time format (seconds since 1970-01-01 00:00:00 UTC). + + +**Example:** + +```py +url = "https://mega.nz/folder/kyVnRBTC#LNLkMU1XRSlgds9dmtricA" +async with ClientSession() as session: + params = {"id": 0, "n": root_folder} + data = json.dumps(data) + async with session.post("https://g.api.mega.co.nz/cs", params=params, data=data) as resp: + nodes = (await resp.json())[0]["f"] +``` +```json +[ + { + "h": "sysQhDbK", + "p": "V6V01DBB", + "u": "TnKrSNAJt4U", + "t": 1, + "a": "bnsrEUjW2A5gNe1qXiiQ_oySfftOThPhiowtaoU5T48", + "k": "sysQhDbK:QXelBbZDeCYXN-M_Pu7VkQ", + "ts": 1700099874, + }, + { + "h": "xmt0EYSB", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "l-bKSQqGAE2TNV9IY1BM_toJc_CLLTAxmD1X_O9roNwzpaaj7V6d7x04eKCWE8IXgSE2-nolNc587JOcWoIymQ", + "k": "sysQhDbK:i4AffLKntCDlSttMmehcOynL5G8JyERenLpg99AeX6s", + "s": 13122, + "fa": "890:1*VZQRRp1QKYI/890:0*CQL62liSrUI", + "ts": 1700101974, + }, + { + "h": "MjMTVbJS", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "KQYy1iBboKogMuTmJdVmbiQqktBlc2w0pQnKARE8HUs", + "k": "sysQhDbK:JnFnmmshH15OLYl6FXZMqBoVffh6KUDb3qrZa5cyWTg", + "s": 1593497, + "ts": 1700329169, + }, + { + "h": "hrcGyLbY", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "Pm7H0Dnv_CmJ8ez821kWNt0NaM9-ZT78ayJPQMrH52A", + "k": "sysQhDbK:dPId-z-aap5LocAUmX98dkQuddbpDlahLJ-zWf1VoB4", + "s": 1593497, + "ts": 1700329639, + }, + { + "h": "Jjsy0LKY", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "oCCVXOeJlgxTIlyk8hz6qs2h1DB2lJF4o-FBXwKCkWfhfLX8Ci53G2a1QLBuBVsdY3Vt1nzSXkvw2bdbg8YTIA", + "k": "sysQhDbK:njevAZbQbP3cuRMfROwvA8vwLiFNJdATqdPX5rw7ayI", + "s": 1593497, + "ts": 1700329763, + }, + { + "h": "JvMQFRIK", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "KR3smaRfDHQ_XRpOLdn_e9slaxe_zYqsfWcPT7gInSW-oDYS0kc_ELZJwY61BkffdGEVXrfNPFrfKWtBH95yLw", + "k": "", + "s": 80299, + "ts": 1703303819, + }, + { + "h": "B70kHJiA", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "ohhUSw5SoCGX_KPpfOe-KNxBI5e_ArNxxtqxgRQzMF0", + "k": "", + "s": 309868, + "ts": 1703357584, + }, + { + "h": "QzUAHZaT", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "CLwNRF1cRA4gi2uakRpsXmsdQbOCMl6bnAQpI0JeihM", + "k": "", + "s": 29380545, + "ts": 1703404548, + }, + { + "h": "1q1FVQhB", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "v5w0IeXk1oZU2Y3iORtULh2294FGfmWH4cpesAzaN-9vAbF1Ro1yOhy-JVDuDb_78zbNQqgbmQNBK2SSUYTllA", + "k": "", + "s": 119919, + "ts": 1703533602, + }, + { + "h": "4mUV0RxJ", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "yAKkfAaM73MF4DAceYuPgQTrNFX-ScwH5PGi6usJ9sE", + "k": "", + "s": 119919, + "ts": 1703533697, + }, + { + "h": "QilC3CRS", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "2zGRb-uS-C-daouN6-JfqyONWf7PKYGOurlkNRHGE6b1SuAIrB-1znOu79Lr6SZ6I9dQsIQ0SiGIB2RJD_hgSw", + "k": "", + "s": 119919, + "ts": 1703533756, + }, + { + "h": "pusxRKiQ", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "uxg0W-TGdyKwkWq0FaFkUf7cp9jLvC4rA5e1SRmRmtd2gbw13rKZCrbuRdArO_ZejNlRKjLs28HnLfJS1G7Bfg", + "k": "", + "s": 119919, + "ts": 1703533823, + }, + { + "h": "0zkyBRQB", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "zYoorHD9IZNt_JimVQLW2dFu4MWaoi5oqZteYf2m84FwKZ_IvCIduPiOp_EQ-Vtex3ly8g-t819LWfeHe9L35g", + "k": "", + "s": 119919, + "ts": 1703595696, + }, + { + "h": "Q2kC3ahT", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "e8GCdzo9sWek5bMZwRPV5R9eX3f9euzVKnw_47NwCCU", + "k": "", + "s": 119919, + "ts": 1703608309, + }, + { + "h": "d69mHJaQ", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "VqNaP8wP6r7Lq7itjJeQ2tywbzhd33SYv7KNzNFLbEddFy2jgPyfEEJv-Ki0PsfwVdcw2In9esL-SS76NgZdbw", + "k": "", + "s": 119919, + "fa": "115:0*ZxoCHoZin7w", + "ts": 1703870835, + }, + { + "h": "Z6tXQbZD", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "-NQnlI_wgBq5SDuDN9nvqM8DzIk7LAI3eDKdSnr5jQQ4CzWnf0P8Zw0ajiOsaJf2LTSyEPYM_KYT5WOFPRq3zw", + "k": "", + "s": 119919, + "fa": "700:0*hJxqzZ4BN8s", + "ts": 1703871085, + }, + { + "h": "wvFRADiK", + "p": "sysQhDbK", + "u": "TnKrSNAJt4U", + "t": 0, + "a": "4W0R8WSdUCGkyU610L6w_ohceu_X6dZBbm7DY61Nzc6zR3wzJFDOkJvqhkpV0Au1t0vCkFANikUlSmOJ3-r9MQ", + "k": "", + "s": 16832, + "fa": "701:0*mDTZFNtqpG8", + "ts": 1703873565, + }, +] +``` \ No newline at end of file diff --git a/megadl/modules/admin.py b/megadl/modules/admin.py new file mode 100644 index 00000000..90737d2c --- /dev/null +++ b/megadl/modules/admin.py @@ -0,0 +1,94 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Admin functions + +from pyrogram import filters +from pyrogram.types import Message + +from megadl import CypherClient + + +@CypherClient.on_message(filters.command("info")) +@CypherClient.run_checks +async def admin_user_info(client: CypherClient, msg: Message): + if client.auth_users == "*" or msg.from_user.id not in client.auth_users: + return await msg.reply("Getting user info can only be done by admins!") + + buid = None + try: + buid = int(msg.text.split(None, 1)[1]) + except: + pass + if not buid or not isinstance(buid, int): + return await msg.reply("Provide a user id to get info \n\nEx: `/info 12345`") + + _user = await client.database.is_there(buid) + if not _user: + return await msg.reply("Unable to find user in the database πŸ”Ž") + + status = _user["status"] + is_ban = status["banned"] + ban_rsn = status["reason"] + dl_count = _user["total_downloads"] + up_count = _user["total_uploads"] + + ban_status = ( + f"`Banned`\n β€· `{ban_rsn}`" + if is_ban + else "Admin" + if buid in client.auth_users + else "`Active`" + ) + await msg.reply( + f""" +**User Info** + β€· **ID:** `{buid}` + β€· **Status:** {ban_status} + β€· **Total downloads:** `{dl_count}` + β€· **Total uploads:** `{up_count}` + + +**Detect abuse?** +If you think above total counts are abnormal, you can ban the user with `/ban {buid} abusing` +""" + ) + + +@CypherClient.on_message(filters.command("ban")) +@CypherClient.run_checks +async def admin_ban_user(client: CypherClient, msg: Message): + if msg.from_user.id not in client.auth_users: + return await msg.reply("Banning users can only be done by admins!") + + buid = None + reason = None + try: + _splt = msg.text.split(None, 1) + buid = int(_splt[1]) + reason = _splt[2] if len(_splt) >= 3 else "No reason given" + except: + return await msg.reply("Provide a user id to ban \n\nEx: `/ban 12345 spamming`") + + if buid in client.auth_users: + return await msg.reply("Why do you want to ban an admin πŸ˜Άβ€πŸŒ«οΈ?") + + await client.database.ban_user(buid, reason) + await msg.reply(f"Banned user `{buid}` βœ…") + + +@CypherClient.on_message(filters.command("unban")) +@CypherClient.run_checks +async def admin_unban_user(client: CypherClient, msg: Message): + if msg.from_user.id not in client.auth_users: + return await msg.reply("Unbanning users can only be done by admins!") + buid = None + try: + buid = int(msg.text.split(None, 1)[1]) + except: + pass + if not buid or not isinstance(buid, int): + return await msg.reply("Provide a user id to unban \n\nEx: `/unban 12345`") + + await client.database.unban_user(buid) + await msg.reply(f"Unbanned user `{buid}` βœ…") diff --git a/megadl/modules/auth.py b/megadl/modules/auth.py new file mode 100644 index 00000000..32bfde98 --- /dev/null +++ b/megadl/modules/auth.py @@ -0,0 +1,44 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Authorize mega account of users + + +from pyrogram import filters +from pyrogram.enums import ChatType +from pyrogram.types import Message + +from megadl import CypherClient + + +@CypherClient.on_message(filters.command("login")) +@CypherClient.run_checks +async def mega_logger(client: CypherClient, msg: Message): + if msg.chat.type != ChatType.PRIVATE: + return await msg.reply("`You can only login in private chats for obivious reasons`") + user_id = msg.chat.id + + email = await client.ask(user_id, "`Enter your Mega.nz email:`") + if not email: + return await msg.reply("You` **must** `send your Mega.nz email in order to login") + password = await client.ask(user_id, "`Enter your Mega.nz password:`") + if not password: + return await msg.reply("`You` **must** `send your Mega.nz password in order to login`") + + # encrypt the email and password for security + email = client.cipher.encrypt(email.text.encode()) + password = client.cipher.encrypt(password.text.encode()) + + await client.database.mega_login(user_id, email, password) + await msg.reply("`Successfully logged in βœ…`") + + +@CypherClient.on_message(filters.command("logout")) +@CypherClient.run_checks +async def mega_logoutter(client: CypherClient, msg: Message): + really = await client.ask(msg.chat.id, "Are you sure you want to logout? (y/n)") + if really.text.lower() == "y": + await client.database.mega_logout(msg.chat.id) + await msg.reply("`Successfully logged out βœ…`") + else: + await msg.reply("`Logout cancelled ❌`") diff --git a/megadl/modules/bonus.py b/megadl/modules/bonus.py new file mode 100644 index 00000000..46c56a1d --- /dev/null +++ b/megadl/modules/bonus.py @@ -0,0 +1,105 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Bonus functions such as file/folder info, proxy setting and account status + +from pyrogram import filters +from aiohttp import ClientSession +from pyrogram.types import ( + Message, + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, +) + +from megadl import CypherClient +from megadl.lib.megatools import Regexes, MegaTools + + +@CypherClient.on_callback_query(filters.regex(r"info_mg?.+")) +@CypherClient.run_checks +async def info_from_cb(client: CypherClient, query: CallbackQuery): + url = client.glob_tmp.get(query.from_user.id)[0] + await query.edit_message_text("`Getting info ℹ️...`") + retrieved = await MegaTools.get_info(url) + + if isinstance(retrieved, list): + await query.edit_message_text( + f""" +》 **File Details** + +**πŸ“› Name:** `{retrieved[0]}` +**πŸ—‚ Size:** `{retrieved[1]}` +**πŸ“Ž URL:** `{url}` +""", + reply_markup=None, + ) + + else: + async with ClientSession() as nekoc: + resp = await nekoc.post( + "https://nekobin.com/api/documents", json={"content": retrieved} + ) + if resp.status == 201: + nekourl = f"https://nekobin.com/{(await resp.json())['result']['key']}" + await query.edit_message_text( + f"**πŸ‘€ View is the generated [folder info]({nekourl})**", + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton("Visit πŸ”—", url=nekourl)]] + ), + ) + else: + await query.edit_message_text( + "`Failed to generate folder info πŸ˜•, Please try again later`" + ) + + +@CypherClient.on_message(filters.command("acc")) +@CypherClient.run_checks +async def acc(_: CypherClient, msg: Message): + # Check if the user exists in database + usr = msg.from_user.id + udoc = await _.database.is_there(usr, True) + if not udoc: + return await msg.reply( + "`You must be logged in first to check account status πŸ˜‘`" + ) + + # Get user data + email = _.cipher.decrypt(udoc["email"]).decode() + password = _.cipher.decrypt(udoc["password"]).decode() + conf = f"--username {email} --password {password}" + cli = MegaTools(_, conf) + total, used, free = await cli.user_fs() + + return await msg.reply( + f""" +**~ Your User Account Info ~** + +✦ **Email:** `{email}` +✦ **Password:** `{password}` +✦ **Storage,** + β€· **Total:** `{total}` + β€· **Used:** `{used}` + β€· **Free:** `{free}` +""" + ) + + +@CypherClient.on_message(filters.command("proxy")) +@CypherClient.run_checks +async def set_user_proxy(client: CypherClient, msg: Message): + prxy = None + try: + prxy = msg.text.split(None, 1)[1] + except: + pass + if not prxy or not Regexes.proxy_regex.match(prxy): + return await msg.reply( + "Provide a proxy to set \n\nEx: `/proxy socks5h://localhost:9050`" + ) + + await client.database.update_proxy(msg.from_user.id, prxy) + await msg.reply( + f"Proxy set to: `{prxy}` \n\n**Note:**\nIf your download seems to stuck, it's likely due to proxy not working properly. Free proxies on internet do not work!" + ) diff --git a/megadl/modules/callbacks.py b/megadl/modules/callbacks.py index e2ba1670..4bf650af 100644 --- a/megadl/modules/callbacks.py +++ b/megadl/modules/callbacks.py @@ -1,78 +1,41 @@ -# Copyright (c) 2022 Itz-fork -# Don't kang this else your dad is gae - -import shutil - -from pyrogram import Client, filters, __version__ as pyrogram_version -from pyrogram.types import Message, CallbackQuery - -from megadl.helpers_nexa.mega_help import send_errors -from megadl.data import get_buttons, get_msg -from config import Config - - -# Callbacks -@Client.on_callback_query() -async def meganz_cb(megabot: Client, query: CallbackQuery): - if query.data == "startcallback": - await query.edit_message_text(f"Hi **{query.from_user.first_name}** πŸ˜‡!, \n\nI'm **@{(await megabot.get_me()).username}**, \nA Simple Mega.nz Downloader Bot πŸ˜‰! \n\nUse Below Buttons to Know More About Me and My Commands 😁 \n\n**Made with ❀️ by @NexaBotsUpdates**", reply_markup=await get_buttons("start")) - - elif query.data == "helpcallback": - await query.edit_message_text(f"**Here is the Commands Help Menu Of @{(await megabot.get_me()).username}** \n\nUse Below Buttons to Get Help Menu of That Module 😊", reply_markup=await get_buttons("help")) - - elif query.data == "meganzdownloadercb": - user_id = query.from_user.id - if not Config.IS_PUBLIC_BOT: - if user_id not in Config.AUTH_USERS: - return await query.answer("Sorry This Bot is a Private Bot πŸ˜”! \n\nJoin @NexaBotsUpdates to Make your own bot!", show_alert=True) - await query.edit_message_text(await get_msg("dl"), reply_markup=await get_buttons("mod_help")) - - elif query.data == "meganzuploadercb": - user_id = query.from_user.id - if not Config.IS_PUBLIC_BOT: - if user_id not in Config.AUTH_USERS: - return await query.answer("Sorry This Bot is a Private Bot πŸ˜”! \n\nJoin @NexaBotsUpdates to Make your own bot!", show_alert=True) - await query.edit_message_text(await get_msg("up"), reply_markup=await get_buttons("mod_help")) - - elif query.data == "meganzimportercb": - user_id = query.from_user.id - if not Config.IS_PUBLIC_BOT: - if user_id not in Config.AUTH_USERS: - return await query.answer("Sorry This Bot is a Private Bot πŸ˜”! \n\nJoin @NexaBotsUpdates to Make your own bot!", show_alert=True) - await query.edit_message_text(await get_msg("import"), reply_markup=await get_buttons("mod_help")) - - elif query.data == "aboutcallback": - await query.edit_message_text(f"**About Mega.nz Bot** \n\n\n ✘ **Username:** @{(await megabot.get_me()).username} \n\n ✘ **Language:** [Python](https://www.python.org/) \n\n ✘ **Library:** [Pyrogram](https://docs.pyrogram.org/) \n\n ✘ **Pyrogram Version:** `{pyrogram_version}` \n\n ✘ **Source Code:** [Mega.nz-Bot](https://github.com/Itz-fork/Mega.nz-Bot) \n\n ✘ **Developer:** [Itz-fork](https://github.com/Itz-fork) \n\n**Made with ❀️ by @NexaBotsUpdates**", reply_markup=await get_buttons("about"), disable_web_page_preview=True) - - elif query.data == "inlinehelpcallback": - await query.edit_message_text(f"**Here is the Commands Help Menu Of @{(await megabot.get_me()).username}** \n\nUse Below Buttons to Get Help Menu of That Module 😊", reply_markup=await get_buttons("inline_help")) - - elif query.data == "getfiledetailscb": - await query.edit_message_text((await get_msg("file_info")).format(uname=(await megabot.get_me()).username), reply_markup=await get_buttons("imod_help")) - - elif query.data == "getaccoutinfo": - await query.edit_message_text((await get_msg("acc_info")).format(uname=(await megabot.get_me()).username), reply_markup=await get_buttons("imod_help")) - - elif query.data == "cancelvro": - userpath = str(query.from_user.id) - try: - shutil.rmtree(Config.DOWNLOAD_LOCATION + "/" + userpath) - await query.message.delete() - await query.message.reply_text("`Process Cancelled by User`") - except Exception as e: - await send_errors(e) - - elif query.data == "closeqcb": - try: - await query.message.delete() - await query.answer(f"Closed Help Menu of @{(await megabot.get_me()).username}") - except: - await query.answer(f"Can't Close Via Inline Messages!") - - -# Start message -@Client.on_message(filters.command("start")) -async def startcmd(megabot: Client, message: Message): - await message.reply_text( - f"Hi **{message.from_user.first_name}** πŸ˜‡!, \n\nI'm **@{(await megabot.get_me()).username}**, \nA Simple Mega.nz Downloader Bot with some cool features πŸ˜‰! \n\nUse Below Buttons to Know More About Me and My Commands 😁 \n\n**Made with ❀️ by @NexaBotsUpdates**", - reply_markup=await get_buttons("start")) +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Callbacks that can be used globally + +import asyncio +from pyrogram import filters +from pyrogram.types import CallbackQuery + +from megadl import CypherClient +from megadl.helpers.sysfncs import kill_family + + +@CypherClient.on_callback_query(filters.regex(r"cancelqcb?.+")) +@CypherClient.run_checks +async def close_gb(client: CypherClient, query: CallbackQuery): + usr = int(query.data.split("-")[1]) + try: + # get user from global temp db + dtmp = client.glob_tmp.get(usr) + + # cancel if user has a download running + # this cancels both mega and ddl downloads/uploads + # it should'nt be a problem since user can do either of those task at a time + mg_prc = client.mega_running.get(usr) + dl_prc = client.ddl_running.get(usr) + if mg_prc: + await kill_family(mg_prc) + if dl_prc: + dl_prc.cancel() + await asyncio.sleep(0) + + # Remove download folder of the user + if dtmp: + await client.full_cleanup(dtmp[1], usr) + except Exception as e: + print(e) + await query.edit_message_text( + "`Process was canceled by the user ❌`", + reply_markup=None, + ) diff --git a/megadl/modules/generals.py b/megadl/modules/generals.py new file mode 100644 index 00000000..cb24beb8 --- /dev/null +++ b/megadl/modules/generals.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: General commands of the bot + + +from pyrogram import filters +from pyrogram.types import Message + +from megadl import CypherClient + + +@CypherClient.on_message(filters.command("start")) +@CypherClient.run_checks +async def start_msg(_: CypherClient, msg: Message): + await msg.reply_text( + f""" +Hi `{msg.from_user.first_name}` πŸ‘‹, I'm [Mega.nz-Bot](https://github.com/Itz-fork/Mega.nz-Bot)! + +I can help you download, upload files or folders from telegram. +Not sure what to do? Check /help for more info πŸ˜‡ + + +**Made with ❀️ by @NexaBotsUpdates** + """, + disable_web_page_preview=True, + ) + + +@CypherClient.on_message(filters.command("help")) +@CypherClient.run_checks +async def help_msg(_: CypherClient, msg: Message): + await msg.reply_text( + f""" +**✘ How do I login?** + β€· Send /login command and enter your details when I ask you. Don't worry we encrypt your data before sending it anywhere πŸ€— + +**✘ How to download from mega link?** + β€· It's very easy. Just send the link you want to download and I'll download it for you πŸ˜‰. + β€· For private content you need to login first then send path to the file or folder you want to download starting with `/Root/`. + +**✘ How to upload files to Mega.nz?** + β€· Just send me the files and I'll ask you whether you want to upload it or not. Same goes for direct download links 😎 + +**✘ How to setup proxy?** + β€· Send /proxy command alongside the proxy πŸ“‘ (Ex: `/proxy https://example.com:8080`) + Please note that the free proxies you see on internet are **not working** + +** ✘ How to get my account details?** + β€· Send /acc command and I'll send you your account details 🫣 + + +**Made with ❀️ by @NexaBotsUpdates** + """ + ) diff --git a/megadl/modules/inline_megadl.py b/megadl/modules/inline_megadl.py deleted file mode 100644 index 3dfd9e92..00000000 --- a/megadl/modules/inline_megadl.py +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright (c) 2022 Itz-fork -# Don't kang this else your dad is gae - -import json - -from pyrogram import Client -from pyrogram.types import InlineQueryResultArticle, InputTextMessageContent, InlineKeyboardMarkup, InlineKeyboardButton -from functools import partial -from asyncio import get_running_loop - -from megadl.helpers_nexa.account import m -from megadl.helpers_nexa.mega_help import humanbytes, send_errors -from .callbacks import get_buttons -from .user_account import USER_ACC_INFO -from config import Config - - -@Client.on_inline_query() -async def inline_megadl(client, query): - try: - answers = [] - megadl_q = query.query.strip().lower() - if megadl_q.strip() == "": - aboutinlinemsg = """ - βœͺ **[Mega.nz-Bot](https://github.com/Itz-fork/Mega.nz-Bot)** -`A Simple ` [Open Source](https://github.com/Itz-fork/Mega.nz-Bot)` Telegram Bot to Do `[More](https://github.com/Itz-fork/Mega.nz-Bot#features)` with Mega.nz Cloud Storage.` - -**Developed with ❀️ by [Itz-fork](https://github.com/Itz-fork)** -""" - helpinlinemsg = """ -**Hey, Choose the help mod using below buttons πŸ˜‡** -""" - answers_nexa = ( - InlineQueryResultArticle( - title="About Mega.nz-Bot", - description="A Simple Bot to Do More with Mega.nz", - thumb_url="https://telegra.ph/file/583f46da57641b90c28f9.png", - input_message_content=InputTextMessageContent( - aboutinlinemsg, disable_web_page_preview=True), - reply_markup=await get_buttons("start"), - ), - InlineQueryResultArticle( - title="Get Help", - description="Click here if you don't know how to use this bot.", - thumb_url="https://telegra.ph/file/92df448c01c9a46ec32b3.png", - input_message_content=InputTextMessageContent( - helpinlinemsg, disable_web_page_preview=True), - reply_markup=await get_buttons("inline"), - ) - ) - await client.answer_inline_query( - query.id, - results=answers_nexa, - switch_pm_text=f"@{(await client.get_me()).username}'s Inline Functions", - switch_pm_parameter="inline", - cache_time=15) - return - - elif megadl_q.split()[0] == "details": - inline_down_butotns = [] - if not Config.IS_PUBLIC_BOT: - if query.from_user.id not in Config.AUTH_USERS: - await client.answer_inline_query( - query.id, - results=answers, - switch_pm_text=f"Only Admins Can Use Bot's Inline Dwonload Mode!", - switch_pm_parameter="inline", - cache_time=10) - return - else: - pass - if len(megadl_q.split()) < 2: - await client.answer_inline_query( - query.id, - results=answers, - switch_pm_text=f"Usage: your_mega_link_here", - switch_pm_parameter="inline", - cache_time=10) - return - # Getting file size before download - try: - url = query.query.strip().split(None, 1)[1] - json_f_info = m.get_public_url_info(url) - dumped_j_info = json.dumps(json_f_info) - loaded_f_info = json.loads(dumped_j_info) - mega_f_size = loaded_f_info['size'] - mega_f_name = loaded_f_info['name'] - readable_f_size = humanbytes(mega_f_size) - except Exception as e: - await client.answer_inline_query( - query.id, - results=answers, - switch_pm_text="An Error Happend!", - switch_pm_parameter="inline", - cache_time=10) - send_errors(e) - return - details_inline_msg = f""" -》 **File Details** - - -**File Name:** `{mega_f_name}` -**File Size:** `{readable_f_size}` -**Url:** {url} - -**Powered by @NexaBotsUpdates** -""" - d_inline_keyborad = [InlineKeyboardButton("PM Mega.nz-Bot", url=f"https://t.me/{(await client.get_me()).username}")] - inline_down_butotns.append(d_inline_keyborad) - INLINE_DWN_B = InlineKeyboardMarkup(inline_down_butotns) - answers.append( - InlineQueryResultArticle( - title="Got Requested Url's Details", - description="Hey, I got your mega.nz url details. Click here to see them", - input_message_content=InputTextMessageContent( - details_inline_msg, disable_web_page_preview=True), - reply_markup=INLINE_DWN_B, - ) - ) - await client.answer_inline_query( - query.id, - results=answers, - switch_pm_text=f"@{(await client.get_me()).username}'s Inline Functions!", - switch_pm_parameter="inline", - cache_time=20) - - elif megadl_q.split()[0] == "info": - if query.from_user.id not in Config.AUTH_USERS: - await client.answer_inline_query( - query.id, - results=answers, - switch_pm_text=f"Not Authorized to Use This Bot!", - switch_pm_parameter="inline", - cache_time=10) - return - if not Config.MEGA_EMAIL or not Config.MEGA_PASSWORD: - await client.answer_inline_query( - query.id, - results=answers, - switch_pm_text=f"Setup an User Account to Use this Feature!", - switch_pm_parameter="inline", - cache_time=10) - return - loop = get_running_loop() - inf = await loop.run_in_executor(None, partial(USER_ACC_INFO)) - answers.append( - InlineQueryResultArticle( - title="About Your Mega.nz Account", - description="Some info about your Mega.nz Account", - input_message_content=InputTextMessageContent(inf) - ) - ) - await client.answer_inline_query( - query.id, - results=answers, - switch_pm_text=f"@{(await client.get_me()).username}'s Inline Functions", - switch_pm_parameter="inline", - cache_time=20) - else: - await client.answer_inline_query( - query.id, - results=answers, - switch_pm_text=f"No Result! Go ahead and learn how to use this", - switch_pm_parameter="inline", - cache_time=10) - except Exception as e: - await send_errors(e) diff --git a/megadl/modules/mega_dl.py b/megadl/modules/mega_dl.py index 6a6e9fb1..95957db6 100644 --- a/megadl/modules/mega_dl.py +++ b/megadl/modules/mega_dl.py @@ -1,152 +1,111 @@ -# Copyright (c) 2022 Itz-fork -# Don't kang this else your dad is gae +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Handle mega.nz download function -import os -import re -import shutil - -from pyrogram import Client, filters -from pyrogram.types import Message -from filesplit.split import Split -from functools import partial -from asyncio import get_running_loop - -from megadl.data import get_buttons -from megadl.helpers_nexa.account import m -from megadl.helpers_nexa.decorators import is_public -from megadl.helpers_nexa.mega_help import send_errors, send_logs -from megadl.helpers_nexa.up_helper import guess_and_send -from megadl.helpers_nexa.megatools import MegaTools -from config import Config - - -# Automatic Url Detect (From stackoverflow. Can't find link lol) -MEGA_REGEX = "https?:\/\/mega\.nz\/(?:[^\/\s]+\/)+" - - -# Function to download Mega Link -def DownloadMegaLink(url, alreadylol, download_msg): - try: - m.download_url(url, alreadylol, statusdl_msg=download_msg) - except Exception as e: - send_errors(e) - - -# Function to split large files -def split_files(input_file, out_path): - split = Split(input_file, out_path) - split.bysize(2040108421) - - -# Uses mega.py package -@Client.on_message(filters.regex(MEGA_REGEX) & filters.command("megapy") & filters.private) -@is_public -async def megadl_megapy(_, message: Message): - url = message.text.split(None, 1)[1] - userpath = str(message.from_user.id) - the_chat_id = str(message.chat.id) - megadl_path = Config.DOWNLOAD_LOCATION + "/" + userpath - # Temp fix for the https://github.com/Itz-fork/Mega.nz-Bot/issues/11 - if os.path.isdir(megadl_path): - return await message.reply_text("`Already One Process is Going On. Please wait until it's finished!`") - else: - os.makedirs(megadl_path) - try: - download_msg = await message.reply_text("**Starting to Download The Content! This may take while 😴**", reply_markup=await get_buttons("cancel")) - await send_logs(user_id=userpath, mchat_id=the_chat_id, mega_url=url, download_logs=True) - loop = get_running_loop() - await loop.run_in_executor(None, partial(DownloadMegaLink, url, megadl_path, download_msg)) - folder_f = [val for sublist in [[os.path.join( - i[0], j) for j in i[2]] for i in os.walk(megadl_path)] for val in sublist] - await download_msg.edit("**Successfully Downloaded The Content!**") - except Exception as e: - if os.path.isdir(megadl_path): - await download_msg.edit(f"**Error:** `{e}`") - shutil.rmtree(Config.DOWNLOAD_LOCATION + "/" + userpath) - await send_errors(e) - return - if os.path.isdir(megadl_path) is False: +import re +from os import path, makedirs + +from pyrogram import filters +from pyrogram.types import ( + Message, + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, +) + +from megadl import CypherClient +from megadl.lib.megatools import MegaTools + + +@CypherClient.on_message( + filters.regex(r"(https?:\/\/mega\.nz\/(file|folder|#)?.+)|(\/Root\/?.+)") +) +@CypherClient.run_checks +async def dl_from(client: CypherClient, msg: Message): + # Push info to temp db + _mid = msg.id + _usr = msg.from_user.id + client.glob_tmp[_usr] = [msg.text, f"{client.dl_loc}/{_usr}"] + await msg.reply( + "**Select what you want to do πŸ€—**", + reply_markup=InlineKeyboardMarkup( + [ + [InlineKeyboardButton("Download πŸ’Ύ", callback_data=f"dwn_mg-{_mid}")], + [InlineKeyboardButton("Info ℹ️", callback_data=f"info_mg-{_mid}")], + [InlineKeyboardButton("Cancel ❌", callback_data=f"cancelqcb-{_usr}")], + ] + ), + ) + + +prv_rgx = r"(\/Root\/?.+)" + + +@CypherClient.on_callback_query(filters.regex(r"dwn_mg?.+")) +@CypherClient.run_checks +async def dl_from_cb(client: CypherClient, query: CallbackQuery): + # Access saved info + _mid = int(query.data.split("-")[1]) + qcid = query.message.chat.id + qusr = query.from_user.id + dtmp = client.glob_tmp.get(qusr) + url = dtmp[0] + dlid = dtmp[1] + + # weird workaround to add support for private mode + conf = None + if client.is_public: + udoc = await client.database.is_there(qusr, True) + if not udoc and re.match(prv_rgx, url): + return await query.edit_message_text( + "`You must be logged in first to download this file πŸ˜‘`" + ) + if udoc: + email = client.cipher.decrypt(udoc["email"]).decode() + password = client.cipher.decrypt(udoc["password"]).decode() + proxy = f"--proxy {udoc['proxy']}" if udoc["proxy"] else "" + conf = f"--username {email} --password {password} {proxy}" + + # Create unique download folder + if not path.isdir(dlid): + makedirs(dlid) + + # Download the file/folder + resp = await query.edit_message_text( + "`Your download is starting πŸ“₯...`", reply_markup=None + ) + + cli = MegaTools(client, conf) + + f_list = None + f_list = await cli.download( + url, + qusr, + qcid, + resp.id, + path=dlid, + reply_markup=InlineKeyboardMarkup( + [ + [InlineKeyboardButton("Cancel ❌", callback_data=f"cancelqcb-{qusr}")], + ] + ), + ) + if not f_list: return - try: - for mg_file in folder_f: - file_size = os.stat(mg_file).st_size - if file_size > Config.TG_MAX_SIZE: - base_splt_out_dir = megadl_path + "splitted_files" - await download_msg.edit("`Large File Detected, Trying to split it!`") - loop = get_running_loop() - await loop.run_in_executor(None, partial(split_files(mg_file, base_splt_out_dir))) - split_out_dir = [val for sublist in [[os.path.join( - i[0], j) for j in i[2]] for i in os.walk(megadl_path)] for val in sublist] - for spl_f in split_out_dir: - await guess_and_send(spl_f, int(the_chat_id), "cache", download_msg) - else: - await guess_and_send(mg_file, int(the_chat_id), "cache", download_msg) - except Exception as e: - await download_msg.edit(f"**Error:** \n`{e}`") - await send_errors(e) - try: - shutil.rmtree(Config.DOWNLOAD_LOCATION + "/" + userpath) - print("Successfully Removed Downloaded File and the folder!") - except Exception as e: - return await send_errors(e) - - -# Uses megatools cli -@Client.on_message(filters.regex(MEGA_REGEX) & filters.private & ~filters.command("megapy")) -@is_public -async def megadl_megatools(_, message: Message): - url = message.text - if not re.match(MEGA_REGEX, url): - return await message.reply("`This isn't a mega url!`") - userpath = str(message.from_user.id) - the_chat_id = str(message.chat.id) - megadl_path = Config.DOWNLOAD_LOCATION + "/" + userpath - # Temp fix for the https://github.com/Itz-fork/Mega.nz-Bot/issues/11 - if os.path.isdir(megadl_path): - return await message.reply_text("`Already One Process is Going On. Please wait until it's finished!`") - else: - os.makedirs(megadl_path) - try: - download_msg = await message.reply_text("**Starting to Download The Content! This may take while 😴** \n\n`Note: You can't cancel this!`") - await send_logs(user_id=userpath, mchat_id=the_chat_id, mega_url=url, download_logs=True) - mcli = MegaTools() - dl_files = await mcli.download(url, download_msg.chat.id, download_msg.id, megadl_path) - await download_msg.edit("**Successfully Downloaded The Content!**") - except Exception as e: - if os.path.isdir(megadl_path): - await download_msg.edit(f"**Error:** `{e}`") - shutil.rmtree(Config.DOWNLOAD_LOCATION + "/" + userpath) - await send_errors(e) - return - try: - for mg_file in dl_files: - file_size = os.stat(mg_file).st_size - if file_size > Config.TG_MAX_SIZE: - base_splt_out_dir = megadl_path + "splitted_files" - await download_msg.edit("`Large file detected, Trying to split!`") - loop = get_running_loop() - await loop.run_in_executor(None, partial(split_files(mg_file, base_splt_out_dir))) - split_out_dir = [val for sublist in [[os.path.join( - i[0], j) for j in i[2]] for i in os.walk(megadl_path)] for val in sublist] - for spl_f in split_out_dir: - await guess_and_send(spl_f, int(the_chat_id), download_msg) - else: - await guess_and_send(mg_file, int(the_chat_id), download_msg) - await download_msg.edit("**Successfully Uploaded The Content!**") - except Exception as e: - await download_msg.edit(f"**Error:** \n`{e}`") - await send_errors(e) - try: - shutil.rmtree(Config.DOWNLOAD_LOCATION + "/" + userpath) - print("Successfully Removed Downloaded File and the folder!") - except Exception as e: - await send_errors(e) - - -# Replying If There is no mega url in the message -@Client.on_message(~filters.command(["start", "help", "info", "upload", "import", "megapy"]) & ~filters.regex(MEGA_REGEX) & filters.private & ~filters.media) -@is_public -async def nomegaurl(_, message: Message): - await message.reply_text("Sorry, I can't find a **valid mega.nz url** in your message! Can you check it again? \n\nAlso Make sure your url **doesn't** contain `mega.co.nz`. \n\n**If there is,** \n - Open that url in a web-browser and wait till webpage loads. \n - Then simply copy url of the webpage that you're in \n - Try Again") + await query.edit_message_text("`Successfully downloaded the content πŸ₯³`") + # update download count + await client.database.plus_fl_count(qusr, downloads=len(f_list)) + # Send file(s) to the user + await resp.edit("`Trying to upload now πŸ“€...`") + await client.send_files( + f_list, + qcid, + resp.id, + reply_to_message_id=_mid, + caption=f"**Join @NexaBotsUpdates ❀️**", + ) + await client.full_cleanup(dlid, qusr) + await resp.delete() diff --git a/megadl/modules/mega_up.py b/megadl/modules/mega_up.py new file mode 100644 index 00000000..d61cee90 --- /dev/null +++ b/megadl/modules/mega_up.py @@ -0,0 +1,104 @@ +# Copyright (c) 2023 Itz-fork +# Author: https://github.com/Itz-fork +# Project: https://github.com/Itz-fork/Mega.nz-Bot +# Description: Handle mega.nz upload function + +from time import time +from pyrogram import filters +from pyrogram.types import ( + Message, + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, +) + +from megadl import CypherClient +from megadl.lib.ddl import Downloader +from megadl.lib.megatools import MegaTools +from megadl.helpers.pyros import track_progress + + +# Respond only to Documents, Photos, Videos, GIFs, Audio and to urls other than mega +@CypherClient.on_message( + filters.document + | filters.photo + | filters.video + | filters.animation + | filters.audio + | filters.regex( + r"((http|https)://)(www.)?(?!mega)[a-zA-Z0-9@:%._\+~#?&//=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%._\+~#?&//=]*)" + ) +) +@CypherClient.run_checks +async def up_to(_: CypherClient, msg: Message): + _mid = msg.id + await msg.reply( + "**Select what you want to do πŸ€—**", + reply_markup=InlineKeyboardMarkup( + [ + [InlineKeyboardButton("Upload πŸ—ƒ", callback_data=f"up_tgdl-{_mid}")], + [ + InlineKeyboardButton( + "Cancel ❌", callback_data=f"cancelqcb-{msg.from_user.id}" + ) + ], + ] + ), + ) + + +@CypherClient.on_callback_query(filters.regex(r"up_tgdl?.+")) +@CypherClient.run_checks +async def to_up_cb(client: CypherClient, query: CallbackQuery): + # Get message content + _mid = int(query.data.split("-")[1]) + qmid = query.message.id + qcid = query.message.chat.id + qusr = query.from_user.id + + # weird workaround to add support for private mode + conf = None + if client.is_public: + udoc = await client.database.is_there(qusr, True) + if not udoc: + return await query.edit_message_text( + "`You must be logged in first to download this file πŸ˜‘`" + ) + if udoc: + conf = f"--username {client.cipher.decrypt(udoc['email']).decode()} --password {client.cipher.decrypt(udoc['password']).decode()}" + + strtim = time() + msg = await client.get_messages(qcid, _mid) + # Status msg + await query.edit_message_text("`Trying to download the file πŸ“₯`", reply_markup=None) + # update upload count + await client.database.plus_fl_count(qusr, uploads=1) + + # Download files accordingly + dl_path = None + if msg.media: + dl_path = await client.download_media( + msg, progress=track_progress, progress_args=(client, [qcid, qmid], strtim) + ) + else: + dl = Downloader(client) + dl_path = await dl.download( + msg.text, + client.dl_loc, + (qcid, qmid, qusr), + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton("Cancel ❌", callback_data=f"cancelqcb-{qusr}")]] + ), + ) + + # Upload the file + cli = MegaTools(client, conf) + + limk = await cli.upload(dl_path, qusr, qcid, qmid) + await query.edit_message_text( + f"`Your file has been uploaded to Mega.nz βœ…`\n**Link πŸ”—:** `{limk}`", + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton("Visit πŸ”—", url=limk)]] + ), + ) + await client.full_cleanup(dl_path, qusr) diff --git a/megadl/modules/user_account.py b/megadl/modules/user_account.py deleted file mode 100644 index 6f7a7708..00000000 --- a/megadl/modules/user_account.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright (c) 2022 Itz-fork -# Don't kang this else your dad is gae - -import os -import json -import time -import wget -import shutil - -from functools import partial -from asyncio import get_running_loop -from pyrogram import Client, filters -from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton - -from megadl.helpers_nexa.account import m -from megadl.helpers_nexa.megatools import MegaTools -from megadl.helpers_nexa.decorators import is_public -from megadl.helpers_nexa.mega_help import progress_for_pyrogram, humanbytes, send_errors, send_logs -from config import Config - - -# Get Mega user Account info -def USER_ACC_INFO(): - try: - get_user = m.get_user() - imported_user = json.dumps(get_user) - uacc_info = json.loads(imported_user) - acc_email = uacc_info['email'] - acc_name = uacc_info['name'] - acc_quota = m.get_quota() - js_acc_space = m.get_storage_space() - acc_space_f = json.dumps(js_acc_space) - acc_space = json.loads(acc_space_f) - btotal_space = acc_space['total'] - bused_space = acc_space['used'] - bfree_space = btotal_space - bused_space - total_space = humanbytes(btotal_space) - used_space = humanbytes(bused_space) - free_space = humanbytes(bfree_space) - return f""" -**~ Your User Account Info ~** - -✦ **Account Name:** `{acc_name}` -✦ **Email:** `{acc_email}` -✦ **Storage,** - - **Total:** `{total_space}` - - **Used:** `{used_space}` - - **Free:** `{free_space}` -✦ **Quota:** `{acc_quota} MB` -""" - except Exception as e: - return f"Error: \n{e}" - - -@Client.on_message(filters.command("info") & filters.private) -@is_public -async def accinfo(_, message: Message): - acc_info_msg = await message.reply_text("`Processing βš™οΈ...`") - if not Config.MEGA_EMAIL or not Config.MEGA_PASSWORD: - return await acc_info_msg.edit("`Setup an User Account to Use this Feature!`") - loop = get_running_loop() - inf = await loop.run_in_executor(None, partial(USER_ACC_INFO)) - await acc_info_msg.edit(inf) - - -@Client.on_message(filters.command("upload") & filters.private) -@is_public -async def uptomega(client: Client, message: Message): - uid = message.from_user.id - cid = message.chat.id - megauplaod_msg = await message.reply_text("`Processing βš™οΈ...`") - if not Config.MEGA_EMAIL or not Config.MEGA_PASSWORD: - return await megauplaod_msg.edit("`Setup an User Account to Use this Feature!`") - todownfile = message.reply_to_message - if todownfile is None: - return await megauplaod_msg.edit("**Please reply to a Media File or Direct Link to Upload!**") - megaupmsg = await megauplaod_msg.edit("**Trying to Download The Content to My Server! This may take while 😴**") - mcli = MegaTools() - - if todownfile.media is None: - try: - dl_path = f"{Config.DOWNLOAD_LOCATION}/{uid}" - url = todownfile.text - if os.path.isdir(dl_path): - return await megauplaod_msg.edit("`Already One Process is Going On. Please wait until it's finished!`") - else: - os.makedirs(dl_path) - await send_logs(user_id=uid, mchat_id=cid, mega_url=url, upload_logs=True) - loop = get_running_loop() - toupload = await loop.run_in_executor(None, partial(wget.download, url, dl_path)) - link = await mcli.upload(toupload, megaupmsg.chat.id, megaupmsg.id) - await megaupmsg.edit(f"**Successfully Uploaded To Mega.nz** \n\n**Link:** `{link}` \n\n**Powered by @NexaBotsUpdates**", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("πŸ“₯ Mega.nz Link πŸ“₯", url=f"{link}")]])) - shutil.rmtree(dl_path) - return - except Exception as e: - return await send_errors(e) - - try: - start_time = time.time() - await send_logs(user_id=uid, mchat_id=cid, up_file=todownfile, upload_logs=True) - toupload = await client.download_media(message=todownfile, progress=progress_for_pyrogram, progress_args=("**Trying to Download!** \n", megaupmsg, start_time)) - await megaupmsg.edit("**Successfully Downloaded the File!**") - await megaupmsg.edit("**Trying to Upload to Mega.nz! This may take while 😴**") - link = await mcli.upload(toupload, megaupmsg.chat.id, megaupmsg.id) - await megaupmsg.edit(f"**Successfully Uploaded To Mega.nz** \n\n**Link:** `{link}` \n\n**Powered by @NexaBotsUpdates**", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("πŸ“₯ Mega.nz Link πŸ“₯", url=f"{link}")]])) - os.remove(toupload) - except Exception as e: - await megaupmsg.edit(f"**Error:** `{e}`") - await send_errors(e) - os.remove(toupload) - - -# Import files from a public url -@Client.on_message(filters.command("import") & filters.private) -@is_public -async def importurlf(_, message: Message): - importing_msg = await message.reply_text("`Processing βš™οΈ...`") - reply_msg = message.reply_to_message - try: - if reply_msg: - replied_txt_msg = reply_msg.text - if "mega.nz" not in replied_txt_msg: - return await importing_msg.edit("Send me a **Valid Mega.nz** Link to Import 😏!") - else: - msg_text = replied_txt_msg - else: - msg_txt_url = message.text - if "mega.nz" not in msg_txt_url: - return await importing_msg.edit("Send me a **Valid Mega.nz** Link to Import 😏!") - else: - msg_text = msg_txt_url - except Exception as e: - return await importing_msg.edit("Hmmm... Looks like there is something other than text! Mind if check it again πŸ€”?") - else: - try: - await send_logs(user_id=message.from_user.id, mchat_id=message.chat.id, mega_url=msg_text, import_logs=True) - import_file = m.import_public_url(msg_text) - imported_link = m.get_upload_link(import_file) - await importing_msg.delete() - await message.reply_text(f"**Successfully Imported 😌** \n\n**Link:** `{imported_link}` \n\n**Powered by @NexaBotsUpdates**", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("πŸ“₯ Imported Link πŸ“₯", url=f"{imported_link}")]])) - except Exception as e: - await message.reply_text(f"**Error:** `{e}`") - await send_errors(e) diff --git a/requirements.txt b/requirements.txt index c84d8948..a9d5b9d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,13 @@ pyrogram -TgCrypto -filetype +Tgcrypto filesplit -wget -git+https://github.com/Itz-fork/pyro-mega.py.git \ No newline at end of file +filetype +python-dotenv +aiofiles +aiohttp[speedups] +pymongo +dnspython +cryptography +pycryptodome +psutil +humans-formatter \ No newline at end of file diff --git a/startup.sh b/startup.sh deleted file mode 100644 index 36f1cd69..00000000 --- a/startup.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/bash - -echo " -β–ˆβ–€β–„β–€β–ˆβ€ƒβ–ˆβ–€β–€β€ƒβ–ˆβ–€β–€β€ƒβ–„β–€β–ˆβ€ƒ β€ƒβ–ˆβ–„ β–ˆβ€ƒβ–€β–ˆβ€ƒ -β–ˆ β–€ β–ˆβ€ƒβ–ˆβ–ˆβ–„β€ƒβ–ˆβ–„β–ˆβ€ƒβ–ˆβ–€β–ˆβ€ƒβ–„β€ƒβ–ˆ β–€β–ˆβ€ƒβ–ˆβ–„ - - β–ˆβ–„β–„β€ƒβ–ˆβ–€β–ˆβ€ƒβ–€β–ˆβ–€ - β–ˆβ–„β–ˆβ€ƒβ–ˆβ–„β–ˆβ€ƒ β–ˆ - - -Copyright (c) 2021 Itz-fork | @NexaBotsUpdates -" - -# Colors -White="\033[1;37m" -Reset="\033[0m" - -# Helper functions to show process / error messages -function show_process_msg() { - echo -e "$White ==> $1 $Reset" -} - -function show_error_msg() { - echo -e "ERROR: $1" - exit -} - -# Function to install megatools (https://github.com/Itz-fork/Mega.nz-Bot/blob/abe1b0736c9e85b547b806eef82c78b0f574f215/installer.sh#L70) - -function _aur_megatools() { - git clone https://aur.archlinux.org/megatools.git - cd megatools || show_error_msg "megatools dir doesn't exists rn! Tf did you do?" - makepkg -si - cd .. - rm -rf megatools -} - -function install_megatools() { - sudo apt install megatools || - _aur_megatools || - sudo dnf install megatools || - show_error_msg "Your system deosn't match the current list of Oses. Please install 'megatools' from - https://megatools.megous.com/" -} - - -function check_depends() { - is_megatools=$(command -v megatools &> /dev/null) - is_ffmpeg=$(command -v ffmpeg &> /dev/null) - # Checks if megatools is installed - if ! $is_megatools ; then - show_process_msg "Installing megatools" - install_megatools - # Checks if ffmpeg is installed - elif ! $is_ffmpeg ; then - show_process_msg "Installing ffmpeg" - curl -sS https://webinstall.dev/ffmpeg | bash || - show_error_msg "Ffmpeg is not installed. Visit - https://ffmpeg.org/download.html" - fi -} - -function run() { - show_process_msg "Starting the main repo" - python3 -m megadl -} - -check_depends -run \ No newline at end of file