├── .env-example
├── .github
└── images
│ └── demo.png
├── .gitignore
├── Dockerfile
├── INSTALL.bat
├── INSTALL.sh
├── README-RU.md
├── README.md
├── START.bat
├── START.sh
├── bot
├── __init__.py
├── config
│ ├── __init__.py
│ ├── config.py
│ └── proxies.txt
├── core
│ ├── __init__.py
│ ├── claimer.py
│ ├── headers.py
│ └── registrator.py
├── exceptions
│ └── __init__.py
└── utils
│ ├── __init__.py
│ ├── launcher.py
│ └── logger.py
├── docker-compose.yml
├── main.py
└── requirements.txt
/.env-example:
--------------------------------------------------------------------------------
1 | API_ID=
2 | API_HASH=
3 |
4 | SLEEP_BETWEEN_START=
5 | ERRORS_BEFORE_STOP=
6 | USE_PROXY_FROM_FILE=
7 | DEBUG_MODE=
--------------------------------------------------------------------------------
/.github/images/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alexell/MMProBumpBot/5f080ad80a49c6f1e110b6ed4bc6158a64c55891/.github/images/demo.png
--------------------------------------------------------------------------------
/.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 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
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 | # DB
65 | sessions/
66 |
67 | # Flask stuff:
68 | instance/
69 | .webassets-cache
70 |
71 | # Scrapy stuff:
72 | .scrapy
73 |
74 | # Sphinx documentation
75 | docs/_build/
76 |
77 | # PyBuilder
78 | .pybuilder/
79 | target/
80 |
81 | # Jupyter Notebook
82 | .ipynb_checkpoints
83 |
84 | # IPython
85 | profile_default/
86 | ipython_config.py
87 |
88 | # pyenv
89 | # For a library or package, you might want to ignore these files since the code is
90 | # intended to run in multiple environments; otherwise, check them in:
91 | # .python-version
92 |
93 | # pipenv
94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
97 | # install all needed dependencies.
98 | #Pipfile.lock
99 |
100 | # poetry
101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
102 | # This is especially recommended for binary packages to ensure reproducibility, and is more
103 | # commonly ignored for libraries.
104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
105 | #poetry.lock
106 |
107 | # pdm
108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
109 | #pdm.lock
110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
111 | # in version control.
112 | # https://pdm.fming.dev/#use-with-ide
113 | .pdm.toml
114 |
115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
116 | __pypackages__/
117 |
118 | # Celery stuff
119 | celerybeat-schedule
120 | celerybeat.pid
121 |
122 | # SageMath parsed files
123 | *.sage.py
124 |
125 | # Environments
126 | .env
127 | .venv
128 | env/
129 | venv/
130 | ENV/
131 | env.bak/
132 | venv.bak/
133 |
134 | # Spyder project settings
135 | .spyderproject
136 | .spyproject
137 |
138 | # Rope project settings
139 | .ropeproject
140 |
141 | # mkdocs documentation
142 | /site
143 |
144 | # mypy
145 | .mypy_cache/
146 | .dmypy.json
147 | dmypy.json
148 |
149 | # Pyre type checker
150 | .pyre/
151 |
152 | # pytype static type analyzer
153 | .pytype/
154 |
155 | # Cython debug symbols
156 | cython_debug/
157 |
158 | # PyCharm
159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
161 | # and can be added to the global gitignore or merged into this file. For a more nuclear
162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163 | .idea/
164 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10.11-alpine3.18
2 |
3 | WORKDIR /app
4 |
5 | COPY requirements.txt requirements.txt
6 |
7 | RUN pip3 install --upgrade pip setuptools wheel
8 | RUN pip3 install --no-warn-script-location --no-cache-dir -r requirements.txt
9 |
10 | COPY . .
11 |
12 | ENTRYPOINT ["python3", "main.py"]
13 | CMD ["-a", "2"]
--------------------------------------------------------------------------------
/INSTALL.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | echo Creating virtual environment...
3 | python -m venv venv
4 | echo Activating virtual environment...
5 | call venv\Scripts\activate
6 | echo Installing dependencies...
7 | pip install -r requirements.txt
8 | echo Copying .env-example to .env...
9 | copy .env-example .env
10 | echo Please edit the .env file to add your API_ID and API_HASH.
11 | pause
12 |
--------------------------------------------------------------------------------
/INSTALL.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | install_python() {
4 | echo "Select the Python version to install:"
5 | echo "1) Python 3.10"
6 | echo "2) Python 3.11"
7 | echo "3) Python 3.12"
8 | read -p "Enter the number of your choice: " choice
9 |
10 | case $choice in
11 | 1) version="3.10" ;;
12 | 2) version="3.11" ;;
13 | 3) version="3.12" ;;
14 | *) echo "Invalid choice"; exit 1 ;;
15 | esac
16 |
17 | if command -v apt-get &> /dev/null; then
18 | sudo apt-get update
19 | sudo apt-get install -y python$version python$version-venv python$version-pip
20 | elif command -v yum &> /dev/null; then
21 | sudo yum install -y https://repo.ius.io/ius-release-el$(rpm -E %{rhel}).rpm
22 | sudo yum install -y python$version python$version-venv python$version-pip
23 | elif command -v dnf &> /dev/null; then
24 | sudo dnf install -y python$version python$version-venv python$version-pip
25 | else
26 | echo "Package manager not supported. Please install Python manually."
27 | exit 1
28 | fi
29 | }
30 |
31 | if ! command -v python3 &> /dev/null; then
32 | install_python
33 | fi
34 |
35 | echo "Creating virtual environment..."
36 | python3 -m venv venv
37 |
38 | echo "Activating virtual environment..."
39 | source venv/bin/activate
40 |
41 | echo "Installing dependencies..."
42 | pip install -r requirements.txt
43 |
44 | echo "Copying .env-example to .env..."
45 | cp .env-example .env
46 |
47 | echo "Please edit the .env file to add your API_ID and API_HASH."
48 | read -p "Press any key to continue..."
--------------------------------------------------------------------------------
/README-RU.md:
--------------------------------------------------------------------------------
1 | # Бот для [MMPro Bump](https://alexell.pro/cc/mmpro)
2 |
3 | 
4 |
5 | > 🇺🇸 README in english available [here](README.md)
6 |
7 | ## Функционал
8 | | Функция | Поддерживается |
9 | |----------------------------------------------------------------|:---------------:|
10 | | Многопоточность | ✅ |
11 | | Привязка прокси к сессии | ✅ |
12 | | Получение ежедневной награды | ✅ |
13 | | Получение награды за друзей | ✅ |
14 | | Получение награды за задания | ✅ |
15 | | Автоматический фарминг | ✅ |
16 | | Автоматические тапы с учетом бустов | ✅ |
17 | | Docker | ✅ |
18 |
19 | ## [Настройки](https://github.com/Alexell/MMProBumpBot/blob/main/.env-example)
20 | | Опция | Описание |
21 | |-------------------------|----------------------------------------------------------------------------|
22 | | **API_ID / API_HASH** | Данные платформы, с которой запускать сессию Telegram (сток - Android) |
23 | | **SLEEP_BETWEEN_START** | Задержка перед запуском каждой сессии (напр. [20, 360]) |
24 | | **ERRORS_BEFORE_STOP** | Количество неудачных запросов, по достижению которых, бот остановится |
25 | | **USE_PROXY_FROM_FILE** | Использовать-ли прокси из файла `bot/config/proxies.txt` (True / False) |
26 |
27 | **API_ID** и **API_HASH** вы можете получить после создания приложения на [my.telegram.org/apps](https://my.telegram.org/apps)
28 |
29 | ## Быстрый старт
30 | ### Windows
31 | 1. Убедитесь, что у вас установлен **Python 3.10** или более новая версия.
32 | 2. Используйте `INSTALL.bat` для установки, затем укажите ваши API_ID и API_HASH в .env
33 | 3. Используйте `START.bat` для запуска бота (или в консоли: `python main.py`)
34 |
35 | ### Linux
36 | 1. Клонируйте репозиторий: `git clone https://github.com/Alexell/MMProBumpBot.git && cd MMProBumpBot`
37 | 2. Выполните установку: `chmod +x INSTALL.sh START.sh && ./INSTALL.sh`, затем укажите ваши API_ID и API_HASH в .env
38 | 3. Используйте `./START.sh` для запуска бота (или в консоли: `python3 main.py`)
39 |
40 | ## Запуск в Docker
41 | ```
42 | $ git clone https://github.com/Alexell/MMProBumpBot.git
43 | $ cd MMProBumpBot
44 | $ cp .env-example .env
45 | $ nano .env # укажите ваши API_ID и API_HASH, остальное можно оставить по умолчанию
46 | ```
47 | ### Docker Compose (рекомендуется)
48 | ```
49 | $ docker-compose run bot -a 1 # первый запуск для авторизации (переопределяем аргументы)
50 | $ docker-compose start # запуск в фоновом режиме (аргументы по умолчанию: -a 2)
51 | ```
52 | ### Docker
53 | ```
54 | $ docker build -t mmpro_bump_bot .
55 | $ docker run --name MMProBumpBot -v .:/app -it mmpro_bump_bot -a 1 # первый запуск для авторизации
56 | $ docker rm MMProBumpBot # удаляем контейнер для пересоздания с аргументами по умолчанию
57 | $ docker run -d --restart unless-stopped --name MMProBumpBot -v .:/app mmpro_bump_bot # запуск в фоновом режиме (аргументы по умолчанию: -a 2)
58 | ```
59 |
60 | ## Ручная установка
61 | Вы можете скачать [**Репозиторий**](https://github.com/Alexell/MMProBumpBot) клонированием на вашу систему и установкой необходимых зависимостей:
62 | ```
63 | $ git clone https://github.com/Alexell/MMProBumpBot.git
64 | $ cd MMProBumpBot
65 |
66 | # Linux
67 | $ python3 -m venv venv
68 | $ source venv/bin/activate
69 | $ pip3 install -r requirements.txt
70 | $ cp .env-example .env
71 | $ nano .env # укажите ваши API_ID и API_HASH, остальное можно оставить по умолчанию
72 | $ python3 main.py
73 |
74 | # Windows (сначала установите Python 3.10 или более новую версию)
75 | > python -m venv venv
76 | > venv\Scripts\activate
77 | > pip install -r requirements.txt
78 | > copy .env-example .env
79 | > # укажите ваши API_ID и API_HASH, остальное можно оставить по умолчанию
80 | > python main.py
81 | ```
82 |
83 | Также для быстрого запуска вы можете использовать аргументы:
84 | ```
85 | $ python3 main.py --action (1/2)
86 | # или
87 | $ python3 main.py -a (1/2)
88 |
89 | # 1 - создать сессию
90 | # 2 - запустить бот
91 | ```
92 |
93 | ## Запуск бота в фоновом режиме (Linux)
94 | ```
95 | $ cd MMProBumpBot
96 |
97 | # с логированием
98 | $ setsid venv/bin/python3 main.py --action 2 >> app.log 2>&1 &
99 |
100 | # без логирования
101 | $ setsid venv/bin/python3 main.py --action 2 > /dev/null 2>&1 &
102 |
103 | # Теперь вы можете закрыть консоль и бот продолжит свою работу.
104 | ```
105 |
106 | ### Найти процесс бота
107 | ```
108 | $ ps aux | grep "python3 main.py" | grep -v grep
109 | ```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bot for [MMPro Bump](https://alexell.pro/cc/mmpro)
2 |
3 | 
4 |
5 | > 🇷🇺 README на русском доступен [здесь](README-RU.md)
6 |
7 | ## Functionality
8 | | Feature | Supported |
9 | |----------------------------------------------------------------|:----------:|
10 | | Multithreading | ✅ |
11 | | Binding a proxy to a session | ✅ |
12 | | Claim daily grant | ✅ |
13 | | Claim reward for friends | ✅ |
14 | | Claim reward for tasks | ✅ |
15 | | Automatic farming | ✅ |
16 | | Automatic taps that account for enabled boosts | ✅ |
17 | | Docker | ✅ |
18 |
19 | ## [Options](https://github.com/Alexell/MMProBumpBot/blob/main/.env-example)
20 | | Option | Description |
21 | |-------------------------|----------------------------------------------------------------------------|
22 | | **API_ID / API_HASH** | Platform data from which to launch a Telegram session (stock - Android) |
23 | | **SLEEP_BETWEEN_START** | Sleep before start each session (e.g. [20, 360]) |
24 | | **ERRORS_BEFORE_STOP** | The number of failed requests after which the bot will stop |
25 | | **USE_PROXY_FROM_FILE** | Whether to use proxy from the `bot/config/proxies.txt` file (True / False) |
26 |
27 | You can obtain the **API_ID** and **API_HASH** after creating an application at [my.telegram.org/apps](https://my.telegram.org/apps)
28 |
29 | ## Quick start
30 | ### Windows
31 | 1. Ensure you have **Python 3.10** or a newer version installed.
32 | 2. Use `INSTALL.bat` to install, then specify your API_ID and API_HASH in the .env file.
33 | 3. Use `START.bat` to launch the bot (or in the console: `python main.py`).
34 |
35 | ### Linux
36 | 1. Clone the repository: `git clone https://github.com/Alexell/MMProBumpBot.git && cd MMProBumpBot`
37 | 2. Run the installation: `chmod +x INSTALL.sh START.sh && ./INSTALL.sh`, then specify your API_ID and API_HASH in the .env file.
38 | 3. Use `./START.sh` to run the bot (or in the console: `python3 main.py`).
39 |
40 | ## Running in Docker
41 | ```
42 | $ git clone https://github.com/Alexell/MMProBumpBot.git
43 | $ cd MMProBumpBot
44 | $ cp .env-example .env
45 | $ nano .env # specify your API_ID and API_HASH, the rest can be left as default
46 | ```
47 | ### Docker Compose (recommended)
48 | ```
49 | $ docker-compose run bot -a 1 # first run for authorization (override arguments)
50 | $ docker-compose start # start in background mode (default arguments: -a 2)
51 | ```
52 | ### Docker
53 | ```
54 | $ docker build -t mmpro_bump_bot .
55 | $ docker run --name MMProBumpBot -v .:/app -it mmpro_bump_bot -a 1 # first run for authorization
56 | $ docker rm MMProBumpBot # remove container to recreate with default arguments
57 | $ docker run -d --restart unless-stopped --name MMProBumpBot -v .:/app mmpro_bump_bot # start in background mode (default arguments: -a 2)
58 | ```
59 |
60 | ## Manual installation
61 | You can download [**Repository**](https://github.com/Alexell/MMProBumpBot) by cloning it to your system and installing the necessary dependencies:
62 | ```
63 | $ git clone https://github.com/Alexell/MMProBumpBot.git
64 | $ cd MMProBumpBot
65 |
66 | # Linux
67 | $ python3 -m venv venv
68 | $ source venv/bin/activate
69 | $ pip3 install -r requirements.txt
70 | $ cp .env-example .env
71 | $ nano .env # specify your API_ID and API_HASH, the rest can be left as default
72 | $ python3 main.py
73 |
74 | # Windows (first, install Python 3.10 or a newer version)
75 | > python -m venv venv
76 | > venv\Scripts\activate
77 | > pip install -r requirements.txt
78 | > copy .env-example .env
79 | > # specify your API_ID and API_HASH, the rest can be left as default
80 | > python main.py
81 | ```
82 |
83 | Also for quick launch you can use arguments:
84 | ```
85 | $ python3 main.py --action (1/2)
86 | # or
87 | $ python3 main.py -a (1/2)
88 |
89 | # 1 - Create session
90 | # 2 - Run bot
91 | ```
92 |
93 | ## Running a bot in the background (Linux)
94 | ```
95 | $ cd MMProBumpBot
96 |
97 | # with logging
98 | $ setsid venv/bin/python3 main.py --action 2 >> app.log 2>&1 &
99 |
100 | # without logging
101 | $ setsid venv/bin/python3 main.py --action 2 > /dev/null 2>&1 &
102 |
103 | # Now you can close the console, and the bot will continue its work.
104 | ```
105 |
106 | ### Find the bot process
107 | ```
108 | $ ps aux | grep "python3 main.py" | grep -v grep
109 | ```
--------------------------------------------------------------------------------
/START.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | echo Activating virtual environment...
3 | call venv\Scripts\activate
4 | echo Starting the bot...
5 | python main.py
6 | pause
7 |
--------------------------------------------------------------------------------
/START.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "Activating virtual environment..."
4 | source venv/bin/activate
5 |
6 | echo "Starting the bot..."
7 | python main.py
8 |
9 | echo "Press any key to continue..."
10 | read -n 1 -s
11 |
--------------------------------------------------------------------------------
/bot/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = '1.0'
2 |
--------------------------------------------------------------------------------
/bot/config/__init__.py:
--------------------------------------------------------------------------------
1 | from .config import settings
2 |
--------------------------------------------------------------------------------
/bot/config/config.py:
--------------------------------------------------------------------------------
1 | from pydantic_settings import BaseSettings, SettingsConfigDict
2 | from bot.utils import logger
3 |
4 |
5 | class Settings(BaseSettings):
6 | model_config = SettingsConfigDict(env_file=".env", env_ignore_empty=True)
7 |
8 | API_ID: int
9 | API_HASH: str
10 |
11 | SLEEP_BETWEEN_START: list[int] = [20, 360]
12 | ERRORS_BEFORE_STOP: int = 3
13 | USE_PROXY_FROM_FILE: bool = False
14 | DEBUG_MODE: bool = False
15 |
16 |
17 | try:
18 | settings = Settings()
19 | except Exception as error:
20 | logger.error(error)
21 | settings = False
22 |
--------------------------------------------------------------------------------
/bot/config/proxies.txt:
--------------------------------------------------------------------------------
1 | type://user:pass@ip:port
2 | type://user:pass:ip:port
3 | type://ip:port:user:pass
4 | type://ip:port@user:pass
5 | type://ip:port
--------------------------------------------------------------------------------
/bot/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alexell/MMProBumpBot/5f080ad80a49c6f1e110b6ed4bc6158a64c55891/bot/core/__init__.py
--------------------------------------------------------------------------------
/bot/core/claimer.py:
--------------------------------------------------------------------------------
1 | import asyncio, aiohttp, random, math, hashlib, hmac, json, traceback
2 | from time import time, strftime, localtime
3 | from urllib.parse import quote, unquote
4 | from typing import Any, Dict, List
5 | from aiohttp_proxy import ProxyConnector
6 | from better_proxy import Proxy
7 | from pyrogram import Client
8 | from pyrogram.errors import Unauthorized, UserDeactivated, AuthKeyUnregistered
9 | from pyrogram.raw.functions.messages import RequestWebView
10 |
11 | from bot.config import settings
12 | from bot.utils import logger
13 | from bot.exceptions import InvalidSession
14 | from .headers import headers
15 |
16 | class Claimer:
17 | def __init__(self, tg_client: Client):
18 | self.session_name = tg_client.name
19 | self.tg_client = tg_client
20 | self.user_id = None
21 | self.api_url = 'https://api.mmbump.pro/v1'
22 | self.errors = 0
23 |
24 | async def get_tg_web_data(self, proxy: str | None) -> str:
25 | if proxy:
26 | proxy = Proxy.from_str(proxy)
27 | proxy_dict = dict(
28 | scheme=proxy.protocol,
29 | hostname=proxy.host,
30 | port=proxy.port,
31 | username=proxy.login,
32 | password=proxy.password
33 | )
34 | else:
35 | proxy_dict = None
36 |
37 | self.tg_client.proxy = proxy_dict
38 |
39 | try:
40 | if not self.tg_client.is_connected:
41 | try:
42 | await self.tg_client.connect()
43 | if self.user_id is None:
44 | user = await self.tg_client.get_me()
45 | self.user_id = user.id
46 | self.http_client.headers["user_auth"] = str(self.user_id)
47 | headers["user_auth"] = str(self.user_id)
48 | except (Unauthorized, UserDeactivated, AuthKeyUnregistered):
49 | raise InvalidSession(self.session_name)
50 | web_view = await self.tg_client.invoke(RequestWebView(
51 | peer=await self.tg_client.resolve_peer('MMproBump_bot'),
52 | bot=await self.tg_client.resolve_peer('MMproBump_bot'),
53 | platform='android',
54 | from_bot_menu=False,
55 | url='https://api.mmbump.pro/'
56 | ))
57 | auth_url = web_view.url
58 | tg_web_data = unquote(
59 | string=auth_url.split('tgWebAppData=', maxsplit=1)[1].split('&tgWebAppVersion', maxsplit=1)[0])
60 | if self.tg_client.is_connected:
61 | await self.tg_client.disconnect()
62 |
63 | return tg_web_data
64 |
65 | except InvalidSession as error:
66 | raise error
67 |
68 | except Exception as error:
69 | logger.error(f"{self.session_name} | Unknown error during Authorization: {error}" + (f"\nTraceback: {traceback.format_exc()}" if settings.DEBUG_MODE else ""))
70 | await asyncio.sleep(delay=3)
71 |
72 | async def login(self, init_data: str) -> str:
73 | url = self.api_url + '/loginJwt'
74 | try:
75 | await self.http_client.options(url)
76 | json_data = {"initData": init_data}
77 | response = await self.http_client.post(url, json=json_data)
78 | response.raise_for_status()
79 | response_text = await response.text()
80 | if settings.DEBUG_MODE:
81 | print(f"Login response:\n{response_text}")
82 | if self.isValidJson(response_text):
83 | response_json = json.loads(response_text)
84 | token = response_json.get('access_token', '')
85 | return token
86 | return False
87 | except Exception as error:
88 | logger.error(f"{self.session_name} | Unknown error when log in: {error}" + (f"\nTraceback: {traceback.format_exc()}" if settings.DEBUG_MODE else ""))
89 | self.errors += 1
90 | await asyncio.sleep(delay=3)
91 | return False
92 |
93 | async def refresh_token(self) -> str | bool:
94 | url = self.api_url + '/auth/refresh'
95 | try:
96 | await self.http_client.options(url)
97 | response = await self.http_client.post(url)
98 | response.raise_for_status()
99 | response_text = await response.text()
100 | if settings.DEBUG_MODE:
101 | print(f"Refresh auth tokens response:\n{response_text}")
102 | if self.isValidJson(response_text):
103 | response_json = json.loads(response_text)
104 | self.access_token = response_json.get('access', '')
105 | return True if self.access_token != '' else False
106 | return False
107 | except Exception as error:
108 | logger.error(f"{self.session_name} | Unknown error when Refresh auth tokens: {error}" + (f"\nTraceback: {traceback.format_exc()}" if settings.DEBUG_MODE else ""))
109 | await asyncio.sleep(delay=3)
110 | return False
111 |
112 | async def get_profile(self) -> Dict[str, Any]:
113 | url = self.api_url + '/farming'
114 | try:
115 | await self.http_client.options(url)
116 | response = await self.http_client.post(url)
117 | response.raise_for_status()
118 | response_text = await response.text()
119 | if settings.DEBUG_MODE:
120 | print(f"Profile Data response:\n{response_text}")
121 | if self.isValidJson(response_text):
122 | response_json = json.loads(response_text)
123 | return response_json
124 | return False
125 | except Exception as error:
126 | logger.error(f"{self.session_name} | Unknown error when getting Profile Data: {error}" + (f"\nTraceback: {traceback.format_exc()}" if settings.DEBUG_MODE else ""))
127 | self.errors += 1
128 | await asyncio.sleep(delay=3)
129 | return {}
130 |
131 | async def daily_grant(self) -> bool:
132 | url = self.api_url + '/grant-day/claim'
133 | url_reset = self.api_url + '/grant-day/reset'
134 | try:
135 | json_data = {}
136 | data_list = []
137 | json_data['hash'] = await self.create_hash(data_list)
138 | await self.http_client.options(url)
139 | response = await self.http_client.post(url, json=json_data)
140 | #response.raise_for_status()
141 | if response.status == 400:
142 | await self.http_client.options(url_reset)
143 | await self.http_client.post(url_reset)
144 | await asyncio.sleep(delay=2)
145 | json_data['hash'] = await self.create_hash(data_list)
146 | response = await self.http_client.post(url)
147 | response_text = await response.text()
148 | if settings.DEBUG_MODE:
149 | print(f"Daily grant response:\n{response_text}")
150 | if self.isValidJson(response_text):
151 | response_json = json.loads(response_text)
152 | balance = response_json.get('balance', False)
153 | if balance is not False:
154 | self.balance = int(balance)
155 | return True
156 | return False
157 | except Exception as error:
158 | logger.error(f"{self.session_name} | Unknown error when getting daily grant: {error}" + (f"\nTraceback: {traceback.format_exc()}" if settings.DEBUG_MODE else ""))
159 | self.errors += 1
160 | await asyncio.sleep(delay=3)
161 | return False
162 |
163 | async def friends_claim(self) -> bool:
164 | url_friends = self.api_url + '/friends'
165 | url_claim = self.api_url + '/friends/claim'
166 | try:
167 | json_data = {'offset': 0, 'limit': 20}
168 | data_list = [json_data]
169 | json_data['hash'] = await self.create_hash(data_list)
170 | await self.http_client.options(url_friends)
171 | response = await self.http_client.post(url_friends, json=json_data)
172 | response.raise_for_status()
173 | response_text = await response.text()
174 | if settings.DEBUG_MODE:
175 | print(f"Friends response:\n{response_text}")
176 | if not self.isValidJson(response_text): return False
177 | response_json = json.loads(response_text)
178 | friend_claim = int(response_json.get('friend_claim', 0))
179 | if friend_claim > 0:
180 | await asyncio.sleep(delay=2)
181 | logger.info(f"{self.session_name} | Friends reward available")
182 | json_data = {}
183 | data_list = []
184 | json_data['hash'] = await self.create_hash(data_list)
185 | await self.http_client.options(url_claim)
186 | response = await self.http_client.post(url_claim, json=json_data)
187 | #response.raise_for_status()
188 | if response.status == 200: # Sometimes server errors occur
189 | response_text = await response.text()
190 | if settings.DEBUG_MODE:
191 | print(f"Friends claim response:\n{response_text}")
192 | if self.isValidJson(response_text):
193 | response_json = json.loads(response_text)
194 | balance = response_json.get('balance', False)
195 | if balance is not False:
196 | logger.success(f"{self.session_name} | Friends reward claimed")
197 | self.balance = int(balance)
198 | self.errors = 0
199 | return True
200 | return False
201 | else: return False
202 | except Exception as error:
203 | logger.error(f"{self.session_name} | Unknown error when claiming friends reward: {error}" + (f"\nTraceback: {traceback.format_exc()}" if settings.DEBUG_MODE else ""))
204 | self.errors += 1
205 | await asyncio.sleep(delay=3)
206 | return False
207 |
208 | async def send_claim(self, taps: int) -> bool:
209 | url = self.api_url + '/farming/finish'
210 | try:
211 | json_data = {"tapCount":taps}
212 | data_list = [json_data]
213 | json_data['hash'] = await self.create_hash(data_list)
214 | await self.http_client.options(url)
215 | response = await self.http_client.post(url, json=json_data)
216 | response.raise_for_status()
217 | response_text = await response.text()
218 | if settings.DEBUG_MODE:
219 | print(f"Claiming response:\n{response_text}")
220 | if self.isValidJson(response_text):
221 | response_json = json.loads(response_text)
222 | balance = response_json.get('balance', False)
223 | if balance is not False:
224 | self.balance = int(balance)
225 | return True
226 | return False
227 | except Exception as error:
228 | logger.error(f"{self.session_name} | Unknown error when Claiming: {error}" + (f"\nTraceback: {traceback.format_exc()}" if settings.DEBUG_MODE else ""))
229 | self.errors += 1
230 | await asyncio.sleep(delay=3)
231 | return False
232 |
233 | async def start_farming(self) -> bool:
234 | url = self.api_url + '/farming/start'
235 | await asyncio.sleep(delay=6)
236 | try:
237 | json_data = {"status":"inProgress"}
238 | data_list = [json_data]
239 | json_data['hash'] = await self.create_hash(data_list)
240 | await self.http_client.options(url)
241 | response = await self.http_client.post(url, json=json_data)
242 | response.raise_for_status()
243 | response_text = await response.text()
244 | if settings.DEBUG_MODE:
245 | print(f"Login response:\n{response_text}")
246 | if self.isValidJson(response_text):
247 | response_json = json.loads(response_text)
248 | status = response_json.get('status', False)
249 | if status is False: return False
250 | else: return True
251 | return False
252 | except Exception as error:
253 | logger.error(f"{self.session_name} | Unknown error when Start Farming: {error}" + (f"\nTraceback: {traceback.format_exc()}" if settings.DEBUG_MODE else ""))
254 | self.errors += 1
255 | await asyncio.sleep(delay=3)
256 | return False
257 |
258 | async def perform_tasks(self) -> None:
259 | url = self.api_url + '/task-list'
260 | try:
261 | json_data = {}
262 | data_list = []
263 | json_data['hash'] = await self.create_hash(data_list)
264 | await self.http_client.options(url)
265 | response = await self.http_client.post(url, json=json_data)
266 | response.raise_for_status()
267 | response_text = await response.text()
268 | if settings.DEBUG_MODE:
269 | print(f"Tasks response:\n{response_text}")
270 | if not self.isValidJson(response_text): return
271 | response_json = json.loads(response_text)
272 | completed = 0
273 | for task in response_json:
274 | if completed == 2: break # perform a maximum of 2 tasks in a row
275 | if int(task['is_active']) == 0: continue
276 | if task['type'] == 'tonkeeper_wallet': continue # ignore task with connecting Tonkeeper wallet
277 | if '//forms.gle' in task['url']: continue
278 | if '//t.me' in task['url']:
279 | continue # ignore all Telegram tasks (they are verified on the server side)
280 | if task['status'] == 'possible':
281 | logger.info(f"{self.session_name} | Try to perform task {task['id']}")
282 | await asyncio.sleep(random.randint(4, 8))
283 | json_data2 = {"id":task['id']}
284 | data_list2 = [json_data2]
285 | json_data2['hash'] = await self.create_hash(data_list2)
286 | response2 = await self.http_client.post(f"{url}/complete", json=json_data2)
287 | response2.raise_for_status()
288 | response_text2 = await response2.text()
289 | if settings.DEBUG_MODE:
290 | print(f"Complete task response:\n{response_text2}")
291 | if self.isValidJson(response_text2):
292 | response_json2 = json.loads(response_text2)
293 | status = response_json2.get('task', {}).get('status', False)
294 | if status == 'granted':
295 | logger.success(f"{self.session_name} | Task {task['id']} completed. Reward claimed.")
296 | await asyncio.sleep(random.randint(2, 4))
297 | completed += 1
298 | self.errors = 0
299 | else:
300 | logger.info(f"{self.session_name} | Failed to perform task {task['id']}")
301 | except Exception as error:
302 | logger.error(f"{self.session_name} | Unknown error while Performing tasks: {error}" + (f"\nTraceback: {traceback.format_exc()}" if settings.DEBUG_MODE else ""))
303 | self.errors += 1
304 | await asyncio.sleep(delay=3)
305 |
306 | async def check_proxy(self, proxy: Proxy) -> None:
307 | try:
308 | response = await self.http_client.get(url='https://httpbin.org/ip', timeout=aiohttp.ClientTimeout(5))
309 | ip = (await response.json()).get('origin')
310 | logger.info(f"{self.session_name} | Proxy IP: {ip}")
311 | except Exception as error:
312 | logger.error(f"{self.session_name} | Proxy: {proxy} | Error: {error}")
313 |
314 | async def check_daily_grant(self, start_time: int | None, cur_time: int, day: int | None) -> tuple[bool, int]:
315 | if start_time is None or day is None:
316 | logger.info(f"{self.session_name} | First daily grant available")
317 | return True, 0
318 |
319 | seconds = cur_time - start_time
320 | days = seconds / 86400
321 | if days > day:
322 | logger.info(f"{self.session_name} | Daily grant available")
323 | return True, 0
324 | else:
325 | next_grant_time = start_time + (day * 86400)
326 | time_to_wait = next_grant_time - cur_time
327 | logger.info(f"{self.session_name} | Next daily grant: {strftime('%Y-%m-%d %H:%M:%S', localtime(next_grant_time))}")
328 | return False, time_to_wait
329 |
330 | async def calculate_taps(self, farm: int, boost: int | bool) -> int:
331 | if isinstance(boost, int) and boost > 0:
332 | full_farm = farm * boost
333 | else:
334 | full_farm = farm
335 |
336 | perc = random.randint(100, 200)
337 | taps = int(full_farm * (perc / 100))
338 | return taps
339 |
340 | async def create_hash(self, data_list: List[Dict[str, Any]], secret_key: str = 'super-key') -> str:
341 | params = []
342 | for item in data_list:
343 | for key, value in item.items():
344 | params.append(f"{key}={quote(str(value))}")
345 |
346 | time_param = f"time={math.ceil(time() / 60)}"
347 | if params:
348 | complete_str = '&'.join(params + [time_param])
349 | else:
350 | complete_str = time_param
351 | hashed = hmac.new(secret_key.encode(), complete_str.encode(), hashlib.sha256)
352 | return hashed.hexdigest()
353 |
354 | def isValidJson(self, text: str) -> bool:
355 | try:
356 | json.loads(text)
357 | return True
358 | except ValueError:
359 | return False
360 |
361 | async def run(self, proxy: str | None) -> None:
362 | access_token_created_time = 0
363 | proxy_conn = ProxyConnector().from_url(proxy) if proxy else None
364 |
365 | async with aiohttp.ClientSession(headers=headers, connector=proxy_conn) as http_client:
366 | self.http_client = http_client
367 | if proxy:
368 | await self.check_proxy(proxy=proxy)
369 |
370 | self.authorized = False
371 | while True:
372 | if self.errors >= settings.ERRORS_BEFORE_STOP:
373 | logger.error(f"{self.session_name} | Too many errors. Bot stopped.")
374 | break
375 | try:
376 | if not self.authorized:
377 | tg_web_data = await self.get_tg_web_data(proxy=proxy)
378 | access_token = await self.login(init_data=tg_web_data)
379 | if access_token is not False:
380 | self.authorized = True
381 | self.access_token = access_token
382 | self.http_client.headers['Authorization'] = 'Bearer ' + access_token
383 | headers['Authorization'] = 'Bearer ' + access_token
384 | access_token_created_time = time()
385 | else: continue
386 |
387 | if time() - access_token_created_time >= 3600:
388 | self.authorized = False
389 | continue
390 | #refresh_success = await self.refresh_token()
391 | #if refresh_success:
392 | # self.http_client.headers['Authorization'] = 'Bearer ' + self.access_token
393 | # headers['Authorization'] = 'Bearer ' + self.access_token
394 | # access_token_created_time = time()
395 | #else:
396 | # self.authorized = False
397 | # continue
398 |
399 | profile = await self.get_profile()
400 | info = profile['info']
401 | farm = info['farm']
402 | boost = info.get('boost', False)
403 | if boost: boost = int(boost[1:])
404 | system_time = profile['system_time']
405 | self.balance = profile['balance']
406 | day_grant_first = profile.get('day_grant_first', None)
407 | day_grant_day = profile.get('day_grant_day', None)
408 | session = profile['session']
409 | status = session['status']
410 | if status == 'inProgress':
411 | start_time = session['start_at']
412 |
413 | # Log current balance
414 | logger.info(f"{self.session_name} | Balance: {self.balance}")
415 |
416 | daily_grant_awail, daily_grant_wait = await self.check_daily_grant(start_time=day_grant_first, cur_time=system_time, day=day_grant_day)
417 | if daily_grant_awail:
418 | if await self.daily_grant():
419 | logger.success(f"{self.session_name} | Daily grant claimed.")
420 | self.errors = 0
421 | continue
422 |
423 | await asyncio.sleep(random.randint(2, 4))
424 | await self.friends_claim()
425 |
426 | await asyncio.sleep(random.randint(2, 4))
427 | await self.perform_tasks()
428 |
429 | # Log current balance
430 | logger.info(f"{self.session_name} | Balance: {self.balance}")
431 |
432 | if status == 'await':
433 | logger.info(f"{self.session_name} | Farm not active. Starting farming.")
434 | if await self.start_farming():
435 | logger.success(f"{self.session_name} | Farming started successfully.")
436 | self.errors = 0
437 | continue
438 | else:
439 | time_elapsed = system_time - start_time
440 | claim_wait = (6 * 3600) - time_elapsed
441 | if claim_wait > 0:
442 | if daily_grant_wait > 0 and daily_grant_wait < claim_wait:
443 | hours = daily_grant_wait // 3600
444 | minutes = (daily_grant_wait % 3600) // 60
445 | logger.info(f"{self.session_name} | Farming active. Waiting for {hours} hours and {minutes} minutes before claiming daily grant.")
446 | await asyncio.sleep(daily_grant_wait)
447 | continue
448 | else:
449 | hours = claim_wait // 3600
450 | minutes = (claim_wait % 3600) // 60
451 | logger.info(f"{self.session_name} | Farming active. Waiting for {hours} hours and {minutes} minutes before claiming and restarting.")
452 | await asyncio.sleep(claim_wait)
453 | continue
454 |
455 | logger.info(f"{self.session_name} | Time to claim and restart farming.")
456 | taps = await self.calculate_taps(farm=farm, boost=boost)
457 | if await self.send_claim(taps=taps):
458 | logger.success(f"{self.session_name} | Claim successful.")
459 | self.errors = 0
460 | if await self.start_farming():
461 | logger.success(f"{self.session_name} | Farming restarted successfully.")
462 | self.errors = 0
463 |
464 | # Log current balance
465 | logger.info(f"{self.session_name} | Balance: {self.balance}")
466 |
467 | except InvalidSession as error:
468 | raise error
469 | except Exception as error:
470 | logger.error(f"{self.session_name} | Unknown error: {error}" + (f"\nTraceback: {traceback.format_exc()}" if settings.DEBUG_MODE else ""))
471 | self.errors += 1
472 | await asyncio.sleep(delay=3)
473 | else:
474 | logger.info(f"Sleep 1 min")
475 | await asyncio.sleep(delay=60)
476 |
477 | async def run_claimer(tg_client: Client, proxy: str | None):
478 | try:
479 | await Claimer(tg_client=tg_client).run(proxy=proxy)
480 | except InvalidSession:
481 | logger.error(f"{tg_client.name} | Invalid Session")
482 |
--------------------------------------------------------------------------------
/bot/core/headers.py:
--------------------------------------------------------------------------------
1 | headers = {
2 | 'Accept': '*/*',
3 | 'Accept-Language': 'en-US,en;q=0.9,uk-UA;q=0.8,uk;q=0.7,ru;q=0.6,zh-CN;q=0.5,zh;q=0.4',
4 | 'Connection': 'keep-alive',
5 | 'Origin': 'https://mmbump.pro',
6 | 'Referer': 'https://mmbump.pro/',
7 | 'Sec-Fetch-Dest': 'empty',
8 | 'Sec-Fetch-Mode': 'cors',
9 | 'Sec-Fetch-Site': 'same-site',
10 | 'User-Agent': 'Mozilla/5.0 (Linux; Android 14; 2210132G Build/UKQ1.230804.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.71 Mobile Safari/537.36',
11 | 'sec-ch-ua': '"Chromium";v="126", "Google Chrome";v="126", "Not;A Brand";v="99"',
12 | 'sec-ch-ua-mobile': '?1',
13 | 'sec-ch-ua-platform': '"Android"',
14 | }
15 |
--------------------------------------------------------------------------------
/bot/core/registrator.py:
--------------------------------------------------------------------------------
1 | from pyrogram import Client
2 |
3 | from bot.config import settings
4 | from bot.utils import logger
5 |
6 |
7 | async def register_sessions() -> None:
8 | API_ID = settings.API_ID
9 | API_HASH = settings.API_HASH
10 |
11 | if not API_ID or not API_HASH:
12 | raise ValueError("API_ID and API_HASH not found in the .env file.")
13 |
14 | session_name = input('\nEnter the session name (press Enter to exit): ')
15 |
16 | if not session_name:
17 | return None
18 |
19 | session = Client(
20 | name=session_name,
21 | api_id=API_ID,
22 | api_hash=API_HASH,
23 | workdir="sessions/"
24 | )
25 |
26 | async with session:
27 | user_data = await session.get_me()
28 |
29 | logger.success(f'Session added successfully @{user_data.username} | {user_data.first_name} {user_data.last_name}')
30 |
--------------------------------------------------------------------------------
/bot/exceptions/__init__.py:
--------------------------------------------------------------------------------
1 | class InvalidSession(BaseException):
2 | ...
3 |
--------------------------------------------------------------------------------
/bot/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from .logger import logger
2 | from . import launcher
3 |
4 |
5 | import os
6 |
7 | if not os.path.exists('sessions'):
8 | os.mkdir('sessions')
9 |
--------------------------------------------------------------------------------
/bot/utils/launcher.py:
--------------------------------------------------------------------------------
1 | import os
2 | import glob
3 | import asyncio
4 | import argparse
5 | import random
6 | from itertools import cycle
7 | from pathlib import Path
8 |
9 | from pyrogram import Client
10 | from better_proxy import Proxy
11 |
12 | from bot.config import settings
13 | from bot.utils import logger
14 | from bot.core.claimer import run_claimer
15 | from bot.core.registrator import register_sessions
16 |
17 |
18 | start_text = """
19 |
20 |
21 | ███ ███ ███ ███ ██████ ██████ ██████ ██████ ██ ██ ███ ███ ██████
22 | ████ ████ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ██
23 | ██ ████ ██ ██ ████ ██ ██████ ██████ ██ ██ ██████ ██ ██ ██ ████ ██ ██████
24 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
25 | ██ ██ ██ ██ ██ ██ ██ ██████ ██████ ██████ ██ ██ ██
26 |
27 |
28 | Select an action:
29 |
30 | 1. Create session
31 | 2. Run bot
32 | """
33 |
34 |
35 | def get_session_names() -> list[str]:
36 | session_path = Path('sessions')
37 | session_files = session_path.glob('*.session')
38 | session_names = sorted([file.stem for file in session_files])
39 | return session_names
40 |
41 | def get_proxies() -> list[Proxy]:
42 | if settings.USE_PROXY_FROM_FILE:
43 | with open(file='bot/config/proxies.txt', encoding='utf-8-sig') as file:
44 | proxies = sorted([Proxy.from_str(proxy=row.strip()).as_url for row in file if row.strip()])
45 | else:
46 | proxies = []
47 |
48 | return proxies
49 |
50 | async def get_tg_clients() -> list[Client]:
51 | session_names = get_session_names()
52 |
53 | if not session_names:
54 | raise FileNotFoundError("Not found session files")
55 |
56 | tg_clients = [Client(
57 | name=session_name,
58 | api_id=settings.API_ID,
59 | api_hash=settings.API_HASH,
60 | workdir='sessions/',
61 | plugins=dict(root='bot/plugins')
62 | ) for session_name in session_names]
63 |
64 | return tg_clients
65 |
66 | async def run_bot_with_delay(tg_client, proxy, delay):
67 | if delay > 0:
68 | logger.info(f"{tg_client.name} | Wait {delay} seconds before start")
69 | await asyncio.sleep(delay)
70 | await run_claimer(tg_client=tg_client, proxy=proxy)
71 |
72 | async def run_clients(tg_clients: list[Client]):
73 | proxies = get_proxies()
74 | proxies_cycle = cycle(proxies) if proxies else cycle([None])
75 | tasks = []
76 | delay = 0
77 | for index, tg_client in enumerate(tg_clients):
78 | if index > 0:
79 | delay = random.randint(*settings.SLEEP_BETWEEN_START)
80 | proxy = next(proxies_cycle)
81 | task = asyncio.create_task(run_bot_with_delay(tg_client=tg_client, proxy=proxy, delay=delay))
82 | tasks.append(task)
83 | await asyncio.gather(*tasks)
84 |
85 | async def process() -> None:
86 | if not settings:
87 | logger.warning(f"Please fix the above errors in the .env file")
88 | return
89 | parser = argparse.ArgumentParser()
90 | parser.add_argument('-a', '--action', type=int, help='Action to perform')
91 |
92 | logger.info(f"Detected {len(get_session_names())} sessions | {len(get_proxies())} proxies")
93 |
94 | action = parser.parse_args().action
95 |
96 | if not action:
97 | print(start_text)
98 |
99 | while True:
100 | action = input("> ")
101 |
102 | if not action.isdigit():
103 | logger.warning("Action must be number")
104 | elif action not in ['1', '2']:
105 | logger.warning("Action must be 1 or 2")
106 | else:
107 | action = int(action)
108 | break
109 |
110 | if action == 1:
111 | await register_sessions()
112 | elif action == 2:
113 | tg_clients = await get_tg_clients()
114 |
115 | await run_clients(tg_clients=tg_clients)
--------------------------------------------------------------------------------
/bot/utils/logger.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from loguru import logger
3 |
4 |
5 | logger.remove()
6 | logger.add(sink=sys.stdout, format="{time:YYYY-MM-DD HH:mm:ss}"
7 | " | {level: <8}"
8 | " | {line}"
9 | " - {message}")
10 | logger = logger.opt(colors=True)
11 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | bot:
4 | container_name: 'MMProBumpBot'
5 | stop_signal: SIGINT
6 | build:
7 | context: .
8 | working_dir: /app
9 | volumes:
10 | - .:/app
11 | entrypoint: "python3 main.py"
12 | command: ["-a", "2"]
13 | restart: unless-stopped
14 | env_file: .env
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from contextlib import suppress
3 |
4 | from bot.utils.launcher import process
5 |
6 |
7 | async def main():
8 | await process()
9 |
10 |
11 | if __name__ == '__main__':
12 | with suppress(KeyboardInterrupt):
13 | asyncio.run(main())
14 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alexell/MMProBumpBot/5f080ad80a49c6f1e110b6ed4bc6158a64c55891/requirements.txt
--------------------------------------------------------------------------------