├── .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
2 |
` - Выполнить код
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="