├── .gitignore
├── README.md
├── dj_bot.conf
├── main.py
├── requirements.txt
├── settings.ini
└── tgbot
├── __init__.py
├── data
├── __init__.py
└── config.py
├── database
├── __init__.py
├── db_helper.py
├── db_settings.py
└── db_users.py
├── keyboards
├── __init__.py
├── inline_main.py
├── inline_misc.py
├── reply_main.py
└── reply_misc.py
├── middlewares
├── __init__.py
├── middleware_throttling.py
└── middleware_user.py
├── routers
├── __init__.py
├── admin
│ ├── __init__.py
│ └── admin_menu.py
├── main_errors.py
├── main_missed.py
├── main_start.py
└── user
│ ├── __init__.py
│ └── user_menu.py
├── services
├── __init__.py
└── api_session.py
└── utils
├── __init__.py
├── const_functions.py
├── misc
├── __init__.py
├── bot_commands.py
├── bot_filters.py
├── bot_logging.py
└── bot_models.py
└── misc_functions.py
/.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 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 | ### VirtualEnv template
91 | # Virtualenv
92 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
93 | [Bb]in
94 | [Ii]nclude
95 | [Ll]ib
96 | [Ll]ib64
97 | [Ll]ocal
98 | [Ss]cripts
99 | pyvenv.cfg
100 | .venv
101 | pip-selfcheck.json
102 |
103 | ### JetBrains template
104 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
105 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
106 |
107 | # User-specific stuff
108 | .idea/**/workspace.xml
109 | .idea/**/tasks.xml
110 | .idea/**/usage.statistics.xml
111 | .idea/**/dictionaries
112 | .idea/**/shelf
113 |
114 | # AWS User-specific
115 | .idea/**/aws.xml
116 |
117 | # Generated files
118 | .idea/**/contentModel.xml
119 |
120 | # Sensitive or high-churn files
121 | .idea/**/dataSources/
122 | .idea/**/dataSources.ids
123 | .idea/**/dataSources.local.xml
124 | .idea/**/sqlDataSources.xml
125 | .idea/**/dynamic.xml
126 | .idea/**/uiDesigner.xml
127 | .idea/**/dbnavigator.xml
128 |
129 | # Gradle
130 | .idea/**/gradle.xml
131 | .idea/**/libraries
132 |
133 | # Gradle and Maven with auto-import
134 | # When using Gradle or Maven with auto-import, you should exclude module files,
135 | # since they will be recreated, and may cause churn. Uncomment if using
136 | auto-import.
137 | .idea/artifacts
138 | .idea/compiler.xml
139 | .idea/jarRepositories.xml
140 | .idea/modules.xml
141 | .idea/*.iml
142 | .idea/modules
143 | *.iml
144 | *.ipr
145 |
146 | # CMake
147 | cmake-build-*/
148 |
149 | # Mongo Explorer plugin
150 | .idea/**/mongoSettings.xml
151 |
152 | # File-based project format
153 | *.iws
154 |
155 | # IntelliJ
156 | out/
157 |
158 | # mpeltonen/sbt-idea plugin
159 | .idea_modules/
160 |
161 | # JIRA plugin
162 | atlassian-ide-plugin.xml
163 |
164 | # Cursive Clojure plugin
165 | .idea/replstate.xml
166 |
167 | # SonarLint plugin
168 | .idea/sonarlint/
169 |
170 | # Crashlytics plugin (for Android Studio and IntelliJ)
171 | com_crashlytics_export_strings.xml
172 | crashlytics.properties
173 | crashlytics-build.properties
174 | fabric.properties
175 |
176 | # Editor-based Rest Client
177 | .idea/httpRequests
178 |
179 | # Android studio 3.1+ serialized cache file
180 | .idea/caches/build_file_checksums.ser
181 |
182 | # idea folder, uncomment if you don't need it
183 | .idea
184 |
185 | # Users exceptions
186 | /other/
187 | /tgbot/data/database.db
188 | /tgbot/data/logs.log
189 | settings.ini
190 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### [](https://www.python.org/downloads/release/python-399/) [](https://pypi.org/project/aiogram/)
2 |
3 | # Template telegram bot for Aiogram 3 by Djimbo
4 |
--------------------------------------------------------------------------------
/dj_bot.conf:
--------------------------------------------------------------------------------
1 | # supervisorctl
2 | [program:dj_bot]
3 | directory=/root/djimbo_template/
4 | command=python3.10 main.py
5 |
6 | autostart=True
7 | autorestart=True
8 |
9 | stderr_logfile=/root/djimbo_template/tgbot/data/sv_log_err.log
10 | ; stderr_logfile_maxbytes=10MB
11 | stdout_logfile=/root/djimbo_template/tgbot/data/sv_log_out.log
12 | ; stdout_logfile_maxbytes=10MB
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | import asyncio
3 | import os
4 | import sys
5 |
6 | import colorama
7 | from aiogram import Bot, Dispatcher
8 | from aiogram.client.default import DefaultBotProperties
9 |
10 | from tgbot.data.config import BOT_TOKEN, BOT_SCHEDULER, get_admins
11 | from tgbot.database.db_helper import create_dbx
12 | from tgbot.middlewares import register_all_middlwares
13 | from tgbot.routers import register_all_routers
14 | from tgbot.services.api_session import AsyncRequestSession
15 | from tgbot.utils.misc.bot_commands import set_commands
16 | from tgbot.utils.misc.bot_logging import bot_logger
17 | from tgbot.utils.misc_functions import autobackup_admin, startup_notify
18 |
19 | colorama.init()
20 |
21 |
22 | # Запуск шедулеров
23 | async def scheduler_start(bot):
24 | BOT_SCHEDULER.add_job(autobackup_admin, trigger="cron", hour=00, args=(bot,)) # Ежедневный Автобэкап в 00:00
25 |
26 |
27 | # Запуск бота и базовых функций
28 | async def main():
29 | BOT_SCHEDULER.start() # Запуск Шедулера
30 | dp = Dispatcher() # Образ Диспетчера
31 | arSession = AsyncRequestSession() # Пул асинхронной сессии запросов
32 | bot = Bot( # Образ Бота
33 | token=BOT_TOKEN,
34 | default=DefaultBotProperties(
35 | parse_mode="HTML",
36 | ),
37 | )
38 |
39 | register_all_middlwares(dp) # Регистрация всех мидлварей
40 | register_all_routers(dp) # Регистрация всех роутеров
41 |
42 | try:
43 | await set_commands(bot) # Установка пользовательских команд в боте
44 | await startup_notify(bot) # Уведомление админов о запуске бота
45 | await scheduler_start(bot) # Подключение шедулеров
46 |
47 | bot_logger.warning("BOT WAS STARTED")
48 | print(colorama.Fore.LIGHTYELLOW_EX + f"~~~~~ Bot was started - @{(await bot.get_me()).username} ~~~~~")
49 | print(colorama.Fore.LIGHTBLUE_EX + "~~~~~ TG developer - @djimbox ~~~~~")
50 | print(colorama.Fore.RESET)
51 |
52 | if len(get_admins()) == 0: print("***** ENTER ADMIN ID IN settings.ini *****")
53 |
54 | await bot.delete_webhook() # Удаление вебхуков, если они имеются
55 | await bot.get_updates(offset=-1) # Сброс пендинг апдейтов
56 |
57 | # Запуск бота (поллинга)
58 | await dp.start_polling(
59 | bot,
60 | arSession=arSession,
61 | allowed_updates=dp.resolve_used_update_types(),
62 | )
63 | finally:
64 | await arSession.close() # Закрытие Асинхронной Сессии для прочих запросов
65 | await bot.session.close() # Закрытие сессии бота
66 |
67 |
68 | if __name__ == "__main__":
69 | create_dbx() # Генерация Базы Данных и Таблиц
70 |
71 | try:
72 | if sys.platform == 'win32': # Запуск на 32-х битных системах
73 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
74 | loop = asyncio.ProactorEventLoop()
75 | asyncio.set_event_loop(loop)
76 | loop.run_until_complete(main())
77 | else:
78 | asyncio.run(main()) # Запуск на всех других системах
79 | except (KeyboardInterrupt, SystemExit):
80 | bot_logger.warning("Bot was stopped")
81 | finally:
82 | if sys.platform.startswith("win"):
83 | os.system("cls")
84 | else:
85 | os.system("clear")
86 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | APScheduler==3.10.4
2 | aiogram==3.5.0
3 | colorlog==6.8.2
4 | pytz==2024.1
5 | typing~=3.7.4.3
6 | aiohttp~=3.9.1
7 | cachetools==5.3.3
8 | colorama~=0.4.5
9 | aiofiles~=23.2.1
10 | pydantic~=2.5.2
--------------------------------------------------------------------------------
/settings.ini:
--------------------------------------------------------------------------------
1 | [settings]
2 | bot_token=
3 | admin_id=
4 |
--------------------------------------------------------------------------------
/tgbot/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tgbot/data/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djimboy/djimbo_template_aio3/9a48d6f60f7be596087ae24a154f9f8807b1f188/tgbot/data/__init__.py
--------------------------------------------------------------------------------
/tgbot/data/config.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | import configparser
3 |
4 | from apscheduler.schedulers.asyncio import AsyncIOScheduler
5 |
6 | # Токен бота
7 | BOT_TOKEN = configparser.ConfigParser()
8 | BOT_TOKEN.read("settings.ini")
9 | BOT_TOKEN = BOT_TOKEN['settings']['bot_token'].strip().replace(' ', '')
10 |
11 | # Пути к файлам
12 | PATH_DATABASE = "tgbot/data/database.db" # Путь к БД
13 | PATH_LOGS = "tgbot/data/logs.log" # Путь к Логам
14 |
15 | # Образы и конфиги
16 | BOT_STATUS_NOTIFICATION = True # Оповещение админам о запуске бота (True или False)
17 | BOT_TIMEZONE = "Europe/Moscow" # Временная зона бота
18 | BOT_SCHEDULER = AsyncIOScheduler(timezone=BOT_TIMEZONE) # Образ шедулера
19 |
20 |
21 | # Получение администраторов бота
22 | def get_admins() -> list[int]:
23 | read_admins = configparser.ConfigParser()
24 | read_admins.read('settings.ini')
25 |
26 | admins = read_admins['settings']['admin_id'].strip().replace(" ", "")
27 |
28 | if "," in admins:
29 | admins = admins.split(",")
30 | else:
31 | if len(admins) >= 1:
32 | admins = [admins]
33 | else:
34 | admins = []
35 |
36 | while "" in admins: admins.remove("")
37 | while " " in admins: admins.remove(" ")
38 | while "," in admins: admins.remove(",")
39 | while "\r" in admins: admins.remove("\r")
40 | while "\n" in admins: admins.remove("\n")
41 |
42 | return list(map(int, admins))
43 |
--------------------------------------------------------------------------------
/tgbot/database/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djimboy/djimbo_template_aio3/9a48d6f60f7be596087ae24a154f9f8807b1f188/tgbot/database/__init__.py
--------------------------------------------------------------------------------
/tgbot/database/db_helper.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | import sqlite3
3 |
4 | from tgbot.data.config import PATH_DATABASE
5 | from tgbot.utils.const_functions import ded
6 |
7 |
8 | # Преобразование полученного списка в словарь
9 | def dict_factory(cursor, row) -> dict:
10 | save_dict = {}
11 |
12 | for idx, col in enumerate(cursor.description):
13 | save_dict[col[0]] = row[idx]
14 |
15 | return save_dict
16 |
17 |
18 | # Форматирование запроса без аргументов
19 | def update_format(sql, parameters: dict) -> tuple[str, list]:
20 | values = ", ".join([
21 | f"{item} = ?" for item in parameters
22 | ])
23 | sql += f" {values}"
24 |
25 | return sql, list(parameters.values())
26 |
27 |
28 | # Форматирование запроса с аргументами
29 | def update_format_where(sql, parameters: dict) -> tuple[str, list]:
30 | sql += " WHERE "
31 |
32 | sql += " AND ".join([
33 | f"{item} = ?" for item in parameters
34 | ])
35 |
36 | return sql, list(parameters.values())
37 |
38 |
39 | ################################################################################
40 | # Создание всех таблиц для БД
41 | def create_dbx():
42 | with sqlite3.connect(PATH_DATABASE) as con:
43 | con.row_factory = dict_factory
44 |
45 | ############################################################
46 | # Создание таблицы с хранением - пользователей
47 | if len(con.execute("PRAGMA table_info(storage_users)").fetchall()) == 7:
48 | print("DB was found(1/2)")
49 | else:
50 | con.execute(
51 | ded(f"""
52 | CREATE TABLE storage_users(
53 | increment INTEGER PRIMARY KEY AUTOINCREMENT,
54 | user_id INTEGER,
55 | user_login TEXT,
56 | user_name TEXT,
57 | user_surname TEXT,
58 | user_fullname TEXT,
59 | user_unix INTEGER
60 | )
61 | """)
62 | )
63 | print("DB was not found(1/2) | Creating...")
64 |
65 | # Создание таблицы с хранением - настроек
66 | if len(con.execute("PRAGMA table_info(storage_settings)").fetchall()) == 1:
67 | print("DB was found(2/2)")
68 | else:
69 | con.execute(
70 | ded(f"""
71 | CREATE TABLE storage_settings(
72 | status_work TEXT
73 | )
74 | """)
75 | )
76 |
77 | con.execute(
78 | ded(f"""
79 | INSERT INTO storage_settings(
80 | status_work
81 | )
82 | VALUES (?)
83 | """),
84 | [
85 | 'True',
86 | ]
87 | )
88 | print("DB was not found(2/2) | Creating...")
89 |
--------------------------------------------------------------------------------
/tgbot/database/db_settings.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | import sqlite3
3 |
4 | from pydantic import BaseModel
5 |
6 | from tgbot.data.config import PATH_DATABASE
7 | from tgbot.database.db_helper import dict_factory, update_format
8 |
9 |
10 | # Модель таблицы
11 | class SettingsModel(BaseModel):
12 | status_work: str # Статус работы бота
13 |
14 |
15 | # Работа с настройками
16 | class Settingsx:
17 | storage_name = "storage_settings"
18 |
19 | # Получение записи
20 | @staticmethod
21 | def get() -> SettingsModel:
22 | with sqlite3.connect(PATH_DATABASE) as con:
23 | con.row_factory = dict_factory
24 | sql = f"SELECT * FROM {Settingsx.storage_name}"
25 |
26 | return SettingsModel(**con.execute(sql).fetchone())
27 |
28 | # Редактирование записи
29 | @staticmethod
30 | def update(**kwargs):
31 | with sqlite3.connect(PATH_DATABASE) as con:
32 | con.row_factory = dict_factory
33 | sql = f"UPDATE {Settingsx.storage_name} SET"
34 | sql, parameters = update_format(sql, kwargs)
35 |
36 | con.execute(sql, parameters)
37 |
--------------------------------------------------------------------------------
/tgbot/database/db_users.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | import sqlite3
3 |
4 | from pydantic import BaseModel
5 |
6 | from tgbot.data.config import PATH_DATABASE
7 | from tgbot.database.db_helper import dict_factory, update_format_where, update_format
8 | from tgbot.utils.const_functions import get_unix, ded
9 |
10 |
11 | # Модель таблицы
12 | class UserModel(BaseModel):
13 | increment: int # Инкремент записи
14 | user_id: int # Айди пользователя
15 | user_login: str # Юзернейм пользователя
16 | user_name: str # Имя пользователя
17 | user_surname: str # Фамилия пользователя
18 | user_fullname: str # Полное имя + фамилия пользователя
19 | user_unix: int # Дата регистрации пользователя в боте (в UNIX времени)
20 |
21 |
22 | # Работа с юзером
23 | class Userx:
24 | storage_name = "storage_users"
25 |
26 | # Добавление записи
27 | @staticmethod
28 | def add(
29 | user_id: int,
30 | user_login: str,
31 | user_name: str,
32 | user_surname: str,
33 | user_fullname: str,
34 | ):
35 | user_unix = get_unix()
36 |
37 | with sqlite3.connect(PATH_DATABASE) as con:
38 | con.row_factory = dict_factory
39 |
40 | con.execute(
41 | ded(f"""
42 | INSERT INTO {Userx.storage_name} (
43 | user_id,
44 | user_login,
45 | user_name,
46 | user_surname,
47 | user_fullname,
48 | user_unix
49 | ) VALUES (?, ?, ?, ?, ?, ?)
50 | """),
51 | [
52 | user_id,
53 | user_login,
54 | user_name,
55 | user_surname,
56 | user_fullname,
57 | user_unix,
58 | ],
59 | )
60 |
61 | # Получение записи
62 | @staticmethod
63 | def get(**kwargs) -> UserModel:
64 | with sqlite3.connect(PATH_DATABASE) as con:
65 | con.row_factory = dict_factory
66 | sql = f"SELECT * FROM {Userx.storage_name}"
67 | sql, parameters = update_format_where(sql, kwargs)
68 |
69 | response = con.execute(sql, parameters).fetchone()
70 |
71 | if response is not None:
72 | response = UserModel(**response)
73 |
74 | return response
75 |
76 | # Получение записей
77 | @staticmethod
78 | def gets(**kwargs) -> list[UserModel]:
79 | with sqlite3.connect(PATH_DATABASE) as con:
80 | con.row_factory = dict_factory
81 | sql = f"SELECT * FROM {Userx.storage_name}"
82 | sql, parameters = update_format_where(sql, kwargs)
83 |
84 | response = con.execute(sql, parameters).fetchall()
85 |
86 | if len(response) >= 1:
87 | response = [UserModel(**cache_object) for cache_object in response]
88 |
89 | return response
90 |
91 | # Получение всех записей
92 | @staticmethod
93 | def get_all() -> list[UserModel]:
94 | with sqlite3.connect(PATH_DATABASE) as con:
95 | con.row_factory = dict_factory
96 | sql = f"SELECT * FROM {Userx.storage_name}"
97 |
98 | response = con.execute(sql).fetchall()
99 |
100 | if len(response) >= 1:
101 | response = [UserModel(**cache_object) for cache_object in response]
102 |
103 | return response
104 |
105 | # Редактирование записи
106 | @staticmethod
107 | def update(user_id, **kwargs):
108 | with sqlite3.connect(PATH_DATABASE) as con:
109 | con.row_factory = dict_factory
110 | sql = f"UPDATE {Userx.storage_name} SET"
111 | sql, parameters = update_format(sql, kwargs)
112 | parameters.append(user_id)
113 |
114 | con.execute(sql + "WHERE user_id = ?", parameters)
115 |
116 | # Удаление записи
117 | @staticmethod
118 | def delete(**kwargs):
119 | with sqlite3.connect(PATH_DATABASE) as con:
120 | con.row_factory = dict_factory
121 | sql = f"DELETE FROM {Userx.storage_name}"
122 | sql, parameters = update_format_where(sql, kwargs)
123 |
124 | con.execute(sql, parameters)
125 |
126 | # Очистка всех записей
127 | @staticmethod
128 | def clear():
129 | with sqlite3.connect(PATH_DATABASE) as con:
130 | con.row_factory = dict_factory
131 | sql = f"DELETE FROM {Userx.storage_name}"
132 |
133 | con.execute(sql)
134 |
--------------------------------------------------------------------------------
/tgbot/keyboards/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djimboy/djimbo_template_aio3/9a48d6f60f7be596087ae24a154f9f8807b1f188/tgbot/keyboards/__init__.py
--------------------------------------------------------------------------------
/tgbot/keyboards/inline_main.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram.types import InlineKeyboardMarkup
3 | from aiogram.utils.keyboard import InlineKeyboardBuilder
4 |
5 | from tgbot.data.config import get_admins
6 | from tgbot.utils.const_functions import ikb
7 |
8 |
9 | # Кнопки инлайн меню
10 | def menu_finl(user_id: int) -> InlineKeyboardMarkup:
11 | keyboard = InlineKeyboardBuilder()
12 |
13 | keyboard.row(
14 | ikb("User X", data="user_inline_x"),
15 | ikb("User 1", data="user_inline:user_btn"),
16 | ikb("User 2", data="..."),
17 | )
18 |
19 | if user_id in get_admins():
20 | keyboard.row(
21 | ikb("Admin X", data="admin_inline_x"),
22 | ikb("Admin 1", data="admin_inline:admin_btn"),
23 | ikb("Admin 2", data="unknown"),
24 | )
25 |
26 | return keyboard.as_markup()
27 |
--------------------------------------------------------------------------------
/tgbot/keyboards/inline_misc.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram.utils.keyboard import InlineKeyboardBuilder
3 |
4 | from tgbot.utils.const_functions import ikb
5 |
6 | # Тестовые админ инлайн кнопки
7 | admin_inl = InlineKeyboardBuilder(
8 | ).row(
9 | ikb("Admin Inline 1", data="..."),
10 | ikb("Admin Inline 2", data="..."),
11 | ).row(
12 | ikb("Admin Inline 3", data="..."),
13 | ).as_markup()
14 |
15 | # Тестовые юзер инлайн кнопки
16 | user_inl = InlineKeyboardBuilder(
17 | ).row(
18 | ikb("User Inline 1", data="..."),
19 | ikb("User Inline 2", data="..."),
20 | ).row(
21 | ikb("User Inline 3", data="..."),
22 | ).as_markup()
23 |
--------------------------------------------------------------------------------
/tgbot/keyboards/reply_main.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram.types import ReplyKeyboardMarkup
3 | from aiogram.utils.keyboard import ReplyKeyboardBuilder
4 |
5 | from tgbot.data.config import get_admins
6 | from tgbot.utils.const_functions import rkb
7 |
8 |
9 | # Кнопки главного меню
10 | def menu_frep(user_id: int) -> ReplyKeyboardMarkup:
11 | keyboard = ReplyKeyboardBuilder()
12 |
13 | keyboard.row(
14 | rkb("User Inline"), rkb("User Reply"),
15 | )
16 |
17 | if user_id in get_admins():
18 | keyboard.row(
19 | rkb("Admin Inline"), rkb("Admin Reply"),
20 | )
21 |
22 | return keyboard.as_markup(resize_keyboard=True)
23 |
--------------------------------------------------------------------------------
/tgbot/keyboards/reply_misc.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram.utils.keyboard import ReplyKeyboardBuilder
3 |
4 | from tgbot.utils.const_functions import rkb
5 |
6 | # Тестовые админ реплай кнопки
7 | admin_rep = ReplyKeyboardBuilder(
8 | ).row(
9 | rkb("Admin Reply 1"),
10 | rkb("Admin Reply 2"),
11 | ).row(
12 | rkb("🔙 Main menu"),
13 | ).as_markup(resize_keyboard=True)
14 |
15 | # Тестовые юзер реплай кнопки
16 | user_rep = ReplyKeyboardBuilder(
17 | ).row(
18 | rkb("User Reply 1"),
19 | rkb("User Reply 2"),
20 | ).row(
21 | rkb("🔙 Main menu"),
22 | ).as_markup(resize_keyboard=True)
23 |
--------------------------------------------------------------------------------
/tgbot/middlewares/__init__.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram import Dispatcher
3 |
4 | from tgbot.middlewares.middleware_user import ExistsUserMiddleware
5 | from tgbot.middlewares.middleware_throttling import ThrottlingMiddleware
6 |
7 |
8 | # Регистрация всех миддлварей
9 | def register_all_middlwares(dp: Dispatcher):
10 | dp.callback_query.outer_middleware(ExistsUserMiddleware())
11 | dp.message.outer_middleware(ExistsUserMiddleware())
12 |
13 | dp.message.middleware(ThrottlingMiddleware())
14 |
--------------------------------------------------------------------------------
/tgbot/middlewares/middleware_throttling.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | import time
3 | from typing import Any, Awaitable, Callable, Dict, Union
4 |
5 | from aiogram import BaseMiddleware
6 | from aiogram.dispatcher.flags import get_flag
7 | from aiogram.types import Message, User
8 | from cachetools import TTLCache
9 |
10 |
11 | # Антиспам
12 | class ThrottlingMiddleware(BaseMiddleware):
13 | def __init__(self, default_rate: Union[int, float] = 0.5) -> None:
14 | self.default_rate = default_rate
15 |
16 | self.users = TTLCache(maxsize=10_000, ttl=600)
17 |
18 | async def __call__(self, handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], event: Message, data):
19 | this_user: User = data.get("event_from_user")
20 |
21 | if get_flag(data, "rate") is not None:
22 | self.default_rate = get_flag(data, "rate")
23 |
24 | if self.default_rate == 0:
25 | return await handler(event, data)
26 |
27 | if this_user.id not in self.users:
28 | self.users[this_user.id] = {
29 | 'last_throttled': int(time.time()),
30 | 'count_throttled': 0,
31 | 'now_rate': self.default_rate,
32 | }
33 |
34 | return await handler(event, data)
35 | else:
36 | if int(time.time()) - self.users[this_user.id]['last_throttled'] >= self.users[this_user.id]['now_rate']:
37 | self.users.pop(this_user.id)
38 |
39 | return await handler(event, data)
40 | else:
41 | self.users[this_user.id]['last_throttled'] = int(time.time())
42 |
43 | if self.users[this_user.id]['count_throttled'] == 0:
44 | self.users[this_user.id]['count_throttled'] = 1
45 | self.users[this_user.id]['now_rate'] = self.default_rate + 2
46 |
47 | return await handler(event, data)
48 | elif self.users[this_user.id]['count_throttled'] == 1:
49 | self.users[this_user.id]['count_throttled'] = 2
50 | self.users[this_user.id]['now_rate'] = self.default_rate + 3
51 |
52 | await event.reply(
53 | "❗ Пожалуйста, не спамьте.\n"
54 | "❗ Please, do not spam.",
55 | )
56 | elif self.users[this_user.id]['count_throttled'] == 2:
57 | self.users[this_user.id]['count_throttled'] = 3
58 | self.users[this_user.id]['now_rate'] = self.default_rate + 5
59 |
60 | await event.reply(
61 | "❗ Бот не будет отвечать до прекращения спама.\n"
62 | "❗ The bot will not respond until the spam stops.",
63 | )
64 |
--------------------------------------------------------------------------------
/tgbot/middlewares/middleware_user.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram import BaseMiddleware
3 |
4 | from tgbot.database.db_users import Userx
5 | from tgbot.utils.const_functions import clear_html
6 |
7 |
8 | # Проверка юзера в БД и его добавление
9 | class ExistsUserMiddleware(BaseMiddleware):
10 | async def __call__(self, handler, event, data):
11 | this_user = data.get("event_from_user")
12 |
13 | if not this_user.is_bot:
14 | get_user = Userx.get(user_id=this_user.id)
15 |
16 | user_id = this_user.id
17 | user_login = this_user.username
18 | user_name = clear_html(this_user.first_name)
19 | user_surname = clear_html(this_user.last_name)
20 | user_fullname = clear_html(this_user.first_name)
21 | user_language = this_user.language_code
22 |
23 | if user_login is None: user_login = ""
24 | if user_name is None: user_name = ""
25 | if user_surname is None: user_surname = ""
26 | if user_fullname is None: user_fullname = ""
27 | if user_language != "ru": user_language = "en"
28 |
29 | if len(user_surname) >= 1: user_fullname += f" {user_surname}"
30 |
31 | if get_user is None:
32 | Userx.add(user_id, user_login.lower(), user_name, user_surname, user_fullname)
33 | else:
34 | if user_name != get_user.user_name:
35 | Userx.update(get_user.user_id, user_name=user_name)
36 |
37 | if user_surname != get_user.user_surname:
38 | Userx.update(get_user.user_id, user_surname=user_surname)
39 |
40 | if user_fullname != get_user.user_fullname:
41 | Userx.update(get_user.user_id, user_fullname=user_fullname)
42 |
43 | if user_login.lower() != get_user.user_login:
44 | Userx.update(get_user.user_id, user_login=user_login.lower())
45 |
46 | data['User'] = Userx.get(user_id=user_id)
47 |
48 | return await handler(event, data)
49 |
--------------------------------------------------------------------------------
/tgbot/routers/__init__.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram import Dispatcher, F
3 |
4 | from tgbot.routers import main_errors, main_missed, main_start
5 | from tgbot.routers.admin import admin_menu
6 | from tgbot.routers.user import user_menu
7 | from tgbot.utils.misc.bot_filters import IsAdmin
8 |
9 |
10 | # Регистрация всех роутеров
11 | def register_all_routers(dp: Dispatcher):
12 | # Подключение фильтров
13 | main_errors.router.message.filter(F.chat.type == "private")
14 | main_start.router.message.filter(F.chat.type == "private")
15 |
16 | user_menu.router.message.filter(F.chat.type == "private")
17 | admin_menu.router.message.filter(F.chat.type == "private", IsAdmin())
18 |
19 | main_missed.router.message.filter(F.chat.type == "private")
20 |
21 | # Подключение обязательных роутеров
22 | dp.include_router(main_errors.router) # Роутер ошибки
23 | dp.include_router(main_start.router) # Роутер основных команд
24 |
25 | # Подключение пользовательских роутеров (юзеров и админов)
26 | dp.include_router(user_menu.router) # Юзер роутер
27 | dp.include_router(admin_menu.router) # Админ роутер
28 |
29 | # Подключение обязательных роутеров
30 | dp.include_router(main_missed.router) # Роутер пропущенных апдейтов
31 |
--------------------------------------------------------------------------------
/tgbot/routers/admin/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djimboy/djimbo_template_aio3/9a48d6f60f7be596087ae24a154f9f8807b1f188/tgbot/routers/admin/__init__.py
--------------------------------------------------------------------------------
/tgbot/routers/admin/admin_menu.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | import os
3 |
4 | import aiofiles
5 | from aiogram import Router, Bot, F
6 | from aiogram.filters import Command
7 | from aiogram.types import FSInputFile, Message, CallbackQuery
8 | from aiogram.utils.media_group import MediaGroupBuilder
9 |
10 | from tgbot.data.config import PATH_DATABASE, PATH_LOGS
11 | from tgbot.database.db_users import UserModel
12 | from tgbot.keyboards.inline_misc import admin_inl
13 | from tgbot.keyboards.reply_misc import admin_rep
14 | from tgbot.utils.const_functions import get_date
15 | from tgbot.utils.misc.bot_models import FSM, ARS
16 |
17 | router = Router(name=__name__)
18 |
19 |
20 | # Кнопка - Admin Inline
21 | @router.message(F.text == 'Admin Inline')
22 | async def admin_button_inline(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
23 | await state.clear()
24 |
25 | await message.answer("Click Button - Admin Inline", reply_markup=admin_inl)
26 |
27 |
28 | # Кнопка - Admin Reply
29 | @router.message(F.text == 'Admin Reply')
30 | async def admin_button_reply(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
31 | await state.clear()
32 |
33 | await message.answer("Click Button - Admin Reply", reply_markup=admin_rep)
34 |
35 |
36 | # Колбэк - Admin X
37 | @router.callback_query(F.data == 'admin_inline_x')
38 | async def admin_callback_inline_x(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
39 | await call.answer(f"Click Admin X")
40 |
41 |
42 | # Колбэк - Admin
43 | @router.callback_query(F.data.startswith('admin_inline:'))
44 | async def admin_callback_inline(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
45 | get_data = call.data.split(":")[1]
46 |
47 | await call.answer(f"Click Admin - {get_data}", True)
48 |
49 |
50 | # Получение Базы Данных
51 | @router.message(Command(commands=['db', 'database']))
52 | async def admin_database(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
53 | await state.clear()
54 |
55 | await message.answer_document(
56 | FSInputFile(PATH_DATABASE),
57 | caption=f"📦 #BACKUP | {get_date()}
",
58 | )
59 |
60 |
61 | # Получение логов
62 | @router.message(Command(commands=['log', 'logs']))
63 | async def admin_log(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
64 | await state.clear()
65 |
66 | media_group = MediaGroupBuilder(
67 | caption=f"🖨 #LOGS | {get_date(full=False)}
",
68 | )
69 |
70 | if os.path.isfile(PATH_LOGS):
71 | media_group.add_document(media=FSInputFile(PATH_LOGS))
72 |
73 | if os.path.isfile("tgbot/data/sv_log_err.log"):
74 | media_group.add_document(media=FSInputFile("tgbot/data/sv_log_err.log"))
75 |
76 | if os.path.isfile("tgbot/data/sv_log_out.log"):
77 | media_group.add_document(media=FSInputFile("tgbot/data/sv_log_out.log"))
78 |
79 | await message.answer_media_group(media=media_group.build())
80 |
81 |
82 | # Очистить логи
83 | @router.message(Command(commands=['clear_log', 'clear_logs', 'log_clear', 'logs_clear']))
84 | async def admin_logs_clear(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
85 | await state.clear()
86 |
87 | if os.path.isfile(PATH_LOGS):
88 | async with aiofiles.open(PATH_LOGS, "w") as file:
89 | await file.write(f"{get_date()} | LOGS WAS CLEAR")
90 |
91 | if os.path.isfile("tgbot/data/sv_log_err.log"):
92 | async with aiofiles.open("tgbot/data/sv_log_err.log", "w") as file:
93 | await file.write(f"{get_date()} | LOGS WAS CLEAR")
94 |
95 | if os.path.isfile("tgbot/data/sv_log_out.log"):
96 | async with aiofiles.open("tgbot/data/sv_log_out.log", "w") as file:
97 | await file.write(f"{get_date()} | LOGS WAS CLEAR")
98 |
99 | await message.answer("🖨 The logs have been cleared")
100 |
--------------------------------------------------------------------------------
/tgbot/routers/main_errors.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram import Router
3 | from aiogram.filters import ExceptionMessageFilter
4 | from aiogram.handlers import ErrorHandler
5 |
6 | from tgbot.utils.misc.bot_logging import bot_logger
7 |
8 | router = Router(name=__name__)
9 |
10 |
11 | # Ошибка с блокировкой бота пользователем
12 | # @router.errors(ExceptionTypeFilter(TelegramForbiddenError))
13 | # class MyHandler(ErrorHandler):
14 | # async def handle(self):
15 | # ...
16 |
17 |
18 | # Ошибка с редактированием одинакового сообщения
19 | @router.errors(ExceptionMessageFilter(
20 | "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message")
21 | )
22 | class MyHandler(ErrorHandler):
23 | async def handle(self):
24 | bot_logger.exception(
25 | f"====================\n"
26 | f"Exception name: {self.exception_name}\n"
27 | f"Exception message: {self.exception_message}\n"
28 | f"===================="
29 | )
30 |
--------------------------------------------------------------------------------
/tgbot/routers/main_missed.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram import Router, Bot, F
3 | from aiogram.types import CallbackQuery, Message
4 |
5 | from tgbot.database.db_users import UserModel
6 | from tgbot.utils.const_functions import del_message
7 | from tgbot.utils.misc.bot_models import FSM, ARS
8 |
9 | router = Router(name=__name__)
10 |
11 |
12 | # Колбэк с удалением сообщения
13 | @router.callback_query(F.data == 'close_this')
14 | async def main_callback_close(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
15 | await del_message(call.message)
16 |
17 |
18 | # Колбэк с обработкой кнопки
19 | @router.callback_query(F.data == '...')
20 | async def main_callback_answer(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
21 | await call.answer(cache_time=30)
22 |
23 |
24 | # Колбэк с обработкой удаления сообщений потерявших стейт
25 | @router.callback_query()
26 | async def main_callback_missed(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
27 | await call.answer(f"❗️ Miss callback: {call.data}", True)
28 |
29 |
30 | # Обработка всех неизвестных команд
31 | @router.message()
32 | async def main_message_missed(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
33 | await message.answer(
34 | "♦️ Unknown command.\n"
35 | "♦️ Enter /start",
36 | )
37 |
--------------------------------------------------------------------------------
/tgbot/routers/main_start.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram import Router, Bot, F
3 | from aiogram.filters import Command
4 | from aiogram.types import Message
5 |
6 | from tgbot.database.db_users import UserModel
7 | from tgbot.keyboards.reply_main import menu_frep
8 | from tgbot.utils.const_functions import ded
9 | from tgbot.utils.misc.bot_models import FSM, ARS
10 |
11 | router = Router(name=__name__)
12 |
13 |
14 | # Открытие главного меню
15 | @router.message(F.text.in_(('🔙 Main menu', '🔙 Return')))
16 | @router.message(Command(commands=['start']))
17 | async def main_start(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
18 | await state.clear()
19 |
20 | await message.answer(
21 | ded(f"""
22 | 🔸 Hello, {User.user_name}
23 | 🔸 Enter /start or /inline
24 | """),
25 | reply_markup=menu_frep(message.from_user.id),
26 | )
27 |
--------------------------------------------------------------------------------
/tgbot/routers/user/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djimboy/djimbo_template_aio3/9a48d6f60f7be596087ae24a154f9f8807b1f188/tgbot/routers/user/__init__.py
--------------------------------------------------------------------------------
/tgbot/routers/user/user_menu.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram import Router, Bot, F
3 | from aiogram.filters import Command
4 | from aiogram.types import Message, CallbackQuery
5 |
6 | from tgbot.database.db_users import UserModel
7 | from tgbot.keyboards.inline_main import menu_finl
8 | from tgbot.keyboards.inline_misc import user_inl
9 | from tgbot.keyboards.reply_misc import user_rep
10 | from tgbot.utils.misc.bot_models import FSM, ARS
11 |
12 | router = Router(name=__name__)
13 |
14 |
15 | # Кнопка - User Inline
16 | @router.message(F.text == 'User Inline')
17 | async def user_button_inline(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
18 | await state.clear()
19 |
20 | await message.answer(
21 | "Click Button - User Inline",
22 | reply_markup=user_inl,
23 | )
24 |
25 |
26 | # Кнопка - User Reply
27 | @router.message(F.text == 'User Reply')
28 | async def user_button_reply(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
29 | await state.clear()
30 |
31 | await message.answer(
32 | "Click Button - User Reply",
33 | reply_markup=user_rep,
34 | )
35 |
36 |
37 | # Команда - /inline
38 | @router.message(Command(commands="inline"))
39 | async def user_command_inline(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
40 | await state.clear()
41 |
42 | await message.answer(
43 | "Click command - /inline",
44 | reply_markup=menu_finl(message.from_user.id),
45 | )
46 |
47 |
48 | # Колбэк - User X
49 | @router.callback_query(F.data == 'user_inline_x')
50 | async def user_callback_inline_x(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
51 | await call.answer(f"Click User X")
52 |
53 |
54 | # Колбэк - User
55 | @router.callback_query(F.data.startswith('user_inline:'))
56 | async def user_callback_inline(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
57 | get_data = call.data.split(":")[1]
58 |
59 | await call.answer(f"Click User - {get_data}", True)
60 |
--------------------------------------------------------------------------------
/tgbot/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djimboy/djimbo_template_aio3/9a48d6f60f7be596087ae24a154f9f8807b1f188/tgbot/services/__init__.py
--------------------------------------------------------------------------------
/tgbot/services/api_session.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from typing import Optional
3 |
4 | import aiohttp
5 |
6 |
7 | # In handler
8 | # session = await arSession.get_session()
9 | # response = await session.get(...)
10 | # response = await session.post(...)
11 |
12 | # Асинхронная сессия для запросов
13 | class AsyncRequestSession:
14 | def __init__(self) -> None:
15 | self._session: Optional[aiohttp.ClientSession] = None
16 |
17 | # Получение сессии
18 | async def get_session(self) -> aiohttp.ClientSession:
19 | if self._session is None:
20 | new_session = aiohttp.ClientSession()
21 | self._session = new_session
22 |
23 | return self._session
24 |
25 | # Закрытие сессии
26 | async def close(self) -> None:
27 | if self._session is None:
28 | return None
29 |
30 | await self._session.close()
31 |
--------------------------------------------------------------------------------
/tgbot/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djimboy/djimbo_template_aio3/9a48d6f60f7be596087ae24a154f9f8807b1f188/tgbot/utils/__init__.py
--------------------------------------------------------------------------------
/tgbot/utils/const_functions.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | import random
3 | import time
4 | import uuid
5 | from datetime import datetime
6 | from typing import Union
7 |
8 | import pytz
9 | from aiogram import Bot
10 | from aiogram.types import InlineKeyboardButton, KeyboardButton, WebAppInfo, Message, InlineKeyboardMarkup, \
11 | ReplyKeyboardMarkup
12 |
13 | from tgbot.data.config import get_admins, BOT_TIMEZONE
14 |
15 |
16 | ######################################## AIOGRAM ########################################
17 | # Генерация реплай кнопки
18 | def rkb(text: str) -> KeyboardButton:
19 | return KeyboardButton(text=text)
20 |
21 |
22 | # Генерация инлайн кнопки
23 | def ikb(text: str, data: str = None, url: str = None, switch: str = None, web: str = None) -> InlineKeyboardButton:
24 | if data is not None:
25 | return InlineKeyboardButton(text=text, callback_data=data)
26 | elif url is not None:
27 | return InlineKeyboardButton(text=text, url=url)
28 | elif switch is not None:
29 | return InlineKeyboardButton(text=text, switch_inline_query=switch)
30 | elif web is not None:
31 | return InlineKeyboardButton(text=text, web_app=WebAppInfo(url=web))
32 |
33 |
34 | # Удаление сообщения с обработкой ошибки от телеграма
35 | async def del_message(message: Message):
36 | try:
37 | await message.delete()
38 | except:
39 | ...
40 |
41 |
42 | # Умная отправка сообщений (автоотправка сообщения с фото или без)
43 | async def smart_message(
44 | bot: Bot,
45 | user_id: int,
46 | text: str,
47 | keyboard: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup] = None,
48 | photo: Union[str, None] = None,
49 | ):
50 | if photo is not None and photo.title() != "None":
51 | await bot.send_photo(
52 | chat_id=user_id,
53 | photo=photo,
54 | caption=text,
55 | reply_markup=keyboard,
56 | )
57 | else:
58 | await bot.send_message(
59 | chat_id=user_id,
60 | text=text,
61 | reply_markup=keyboard,
62 | )
63 |
64 |
65 | # Отправка сообщения всем админам
66 | async def send_admins(bot: Bot, text: str, markup=None, not_me=0):
67 | for admin in get_admins():
68 | try:
69 | if str(admin) != str(not_me):
70 | await bot.send_message(
71 | admin,
72 | text,
73 | reply_markup=markup,
74 | disable_web_page_preview=True,
75 | )
76 | except:
77 | ...
78 |
79 |
80 | ######################################## ПРОЧЕЕ ########################################
81 | # Удаление отступов в многострочной строке ("""text""")
82 | def ded(get_text: str) -> str:
83 | if get_text is not None:
84 | split_text = get_text.split("\n")
85 | if split_text[0] == "": split_text.pop(0)
86 | if split_text[-1] == "": split_text.pop()
87 | save_text = []
88 |
89 | for text in split_text:
90 | while text.startswith(" "):
91 | text = text[1:].strip()
92 |
93 | save_text.append(text)
94 | get_text = "\n".join(save_text)
95 | else:
96 | get_text = ""
97 |
98 | return get_text
99 |
100 |
101 | # Очистка текста от HTML тэгов ('test' -> *b*test*/b*)
102 | def clear_html(get_text: str) -> str:
103 | if get_text is not None:
104 | if "" in get_text: get_text = get_text.replace("<", "*")
105 | if "<" in get_text: get_text = get_text.replace("<", "*")
106 | if ">" in get_text: get_text = get_text.replace(">", "*")
107 | else:
108 | get_text = ""
109 |
110 | return get_text
111 |
112 |
113 | # Очистка пробелов в списке (['', 1, ' ', 2] -> [1, 2])
114 | def clear_list(get_list: list) -> list:
115 | while "" in get_list: get_list.remove("")
116 | while " " in get_list: get_list.remove(" ")
117 | while "." in get_list: get_list.remove(".")
118 | while "," in get_list: get_list.remove(",")
119 | while "\r" in get_list: get_list.remove("\r")
120 | while "\n" in get_list: get_list.remove("\n")
121 |
122 | return get_list
123 |
124 |
125 | # Разбив списка на несколько частей ([1, 2, 3, 4] 2 -> [[1, 2], [3, 4]])
126 | def split_list(get_list: list, count: int) -> list[list]:
127 | return [get_list[i:i + count] for i in range(0, len(get_list), count)]
128 |
129 |
130 | # Получение текущей даты (True - дата с временем, False - дата без времени)
131 | def get_date(full: bool = True) -> str:
132 | if full:
133 | return datetime.now(pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y %H:%M:%S")
134 | else:
135 | return datetime.now(pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y")
136 |
137 |
138 | # Получение текущего unix времени (True - время в наносекундах, False - время в секундах)
139 | def get_unix(full: bool = False) -> int:
140 | if full:
141 | return time.time_ns()
142 | else:
143 | return int(time.time())
144 |
145 |
146 | # Конвертация unix в дату и даты в unix
147 | def convert_date(from_time, full=True, second=True) -> Union[str, int]:
148 | from tgbot.data.config import BOT_TIMEZONE
149 |
150 | if "-" in str(from_time):
151 | from_time = from_time.replace("-", ".")
152 |
153 | if str(from_time).isdigit():
154 | if full:
155 | to_time = datetime.fromtimestamp(from_time, pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y %H:%M:%S")
156 | elif second:
157 | to_time = datetime.fromtimestamp(from_time, pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y %H:%M")
158 | else:
159 | to_time = datetime.fromtimestamp(from_time, pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y")
160 | else:
161 | if " " in str(from_time):
162 | cache_time = from_time.split(" ")
163 |
164 | if ":" in cache_time[0]:
165 | cache_date = cache_time[1].split(".")
166 | cache_time = cache_time[0].split(":")
167 | else:
168 | cache_date = cache_time[0].split(".")
169 | cache_time = cache_time[1].split(":")
170 |
171 | if len(cache_date[0]) == 4:
172 | x_year, x_month, x_day = cache_date[0], cache_date[1], cache_date[2]
173 | else:
174 | x_year, x_month, x_day = cache_date[2], cache_date[1], cache_date[0]
175 |
176 | x_hour, x_minute, x_second = cache_time[0], cache_time[1], cache_time[2]
177 |
178 | from_time = f"{x_day}.{x_month}.{x_year} {x_hour}:{x_minute}:{x_second}"
179 | else:
180 | cache_date = from_time.split(".")
181 |
182 | if len(cache_date[0]) == 4:
183 | x_year, x_month, x_day = cache_date[0], cache_date[1], cache_date[2]
184 | else:
185 | x_year, x_month, x_day = cache_date[2], cache_date[1], cache_date[0]
186 |
187 | from_time = f"{x_day}.{x_month}.{x_year}"
188 |
189 | if " " in str(from_time):
190 | to_time = int(datetime.strptime(from_time, "%d.%m.%Y %H:%M:%S").timestamp())
191 | else:
192 | to_time = int(datetime.strptime(from_time, "%d.%m.%Y").timestamp())
193 |
194 | return to_time
195 |
196 |
197 | # Генерация уникального айди
198 | def gen_id(len_id: int = 16) -> int:
199 | mac_address = uuid.getnode()
200 | time_unix = int(str(time.time_ns())[:len_id])
201 | random_int = int(''.join(random.choices('0123456789', k=len_id)))
202 |
203 | return mac_address + time_unix + random_int
204 |
205 |
206 | # Генерация пароля | default, number, letter, onechar
207 | def gen_password(len_password: int = 16, type_password: str = "default") -> str:
208 | if type_password == "default":
209 | char_password = list("1234567890abcdefghigklmnopqrstuvyxwzABCDEFGHIGKLMNOPQRSTUVYXWZ")
210 | elif type_password == "letter":
211 | char_password = list("abcdefghigklmnopqrstuvyxwzABCDEFGHIGKLMNOPQRSTUVYXWZ")
212 | elif type_password == "number":
213 | char_password = list("1234567890")
214 | elif type_password == "onechar":
215 | char_password = list("1234567890")
216 |
217 | random.shuffle(char_password)
218 | random_chars = "".join([random.choice(char_password) for x in range(len_password)])
219 |
220 | if type_password == "onechar":
221 | random_chars = f"{random.choice('abcdefghigklmnopqrstuvyxwzABCDEFGHIGKLMNOPQRSTUVYXWZ')}{random_chars[1:]}"
222 |
223 | return random_chars
224 |
225 |
226 | # Дополнение к числу корректного времени (1 -> 1 день, 3 -> 3 дня)
227 | def convert_times(get_time: int, get_type: str = "day") -> str:
228 | get_time = int(get_time)
229 | if get_time < 0: get_time = 0
230 |
231 | if get_type == "second":
232 | get_list = ['секунда', 'секунды', 'секунд']
233 | elif get_type == "minute":
234 | get_list = ['минута', 'минуты', 'минут']
235 | elif get_type == "hour":
236 | get_list = ['час', 'часа', 'часов']
237 | elif get_type == "day":
238 | get_list = ['день', 'дня', 'дней']
239 | elif get_type == "month":
240 | get_list = ['месяц', 'месяца', 'месяцев']
241 | else:
242 | get_list = ['год', 'года', 'лет']
243 |
244 | if get_time % 10 == 1 and get_time % 100 != 11:
245 | count = 0
246 | elif 2 <= get_time % 10 <= 4 and (get_time % 100 < 10 or get_time % 100 >= 20):
247 | count = 1
248 | else:
249 | count = 2
250 |
251 | return f"{get_time} {get_list[count]}"
252 |
253 |
254 | # Проверка на булевый тип
255 | def is_bool(value: Union[bool, str, int]) -> bool:
256 | value = str(value).lower()
257 |
258 | if value in ('y', 'yes', 't', 'true', 'on', '1'):
259 | return True
260 | elif value in ('n', 'no', 'f', 'false', 'off', '0'):
261 | return False
262 | else:
263 | raise ValueError(f"invalid truth value {value}")
264 |
265 |
266 | ######################################## ЧИСЛА ########################################
267 | # Преобразование экспоненциальных чисел в читаемый вид (1e-06 -> 0.000001)
268 | def snum(amount: Union[int, float], remains: int = 2) -> str:
269 | format_str = "{:." + str(remains) + "f}"
270 | str_amount = format_str.format(float(amount))
271 |
272 | if remains != 0:
273 | if "." in str_amount:
274 | remains_find = str_amount.find(".")
275 | remains_save = remains_find + 8 - (8 - remains) + 1
276 |
277 | str_amount = str_amount[:remains_save]
278 |
279 | if "." in str(str_amount):
280 | while str(str_amount).endswith('0'): str_amount = str(str_amount)[:-1]
281 |
282 | if str(str_amount).endswith('.'): str_amount = str(str_amount)[:-1]
283 |
284 | return str(str_amount)
285 |
286 |
287 | # Конвертация любого числа в вещественное, с удалением нулей в конце (remains - округление)
288 | def to_float(get_number, remains: int = 2) -> Union[int, float]:
289 | if "," in str(get_number):
290 | get_number = str(get_number).replace(",", ".")
291 |
292 | if "." in str(get_number):
293 | get_last = str(get_number).split(".")
294 |
295 | if str(get_last[1]).endswith("0"):
296 | while True:
297 | if str(get_number).endswith("0"):
298 | get_number = str(get_number)[:-1]
299 | else:
300 | break
301 |
302 | get_number = round(float(get_number), remains)
303 |
304 | str_number = snum(get_number)
305 | if "." in str_number:
306 | if str_number.split(".")[1] == "0":
307 | get_number = int(get_number)
308 | else:
309 | get_number = float(get_number)
310 | else:
311 | get_number = int(get_number)
312 |
313 | return get_number
314 |
315 |
316 | # Конвертация вещественного числа в целочисленное
317 | def to_int(get_number: float) -> int:
318 | if "," in get_number:
319 | get_number = str(get_number).replace(",", ".")
320 |
321 | get_number = int(round(float(get_number)))
322 |
323 | return get_number
324 |
325 |
326 | # Проверка ввода на число
327 | def is_number(get_number: Union[str, int, float]) -> bool:
328 | if str(get_number).isdigit():
329 | return True
330 | else:
331 | if "," in str(get_number): get_number = str(get_number).replace(",", ".")
332 |
333 | try:
334 | float(get_number)
335 | return True
336 | except ValueError:
337 | return False
338 |
339 |
340 | # Преобразование числа в читаемый вид (123456789 -> 123 456 789)
341 | def format_rate(amount: Union[float, int], around: int = 2) -> str:
342 | if "," in str(amount): amount = float(str(amount).replace(",", "."))
343 | if " " in str(amount): amount = float(str(amount).replace(" ", ""))
344 | amount = str(round(amount, around))
345 |
346 | out_amount, save_remains = [], ""
347 |
348 | if "." in amount: save_remains = amount.split(".")[1]
349 | save_amount = [char for char in str(int(float(amount)))]
350 |
351 | if len(save_amount) % 3 != 0:
352 | if (len(save_amount) - 1) % 3 == 0:
353 | out_amount.extend([save_amount[0]])
354 | save_amount.pop(0)
355 | elif (len(save_amount) - 2) % 3 == 0:
356 | out_amount.extend([save_amount[0], save_amount[1]])
357 | save_amount.pop(1)
358 | save_amount.pop(0)
359 | else:
360 | print("Error 4388326")
361 |
362 | for x, char in enumerate(save_amount):
363 | if x % 3 == 0: out_amount.append(" ")
364 | out_amount.append(char)
365 |
366 | response = "".join(out_amount).strip() + "." + save_remains
367 |
368 | if response.endswith("."):
369 | response = response[:-1]
370 |
371 | return response
372 |
--------------------------------------------------------------------------------
/tgbot/utils/misc/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djimboy/djimbo_template_aio3/9a48d6f60f7be596087ae24a154f9f8807b1f188/tgbot/utils/misc/__init__.py
--------------------------------------------------------------------------------
/tgbot/utils/misc/bot_commands.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram import Bot
3 | from aiogram.types import BotCommand, BotCommandScopeChat, BotCommandScopeDefault
4 |
5 | from tgbot.data.config import get_admins
6 |
7 | # Команды для юзеров
8 | user_commands = [
9 | BotCommand(command="start", description="♻️ Restart bot"),
10 | BotCommand(command="inline", description="🌀 Get Inline keyboard"),
11 | ]
12 |
13 | # Команды для админов
14 | admin_commands = [
15 | BotCommand(command="start", description="♻️ Restart bot"),
16 | BotCommand(command="inline", description="🌀 Get Inline keyboard"),
17 | BotCommand(command="log", description="🖨 Get Logs"),
18 | BotCommand(command="db", description="📦 Get Database"),
19 | ]
20 |
21 |
22 | # Установка команд
23 | async def set_commands(bot: Bot):
24 | await bot.set_my_commands(user_commands, scope=BotCommandScopeDefault())
25 |
26 | for admin in get_admins():
27 | try:
28 | await bot.set_my_commands(admin_commands, scope=BotCommandScopeChat(chat_id=admin))
29 | except:
30 | ...
31 |
--------------------------------------------------------------------------------
/tgbot/utils/misc/bot_filters.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram.filters import BaseFilter
3 | from aiogram.types import Message
4 |
5 | from tgbot.data.config import get_admins
6 |
7 |
8 | # Проверка на админа
9 | class IsAdmin(BaseFilter):
10 | async def __call__(self, message: Message) -> bool:
11 | if message.from_user.id in get_admins():
12 | return True
13 | else:
14 | return False
15 |
--------------------------------------------------------------------------------
/tgbot/utils/misc/bot_logging.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | import logging as bot_logger
3 |
4 | import colorlog
5 |
6 | from tgbot.data.config import PATH_LOGS
7 |
8 | # Формат логгирования
9 | log_formatter_file = bot_logger.Formatter("%(levelname)s | %(asctime)s | %(filename)s:%(lineno)d | %(message)s")
10 | log_formatter_console = colorlog.ColoredFormatter(
11 | "%(purple)s%(levelname)s %(blue)s|%(purple)s %(asctime)s %(blue)s|%(purple)s %(filename)s:%(lineno)d %(blue)s|%(purple)s %(message)s%(red)s",
12 | datefmt="%d-%m-%Y %H:%M:%S",
13 | )
14 |
15 | # Логгирование в файл logs.log
16 | file_handler = bot_logger.FileHandler(PATH_LOGS, "w", "utf-8")
17 | file_handler.setFormatter(log_formatter_file)
18 | file_handler.setLevel(bot_logger.INFO)
19 |
20 | # Логгирование в консоль
21 | console_handler = bot_logger.StreamHandler()
22 | console_handler.setFormatter(log_formatter_console)
23 | console_handler.setLevel(bot_logger.CRITICAL)
24 |
25 | # Подключение настроек логгирования
26 | bot_logger.basicConfig(
27 | format="%(levelname)s | %(asctime)s | %(filename)s:%(lineno)d | %(message)s",
28 | handlers=[
29 | file_handler,
30 | console_handler
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/tgbot/utils/misc/bot_models.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram.fsm.context import FSMContext
3 |
4 | from tgbot.services.api_session import AsyncRequestSession
5 |
6 | FSM = FSMContext
7 | ARS = AsyncRequestSession
8 |
--------------------------------------------------------------------------------
/tgbot/utils/misc_functions.py:
--------------------------------------------------------------------------------
1 | # - *- coding: utf- 8 - *-
2 | from aiogram import Bot
3 | from aiogram.types import FSInputFile
4 |
5 | from tgbot.data.config import get_admins, PATH_DATABASE, BOT_STATUS_NOTIFICATION
6 | from tgbot.utils.const_functions import get_date, send_admins
7 |
8 |
9 | # Выполнение функции после запуска бота (рассылка админам о запуске бота)
10 | async def startup_notify(bot: Bot):
11 | if len(get_admins()) >= 1 and BOT_STATUS_NOTIFICATION:
12 | await send_admins(bot, "✅ Bot was started")
13 |
14 |
15 | # Автоматические бэкапы БД
16 | async def autobackup_admin(bot: Bot):
17 | for admin in get_admins():
18 | try:
19 | await bot.send_document(
20 | admin,
21 | FSInputFile(PATH_DATABASE),
22 | caption=f"📦 #AUTOBACKUP | {get_date()}
",
23 | )
24 | except:
25 | ...
26 |
--------------------------------------------------------------------------------