├── .dockerignore ├── .gitignore ├── .railway.toml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── BackUp.png ├── assets.jpg ├── backup.jpg ├── banner.png ├── banner.sh ├── bot_pfp.png ├── logs.jpg ├── onload.jpg └── termux.sh ├── docker-compose.yml ├── docker.sh ├── docs └── user_guide.md ├── hikka ├── __init__.py ├── __main__.py ├── _internal.py ├── _local_storage.py ├── _reference_finder.py ├── _types.py ├── compat │ ├── dragon.py │ ├── geek.py │ └── pyroproxy.py ├── configurator.py ├── database.py ├── dispatcher.py ├── inline │ ├── bot_pm.py │ ├── core.py │ ├── events.py │ ├── form.py │ ├── gallery.py │ ├── list.py │ ├── query_gallery.py │ ├── token_obtainment.py │ ├── types.py │ └── utils.py ├── loader.py ├── log.py ├── main.py ├── modules │ ├── ModuleCloud.py │ ├── api_protection.py │ ├── help.py │ ├── inline_stuff.py │ ├── loader.py │ ├── mods.py │ ├── netfoll_backup.py │ ├── netfoll_config.py │ ├── netfoll_security.py │ ├── netfoll_settings.py │ ├── netinfo.py │ ├── ping.py │ ├── presets.py │ ├── python.py │ ├── quickstart.py │ ├── settings.py │ ├── sinfo.py │ ├── test.py │ ├── translations.py │ ├── update_notifier.py │ └── updater.py ├── pointers.py ├── security.py ├── tl_cache.py ├── translations.py ├── types.py ├── utils.py ├── validators.py ├── version.py └── web │ ├── core.py │ ├── debugger.py │ ├── proxypass.py │ └── root.py ├── install.sh ├── requirements.txt └── web-resources ├── base.jinja2 ├── root.jinja2 └── static ├── base.css └── root.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | *__pycache__ 133 | *.pyc 134 | hikka/api_token.py 135 | hikka/loaded_modules 136 | loaded_modules 137 | hikka/debug_modules 138 | hikka*.session* 139 | database-*.json 140 | *.swp 141 | .setup_complete 142 | .tox 143 | .idea/ 144 | venv/ 145 | data/ 146 | api_token.txt 147 | .coverage 148 | .vscode/ 149 | ftg-install.log 150 | config.ini 151 | .cache 152 | config-*.json 153 | config.json 154 | *cache*.json 155 | *.png 156 | *.jpg 157 | *.jpeg 158 | *.webp 159 | *.webm 160 | *.tgs 161 | *.mp4 162 | *.mp3 163 | *.ogg 164 | *.m4a 165 | *.mp3 166 | *.avi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | *__pycache__ 133 | *.pyc 134 | hikka/api_token.py 135 | hikka/loaded_modules 136 | loaded_modules 137 | hikka/debug_modules 138 | hikka*.session* 139 | database-*.json 140 | *.swp 141 | .setup_complete 142 | .tox 143 | .idea/ 144 | venv/ 145 | api_token.txt 146 | .coverage 147 | .vscode/ 148 | ftg-install.log 149 | config.ini 150 | .cache 151 | config-*.json 152 | config.json 153 | *cache*.json 154 | *.png 155 | *.jpg 156 | *.jpeg 157 | *.webp 158 | *.webm 159 | *.tgs 160 | *.mp4 161 | *.mp3 162 | *.ogg 163 | *.m4a 164 | *.mp3 165 | *.avi 166 | 167 | # Heroku-specific rules 168 | .heroku/ 169 | .apt/ 170 | .profile.d/ 171 | vendor/ 172 | Pipfile.lock 173 | 174 | hikka.log.* 175 | unknown_errors.txt -------------------------------------------------------------------------------- /.railway.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "shell" 5 | enabled = true 6 | 7 | [[analyzers]] 8 | name = "javascript" 9 | enabled = true 10 | 11 | [[analyzers]] 12 | name = "python" 13 | enabled = true 14 | 15 | [analyzers.meta] 16 | runtime_version = "3.x.x" 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Netfoll Changelog 2 | 3 | ## 🌑 Netfoll 1.0.0 4 | Coming Soon 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim as python-base 2 | ENV DOCKER=true 3 | ENV GIT_PYTHON_REFRESH=quiet 4 | 5 | ENV PIP_NO_CACHE_DIR=1 \ 6 | PYTHONUNBUFFERED=1 \ 7 | PYTHONDONTWRITEBYTECODE=1 8 | 9 | RUN apt update && apt install libcairo2 git build-essential -y --no-install-recommends 10 | RUN rm -rf /var/lib/apt/lists /var/cache/apt/archives /tmp/* 11 | RUN git clone https://github.com/MXRRI/Netfoll 12 | 13 | WORKDIR /Netfoll 14 | RUN pip install --no-warn-script-location --no-cache-dir -r requirements.txt 15 | 16 | EXPOSE 8080 17 | RUN mkdir /data 18 | 19 | CMD python -m hikka 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netfoll UserBot Netfoll 2 |

Telegram userbot, Based on Hikka

3 | 12 | 13 | # Installation 14 | 15 | 16 | 17 |
18 | 19 | 20 | # Requirements 21 | 22 | 26 | 27 | # Support chat 28 | 29 | # Developers 30 | 36 | -------------------------------------------------------------------------------- /assets/BackUp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netfoll/UserBot-Fork/1d2344416bd342a5207902ceb2490c3e4bc6f741/assets/BackUp.png -------------------------------------------------------------------------------- /assets/assets.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netfoll/UserBot-Fork/1d2344416bd342a5207902ceb2490c3e4bc6f741/assets/assets.jpg -------------------------------------------------------------------------------- /assets/backup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netfoll/UserBot-Fork/1d2344416bd342a5207902ceb2490c3e4bc6f741/assets/backup.jpg -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netfoll/UserBot-Fork/1d2344416bd342a5207902ceb2490c3e4bc6f741/assets/banner.png -------------------------------------------------------------------------------- /assets/banner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | print_center(){ 3 | local x 4 | local y 5 | text="$*" 6 | x=$(( ($(tput cols) - ${#text}) / 2)) 7 | echo -ne "\E[6n";read -sdR y; y=$(echo -ne "${y#*[}" | cut -d';' -f1) 8 | echo -ne "\033[${y};${x}f$*" 9 | } 10 | 11 | echo -ne "\\033[2J\033[3;1f" 12 | print_center " 13 | \033[95m _ _ _ __ _ _ \033[0m 14 | \033[95m| \ | | ___| |_ / _| ___ | | |\033[0m 15 | \033[95m| \| |/ _ \ __| |_ / _ \| | |\033[0m 16 | \033[95m| |\ | __/ |_| _| (_) | | |\033[0m 17 | \033[95m|_| \_|\___|\__|_| \___/|_|_| \033[0m 18 | 19 | \033[95mNetfoll started successfully!\033[0m 20 | \033[95mWeb url: http://localhost:1242\033[0m 21 | " 22 | -------------------------------------------------------------------------------- /assets/bot_pfp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netfoll/UserBot-Fork/1d2344416bd342a5207902ceb2490c3e4bc6f741/assets/bot_pfp.png -------------------------------------------------------------------------------- /assets/logs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netfoll/UserBot-Fork/1d2344416bd342a5207902ceb2490c3e4bc6f741/assets/logs.jpg -------------------------------------------------------------------------------- /assets/onload.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netfoll/UserBot-Fork/1d2344416bd342a5207902ceb2490c3e4bc6f741/assets/onload.jpg -------------------------------------------------------------------------------- /assets/termux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | print_center(){ 3 | local x 4 | local y 5 | text="$*" 6 | x=$(( ($(tput cols) - ${#text}) / 2)) 7 | echo -ne "\E[6n";read -sdR y; y=$(echo -ne "${y#*[}" | cut -d';' -f1) 8 | echo -ne "\033[${y};${x}f$*" 9 | } 10 | 11 | run_in_bg() { 12 | eval "$@" &>/dev/null & disown; 13 | } 14 | 15 | echo -e "\033[0;96mInstalling Netfoll... Just a Moment...\033[0m" 16 | 17 | eval "cd ~/ && 18 | rm -rf Netfoll && 19 | git clone --branch Dev https://github.com/MXRRI/Netfoll && 20 | cd Netfoll && 21 | pip install -U pip && 22 | pip install -r requirements.txt && 23 | echo '' > ~/../usr/etc/motd && 24 | echo 'clear && . <(wget -qO- https://github.com/MXRRI/Netfoll/raw/Dev/assets/banner.sh) && cd ~/Netfoll && python3 -m hikka --port 1242' > ~/.bash_profile" 25 | 26 | echo -e "\033[0;96mStarting Netfoll...\033[0m" 27 | 28 | run_in_bg "python3 -m hikka --port 1242" 29 | sleep 10 30 | 31 | echo -ne "\\033[2J\033[3;1f" 32 | print_center " 33 | \033[95m _ _ _ __ _ _ \033[0m 34 | \033[95m| \ | | ___| |_ / _| ___ | | |\033[0m 35 | \033[95m| \| |/ _ \ __| |_ / _ \| | |\033[0m 36 | \033[95m| |\ | __/ |_| _| (_) | | |\033[0m 37 | \033[95m|_| \_|\___|\__|_| \___/|_|_| \033[0m 38 | 39 | \033[95mNetfoll loaded successfully!\033[0m 40 | \033[95mWeb url: http://localhost:1242\033[0m 41 | " 42 | 43 | eval "termux-open-url http://localhost:1242" 44 | 45 | 46 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | worker: 4 | container_name: "hikka-worker" 5 | build: 6 | context: . 7 | volumes: 8 | - worker:/data 9 | stop_signal: SIGINT 10 | restart: unless-stopped 11 | command: "python -m hikka" 12 | ports: 13 | - "${EXTERNAL_PORT:-8080}:8080" 14 | 15 | volumes: 16 | worker: 17 | -------------------------------------------------------------------------------- /docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PORT=3429 # Port to run the server on 4 | echo "EXTERNAL_PORT=$PORT" >.env 5 | 6 | touch netfoll-install.log 7 | 8 | if ! [ -x "$(command -v docker)" ]; then 9 | printf "\033[0;34mInstalling docker...\e[0m" 10 | if [ -f /etc/debian_version ]; then 11 | sudo apt-get install \ 12 | apt-transport-https \ 13 | ca-certificates \ 14 | curl \ 15 | gnupg-agent \ 16 | software-properties-common -y 1>netfoll-install.log 2>&1 17 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | 18 | sudo apt-key add - 1>netfoll-install.log 2>&1 19 | sudo add-apt-repository \ 20 | "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ 21 | $(lsb_release -cs) \ 22 | stable" 1>netfoll-install.log 2>&1 23 | sudo apt-get update -y 1>netfoll-install.log 2>&1 24 | sudo apt-get install docker-ce docker-ce-cli containerd.io -y 1>netfoll-install.log 2>&1 25 | elif [ -f /etc/arch-release ]; then 26 | sudo pacman -Syu docker --noconfirm 1>netfoll-install.log 2>&1 27 | elif [ -f /etc/redhat-release ]; then 28 | sudo yum install -y yum-utils 1>netfoll-install.log 2>&1 29 | sudo yum-config-manager \ 30 | --add-repo \ 31 | https://download.docker.com/linux/centos/docker-ce.repo 32 | sudo yum install docker-ce docker-ce-cli containerd.io -y 1>netfoll-install.log 2>&1 33 | fi 34 | printf "\033[0;32m - success\e[0m\n" 35 | # Netfoll uses docker-compose so we need to install that too 36 | printf "\033[0;34mInstalling docker-compose...\e[0m" 37 | pip install -U docker-compose 1>netfoll-install.log 2>&1 38 | chmod +x /usr/local/bin/docker-compose 39 | printf "\033[0;32m - success\e[0m\n" 40 | else 41 | printf "\033[0;32mDocker is already installed\e[0m\n" 42 | fi 43 | 44 | printf "\033[0;34mDownloading configuration files...\e[0m" 45 | if [ -f "Dockerfile" ]; then 46 | rm Dockerfile 47 | fi 48 | wget -q https://github.com/MXRRI/Netfoll/raw/stable/Dockerfile 49 | if [ -f "docker-compose.yml" ]; then 50 | rm docker-compose.yml 51 | fi 52 | wget -q https://github.com/MXRRI/Netfoll/raw/stable/docker-compose.yml 53 | printf "\033[0;32m - success\e[0m\n" 54 | 55 | printf "\033[0;34mBuilding docker image...\e[0m" 56 | sudo docker-compose up -d --build 1>netfoll-install.log 2>&1 57 | printf "\033[0;32m - success\e[0m\n" 58 | 59 | printf "\033[0;32mFollow this url to continue installation:\e[0m\n" 60 | ssh "-o StrictHostKeyChecking=no" "-R 80:127.0.0.1:$PORT" "nokey@localhost.run" 2>&1 | grep "tunneled" 61 | -------------------------------------------------------------------------------- /docs/user_guide.md: -------------------------------------------------------------------------------- 1 | # Документация для пользователей 2 | 3 | Здесь вы сможете узнать о базовых командах 4 | 5 | ## Список команд 6 | 7 | - `.dlmod ` - Загрузить модуль по ссылке 8 | 9 | - `.loadmod ` - Загрузить модуль ответом на файл 10 | 11 | - `.help ` - Получить информацию о модуле 12 | 13 | - `.mods` - Получить список модулей 14 | 15 | - `.unloadmod ` - Выгрузить модуль 16 | 17 | - `.settings` - Открыть настройки юзербота 18 | 19 | - `.config` - Открыть конфиг 20 | 21 | - `.restart` - Перезапустить юзербота 22 | 23 | - `.update` - Обновить юзербота 24 | 25 | - `.suspend [time]` - Заморозить юзербота 26 | 27 | - `.tsec <"user" / "chat"> [time]` - Создать таргетированное правило безопасности 28 | 29 | - `.ping` - Пинг 30 | 31 | - `.eval ` - Выполнить код 32 | 33 | - `.terminal ` - Выполнить команду в терминале -------------------------------------------------------------------------------- /hikka/__init__.py: -------------------------------------------------------------------------------- 1 | """Just a placeholder to do relative imports""" 2 | # ©️ Dan Gazizullin, 2021-2023 3 | # This file is a part of Hikka Userbot 4 | # 🌐 https://github.com/hikariatama/Hikka 5 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 6 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 7 | 8 | # Do not delete this file, it will cause errors. 9 | 10 | __author__ = "Dan Gazizullin" 11 | __contact__ = "me@hikariatama.ru" 12 | __copyright__ = "Copyright 2022, Dan Gazizullin" 13 | __credits__ = ["LonamiWebs", "penn5"] 14 | __license__ = "AGPLv3" 15 | __maintainer__ = "developer" 16 | __status__ = "Production" 17 | -------------------------------------------------------------------------------- /hikka/__main__.py: -------------------------------------------------------------------------------- 1 | """Entry point. Checks for user and starts main script""" 2 | 3 | # ©️ Dan Gazizullin, 2021-2023 4 | # This file is a part of Hikka Userbot 5 | # 🌐 https://github.com/hikariatama/Hikka 6 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 7 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 8 | # Netfoll Team modifided Hikka files for Netfoll 9 | # 🌐 https://github.com/MXRRI/Netfoll 10 | 11 | import getpass 12 | import os 13 | import subprocess 14 | import sys 15 | 16 | from ._internal import restart 17 | 18 | if ( 19 | getpass.getuser() == "root" 20 | and "--root" not in " ".join(sys.argv) 21 | and all(trigger not in os.environ for trigger in {"DOCKER", "GOORM"}) 22 | ): 23 | print("🚫" * 15) 24 | print("You attempted to run Netfoll on behalf of root user") 25 | print("Please, create a new user and restart script") 26 | print("If this action was intentional, pass --root argument instead") 27 | print("🚫" * 15) 28 | print() 29 | print("Type force_insecure to ignore this warning") 30 | if input("> ").lower() != "force_insecure": 31 | sys.exit(1) 32 | 33 | 34 | if sys.version_info < (3, 8, 0): 35 | print("🚫 Error: you must use at least Python version 3.8.0") 36 | elif __package__ != "hikka": # In case they did python __main__.py 37 | print("🚫 Error: you cannot run this as a script; you must execute as a package") 38 | else: 39 | try: 40 | # If telethon is not installed, just skip to a part of main startup 41 | # then main.py will through an error and re-install all deps 42 | import telethon 43 | except Exception: 44 | pass 45 | else: 46 | try: 47 | import telethon 48 | 49 | if tuple(map(int, telethon.__version__.split("."))) < (1, 24, 12): 50 | raise ImportError 51 | except ImportError: 52 | print("🔄 Installing Hikka-TL...") 53 | 54 | subprocess.run( 55 | [ 56 | sys.executable, 57 | "-m", 58 | "pip", 59 | "install", 60 | "--force-reinstall", 61 | "-q", 62 | "--disable-pip-version-check", 63 | "--no-warn-script-location", 64 | "hikka-tl", 65 | ], 66 | check=True, 67 | ) 68 | 69 | restart() 70 | 71 | try: 72 | import pyrogram 73 | 74 | if tuple(map(int, pyrogram.__version__.split("."))) < (2, 0, 61): 75 | raise ImportError 76 | except ImportError: 77 | print("🔄 Installing Hikka-Pyro...") 78 | 79 | subprocess.run( 80 | [ 81 | sys.executable, 82 | "-m", 83 | "pip", 84 | "install", 85 | "--force-reinstall", 86 | "-q", 87 | "--disable-pip-version-check", 88 | "--no-warn-script-location", 89 | "hikka-pyro", 90 | ], 91 | check=True, 92 | ) 93 | 94 | restart() 95 | 96 | try: 97 | from . import log 98 | 99 | log.init() 100 | 101 | from . import main 102 | except (ModuleNotFoundError, ImportError) as e: 103 | print(f"{str(e)}\n🔄 Attempting dependencies installation... Just wait ⏱") 104 | 105 | subprocess.run( 106 | [ 107 | sys.executable, 108 | "-m", 109 | "pip", 110 | "install", 111 | "--upgrade", 112 | "-q", 113 | "--disable-pip-version-check", 114 | "--no-warn-script-location", 115 | "-r", 116 | "requirements.txt", 117 | ], 118 | check=True, 119 | ) 120 | 121 | restart() 122 | 123 | if __name__ == "__main__": 124 | if "HIKKA_DO_NOT_RESTART" in os.environ: 125 | del os.environ["HIKKA_DO_NOT_RESTART"] 126 | 127 | main.hikka.main() # Execute main function 128 | -------------------------------------------------------------------------------- /hikka/_internal.py: -------------------------------------------------------------------------------- 1 | # ©️ Dan Gazizullin, 2021-2023 2 | # This file is a part of Hikka Userbot 3 | # 🌐 https://github.com/hikariatama/Hikka 4 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 5 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 6 | # Netfoll Team modifided Hikka files for Netfoll 7 | # 🌐 https://github.com/MXRRI/Netfoll 8 | 9 | import asyncio 10 | import atexit 11 | import logging 12 | import os 13 | import random 14 | import signal 15 | import sys 16 | 17 | 18 | async def fw_protect(): 19 | await asyncio.sleep(random.randint(1000, 3000) / 1000) 20 | 21 | 22 | def get_startup_callback() -> callable: 23 | return lambda *_: os.execl( 24 | sys.executable, 25 | sys.executable, 26 | "-m", 27 | os.path.relpath(os.path.abspath(os.path.dirname(os.path.abspath(__file__)))), 28 | *sys.argv[1:], 29 | ) 30 | 31 | 32 | def die(): 33 | if "DOCKER" in os.environ: 34 | sys.exit(0) 35 | else: 36 | os.killpg(os.getpgid(os.getpid()), signal.SIGTERM) 37 | 38 | 39 | def restart(): 40 | if "HIKKA_DO_NOT_RESTART" in os.environ: 41 | print( 42 | "Got in a loop, exiting\nYou probably need to manually remove existing" 43 | " packages and then restart Netfoll. Run `pip uninstall -y telethon" 44 | " telethon-mod hikka-tl pyrogram hikka-pyro`, then restart Netfoll." 45 | ) 46 | sys.exit(0) 47 | 48 | logging.getLogger().setLevel(logging.CRITICAL) 49 | 50 | print("🔄 Restarting...") 51 | 52 | if "LAVHOST" in os.environ: 53 | os.system("lavhost restart") 54 | return 55 | 56 | os.environ["HIKKA_DO_NOT_RESTART"] = "1" 57 | if "DOCKER" in os.environ: 58 | atexit.register(get_startup_callback()) 59 | else: 60 | signal.signal(signal.SIGTERM, get_startup_callback()) 61 | 62 | die() 63 | -------------------------------------------------------------------------------- /hikka/_local_storage.py: -------------------------------------------------------------------------------- 1 | """Saves modules to disk and fetches them if remote storage is not available.""" 2 | # ©️ Dan Gazizullin, 2021-2023 3 | # This file is a part of Hikka Userbot 4 | # 🌐 https://github.com/hikariatama/Hikka 5 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 6 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 7 | 8 | import asyncio 9 | import contextlib 10 | import hashlib 11 | import logging 12 | import os 13 | import typing 14 | 15 | import requests 16 | 17 | from . import utils 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | MAX_FILESIZE = 1024 * 1024 * 5 # 5 MB 22 | MAX_TOTALSIZE = 1024 * 1024 * 100 # 100 MB 23 | 24 | 25 | class LocalStorage: 26 | """Saves modules to disk and fetches them if remote storage is not available.""" 27 | 28 | def __init__(self): 29 | self._path = os.path.join(os.path.expanduser("~"), ".hikka", "modules_cache") 30 | self._ensure_dirs() 31 | 32 | @property 33 | def _total_size(self) -> int: 34 | return sum(os.path.getsize(f.path) for f in os.scandir(self._path)) 35 | 36 | def _ensure_dirs(self): 37 | """Ensures that the local storage directory exists.""" 38 | if not os.path.isdir(self._path): 39 | os.makedirs(self._path) 40 | 41 | def _get_path(self, repo: str, module_name: str) -> str: 42 | return os.path.join( 43 | self._path, 44 | hashlib.sha256(f"{repo}_{module_name}".encode("utf-8")).hexdigest() + ".py", 45 | ) 46 | 47 | def save(self, repo: str, module_name: str, module_code: str): 48 | """Saves module to disk.""" 49 | size = len(module_code) 50 | if size > MAX_FILESIZE: 51 | logger.warning( 52 | "Module %s from %s is too large (%s bytes) to save to local cache.", 53 | module_name, 54 | repo, 55 | size, 56 | ) 57 | return 58 | 59 | if self._total_size + size > MAX_TOTALSIZE: 60 | logger.warning( 61 | "Local storage is full, cannot save module %s from %s.", 62 | module_name, 63 | repo, 64 | ) 65 | return 66 | 67 | with open(self._get_path(repo, module_name), "w") as f: 68 | f.write(module_code) 69 | 70 | logger.debug("Saved module %s from %s to local cache.", module_name, repo) 71 | 72 | def fetch(self, repo: str, module_name: str) -> typing.Optional[str]: 73 | """Fetches module from disk.""" 74 | path = self._get_path(repo, module_name) 75 | if os.path.isfile(path): 76 | with open(path, "r") as f: 77 | return f.read() 78 | 79 | return None 80 | 81 | 82 | class RemoteStorage: 83 | def __init__(self): 84 | self._local_storage = LocalStorage() 85 | 86 | async def preload(self, urls: typing.List[str]): 87 | """Preloads modules from remote storage.""" 88 | logger.debug("Preloading modules from remote storage.") 89 | for url in urls: 90 | logger.debug("Preloading module %s", url) 91 | 92 | with contextlib.suppress(Exception): 93 | await self.fetch(url) 94 | 95 | await asyncio.sleep(5) 96 | 97 | async def preload_main_repo(self): 98 | """Preloads modules from the main repo.""" 99 | mods_info = ( 100 | await utils.run_sync(requests.get, "https://mods.hikariatama.ru/mods.json") 101 | ).json() 102 | for name, info in mods_info.items(): 103 | _, repo, module_name = self._parse_url(info["link"]) 104 | code = self._local_storage.fetch(repo, module_name) 105 | 106 | if code: 107 | sha = hashlib.sha256(code.encode("utf-8")).hexdigest() 108 | if sha != info["sha"]: 109 | logger.debug("Module %s from main repo is outdated.", name) 110 | code = None 111 | else: 112 | logger.debug("Module %s from main repo is up to date.", name) 113 | 114 | if not code: 115 | logger.debug("Preloading module %s from main repo.", name) 116 | 117 | with contextlib.suppress(Exception): 118 | await self.fetch(info["link"]) 119 | 120 | await asyncio.sleep(5) 121 | continue 122 | 123 | @staticmethod 124 | def _parse_url(url: str) -> typing.Tuple[str, str, str]: 125 | """Parses a URL into a repository and module name.""" 126 | domain_name = url.split("/")[2] 127 | 128 | if domain_name == "raw.githubusercontent.com": 129 | owner, repo, branch = url.split("/")[3:6] 130 | module_name = url.split("/")[-1].split(".")[0] 131 | repo = f"git+{owner}/{repo}:{branch}" 132 | elif domain_name == "github.com": 133 | owner, repo, _, branch = url.split("/")[3:7] 134 | module_name = url.split("/")[-1].split(".")[0] 135 | repo = f"git+{owner}/{repo}:{branch}" 136 | else: 137 | repo, module_name = url.rsplit("/", maxsplit=1) 138 | repo = repo.strip("/") 139 | 140 | return url, repo, module_name 141 | 142 | async def fetch(self, url: str) -> str: 143 | """ 144 | Fetches the module from the remote storage. 145 | :param ref: The module reference. Can be url, or a reference to official repo module. 146 | """ 147 | url, repo, module_name = self._parse_url(url) 148 | try: 149 | r = await utils.run_sync(requests.get, url) 150 | r.raise_for_status() 151 | except Exception: 152 | logger.debug( 153 | "Can't load module from remote storage. Trying local storage.", 154 | exc_info=True, 155 | ) 156 | if module := self._local_storage.fetch(repo, module_name): 157 | logger.debug("Module source loaded from local storage.") 158 | return module 159 | 160 | raise 161 | 162 | self._local_storage.save(repo, module_name, r.text) 163 | 164 | return r.text 165 | -------------------------------------------------------------------------------- /hikka/_reference_finder.py: -------------------------------------------------------------------------------- 1 | import gc as _gc 2 | import inspect 3 | import logging 4 | import types as _types 5 | import typing 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def proxy0(data): 11 | def proxy1(): 12 | return data 13 | 14 | return proxy1 15 | 16 | 17 | _CELLTYPE = type(proxy0(None).__closure__[0]) 18 | 19 | 20 | def replace_all_refs(replace_from: typing.Any, replace_to: typing.Any) -> typing.Any: 21 | """ 22 | :summary: Uses the :mod:`gc` module to replace all references to obj 23 | :attr:`replace_from` with :attr:`replace_to` (it tries it's best, 24 | anyway). 25 | :param replace_from: The obj you want to replace. 26 | :param replace_to: The new objject you want in place of the old one. 27 | :returns: The replace_from 28 | """ 29 | # https://github.com/cart0113/pyjack/blob/dd1f9b70b71f48335d72f53ee0264cf70dbf4e28/pyjack.py 30 | 31 | _gc.collect() 32 | 33 | hit = False 34 | for referrer in _gc.get_referrers(replace_from): 35 | # FRAMES -- PASS THEM UP 36 | if isinstance(referrer, _types.FrameType): 37 | continue 38 | 39 | # DICTS 40 | if isinstance(referrer, dict): 41 | cls = None 42 | 43 | # THIS CODE HERE IS TO DEAL WITH DICTPROXY TYPES 44 | if "__dict__" in referrer and "__weakref__" in referrer: 45 | for cls in _gc.get_referrers(referrer): 46 | if inspect.isclass(cls) and cls.__dict__ == referrer: 47 | break 48 | 49 | for key, value in referrer.items(): 50 | # REMEMBER TO REPLACE VALUES ... 51 | if value is replace_from: 52 | hit = True 53 | value = replace_to 54 | referrer[key] = value 55 | if cls: # AGAIN, CLEANUP DICTPROXY PROBLEM 56 | setattr(cls, key, replace_to) 57 | # AND KEYS. 58 | if key is replace_from: 59 | hit = True 60 | del referrer[key] 61 | referrer[replace_to] = value 62 | 63 | elif isinstance(referrer, list): 64 | for i, value in enumerate(referrer): 65 | if value is replace_from: 66 | hit = True 67 | referrer[i] = replace_to 68 | 69 | elif isinstance(referrer, set): 70 | referrer.remove(replace_from) 71 | referrer.add(replace_to) 72 | hit = True 73 | 74 | elif isinstance( 75 | referrer, 76 | ( 77 | tuple, 78 | frozenset, 79 | ), 80 | ): 81 | new_tuple = [] 82 | for obj in referrer: 83 | if obj is replace_from: 84 | new_tuple.append(replace_to) 85 | else: 86 | new_tuple.append(obj) 87 | replace_all_refs(referrer, type(referrer)(new_tuple)) 88 | 89 | elif isinstance(referrer, _CELLTYPE): 90 | 91 | def _proxy0(data): 92 | def proxy1(): 93 | return data 94 | 95 | return proxy1 96 | 97 | proxy = _proxy0(replace_to) 98 | newcell = proxy.__closure__[0] 99 | replace_all_refs(referrer, newcell) 100 | 101 | elif isinstance(referrer, _types.FunctionType): 102 | localsmap = {} 103 | for key in ["code", "globals", "name", "defaults", "closure"]: 104 | orgattr = getattr(referrer, f"__{key}__") 105 | localsmap[key] = replace_to if orgattr is replace_from else orgattr 106 | localsmap["argdefs"] = localsmap["defaults"] 107 | del localsmap["defaults"] 108 | newfn = _types.FunctionType(**localsmap) 109 | replace_all_refs(referrer, newfn) 110 | 111 | else: 112 | logger.debug("%s is not supported.", referrer) 113 | 114 | if hit is False: 115 | raise AttributeError(f"Object '{replace_from}' not found") 116 | 117 | return replace_from 118 | -------------------------------------------------------------------------------- /hikka/_types.py: -------------------------------------------------------------------------------- 1 | # Legacy code support 2 | from .types import * 3 | -------------------------------------------------------------------------------- /hikka/compat/geek.py: -------------------------------------------------------------------------------- 1 | # ©️ Dan Gazizullin, 2021-2023 2 | # This file is a part of Hikka Userbot 3 | # 🌐 https://github.com/hikariatama/Hikka 4 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 5 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 6 | # Netfoll Team modifided Hikka files for Netfoll 7 | # 🌐 https://github.com/MXRRI/Netfoll 8 | 9 | import re 10 | 11 | 12 | def compat(code: str) -> str: 13 | """ 14 | Reformats modules, built for GeekTG to work with Netfoll 15 | :param code: code to reformat 16 | :return: reformatted code 17 | """ 18 | return "\n".join( 19 | [ 20 | re.sub( 21 | r"^( *)from \.\.inline import (.+)$", 22 | r"\1from ..inline.types import \2", 23 | re.sub( 24 | r"^( *)from \.\.inline import rand[^,]*$", 25 | "\1from ..utils import rand", 26 | re.sub( 27 | r"^( *)from \.\.inline import rand, ?(.+)$", 28 | r"\1from ..inline.types import \2\n\1from ..utils import rand", 29 | re.sub( 30 | r"^( *)from \.\.inline import (.+), ?rand[^,]*$", 31 | r"\1from ..inline.types import \2\n\1from ..utils import" 32 | r" rand", 33 | re.sub( 34 | r"^( *)from \.\.inline import (.+), ?rand, ?(.+)$", 35 | r"\1from ..inline.types import \2, \3\n\1from ..utils" 36 | r" import rand", 37 | line.replace("GeekInlineQuery", "InlineQuery").replace( 38 | "self.inline._bot", 39 | "self.inline.bot", 40 | ), 41 | flags=re.M, 42 | ), 43 | flags=re.M, 44 | ), 45 | flags=re.M, 46 | ), 47 | flags=re.M, 48 | ), 49 | flags=re.M, 50 | ) 51 | for line in code.splitlines() 52 | ] 53 | ) 54 | -------------------------------------------------------------------------------- /hikka/compat/pyroproxy.py: -------------------------------------------------------------------------------- 1 | # ©️ Dan Gazizullin, 2021-2023 2 | # This file is a part of Hikka Userbot 3 | # 🌐 https://github.com/hikariatama/Hikka 4 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 5 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 6 | # Netfoll Team modifided Hikka files for Netfoll 7 | # 🌐 https://github.com/MXRRI/Netfoll 8 | 9 | import asyncio 10 | import copy 11 | import datetime 12 | import functools 13 | import logging 14 | import re 15 | import typing 16 | 17 | import telethon 18 | from pyrogram import Client as PyroClient 19 | from pyrogram import errors as pyro_errors 20 | from pyrogram import raw 21 | 22 | from .. import translations, utils 23 | from ..tl_cache import CustomTelegramClient 24 | from ..version import __version__ 25 | 26 | PROXY = { 27 | pyro_object: telethon.tl.alltlobjects.tlobjects[constructor_id] 28 | for constructor_id, pyro_object in raw.all.objects.items() 29 | if constructor_id in telethon.tl.alltlobjects.tlobjects 30 | } 31 | 32 | REVERSED_PROXY = { 33 | **{tl_object: pyro_object for pyro_object, tl_object in PROXY.items()}, 34 | **{ 35 | tl_object: raw.all.objects[tl_object.CONSTRUCTOR_ID] 36 | for _, tl_object in utils.iter_attrs(telethon.tl.custom) 37 | if getattr(tl_object, "CONSTRUCTOR_ID", None) in raw.all.objects 38 | }, 39 | } 40 | 41 | PYRO_ERRORS = { 42 | cls.ID: cls 43 | for _, cls in utils.iter_attrs(pyro_errors) 44 | if hasattr(cls, "ID") and issubclass(cls, pyro_errors.RPCError) 45 | } 46 | 47 | 48 | logger = logging.getLogger(__name__) 49 | 50 | 51 | class PyroProxyClient(PyroClient): 52 | def __init__(self, tl_client: CustomTelegramClient): 53 | self.tl_client = tl_client 54 | super().__init__( 55 | **{ 56 | "name": "proxied_pyrogram_client", 57 | "api_id": tl_client.api_id, 58 | "api_hash": tl_client.api_hash, 59 | "app_version": ( 60 | f"Netfoll v{__version__[0]}.{__version__[1]}.{__version__[2]}" 61 | ), 62 | "lang_code": tl_client.loader.db.get( 63 | translations.__name__, "lang", "en" 64 | ).split()[0], 65 | "in_memory": True, 66 | "phone_number": tl_client.hikka_me.phone, 67 | } 68 | ) 69 | 70 | # We need to set this to True so pyro thinks he's connected 71 | # even tho it's not. We don't need to connect to Telegram as 72 | # we redirect all requests to telethon's handler 73 | self.is_connected = True 74 | self.conn = tl_client.session._conn 75 | 76 | async def start(self): 77 | self.me = await self.get_me() 78 | self.tl_client.raw_updates_processor = self._on_event 79 | 80 | def _on_event( 81 | self, 82 | event: typing.Union[ 83 | telethon.tl.types.Updates, 84 | telethon.tl.types.UpdatesCombined, 85 | telethon.tl.types.UpdateShort, 86 | ], 87 | ): 88 | asyncio.ensure_future(self.handle_updates(self._tl2pyro(event))) 89 | 90 | async def invoke( 91 | self, 92 | query: raw.core.TLObject, 93 | *args, 94 | **kwargs, 95 | ) -> typing.Union[typing.List[raw.core.TLObject], raw.core.TLObject]: 96 | logger.debug( 97 | "Running Pyrogram's invoke of %s with Telethon proxying", 98 | query.__class__.__name__, 99 | ) 100 | if self.tl_client.session.takeout_id: 101 | query = raw.functions.InvokeWithTakeout( 102 | takeout_id=self.tl_client.session.takeout_id, 103 | query=query, 104 | ) 105 | 106 | try: 107 | r = await self.tl_client(self._pyro2tl(query)) 108 | except telethon.errors.rpcerrorlist.RPCError as e: 109 | raise self._tl_error2pyro(e) 110 | 111 | return self._tl2pyro(r) 112 | 113 | @staticmethod 114 | def _tl_error2pyro( 115 | error: telethon.errors.rpcerrorlist.RPCError, 116 | ) -> pyro_errors.RPCError: 117 | rpc = ( 118 | re.sub(r"([A-Z])", r"_\1", error.__class__.__name__) 119 | .upper() 120 | .strip("_") 121 | .rsplit("ERROR", maxsplit=1)[0] 122 | .strip("_") 123 | ) 124 | if rpc in PYRO_ERRORS: 125 | return PYRO_ERRORS[rpc]() 126 | 127 | return PYRO_ERRORS.get( 128 | f"{rpc}_X", 129 | PYRO_ERRORS.get( 130 | f"{rpc}_0", 131 | pyro_errors.RPCError, 132 | ), 133 | )() 134 | 135 | def _pyro2tl(self, pyro_obj: raw.core.TLObject) -> telethon.tl.TLObject: 136 | """ 137 | Recursively converts Pyrogram TLObjects to Telethon TLObjects (methods, 138 | types and everything else, which is in tl schema) 139 | :param pyro_obj: Pyrogram TLObject 140 | :return: Telethon TLObject 141 | :raises TypeError: if it's not possible to convert Pyrogram TLObject to 142 | Telethon TLObject 143 | """ 144 | pyro_obj = self._convert(pyro_obj) 145 | if isinstance(pyro_obj, list): 146 | return [self._pyro2tl(i) for i in pyro_obj] 147 | elif isinstance(pyro_obj, dict): 148 | return {k: self._pyro2tl(v) for k, v in pyro_obj.items()} 149 | else: 150 | if not isinstance(pyro_obj, raw.core.TLObject): 151 | return pyro_obj 152 | 153 | if type(pyro_obj) not in PROXY: 154 | raise TypeError( 155 | f"Cannot convert Pyrogram's {type(pyro_obj)} to Telethon TLObject" 156 | ) 157 | 158 | return PROXY[type(pyro_obj)]( 159 | **{ 160 | attr: self._pyro2tl(getattr(pyro_obj, attr)) 161 | for attr in pyro_obj.__slots__ 162 | } 163 | ) 164 | 165 | def _tl2pyro(self, tl_obj: telethon.tl.TLObject) -> raw.core.TLObject: 166 | """ 167 | Recursively converts Telethon TLObjects to Pyrogram TLObjects (methods, 168 | types and everything else, which is in tl schema) 169 | :param tl_obj: Telethon TLObject 170 | :return: Pyrogram TLObject 171 | :raises TypeError: if it's not possible to convert Telethon TLObject to 172 | Pyrogram TLObject 173 | """ 174 | tl_obj = self._convert(tl_obj) 175 | if ( 176 | isinstance(getattr(tl_obj, "from_id", None), int) 177 | and tl_obj.from_id 178 | and hasattr(tl_obj, "sender_id") 179 | ): 180 | tl_obj = copy.copy(tl_obj) 181 | tl_obj.from_id = telethon.tl.types.PeerUser(tl_obj.sender_id) 182 | 183 | if isinstance(tl_obj, list): 184 | return [self._tl2pyro(i) for i in tl_obj] 185 | elif isinstance(tl_obj, dict): 186 | return {k: self._tl2pyro(v) for k, v in tl_obj.items()} 187 | else: 188 | if isinstance(tl_obj, int) and str(tl_obj).startswith("-100"): 189 | return int(str(tl_obj)[4:]) 190 | 191 | if not isinstance(tl_obj, telethon.tl.TLObject): 192 | return tl_obj 193 | 194 | if type(tl_obj) not in REVERSED_PROXY: 195 | raise TypeError( 196 | f"Cannot convert Telethon's {type(tl_obj)} to Pyrogram TLObject" 197 | ) 198 | 199 | hints = typing.get_type_hints(REVERSED_PROXY[type(tl_obj)].__init__) or {} 200 | 201 | return REVERSED_PROXY[type(tl_obj)]( 202 | **{ 203 | attr: self._convert_types( 204 | hints.get(attr), 205 | self._tl2pyro(getattr(tl_obj, attr)), 206 | ) 207 | for attr in REVERSED_PROXY[type(tl_obj)].__slots__ 208 | } 209 | ) 210 | 211 | @staticmethod 212 | def _get_origin(hint: typing.Any) -> typing.Any: 213 | try: 214 | return typing.get_origin(hint) 215 | except Exception: 216 | return None 217 | 218 | def _convert_types(self, hint: typing.Any, value: typing.Any) -> typing.Any: 219 | if not value and ( 220 | self._get_origin(hint) in {typing.List, list} 221 | or ( 222 | self._get_origin(hint) is typing.Union 223 | and any( 224 | self._get_origin(i) in {typing.List, list} for i in hint.__args__ 225 | ) 226 | ) 227 | ): 228 | return [] 229 | 230 | return value 231 | 232 | def _convert(self, obj: typing.Any) -> typing.Any: 233 | if isinstance(obj, datetime.datetime): 234 | return int(obj.timestamp()) 235 | 236 | return obj 237 | 238 | async def resolve_peer(self, *args, **kwargs): 239 | return self._tl2pyro(await self.tl_client.get_entity(*args, **kwargs)) 240 | 241 | async def fetch_peers( 242 | self, 243 | peers: typing.List[ 244 | typing.Union[raw.types.User, raw.types.Chat, raw.types.Channel] 245 | ], 246 | ) -> bool: 247 | return any(getattr(peer, "min", False) for peer in peers) 248 | 249 | @property 250 | def iter_chat_members(self): 251 | return self.get_chat_members 252 | 253 | @property 254 | def iter_dialogs(self): 255 | return self.get_dialogs 256 | 257 | @property 258 | def iter_history(self): 259 | return self.get_chat_history 260 | 261 | @property 262 | def iter_profile_photos(self): 263 | return self.get_chat_photos 264 | 265 | async def save_file( 266 | self, 267 | path: typing.Union[str, typing.BinaryIO], 268 | file_id: int = None, 269 | file_part: int = 0, 270 | progress: typing.Callable = None, 271 | progress_args: tuple = (), 272 | ): 273 | return self._tl2pyro( 274 | await self.tl_client.upload_file( 275 | path, 276 | part_size_kb=file_part, 277 | progress_callback=( 278 | functools.partial(progress, *progress_args) 279 | if progress and callable(progress) 280 | else None 281 | ), 282 | ) 283 | ) 284 | -------------------------------------------------------------------------------- /hikka/configurator.py: -------------------------------------------------------------------------------- 1 | # Friendly Telegram (telegram userbot) 2 | # Copyright (C) 2018-2021 The Authors 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | # ©️ Dan Gazizullin, 2021-2023 18 | # This file is a part of Hikka Userbot 19 | # 🌐 https://github.com/hikariatama/Hikka 20 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 21 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 22 | # Netfoll Team modifided Hikka files for Netfoll 23 | # 🌐 https://github.com/MXRRI/Netfoll 24 | 25 | import locale 26 | import os 27 | import string 28 | import sys 29 | import typing 30 | 31 | from dialog import Dialog, ExecutableNotFound 32 | 33 | from . import utils 34 | 35 | 36 | def _safe_input(*args, **kwargs): 37 | try: 38 | return input(*args, **kwargs) 39 | except (EOFError, OSError): 40 | raise 41 | except KeyboardInterrupt: 42 | print() 43 | return None 44 | 45 | 46 | class TDialog: 47 | def inputbox(self, query: str) -> typing.Tuple[bool, str]: 48 | print(query) 49 | print() 50 | inp = _safe_input("Введите значение...:") 51 | return (False, "Cancelled") if not inp else (True, inp) 52 | 53 | def msgbox(self, msg: str) -> bool: 54 | print(msg) 55 | return True 56 | 57 | 58 | TITLE = "" 59 | 60 | if sys.stdout.isatty(): 61 | try: 62 | DIALOG = TDialog() 63 | except (ExecutableNotFound, locale.Error): 64 | DIALOG = Dialog(dialog="dialog", autowidgetsize=True) 65 | locale.setlocale(locale.LC_ALL, "") 66 | else: 67 | DIALOG = TDialog() 68 | 69 | 70 | def api_config(data_root: str): 71 | code, hash_value = DIALOG.inputbox( 72 | """­ 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | _ _ _ __ _ _ 93 | | \ | | ___| |_ / _| ___ | | | 94 | | \| |/ _ \ __| |_ / _ \| | | 95 | | |\ | __/ |_| _| (_) | | | 96 | |_| \_|\___|\__|_| \___/|_|_| 97 | 98 | Пожалуйста, введите API HASH 99 | Для отмены, нажмите Ctrl + Z 100 | """ 101 | ) 102 | if not code: 103 | return 104 | 105 | if len(hash_value) != 32 or any(it not in string.hexdigits for it in hash_value): 106 | DIALOG.msgbox("Неверный HASH") 107 | return 108 | 109 | code, id_value = DIALOG.inputbox( 110 | """­ 111 | Отлично! Теперь введите API ID 112 | """ 113 | ) 114 | 115 | if not id_value or any(it not in string.digits for it in id_value): 116 | DIALOG.msgbox("Неверный ID") 117 | return 118 | 119 | with open( 120 | os.path.join( 121 | data_root or os.path.dirname(utils.get_base_dir()), "api_token.txt" 122 | ), 123 | "w", 124 | ) as file: 125 | file.write(id_value + "\n" + hash_value) 126 | 127 | DIALOG.msgbox( 128 | "API данные сохранены. Осталось только ввести номер и код подтверждения. Приступим!\n" 129 | ) 130 | -------------------------------------------------------------------------------- /hikka/database.py: -------------------------------------------------------------------------------- 1 | # ©️ Dan Gazizullin, 2021-2023 2 | # This file is a part of Hikka Userbot 3 | # 🌐 https://github.com/hikariatama/Hikka 4 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 5 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 6 | # Netfoll Team modifided Hikka files for Netfoll 7 | # 🌐 https://github.com/MXRRI/Netfoll 8 | 9 | import asyncio 10 | import collections 11 | import json 12 | import logging 13 | import os 14 | import time 15 | 16 | try: 17 | import redis 18 | except ImportError as e: 19 | if "RAILWAY" in os.environ: 20 | raise e 21 | 22 | 23 | import typing 24 | 25 | from telethon.errors.rpcerrorlist import ChannelsTooMuchError 26 | from telethon.tl.types import Message 27 | 28 | from . import main, utils 29 | from .pointers import PointerDict, PointerList 30 | from .tl_cache import CustomTelegramClient 31 | from .types import JSONSerializable 32 | 33 | DATA_DIR = ( 34 | os.path.normpath(os.path.join(utils.get_base_dir(), "..")) 35 | if "DOCKER" not in os.environ 36 | else "/data" 37 | ) 38 | 39 | logger = logging.getLogger(__name__) 40 | 41 | 42 | class NoAssetsChannel(Exception): 43 | """Raised when trying to read/store asset with no asset channel present""" 44 | 45 | 46 | class Database(dict): 47 | _next_revision_call = 0 48 | _revisions = [] 49 | _assets = None 50 | _me = None 51 | _redis = None 52 | _saving_task = None 53 | 54 | def __init__(self, client: CustomTelegramClient): 55 | super().__init__() 56 | self._client = client 57 | 58 | def __repr__(self): 59 | return object.__repr__(self) 60 | 61 | def _redis_save_sync(self): 62 | with self._redis.pipeline() as pipe: 63 | pipe.set( 64 | str(self._client.tg_id), 65 | json.dumps(self, ensure_ascii=True), 66 | ) 67 | pipe.execute() 68 | 69 | async def remote_force_save(self) -> bool: 70 | """Force save database to remote endpoint without waiting""" 71 | if not self._redis: 72 | return False 73 | 74 | await utils.run_sync(self._redis_save_sync) 75 | logger.debug("Published db to Redis") 76 | return True 77 | 78 | async def _redis_save(self) -> bool: 79 | """Save database to redis""" 80 | if not self._redis: 81 | return False 82 | 83 | await asyncio.sleep(5) 84 | 85 | await utils.run_sync(self._redis_save_sync) 86 | 87 | logger.debug("Published db to Redis") 88 | 89 | self._saving_task = None 90 | return True 91 | 92 | async def redis_init(self) -> bool: 93 | """Init redis database""" 94 | if REDIS_URI := os.environ.get("REDIS_URL") or main.get_config_key("redis_uri"): 95 | self._redis = redis.Redis.from_url(REDIS_URI) 96 | else: 97 | return False 98 | 99 | async def init(self): 100 | """Asynchronous initialization unit""" 101 | if os.environ.get("REDIS_URL") or main.get_config_key("redis_uri"): 102 | await self.redis_init() 103 | 104 | self._db_path = os.path.join(DATA_DIR, f"config-{self._client.tg_id}.json") 105 | self.read() 106 | 107 | try: 108 | self._assets, _ = await utils.asset_channel( 109 | self._client, 110 | "netfoll-assets", 111 | "🌆 Your Netfoll assets will be stored here", 112 | archive=True, 113 | avatar="https://raw.githubusercontent.com/hikariatama/assets/master/hikka-assets.png", 114 | ) 115 | except ChannelsTooMuchError: 116 | self._assets = None 117 | logger.error( 118 | "Can't find and/or create assets folder\n" 119 | "This may cause several consequences, such as:\n" 120 | "- Non working assets feature (e.g. notes)\n" 121 | "- This error will occur every restart\n\n" 122 | "You can solve this by leaving some channels/groups" 123 | ) 124 | 125 | def read(self): 126 | """Read database and stores it in self""" 127 | if self._redis: 128 | try: 129 | self.update( 130 | **json.loads( 131 | self._redis.get( 132 | str(self._client.tg_id), 133 | ).decode(), 134 | ) 135 | ) 136 | except Exception: 137 | logger.exception("Error reading redis database") 138 | return 139 | 140 | try: 141 | with open(self._db_path, "r", encoding="utf-8") as f: 142 | self.update(**json.load(f)) 143 | except (FileNotFoundError, json.decoder.JSONDecodeError): 144 | logger.warning("Database read failed! Creating new one...") 145 | 146 | def process_db_autofix(self, db: dict) -> bool: 147 | if not utils.is_serializable(db): 148 | return False 149 | 150 | for key, value in db.copy().items(): 151 | if not isinstance(key, (str, int)): 152 | logger.warning( 153 | "DbAutoFix: Dropped key %s, because it is not string or int", 154 | key, 155 | ) 156 | continue 157 | 158 | if not isinstance(value, dict): 159 | # If value is not a dict (module values), drop it, 160 | # otherwise it may cause problems 161 | del db[key] 162 | logger.warning( 163 | "DbAutoFix: Dropped key %s, because it is non-dict, but %s", 164 | key, 165 | type(value), 166 | ) 167 | continue 168 | 169 | for subkey in value: 170 | if not isinstance(subkey, (str, int)): 171 | del db[key][subkey] 172 | logger.warning( 173 | "DbAutoFix: Dropped subkey %s of db key %s, because it is not" 174 | " string or int", 175 | subkey, 176 | key, 177 | ) 178 | continue 179 | 180 | return True 181 | 182 | def save(self) -> bool: 183 | """Save database""" 184 | if not self.process_db_autofix(self): 185 | try: 186 | rev = self._revisions.pop() 187 | while not self.process_db_autofix(rev): 188 | rev = self._revisions.pop() 189 | except IndexError: 190 | raise RuntimeError( 191 | "Can't find revision to restore broken database from " 192 | "database is most likely broken and will lead to problems, " 193 | "so its save is forbidden." 194 | ) 195 | 196 | self.clear() 197 | self.update(**rev) 198 | 199 | raise RuntimeError( 200 | "Rewriting database to the last revision because new one destructed it" 201 | ) 202 | 203 | if self._next_revision_call < time.time(): 204 | self._revisions += [dict(self)] 205 | self._next_revision_call = time.time() + 3 206 | 207 | while len(self._revisions) > 15: 208 | self._revisions.pop() 209 | 210 | if self._redis: 211 | if not self._saving_task: 212 | self._saving_task = asyncio.ensure_future(self._redis_save()) 213 | return True 214 | 215 | try: 216 | with open(self._db_path, "w", encoding="utf-8") as f: 217 | json.dump(self, f, indent=4) 218 | except Exception: 219 | logger.exception("Database save failed!") 220 | return False 221 | 222 | return True 223 | 224 | async def store_asset(self, message: Message) -> int: 225 | """ 226 | Save assets 227 | returns asset_id as integer 228 | """ 229 | if not self._assets: 230 | raise NoAssetsChannel("Tried to save asset to non-existing asset channel") 231 | 232 | return ( 233 | (await self._client.send_message(self._assets, message)).id 234 | if isinstance(message, Message) 235 | else ( 236 | await self._client.send_message( 237 | self._assets, 238 | file=message, 239 | force_document=True, 240 | ) 241 | ).id 242 | ) 243 | 244 | async def fetch_asset(self, asset_id: int) -> typing.Optional[Message]: 245 | """Fetch previously saved asset by its asset_id""" 246 | if not self._assets: 247 | raise NoAssetsChannel( 248 | "Tried to fetch asset from non-existing asset channel" 249 | ) 250 | 251 | asset = await self._client.get_messages(self._assets, ids=[asset_id]) 252 | 253 | return asset[0] if asset else None 254 | 255 | def get( 256 | self, 257 | owner: str, 258 | key: str, 259 | default: typing.Optional[JSONSerializable] = None, 260 | ) -> JSONSerializable: 261 | """Get database key""" 262 | try: 263 | return self[owner][key] 264 | except KeyError: 265 | return default 266 | 267 | def set(self, owner: str, key: str, value: JSONSerializable) -> bool: 268 | """Set database key""" 269 | if not utils.is_serializable(owner): 270 | raise RuntimeError( 271 | "Attempted to write object to " 272 | f"{owner=} ({type(owner)=}) of database. It is not " 273 | "JSON-serializable key which will cause errors" 274 | ) 275 | 276 | if not utils.is_serializable(key): 277 | raise RuntimeError( 278 | "Attempted to write object to " 279 | f"{key=} ({type(key)=}) of database. It is not " 280 | "JSON-serializable key which will cause errors" 281 | ) 282 | 283 | if not utils.is_serializable(value): 284 | raise RuntimeError( 285 | "Attempted to write object of " 286 | f"{key=} ({type(value)=}) to database. It is not " 287 | "JSON-serializable value which will cause errors" 288 | ) 289 | 290 | super().setdefault(owner, {})[key] = value 291 | return self.save() 292 | 293 | def pointer( 294 | self, 295 | owner: str, 296 | key: str, 297 | default: typing.Optional[JSONSerializable] = None, 298 | ) -> typing.Union[JSONSerializable, PointerList, PointerDict]: 299 | """Get a pointer to database key""" 300 | value = self.get(owner, key, default) 301 | mapping = { 302 | list: PointerList, 303 | dict: PointerDict, 304 | collections.abc.Hashable: lambda v: v, 305 | } 306 | 307 | pointer_constructor = next( 308 | (pointer for type_, pointer in mapping.items() if isinstance(value, type_)), 309 | None, 310 | ) 311 | 312 | if pointer_constructor is None: 313 | raise ValueError( 314 | f"Pointer for type {type(value).__name__} is not implemented" 315 | ) 316 | 317 | return pointer_constructor(self, owner, key, default) 318 | -------------------------------------------------------------------------------- /hikka/inline/bot_pm.py: -------------------------------------------------------------------------------- 1 | # ©️ Dan Gazizullin, 2021-2023 2 | # This file is a part of Hikka Userbot 3 | # 🌐 https://github.com/hikariatama/Hikka 4 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 5 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 6 | # Netfoll Team modifided Hikka files for Netfoll 7 | # 🌐 https://github.com/MXRRI/Netfoll 8 | 9 | import logging 10 | import typing 11 | 12 | from .types import InlineUnit 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class BotPM(InlineUnit): 18 | def set_fsm_state( 19 | self, 20 | user: typing.Union[str, int], 21 | state: typing.Union[str, bool], 22 | ) -> bool: 23 | if not isinstance(user, (str, int)): 24 | logger.error( 25 | "Invalid type for `user` in `set_fsm_state`. Expected `str` or `int`," 26 | " got %s", 27 | type(user), 28 | ) 29 | return False 30 | 31 | if not isinstance(state, (str, bool)): 32 | logger.error( 33 | "Invalid type for `state` in `set_fsm_state`. Expected `str` or `bool`," 34 | " got %s", 35 | type(state), 36 | ) 37 | return False 38 | 39 | if state: 40 | self.fsm[str(user)] = state 41 | elif str(user) in self.fsm: 42 | del self.fsm[str(user)] 43 | 44 | return True 45 | 46 | ss = set_fsm_state 47 | 48 | def get_fsm_state(self, user: typing.Union[str, int]) -> typing.Union[bool, str]: 49 | if not isinstance(user, (str, int)): 50 | logger.error( 51 | "Invalid type for `user` in `get_fsm_state`. Expected `str` or `int`," 52 | " got %s", 53 | type(user), 54 | ) 55 | return False 56 | 57 | return self.fsm.get(str(user), False) 58 | 59 | gs = get_fsm_state 60 | -------------------------------------------------------------------------------- /hikka/inline/core.py: -------------------------------------------------------------------------------- 1 | """Inline buttons, galleries and other Telegram-Bot-API stuff""" 2 | 3 | # ©️ Dan Gazizullin, 2021-2023 4 | # This file is a part of Hikka Userbot 5 | # 🌐 https://github.com/hikariatama/Hikka 6 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 7 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 8 | # Netfoll Team modifided Hikka files for Netfoll 9 | # 🌐 https://github.com/MXRRI/Netfoll 10 | 11 | import asyncio 12 | import logging 13 | import time 14 | 15 | from aiogram import Bot, Dispatcher 16 | from aiogram.types import ParseMode 17 | from aiogram.utils.exceptions import TerminatedByOtherGetUpdates, Unauthorized 18 | from telethon.errors.rpcerrorlist import InputUserDeactivatedError, YouBlockedUserError 19 | from telethon.tl.functions.contacts import UnblockRequest 20 | from telethon.utils import get_display_name 21 | 22 | from ..database import Database 23 | from ..tl_cache import CustomTelegramClient 24 | from .bot_pm import BotPM 25 | from .events import Events 26 | from .form import Form 27 | from .gallery import Gallery 28 | from .list import List 29 | from .query_gallery import QueryGallery 30 | from .token_obtainment import TokenObtainment 31 | from .utils import Utils 32 | 33 | logger = logging.getLogger(__name__) 34 | 35 | 36 | class InlineManager( 37 | Utils, 38 | Events, 39 | TokenObtainment, 40 | Form, 41 | Gallery, 42 | QueryGallery, 43 | List, 44 | BotPM, 45 | ): 46 | def __init__( 47 | self, 48 | client: CustomTelegramClient, 49 | db: Database, 50 | allmodules: "Modules", # type: ignore 51 | ): 52 | """Initialize InlineManager to create forms""" 53 | self._client = client 54 | self._db = db 55 | self._allmodules = allmodules 56 | 57 | self._units = {} 58 | self._custom_map = {} 59 | self.fsm = {} 60 | self._web_auth_tokens = [] 61 | 62 | self._markup_ttl = 60 * 60 * 24 63 | self.init_complete = False 64 | 65 | self._token = db.get("hikka.inline", "bot_token", False) 66 | 67 | async def _cleaner(self): 68 | """Cleans outdated inline units""" 69 | while True: 70 | for unit_id, unit in self._units.copy().items(): 71 | if (unit.get("ttl") or (time.time() + self._markup_ttl)) < time.time(): 72 | del self._units[unit_id] 73 | 74 | await asyncio.sleep(5) 75 | 76 | async def register_manager( 77 | self, 78 | after_break: bool = False, 79 | ignore_token_checks: bool = False, 80 | ): 81 | # Get info about user to use it in this class 82 | self._me = self._client.tg_id 83 | self._name = get_display_name(self._client.hikka_me) 84 | 85 | if not ignore_token_checks: 86 | # Assert that token is set to valid, and if not, 87 | # set `init_complete` to `False` and return 88 | is_token_asserted = await self._assert_token() 89 | if not is_token_asserted: 90 | self.init_complete = False 91 | return 92 | 93 | # We successfully asserted token, so set `init_complete` to `True` 94 | self.init_complete = True 95 | 96 | # Create bot instance and dispatcher 97 | self.bot = Bot(token=self._token, parse_mode=ParseMode.HTML) 98 | Bot.set_current(self.bot) 99 | self._bot = self.bot # This is a temporary alias so the 100 | # developers can adapt their code 101 | self._dp = Dispatcher(self.bot) 102 | 103 | # Get bot username to call inline queries 104 | try: 105 | bot_me = await self.bot.get_me() 106 | self.bot_username = bot_me.username 107 | self.bot_id = bot_me.id 108 | except Unauthorized: 109 | logger.critical("Token expired, revoking...") 110 | return await self._dp_revoke_token(False) 111 | 112 | # Start the bot in case it can send you messages 113 | try: 114 | m = await self._client.send_message(self.bot_username, "/start hikka init") 115 | except (InputUserDeactivatedError, ValueError): 116 | self._db.set("hikka.inline", "bot_token", None) 117 | self._token = False 118 | 119 | if not after_break: 120 | return await self.register_manager(True) 121 | 122 | self.init_complete = False 123 | return False 124 | except YouBlockedUserError: 125 | await self._client(UnblockRequest(id=self.bot_username)) 126 | try: 127 | m = await self._client.send_message( 128 | self.bot_username, "/start hikka init" 129 | ) 130 | except Exception: 131 | logger.critical("Can't unblock users bot", exc_info=True) 132 | return False 133 | except Exception: 134 | self.init_complete = False 135 | logger.critical("Initialization of inline manager failed!", exc_info=True) 136 | return False 137 | 138 | await self._client.delete_messages(self.bot_username, m) 139 | 140 | # Register required event handlers inside aiogram 141 | self._dp.register_inline_handler( 142 | self._inline_handler, 143 | lambda _: True, 144 | ) 145 | 146 | self._dp.register_callback_query_handler( 147 | self._callback_query_handler, 148 | lambda _: True, 149 | ) 150 | 151 | self._dp.register_chosen_inline_handler( 152 | self._chosen_inline_handler, 153 | lambda _: True, 154 | ) 155 | 156 | self._dp.register_message_handler( 157 | self._message_handler, 158 | lambda *_: True, 159 | content_types=["any"], 160 | ) 161 | 162 | old = self.bot.get_updates 163 | revoke = self._dp_revoke_token 164 | 165 | async def new(*args, **kwargs): 166 | nonlocal revoke, old 167 | try: 168 | return await old(*args, **kwargs) 169 | except TerminatedByOtherGetUpdates: 170 | await revoke() 171 | except Unauthorized: 172 | logger.critical("Got Unauthorized") 173 | await self._stop() 174 | 175 | self.bot.get_updates = new 176 | 177 | # Start polling as the separate task, just in case we will need 178 | # to force stop this coro. It should be cancelled only by `stop` 179 | # because it stops the bot from getting updates 180 | self._task = asyncio.ensure_future(self._dp.start_polling()) 181 | self._cleaner_task = asyncio.ensure_future(self._cleaner()) 182 | 183 | async def _stop(self): 184 | self._task.cancel() 185 | self._dp.stop_polling() 186 | self._cleaner_task.cancel() 187 | 188 | def pop_web_auth_token(self, token) -> bool: 189 | """Check if web confirmation button was pressed""" 190 | if token not in self._web_auth_tokens: 191 | return False 192 | 193 | self._web_auth_tokens.remove(token) 194 | return True 195 | 196 | 197 | if __name__ == "__main__": 198 | raise Exception("This file must be called as a module") 199 | -------------------------------------------------------------------------------- /hikka/inline/query_gallery.py: -------------------------------------------------------------------------------- 1 | # ©️ Dan Gazizullin, 2021-2023 2 | # This file is a part of Hikka Userbot 3 | # 🌐 https://github.com/hikariatama/Hikka 4 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 5 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 6 | # Netfoll Team modifided Hikka files for Netfoll 7 | # 🌐 https://github.com/MXRRI/Netfoll 8 | 9 | import asyncio 10 | import logging 11 | import time 12 | import typing 13 | 14 | from aiogram.types import InlineQuery, InlineQueryResultArticle, InputTextMessageContent 15 | 16 | from .. import utils 17 | from .types import InlineUnit 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class QueryGallery(InlineUnit): 23 | async def query_gallery( 24 | self, 25 | query: InlineQuery, 26 | items: typing.List[typing.Dict[str, typing.Any]], 27 | *, 28 | force_me: bool = False, 29 | disable_security: bool = False, 30 | always_allow: typing.Optional[typing.List[int]] = None, 31 | ) -> bool: 32 | """ 33 | Answer inline query with a bunch of inline galleries 34 | :param query: `InlineQuery` which should be answered with inline gallery 35 | :param items: Array of dicts with inline results. 36 | Each dict *must* has a: 37 | - `title` - The title of the result 38 | - `description` - Short description of the result 39 | - `next_handler` - Inline gallery handler. Callback or awaitable 40 | Each dict *can* has a: 41 | - `caption` - Caption of photo. Defaults to `""` 42 | - `force_me` - Whether the button must be accessed only by owner. Defaults to `False` 43 | - `disable_security` - Whether to disable the security checks at all. Defaults to `False` 44 | :param force_me: Either this gallery buttons must be pressed only by owner scope or no 45 | :param always_allow: Users, that are allowed to press buttons in addition to previous rules 46 | :param disable_security: By default, Netfoll will try to check security of gallery 47 | If you want to disable all security checks on this gallery in particular, pass `disable_security=True` 48 | :return: Status of answer 49 | """ 50 | if not isinstance(force_me, bool): 51 | logger.error( 52 | "Invalid type for `force_me`. Expected `bool`, got %s", 53 | type(force_me), 54 | ) 55 | return False 56 | 57 | if not isinstance(disable_security, bool): 58 | logger.error( 59 | "Invalid type for `disable_security`. Expected `bool`, got %s", 60 | type(disable_security), 61 | ) 62 | return False 63 | 64 | if always_allow and not isinstance(always_allow, list): 65 | logger.error( 66 | "Invalid type for `always_allow`. Expected `list`, got %s", 67 | type(always_allow), 68 | ) 69 | return False 70 | 71 | if not always_allow: 72 | always_allow = [] 73 | 74 | if ( 75 | not isinstance(items, list) 76 | or not all(isinstance(i, dict) for i in items) 77 | or not all( 78 | "title" in i 79 | and "description" in i 80 | and "next_handler" in i 81 | and ( 82 | callable(i["next_handler"]) 83 | or asyncio.iscoroutinefunction(i) 84 | or isinstance(i, list) 85 | ) 86 | and isinstance(i["title"], str) 87 | and isinstance(i["description"], str) 88 | for i in items 89 | ) 90 | ): 91 | logger.error("Invalid `items` specified in query gallery") 92 | return False 93 | 94 | result = [] 95 | for i in items: 96 | if "thumb_handler" not in i: 97 | photo_url = await self._call_photo(i["next_handler"]) 98 | if not photo_url: 99 | return False 100 | 101 | if isinstance(photo_url, list): 102 | photo_url = photo_url[0] 103 | 104 | if not isinstance(photo_url, str): 105 | logger.error( 106 | "Invalid result from `next_handler`. Expected `str`, got %s", 107 | type(photo_url), 108 | ) 109 | continue 110 | else: 111 | photo_url = await self._call_photo(i["thumb_handler"]) 112 | if not photo_url: 113 | return False 114 | 115 | if isinstance(photo_url, list): 116 | photo_url = photo_url[0] 117 | 118 | if not isinstance(photo_url, str): 119 | logger.error( 120 | "Invalid result from `thumb_handler`. Expected `str`, got %s", 121 | type(photo_url), 122 | ) 123 | continue 124 | 125 | id_ = utils.rand(16) 126 | 127 | self._custom_map[id_] = { 128 | "handler": i["next_handler"], 129 | "ttl": round(time.time()) + 120, 130 | **({"always_allow": always_allow} if always_allow else {}), 131 | **({"force_me": force_me} if force_me else {}), 132 | **({"disable_security": disable_security} if disable_security else {}), 133 | **({"caption": i["caption"]} if "caption" in i else {}), 134 | } 135 | 136 | result += [ 137 | InlineQueryResultArticle( 138 | id=utils.rand(20), 139 | title=i["title"], 140 | description=i["description"], 141 | input_message_content=InputTextMessageContent( 142 | f"🌘 Opening gallery...\n#id: {id_}", 143 | "HTML", 144 | disable_web_page_preview=True, 145 | ), 146 | thumb_url=photo_url, 147 | thumb_width=128, 148 | thumb_height=128, 149 | ) 150 | ] 151 | 152 | await query.answer(result, cache_time=0) 153 | return True 154 | -------------------------------------------------------------------------------- /hikka/inline/token_obtainment.py: -------------------------------------------------------------------------------- 1 | # ©️ Dan Gazizullin, 2021-2023 2 | # This file is a part of Hikka Userbot 3 | # 🌐 https://github.com/hikariatama/Hikka 4 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 5 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 6 | # Netfoll Team modifided Hikka files for Netfoll 7 | # 🌐 https://github.com/MXRRI/Netfoll 8 | 9 | import asyncio 10 | import io 11 | import logging 12 | import os 13 | import re 14 | 15 | from telethon.errors.rpcerrorlist import YouBlockedUserError 16 | from telethon.tl.functions.contacts import UnblockRequest 17 | 18 | from .. import utils 19 | from .._internal import fw_protect 20 | from .types import InlineUnit 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | with open( 25 | os.path.abspath( 26 | os.path.join(os.path.dirname(__file__), "..", "..", "assets", "bot_pfp.png") 27 | ), 28 | "rb", 29 | ) as f: 30 | photo = io.BytesIO(f.read()) 31 | photo.name = "avatar.png" 32 | 33 | 34 | class TokenObtainment(InlineUnit): 35 | async def _create_bot(self): 36 | # This is called outside of conversation, so we can start the new one 37 | # We create new bot 38 | logger.info("User doesn't have bot, attempting creating new one") 39 | async with self._client.conversation("@BotFather", exclusive=False) as conv: 40 | await fw_protect() 41 | m = await conv.send_message("/newbot") 42 | r = await conv.get_response() 43 | 44 | logger.debug(">> %s", m.raw_text) 45 | logger.debug("<< %s", r.raw_text) 46 | 47 | if "20" in r.raw_text: 48 | return False 49 | 50 | await fw_protect() 51 | await m.delete() 52 | await r.delete() 53 | 54 | if self._db.get("hikka.inline", "custom_bot", False): 55 | username = self._db.get("hikka.inline", "custom_bot").strip("@") 56 | username = f"@{username}" 57 | try: 58 | await self._client.get_entity(username) 59 | except ValueError: 60 | pass 61 | else: 62 | # Generate and set random username for bot 63 | uid = utils.rand(6) 64 | username = f"@netfoll_{uid}_bot" 65 | else: 66 | # Generate and set random username for bot 67 | uid = utils.rand(6) 68 | username = f"@netfoll_{uid}_bot" 69 | 70 | for msg in [ 71 | f"👾 Netfoll Userbot of {self._name}"[:64], 72 | username, 73 | "/setuserpic", 74 | username, 75 | ]: 76 | await fw_protect() 77 | m = await conv.send_message(msg) 78 | r = await conv.get_response() 79 | 80 | logger.debug(">> %s", m.raw_text) 81 | logger.debug("<< %s", r.raw_text) 82 | 83 | await fw_protect() 84 | await m.delete() 85 | await r.delete() 86 | 87 | try: 88 | await fw_protect() 89 | m = await conv.send_file(photo) 90 | r = await conv.get_response() 91 | 92 | logger.debug(">> ") 93 | logger.debug("<< %s", r.raw_text) 94 | except Exception: 95 | # In case user was not able to send photo to 96 | # BotFather, it is not a critical issue, so 97 | # just ignore it 98 | await fw_protect() 99 | m = await conv.send_message("/cancel") 100 | r = await conv.get_response() 101 | 102 | logger.debug(">> %s", m.raw_text) 103 | logger.debug("<< %s", r.raw_text) 104 | 105 | await fw_protect() 106 | 107 | await m.delete() 108 | await r.delete() 109 | 110 | # Re-attempt search. If it won't find newly created (or not created?) bot 111 | # it will return `False`, that's why `init_complete` will be `False` 112 | return await self._assert_token(False) 113 | 114 | async def _assert_token( 115 | self, 116 | create_new_if_needed: bool = True, 117 | revoke_token: bool = False, 118 | ) -> bool: 119 | # If the token is set in db 120 | if self._token: 121 | # Just return `True` 122 | return True 123 | 124 | logger.info("Netfoll can't find token in database, taking it from BotFather") 125 | 126 | if not self._db.get(__name__, "no_mute", False): 127 | await utils.dnd( 128 | self._client, 129 | await self._client.get_entity("@BotFather"), 130 | True, 131 | ) 132 | self._db.set(__name__, "no_mute", True) 133 | 134 | # Start conversation with BotFather to attempt search 135 | async with self._client.conversation("@BotFather", exclusive=False) as conv: 136 | # Wrap it in try-except in case user banned BotFather 137 | try: 138 | # Try sending command 139 | await fw_protect() 140 | m = await conv.send_message("/token") 141 | except YouBlockedUserError: 142 | # If user banned BotFather, unban him 143 | await self._client(UnblockRequest(id="@BotFather")) 144 | # And resend message 145 | await fw_protect() 146 | m = await conv.send_message("/token") 147 | 148 | r = await conv.get_response() 149 | 150 | logger.debug(">> %s", m.raw_text) 151 | logger.debug("<< %s", r.raw_text) 152 | 153 | await fw_protect() 154 | 155 | await m.delete() 156 | await r.delete() 157 | 158 | # User do not have any bots yet, so just create new one 159 | if not hasattr(r, "reply_markup") or not hasattr(r.reply_markup, "rows"): 160 | # Cancel current conversation (search) 161 | # bc we don't need it anymore 162 | await conv.cancel_all() 163 | 164 | return await self._create_bot() if create_new_if_needed else False 165 | 166 | for row in r.reply_markup.rows: 167 | for button in row.buttons: 168 | if self._db.get( 169 | "hikka.inline", "custom_bot", False 170 | ) and self._db.get( 171 | "hikka.inline", "custom_bot", False 172 | ) != button.text.strip( 173 | "@" 174 | ): 175 | continue 176 | 177 | if not self._db.get( 178 | "hikka.inline", 179 | "custom_bot", 180 | False, 181 | ) and not re.search(r"@netfoll_[0-9a-zA-Z]{6}_bot", button.text): 182 | continue 183 | 184 | await fw_protect() 185 | 186 | m = await conv.send_message(button.text) 187 | r = await conv.get_response() 188 | 189 | logger.debug(">> %s", m.raw_text) 190 | logger.debug("<< %s", r.raw_text) 191 | 192 | if revoke_token: 193 | await fw_protect() 194 | await m.delete() 195 | await r.delete() 196 | 197 | await fw_protect() 198 | 199 | m = await conv.send_message("/revoke") 200 | r = await conv.get_response() 201 | 202 | logger.debug(">> %s", m.raw_text) 203 | logger.debug("<< %s", r.raw_text) 204 | 205 | await fw_protect() 206 | 207 | await m.delete() 208 | await r.delete() 209 | 210 | await fw_protect() 211 | 212 | m = await conv.send_message(button.text) 213 | r = await conv.get_response() 214 | 215 | logger.debug(">> %s", m.raw_text) 216 | logger.debug("<< %s", r.raw_text) 217 | 218 | token = r.raw_text.splitlines()[1] 219 | 220 | # Save token to database, now this bot is ready-to-use 221 | self._db.set("hikka.inline", "bot_token", token) 222 | self._token = token 223 | 224 | await fw_protect() 225 | 226 | await m.delete() 227 | await r.delete() 228 | 229 | # Enable inline mode or change its 230 | # placeholder in case it is not set 231 | 232 | for msg in [ 233 | "/setinline", 234 | button.text, 235 | "👾 Inline-Commands...", 236 | "/setinlinefeedback", 237 | button.text, 238 | "Enabled", 239 | "/setuserpic", 240 | button.text, 241 | ]: 242 | await fw_protect() 243 | m = await conv.send_message(msg) 244 | r = await conv.get_response() 245 | 246 | await fw_protect() 247 | 248 | logger.debug(">> %s", m.raw_text) 249 | logger.debug("<< %s", r.raw_text) 250 | 251 | await m.delete() 252 | await r.delete() 253 | 254 | try: 255 | await fw_protect() 256 | m = await conv.send_file(photo) 257 | r = await conv.get_response() 258 | 259 | logger.debug(">> ") 260 | logger.debug("<< %s", r.raw_text) 261 | except Exception: 262 | # In case user was not able to send photo to 263 | # BotFather, it is not a critical issue, so 264 | # just ignore it 265 | await fw_protect() 266 | m = await conv.send_message("/cancel") 267 | r = await conv.get_response() 268 | 269 | logger.debug(">> %s", m.raw_text) 270 | logger.debug("<< %s", r.raw_text) 271 | 272 | await fw_protect() 273 | await m.delete() 274 | await r.delete() 275 | 276 | # Return `True` to say, that everything is okay 277 | return True 278 | 279 | # And we are not returned after creation 280 | return await self._create_bot() if create_new_if_needed else False 281 | 282 | async def _reassert_token(self): 283 | is_token_asserted = await self._assert_token(revoke_token=True) 284 | if not is_token_asserted: 285 | self.init_complete = False 286 | else: 287 | await self.register_manager(ignore_token_checks=True) 288 | 289 | async def _dp_revoke_token(self, already_initialised: bool = True): 290 | if already_initialised: 291 | await self._stop() 292 | logger.error("Got polling conflict. Attempting token revocation...") 293 | 294 | self._db.set("hikka.inline", "bot_token", None) 295 | self._token = None 296 | if already_initialised: 297 | asyncio.ensure_future(self._reassert_token()) 298 | else: 299 | return await self._reassert_token() 300 | -------------------------------------------------------------------------------- /hikka/inline/types.py: -------------------------------------------------------------------------------- 1 | # ©️ Dan Gazizullin, 2021-2023 2 | # This file is a part of Hikka Userbot 3 | # 🌐 https://github.com/hikariatama/Hikka 4 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 5 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 6 | # Netfoll Team modifided Hikka files for Netfoll 7 | # 🌐 https://github.com/MXRRI/Netfoll 8 | 9 | import logging 10 | 11 | from aiogram.types import CallbackQuery 12 | from aiogram.types import InlineQuery as AiogramInlineQuery 13 | from aiogram.types import InlineQueryResultArticle, InputTextMessageContent 14 | from aiogram.types import Message as AiogramMessage 15 | 16 | from .. import utils 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class InlineMessage: 22 | """Aiogram message, sent via inline bot""" 23 | 24 | def __init__( 25 | self, 26 | inline_manager: "InlineManager", # type: ignore 27 | unit_id: str, 28 | inline_message_id: str, 29 | ): 30 | self.inline_message_id = inline_message_id 31 | self.unit_id = unit_id 32 | self.inline_manager = inline_manager 33 | self._units = inline_manager._units 34 | self.form = ( 35 | {"id": unit_id, **self._units[unit_id]} if unit_id in self._units else {} 36 | ) 37 | 38 | async def edit(self, *args, **kwargs) -> "InlineMessage": 39 | if "unit_id" in kwargs: 40 | kwargs.pop("unit_id") 41 | 42 | if "inline_message_id" in kwargs: 43 | kwargs.pop("inline_message_id") 44 | 45 | return await self.inline_manager._edit_unit( 46 | *args, 47 | unit_id=self.unit_id, 48 | inline_message_id=self.inline_message_id, 49 | **kwargs, 50 | ) 51 | 52 | async def delete(self) -> bool: 53 | return await self.inline_manager._delete_unit_message( 54 | self, 55 | unit_id=self.unit_id, 56 | ) 57 | 58 | async def unload(self) -> bool: 59 | return await self.inline_manager._unload_unit(unit_id=self.unit_id) 60 | 61 | 62 | class BotInlineMessage: 63 | """Aiogram message, sent through inline bot itself""" 64 | 65 | def __init__( 66 | self, 67 | inline_manager: "InlineManager", # type: ignore 68 | unit_id: str, 69 | chat_id: int, 70 | message_id: int, 71 | ): 72 | self.chat_id = chat_id 73 | self.unit_id = unit_id 74 | self.inline_manager = inline_manager 75 | self.message_id = message_id 76 | self._units = inline_manager._units 77 | self.form = ( 78 | {"id": unit_id, **self._units[unit_id]} if unit_id in self._units else {} 79 | ) 80 | 81 | async def edit(self, *args, **kwargs) -> "BotMessage": 82 | if "unit_id" in kwargs: 83 | kwargs.pop("unit_id") 84 | 85 | if "message_id" in kwargs: 86 | kwargs.pop("message_id") 87 | 88 | if "chat_id" in kwargs: 89 | kwargs.pop("chat_id") 90 | 91 | return await self.inline_manager._edit_unit( 92 | *args, 93 | unit_id=self.unit_id, 94 | chat_id=self.chat_id, 95 | message_id=self.message_id, 96 | **kwargs, 97 | ) 98 | 99 | async def delete(self) -> bool: 100 | return await self.inline_manager._delete_unit_message( 101 | self, 102 | unit_id=self.unit_id, 103 | chat_id=self.chat_id, 104 | message_id=self.message_id, 105 | ) 106 | 107 | async def unload(self, *args, **kwargs) -> bool: 108 | if "unit_id" in kwargs: 109 | kwargs.pop("unit_id") 110 | 111 | return await self.inline_manager._unload_unit( 112 | *args, 113 | unit_id=self.unit_id, 114 | **kwargs, 115 | ) 116 | 117 | 118 | class InlineCall(CallbackQuery, InlineMessage): 119 | """Modified version of classic aiogram `CallbackQuery`""" 120 | 121 | def __init__( 122 | self, 123 | call: CallbackQuery, 124 | inline_manager: "InlineManager", # type: ignore 125 | unit_id: str, 126 | ): 127 | CallbackQuery.__init__(self) 128 | 129 | for attr in { 130 | "id", 131 | "from_user", 132 | "message", 133 | "inline_message_id", 134 | "chat_instance", 135 | "data", 136 | "game_short_name", 137 | }: 138 | setattr(self, attr, getattr(call, attr, None)) 139 | 140 | self.original_call = call 141 | 142 | InlineMessage.__init__( 143 | self, 144 | inline_manager, 145 | unit_id, 146 | call.inline_message_id, 147 | ) 148 | 149 | 150 | class BotInlineCall(CallbackQuery, BotInlineMessage): 151 | """Modified version of classic aiogram `CallbackQuery`""" 152 | 153 | def __init__( 154 | self, 155 | call: CallbackQuery, 156 | inline_manager: "InlineManager", # type: ignore 157 | unit_id: str, 158 | ): 159 | CallbackQuery.__init__(self) 160 | 161 | for attr in { 162 | "id", 163 | "from_user", 164 | "message", 165 | "chat", 166 | "chat_instance", 167 | "data", 168 | "game_short_name", 169 | }: 170 | setattr(self, attr, getattr(call, attr, None)) 171 | 172 | self.original_call = call 173 | 174 | BotInlineMessage.__init__( 175 | self, 176 | inline_manager, 177 | unit_id, 178 | call.message.chat.id, 179 | call.message.message_id, 180 | ) 181 | 182 | 183 | class InlineUnit: 184 | """InlineManager extension type. For internal use only""" 185 | 186 | def __init__(self): 187 | """Made just for type specification""" 188 | 189 | 190 | class BotMessage(AiogramMessage): 191 | """Modified version of original Aiogram Message""" 192 | 193 | def __init__(self): 194 | super().__init__() 195 | 196 | 197 | class InlineQuery(AiogramInlineQuery): 198 | """Modified version of original Aiogram InlineQuery""" 199 | 200 | def __init__(self, inline_query: AiogramInlineQuery): 201 | super().__init__(self) 202 | 203 | for attr in {"id", "from_user", "query", "offset", "chat_type", "location"}: 204 | setattr(self, attr, getattr(inline_query, attr, None)) 205 | 206 | self.inline_query = inline_query 207 | self.args = ( 208 | self.inline_query.query.split(maxsplit=1)[1] 209 | if len(self.inline_query.query.split()) > 1 210 | else "" 211 | ) 212 | 213 | @staticmethod 214 | def _get_res(title: str, description: str, thumb_url: str) -> list: 215 | return [ 216 | InlineQueryResultArticle( 217 | id=utils.rand(20), 218 | title=title, 219 | description=description, 220 | input_message_content=InputTextMessageContent( 221 | "😶‍🌫️ There is nothing here...", 222 | parse_mode="HTML", 223 | ), 224 | thumb_url=thumb_url, 225 | thumb_width=128, 226 | thumb_height=128, 227 | ) 228 | ] 229 | 230 | async def e400(self): 231 | await self.answer( 232 | self._get_res( 233 | "🚫 400", 234 | "Bad request. You need to pass right arguments, follow module's" 235 | " documentation", 236 | "https://img.icons8.com/color/344/swearing-male--v1.png", 237 | ), 238 | cache_time=0, 239 | ) 240 | 241 | async def e403(self): 242 | await self.answer( 243 | self._get_res( 244 | "🚫 403", 245 | "You have no permissions to access this result", 246 | "https://img.icons8.com/external-wanicon-flat-wanicon/344/external-forbidden-new-normal-wanicon-flat-wanicon.png", 247 | ), 248 | cache_time=0, 249 | ) 250 | 251 | async def e404(self): 252 | await self.answer( 253 | self._get_res( 254 | "🚫 404", 255 | "No results found", 256 | "https://img.icons8.com/external-justicon-flat-justicon/344/external-404-error-responsive-web-design-justicon-flat-justicon.png", 257 | ), 258 | cache_time=0, 259 | ) 260 | 261 | async def e426(self): 262 | await self.answer( 263 | self._get_res( 264 | "🚫 426", 265 | "You need to update Netfoll before sending this request", 266 | "https://img.icons8.com/fluency/344/approve-and-update.png", 267 | ), 268 | cache_time=0, 269 | ) 270 | 271 | async def e500(self): 272 | await self.answer( 273 | self._get_res( 274 | "🚫 500", 275 | "Internal userbot error while processing request. More info in logs", 276 | "https://img.icons8.com/external-vitaliy-gorbachev-flat-vitaly-gorbachev/344/external-error-internet-security-vitaliy-gorbachev-flat-vitaly-gorbachev.png", 277 | ), 278 | cache_time=0, 279 | ) 280 | -------------------------------------------------------------------------------- /hikka/modules/ModuleCloud.py: -------------------------------------------------------------------------------- 1 | # 2 | # 🔒 The MIT License (MIT) 3 | # 🌐 https://www.gnu.org/licenses/agpl-3.0.html 4 | # 5 | # --------------------------------------------------------------------------------- 6 | # ▀▄ ▄▀ 👾 Module for Netfoll User Bot (based on Hikka 1.6.0) 7 | # ▄█▀███▀█▄ 🔒 The MIT License (MIT) 8 | # █▀███████▀█ ⚠️ Owner @DarkModules and @Netfoll 9 | # █ █▀▀▀▀▀█ █ 10 | # ▀▀ ▀▀ 11 | # --------------------------------------------------------------------------------- 12 | # meta developer: @Netfoll 13 | 14 | import difflib 15 | import inspect 16 | import io 17 | 18 | from telethon.tl.types import Message 19 | 20 | from .. import loader, utils 21 | 22 | 23 | @loader.tds 24 | class ModuleCloudMod(loader.Module): 25 | """Hikari modules management""" 26 | 27 | strings = { 28 | "name": "ModuleCloud", 29 | "args": "🚫 Args not specified", 30 | "404": "😔 Module not found", 31 | "not_exact": ( 32 | "⚠️ No exact match occured, so the closest result is shown instead" 33 | ), 34 | } 35 | 36 | strings_ru = { 37 | "args": "🚫 Нет аргументов", 38 | "_cls_doc": "Поиск модулей", 39 | "not_exact": ( 40 | "⚠️ Точного совпадения не нашлось, поэтому был выбран наиболее" 41 | " подходящее" 42 | ), 43 | "404": "😔 Модуль не найден", 44 | } 45 | 46 | @loader.command( 47 | ru_doc="<имя модуля> - Отправить ссылку на модуль", 48 | ) 49 | async def ml(self, message: Message): 50 | """ - Send link to module""" 51 | args = utils.get_args_raw(message) 52 | exact = True 53 | if not args: 54 | await utils.answer(message, self.strings("args")) 55 | return 56 | 57 | try: 58 | try: 59 | class_name = next( 60 | module.strings["name"] 61 | for module in self.allmodules.modules 62 | if args.lower() == module.strings["name"].lower() 63 | ) 64 | except Exception: 65 | try: 66 | class_name = next( 67 | reversed( 68 | sorted( 69 | [ 70 | module.strings["name"] 71 | for module in self.allmodules.modules 72 | ], 73 | key=lambda x: difflib.SequenceMatcher( 74 | None, 75 | args.lower(), 76 | x, 77 | ).ratio(), 78 | ) 79 | ) 80 | ) 81 | exact = False 82 | except Exception: 83 | await utils.answer(message, self.strings("404")) 84 | return 85 | 86 | module = next( 87 | filter( 88 | lambda mod: class_name.lower() == mod.strings["name"].lower(), 89 | self.allmodules.modules, 90 | ) 91 | ) 92 | 93 | sys_module = inspect.getmodule(module) 94 | 95 | link = module.__origin__ 96 | 97 | text = ( 98 | f"🧳 {utils.escape_html(class_name)}" 99 | if not utils.check_url(link) 100 | else ( 101 | f'📼 Link for' 102 | f" {utils.escape_html(class_name)}:" 103 | f' {link}\n\n{self.strings("not_exact") if not exact else ""}' 104 | ) 105 | ) 106 | 107 | file = io.BytesIO(sys_module.__loader__.data) 108 | file.name = f"{class_name}.py" 109 | file.seek(0) 110 | 111 | await message.respond(text, file=file) 112 | 113 | if message.out: 114 | await message.delete() 115 | except Exception: 116 | await utils.answer(message, self.strings("404")) 117 | -------------------------------------------------------------------------------- /hikka/modules/help.py: -------------------------------------------------------------------------------- 1 | # ©️ Dan Gazizullin, 2021-2023 2 | # This file is a part of Hikka Userbot 3 | # 🌐 https://github.com/hikariatama/Hikka 4 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 5 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 6 | # Netfoll Team modifided Hikka files for Netfoll 7 | # 🌐 https://github.com/MXRRI/Netfoll 8 | 9 | import difflib 10 | import inspect 11 | import logging 12 | 13 | from telethon.extensions.html import CUSTOM_EMOJIS 14 | from telethon.tl.types import Message 15 | 16 | from .. import loader, utils 17 | from ..compat.dragon import DRAGON_EMOJI 18 | from ..types import DragonModule 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | @loader.tds 24 | class HelpMod(loader.Module): 25 | """Shows help for modules and commands""" 26 | 27 | strings = { 28 | "name": "Help", 29 | "undoc": "🦥 No docs", 30 | "support": ( 31 | "{}\n\n Link to support chat" 32 | ), 33 | "not_exact": ( 34 | "☝️ No exact match" 35 | " occured, so the closest result is shown instead" 36 | ), 37 | "request_join": "You requested link for Netfoll support chat", 38 | "core_notice": ( 39 | "☝️ This is a core" 40 | " module. You can't unload it nor replace" 41 | ), 42 | "info": "⚡️ You didn't specify a module to search for\n\nThe installed modules can be viewed in {}mods", 43 | } 44 | 45 | strings_ru = { 46 | "undoc": "🦥 Нет описания", 47 | "support": ( 48 | "{}\n\n Ссылка на чат помощи" 49 | ), 50 | "_cls_doc": "Показывает помощь по модулям", 51 | "not_exact": ( 52 | "☝️ Точного совпадения" 53 | " не нашлось, поэтому было выбрано наиболее подходящее" 54 | ), 55 | "request_join": "Вы запросили ссылку на чат помощи Netfoll", 56 | "core_notice": ( 57 | "ℹ️ Это встроенный" 58 | " модуль. Вы не можете его выгрузить или заменить" 59 | ), 60 | "info": "⚡️ Вы не указали модуль для поиска\n\nУстановленные модули можно посмотреть в {}mods", 61 | } 62 | 63 | strings_uk = { 64 | "undoc": "🦥 Немає опису", 65 | "support": ( 66 | "{}\n\n Посилання на чат допомоги" 67 | ), 68 | "_cls_doc": "Показує допомогу по модулях", 69 | "not_exact": ( 70 | "☝️ Точного збігу" 71 | " не знайшлося, тому було вибрано найбільш підходяще" 72 | ), 73 | "request_join": "Ви запросили посилання на чат допомоги Netfoll", 74 | "core_notice": ( 75 | "ℹ️ Це вбудований" 76 | " модуль. Ви не можете його вивантажити або замінити" 77 | ), 78 | "info": "⚡️ Ви не вказали модуль для пошуку\n\nВстановлені модулі можна подивитися в {}mods", 79 | } 80 | 81 | def find_aliases(self, command: str) -> list: 82 | """Find aliases for command""" 83 | aliases = [] 84 | _command = self.allmodules.commands[command] 85 | if getattr(_command, "alias", None) and not ( 86 | aliases := getattr(_command, "aliases", None) 87 | ): 88 | aliases = [_command.alias] 89 | 90 | return aliases or [] 91 | 92 | async def modhelp(self, message: Message, args: str): 93 | exact = True 94 | module = self.lookup(args, include_dragon=True) 95 | 96 | if not module: 97 | cmd = args.lower().strip(self.get_prefix()) 98 | if method := self.allmodules.dispatch(cmd)[1]: 99 | module = method.__self__ 100 | 101 | if not module: 102 | module = self.lookup( 103 | next( 104 | ( 105 | reversed( 106 | sorted( 107 | [ 108 | module.strings["name"] 109 | for module in self.allmodules.modules 110 | ], 111 | key=lambda x: difflib.SequenceMatcher( 112 | None, 113 | args.lower(), 114 | x, 115 | ).ratio(), 116 | ) 117 | ) 118 | ), 119 | None, 120 | ) 121 | ) 122 | 123 | exact = False 124 | 125 | is_dragon = isinstance(module, DragonModule) 126 | 127 | try: 128 | name = module.strings("name") 129 | except (KeyError, AttributeError): 130 | name = getattr(module, "name", "ERROR") 131 | 132 | _name = ( 133 | "{} (v{}.{}.{})".format( 134 | utils.escape_html(name), 135 | module.__version__[0], 136 | module.__version__[1], 137 | module.__version__[2], 138 | ) 139 | if hasattr(module, "__version__") 140 | else utils.escape_html(name) 141 | ) 142 | 143 | reply = "{} {}:".format( 144 | ( 145 | DRAGON_EMOJI 146 | if is_dragon 147 | else "👾" 148 | ), 149 | _name, 150 | ) 151 | if module.__doc__: 152 | reply += ( 153 | "\nℹ️ " 154 | + utils.escape_html(inspect.getdoc(module)) 155 | + "\n" 156 | ) 157 | 158 | commands = ( 159 | module.commands 160 | if is_dragon 161 | else { 162 | name: func 163 | for name, func in module.commands.items() 164 | if await self.allmodules.check_security(message, func) 165 | } 166 | ) 167 | 168 | if hasattr(module, "inline_handlers") and not is_dragon: 169 | for name, fun in module.inline_handlers.items(): 170 | reply += ( 171 | "\n🤖" 172 | " {} {}".format( 173 | f"@{self.inline.bot_username} {name}", 174 | ( 175 | utils.escape_html(inspect.getdoc(fun)) 176 | if fun.__doc__ 177 | else self.strings("undoc") 178 | ), 179 | ) 180 | ) 181 | 182 | for name, fun in commands.items(): 183 | reply += ( 184 | "\n▫️" 185 | " {}{}{} {}".format( 186 | self.get_prefix("dragon" if is_dragon else None), 187 | name, 188 | " ({})".format( 189 | ", ".join( 190 | "{}{}".format( 191 | self.get_prefix("dragon" if is_dragon else None), alias 192 | ) 193 | for alias in self.find_aliases(name) 194 | ) 195 | ) 196 | if self.find_aliases(name) 197 | else "", 198 | utils.escape_html(fun) 199 | if is_dragon 200 | else ( 201 | utils.escape_html(inspect.getdoc(fun)) 202 | if fun.__doc__ 203 | else self.strings("undoc") 204 | ), 205 | ) 206 | ) 207 | 208 | await utils.answer( 209 | message, 210 | reply 211 | + (f"\n\n{self.strings('not_exact')}" if not exact else "") 212 | + ( 213 | f"\n\n{self.strings('core_notice')}" 214 | if module.__origin__.startswith("🚫 Specified bot" 29 | " username is invalid. It must end with bot and contain" 30 | " at least 4 symbols" 31 | ), 32 | "bot_username_occupied": ( 33 | "🚫 This username is" 34 | " already occupied" 35 | ), 36 | "bot_updated": ( 37 | "🎉 Config successfully" 38 | " saved. Restart userbot to apply changes" 39 | ), 40 | "this_is_hikka": ( 41 | "👾 Hi! This is Netfoll, UserBot that is based on the best UserBot Hikka. You can" 42 | " install it to your account!\n\n🌍 GitHub\n👾 Чат поддержки' 45 | ), 46 | } 47 | 48 | strings_ru = { 49 | "bot_username_invalid": ( 50 | "🚫 Неправильный ник" 51 | " бота. Он должен заканчиваться на bot и быть не короче" 52 | " чем 5 символов" 53 | ), 54 | "bot_username_occupied": ( 55 | "🚫 Такой ник бота уже" 56 | " занят" 57 | ), 58 | "bot_updated": ( 59 | "🎉 Настройки сохранены." 60 | " Для их применения нужно перезагрузить Netfoll" 61 | ), 62 | "this_is_hikka": ( 63 | "👾 Привет! Это Netfoll, ЮзерБот основанный на Hikka. Вы можете" 64 | " установить на свой аккаунт!\n\n💎 GitHub\n👾 Чат поддержки' 67 | ), 68 | } 69 | 70 | strings_uk = { 71 | "bot_username_invalid": ( 72 | "🚫 Неправильний нік" 73 | " бот. Він повинен закінчуватися на bot і бути не коротше" 74 | " ніж 5 символів" 75 | ), 76 | "bot_username_occupied": ( 77 | "🚫 Такий нік бота вже" 78 | " зайнятий" 79 | ), 80 | "bot_updated": ( 81 | "🎉 Налаштування збережені." 82 | " Для їх застосування потрібно перезавантажити Netfoll" 83 | ), 84 | "this_is_hikka": ( 85 | "👾 Привіт! Це Netfoll, заснований на Hikka. Ви можете" 86 | " встановити на свій аккаунт!\n\n💎 GitHub\n👾 Чат підтримки' 89 | ), 90 | } 91 | 92 | async def watcher(self, message: Message): 93 | if ( 94 | getattr(message, "out", False) 95 | and getattr(message, "via_bot_id", False) 96 | and message.via_bot_id == self.inline.bot_id 97 | and "This message will be deleted automatically" 98 | in getattr(message, "raw_text", "") 99 | ): 100 | await message.delete() 101 | return 102 | 103 | if ( 104 | not getattr(message, "out", False) 105 | or not getattr(message, "via_bot_id", False) 106 | or message.via_bot_id != self.inline.bot_id 107 | or "Opening gallery..." not in getattr(message, "raw_text", "") 108 | ): 109 | return 110 | 111 | id_ = re.search(r"#id: ([a-zA-Z0-9]+)", message.raw_text)[1] 112 | 113 | await message.delete() 114 | 115 | m = await message.respond("👾", reply_to=utils.get_topic(message)) 116 | 117 | await self.inline.gallery( 118 | message=m, 119 | next_handler=self.inline._custom_map[id_]["handler"], 120 | caption=self.inline._custom_map[id_].get("caption", ""), 121 | force_me=self.inline._custom_map[id_].get("force_me", False), 122 | disable_security=self.inline._custom_map[id_].get( 123 | "disable_security", False 124 | ), 125 | silent=True, 126 | ) 127 | 128 | async def _check_bot(self, username: str) -> bool: 129 | async with self._client.conversation("@BotFather", exclusive=False) as conv: 130 | try: 131 | m = await conv.send_message("/token") 132 | except YouBlockedUserError: 133 | await self._client(UnblockRequest(id="@BotFather")) 134 | m = await conv.send_message("/token") 135 | 136 | r = await conv.get_response() 137 | 138 | await m.delete() 139 | await r.delete() 140 | 141 | if not hasattr(r, "reply_markup") or not hasattr(r.reply_markup, "rows"): 142 | return False 143 | 144 | for row in r.reply_markup.rows: 145 | for button in row.buttons: 146 | if username != button.text.strip("@"): 147 | continue 148 | 149 | m = await conv.send_message("/cancel") 150 | r = await conv.get_response() 151 | 152 | await m.delete() 153 | await r.delete() 154 | 155 | return True 156 | 157 | @loader.command( 158 | ru_doc="<юзернейм> - Изменить юзернейм инлайн бота", 159 | it_doc=" - Cambia il nome utente del bot inline", 160 | de_doc=" - Ändere den Inline-Bot-Nutzernamen", 161 | tr_doc=" - İçe aktarma botunun kullanıcı adını değiştirin", 162 | uz_doc=" - Bot foydalanuvchi nomini o'zgartiring", 163 | es_doc=" - Cambia el nombre de usuario del bot de inline", 164 | kk_doc="<пайдаланушы аты> - Инлайн боттың пайдаланушы атын өзгерту", 165 | ) 166 | async def ch_netfoll_bot(self, message: Message): 167 | """ - Change your Netfoll inline bot username""" 168 | args = utils.get_args_raw(message).strip("@") 169 | if ( 170 | not args 171 | or not args.lower().endswith("bot") 172 | or len(args) <= 4 173 | or any( 174 | litera not in (string.ascii_letters + string.digits + "_") 175 | for litera in args 176 | ) 177 | ): 178 | await utils.answer(message, self.strings("bot_username_invalid")) 179 | return 180 | 181 | try: 182 | await self._client.get_entity(f"@{args}") 183 | except ValueError: 184 | pass 185 | else: 186 | if not await self._check_bot(args): 187 | await utils.answer(message, self.strings("bot_username_occupied")) 188 | return 189 | 190 | self._db.set("hikka.inline", "custom_bot", args) 191 | self._db.set("hikka.inline", "bot_token", None) 192 | await utils.answer(message, self.strings("bot_updated")) 193 | 194 | async def aiogram_watcher(self, message: BotInlineMessage): 195 | if message.text != "/start": 196 | return 197 | 198 | await message.answer_photo( 199 | "https://github.com/MXRRI/Netfoll/raw/Dev/assets/banner.png", 200 | caption=self.strings("this_is_hikka"), 201 | ) 202 | 203 | async def client_ready(self): 204 | if self.get("migrated"): 205 | return 206 | 207 | self.set("migrated", True) 208 | async with self._client.conversation("@BotFather") as conv: 209 | for msg in [ 210 | "/cancel", 211 | "/setinline", 212 | f"@{self.inline.bot_username}", 213 | "👾 Netfoll Inline", 214 | ]: 215 | m = await conv.send_message(msg) 216 | r = await conv.get_response() 217 | 218 | await m.delete() 219 | await r.delete() 220 | -------------------------------------------------------------------------------- /hikka/modules/mods.py: -------------------------------------------------------------------------------- 1 | # 2 | # 🔒 The MIT License (MIT) 3 | # 🌐 https://www.gnu.org/licenses/agpl-3.0.html 4 | # 5 | # --------------------------------------------------------------------------------- 6 | # ▀▄ ▄▀ 👾 Module for Netfoll User Bot (based on Hikka 1.6.0) 7 | # ▄█▀███▀█▄ 🔒 The MIT License (MIT) 8 | # █▀███████▀█ ⚠️ Owner @DarkModules and @Netfoll 9 | # █ █▀▀▀▀▀█ █ 10 | # ▀▀ ▀▀ 11 | # --------------------------------------------------------------------------------- 12 | # meta developer: @Netfoll 13 | 14 | from .. import loader, utils 15 | import logging 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | @loader.tds 22 | class ModsMod(loader.Module): 23 | """List of all of the modules currently installed""" 24 | 25 | strings = { 26 | "name": "Mods", 27 | "amount": "📦 Right now there is {} modules loaded:\n", 28 | "partial_load": ( 29 | "\n⚙️ it's not all modules" 30 | " Netfoll is loading" 31 | ), 32 | "cmd": " 💫 To find out the module commands, use {}help\n", 33 | "module": "", 34 | "core_module": "💫", 35 | } 36 | 37 | strings_ru = { 38 | "amount": "📦 Сейчас загружено {} модулей:", 39 | "partial_load": ( 40 | "\n⚙️ Это не все модули," 41 | " Netfoll загружается" 42 | ), 43 | "cmd": "💫 Чтобы узнать команды модуля используй {}help\n", 44 | } 45 | 46 | strings_uk = { 47 | "amount": "📦 Зараз завантажено {} модулей:", 48 | "partial_load": ( 49 | "\n⚙️ Це не всі модулі," 50 | " Netfoll завантажувати" 51 | ), 52 | "cmd": "💫 Щоб дізнатися команди модуля використовуй {}help\n", 53 | } 54 | 55 | @loader.command( 56 | ru_doc="Показать все установленные модули", 57 | ua_doc="Показати всі встановлені модулі", 58 | ) 59 | async def modscmd(self, message): 60 | """- List of all of the modules currently installed""" 61 | 62 | prefix = f"{self.strings('cmd').format(str(self.get_prefix()))}\n" 63 | result = f"{self.strings('amount').format(str(len(self.allmodules.modules)))}\n" 64 | 65 | for mod in self.allmodules.modules: 66 | try: 67 | name = mod.strings["name"] 68 | except KeyError: 69 | name = mod.__clas__.__name__ 70 | emoji = ( 71 | self.strings("core_module") 72 | if mod.__origin__.startswith("{name}" 76 | 77 | result += ( 78 | "" 79 | if self.lookup("Loader").fully_loaded 80 | else f"\n\n{self.strings('partial_load')}" 81 | ) 82 | result += f"\n\n {prefix}" 83 | 84 | await utils.answer(message, result) 85 | -------------------------------------------------------------------------------- /hikka/modules/netfoll_backup.py: -------------------------------------------------------------------------------- 1 | # ©️ Dan Gazizullin, 2021-2023 2 | # This file is a part of Hikka Userbot 3 | # 🌐 https://github.com/hikariatama/Hikka 4 | # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 5 | # 🔑 https://www.gnu.org/licenses/agpl-3.0.html 6 | # Netfoll Team modifided Hikka files for Netfoll 7 | # 🌐 https://github.com/MXRRI/Netfoll 8 | 9 | import asyncio 10 | import datetime 11 | import io 12 | import json 13 | import logging 14 | import time 15 | 16 | from telethon.tl.types import Message 17 | 18 | from .. import loader, utils 19 | from ..inline.types import BotInlineCall 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | @loader.tds 25 | class NetfollBackupMod(loader.Module): 26 | """Automatic database backup""" 27 | 28 | strings = { 29 | "name": "NetfollBackup", 30 | "period": ( 31 | "⌚️ Unit «ALPHA» creates database backups periodically. You can" 32 | " change this behavior later.\n\nPlease, select the periodicity of" 33 | " automatic database backups" 34 | ), 35 | "saved": ( 36 | "✅ Backup period saved. You can re-configure it later with" " .autobackup" 37 | ), 38 | "never": ( 39 | "✅ I will not make automatic backups. You can re-configure it later with" 40 | " .autobackup" 41 | ), 42 | "invalid_args": ( 43 | "🚫 Specify correct backup period in hours, or `0` to disable" 44 | ), 45 | } 46 | 47 | strings_ru = { 48 | "period": ( 49 | "❗️Советую включить функцию АвтоБэкапа (Unit Alpha)" 50 | " Время от времени Юнит будет создавать бэкапы вашего конфига, чтобы легко вернуть все данные в случае сбоя \n" 51 | "В случае потери конфига разработчики никак не вернут ваши данные\n\n" 52 | "‼️ Не с кем не делитесь файлами конфига, даже с разработчиками Netfoll! Они содержат конфиденциальные данные\n\n" 53 | "Чтобы в ручную изменить время автобэкапа используйте .autobackup\n\n" 54 | "🔻 Выберите срок Автобэкапа" 55 | ), 56 | "saved": ("✅ Периодичность сохранена! Ее можно изменить с помощью .autobackup"), 57 | "never": ( 58 | "✅ Я не буду делать автоматические резервные копии. Можно отменить" 59 | " используя .autobackup" 60 | ), 61 | "invalid_args": ( 62 | "🚫 Укажи правильную периодичность в часах, или `0` для отключения" 63 | ), 64 | } 65 | 66 | strings_uk = { 67 | "period": ( 68 | "❗️Раджу включити функцію Автобекапа (Unit Alpha)" 69 | " Час від часу Юніт буде створювати бекапи вашого конфіга, щоб легко повернути всі дані в разі збою \n" 70 | "У разі втрати конфіга розробники ніяк не повернуть ваші дані\n\n" 71 | "‼️ Ні з ким не діліться файлами конфігура, навіть з розробниками Netfol! Вони містять конфіденційні дані\n\n" 72 | "Щоб вручну змінити час автобекапу використовуйте .autobackup\n\n" 73 | "🔻 Виберіть термін Автобекапу" 74 | ), 75 | "saved": ( 76 | "✅ Періодичність збережена! Її можна змінити за допомогою .autobackup" 77 | ), 78 | "never": ( 79 | "✅ Я не буду робити автоматичні резервні копії. Можна скасувати" 80 | " используя .autobackup" 81 | ), 82 | "invalid_args": ( 83 | "🚫 Вкажи правильну періодичність в годинах, або '0' для відключення" 84 | ), 85 | } 86 | 87 | async def client_ready(self): 88 | if not self.get("period"): 89 | await self.inline.bot.send_photo( 90 | self.tg_id, 91 | photo="https://github.com/MXRRI/Netfoll/raw/stable/assets/BackUp.png", 92 | caption=self.strings("period"), 93 | reply_markup=self.inline.generate_markup( 94 | utils.chunks( 95 | [ 96 | { 97 | "text": f"🕰 {i} h", 98 | "callback": self._set_backup_period, 99 | "args": (i,), 100 | } 101 | for i in [2, 12, 24] 102 | ], 103 | 3, 104 | ) 105 | + [ 106 | [ 107 | { 108 | "text": "🚫 Never", 109 | "callback": self._set_backup_period, 110 | "args": (0,), 111 | } 112 | ] 113 | ] 114 | ), 115 | ) 116 | 117 | self._backup_channel, _ = await utils.asset_channel( 118 | self._client, 119 | "netfoll-backups", 120 | "📼 Your database backups will appear here", 121 | silent=True, 122 | archive=True, 123 | avatar="https://github.com/hikariatama/assets/raw/master/hikka-backups.png", 124 | _folder="hikka", 125 | ) 126 | 127 | self.handler.start() 128 | 129 | async def _set_backup_period(self, call: BotInlineCall, value: int): 130 | if not value: 131 | self.set("period", "disabled") 132 | await call.answer(self.strings("never"), show_alert=True) 133 | await call.delete() 134 | return 135 | 136 | self.set("period", value * 60 * 60) 137 | self.set("last_backup", round(time.time())) 138 | 139 | await call.answer(self.strings("saved"), show_alert=True) 140 | await call.delete() 141 | 142 | @loader.command( 143 | ru_doc="<время в часах> - Установить частоту бэкапов", 144 | it_doc=" - Imposta la frequenza dei backup", 145 | de_doc=" - Setze die Backup-Frequenz", 146 | tr_doc=" - Yedekleme periyodunu ayarla", 147 | uz_doc=" - E'lon tartibini belgilash", 148 | es_doc=" - Establecer la frecuencia de copia de seguridad", 149 | kk_doc="<сағатты уақыт> - Резервтік көшірмелер қайдағы кезеңдерде жасалады", 150 | ) 151 | async def autobackup(self, message: Message): 152 | """