mirror of
https://github.com/ijaric/voice_assistant.git
synced 2025-05-24 22:43:26 +00:00
Merge pull request #50 from ijaric/features/tg_bot
[#49] Features/tg bot
This commit is contained in:
commit
671479c248
10
src/bot_aiogram/.env.example
Normal file
10
src/bot_aiogram/.env.example
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
BOT_CONTAINER_NAME=bot_container_name
|
||||||
|
BOT_IMAGE_NAME=botimage_name
|
||||||
|
|
||||||
|
# required parameters
|
||||||
|
BOT_TOKEN=123456:Your-TokEn_ExaMple
|
||||||
|
BOT_ADMINS=123456,654321
|
||||||
|
|
||||||
|
API_PROTOCOL=http
|
||||||
|
API_URL=api
|
||||||
|
API_PORT=8000
|
63
src/bot_aiogram/.gitignore
vendored
Executable file
63
src/bot_aiogram/.gitignore
vendored
Executable file
|
@ -0,0 +1,63 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
.idea/*
|
||||||
|
.env
|
22
src/bot_aiogram/Dockerfile
Executable file
22
src/bot_aiogram/Dockerfile
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
FROM python:3.11
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& DEBIAN_FRONTEND=noninteractive \
|
||||||
|
&& apt-get install -y net-tools netcat-traditional curl \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN mkdir --parents /opt/app
|
||||||
|
COPY pyproject.toml /opt/app/pyproject.toml
|
||||||
|
COPY poetry.lock /opt/app/poetry.lock
|
||||||
|
COPY poetry.toml /opt/app/poetry.toml
|
||||||
|
|
||||||
|
WORKDIR /opt/app
|
||||||
|
|
||||||
|
RUN pip install poetry \
|
||||||
|
&& poetry install --no-dev
|
||||||
|
|
||||||
|
COPY bin /opt/app/bin
|
||||||
|
COPY tgbot /opt/app/tgbot
|
||||||
|
|
||||||
|
CMD [".venv/bin/python", "-m", "bin"]
|
11
src/bot_aiogram/Makefile
Normal file
11
src/bot_aiogram/Makefile
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
include ../../common_makefile.mk
|
||||||
|
|
||||||
|
PROJECT_FOLDERS = tgbot
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
@echo 'Running tests...'
|
||||||
|
|
||||||
|
.PHONY: ci-test
|
||||||
|
ci-test:
|
||||||
|
@echo 'Running tests...'
|
212
src/bot_aiogram/README.md
Normal file
212
src/bot_aiogram/README.md
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
# tgbot_template (aiogram v2.0)
|
||||||
|
|
||||||
|
> ⚠️ **Note**: This template is for aiogram version 2.0. If you're interested in using the latest features and functionalities, consider using the updated [tgbot_template_v3](https://github.com/Latand/tgbot_template_v3) which is compatible with aiogram 3.0.
|
||||||
|
|
||||||
|
<img height="30em" src="https://raw.githubusercontent.com/anki-geo/ultimate-geography/a44a569a922e1d241517113e2917736af808eed7/src/media/flags/ug-flag-united_kingdom.svg" alt="english" align = "center"/>
|
||||||
|
This template is recommended to use in your Telegram bots written on <a href='https://github.com/aiogram/aiogram'>AIOgram</a>.
|
||||||
|
You can see tutorials on how to create, and use it on <a href='https://botfather.dev?utm_source=github_template'>Website with course on Telegram Bots Development</a>.
|
||||||
|
<br/><br/><br/>
|
||||||
|
|
||||||
|
<img height="30em" src="https://raw.githubusercontent.com/anki-geo/ultimate-geography/a44a569a922e1d241517113e2917736af808eed7/src/media/flags/ug-flag-ukraine.svg" alt="ukrainian" align = "center"/>
|
||||||
|
Цей шаблон рекомендовано використовувати для створення ваших Telegram-ботів, написаних на <a href='https://github.com/aiogram/aiogram'>AIOgram</a>.
|
||||||
|
Ви можете переглянути навчальні матеріали щодо створення та використання шаблону на <a href='https://botfather.dev?utm_source=github_template'>веб-сайті з курсом із розробки ботів Telegram</a>
|
||||||
|
<br/><br/><br/>
|
||||||
|
|
||||||
|
<img height="30em" src="https://raw.githubusercontent.com/anki-geo/ultimate-geography/a44a569a922e1d241517113e2917736af808eed7/src/media/flags/ug-flag-russia.svg" alt="russian" align = "center"/>
|
||||||
|
Этот шаблон рекомендуется использовать для создания ваших Telegram-ботов, написанных на <a href='https://github.com/aiogram/aiogram'>AIOgram</a>.
|
||||||
|
Учебные материалы по созданию и использованию шаблона можно найти на <a href='https://botfather.dev?utm_source=github_template'>веб-сайте с курсом по разработке ботов Telegram</a>
|
||||||
|
|
||||||
|
## About the template
|
||||||
|
|
||||||
|
**Structure:**
|
||||||
|
|
||||||
|
```
|
||||||
|
tgbot_template/
|
||||||
|
├── bot.py
|
||||||
|
├── tgbot/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── config.py
|
||||||
|
│ ├── filters/
|
||||||
|
│ ├── handlers/
|
||||||
|
│ └── middlewares/
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `tgbot` package is the root package for the bot, and it contains sub-packages for **filters**, **handlers**,
|
||||||
|
and **middlewares**.
|
||||||
|
|
||||||
|
- The `filters` package contains classes that define **custom filters** for the bot's message handlers.
|
||||||
|
|
||||||
|
- The `handlers` package contains classes that define the bot's **message handlers**, which specify the actions to take
|
||||||
|
in response to incoming messages.
|
||||||
|
|
||||||
|
- The `middlewares` package contains classes that define **custom middlewares** for the bot's dispatcher, which can be
|
||||||
|
used to perform additional processing on incoming messages.
|
||||||
|
|
||||||
|
## Detailed description
|
||||||
|
|
||||||
|
### `bot.py`
|
||||||
|
|
||||||
|
The bot.py script is the entry point for the template Telegram bot. It performs the following steps to start and run the
|
||||||
|
bot:
|
||||||
|
|
||||||
|
1. Set up logging: The `logging` module is imported and configured to log messages to the console.
|
||||||
|
2. Load the configuration: The `load_config()` function from the `tgbot.config` module is called to read the configuration
|
||||||
|
from the environment.
|
||||||
|
3. Set up the storage: Depending on the `use_redis` flag in the configuration, either a `MemoryStorage` or a `RedisStorage2`
|
||||||
|
instance is created to store the bot's state.
|
||||||
|
4. Create the bot and the dispatcher: A `Bot` instance is created using the bot token from the configuration, and a
|
||||||
|
`Dispatcher` instance is created using the `Bot` instance and the storage.
|
||||||
|
5. Register middlewares, filters, and handlers: The `register_all_middlewares()`, `register_all_filters()`, and
|
||||||
|
`register_all_handlers()` functions are called to register all the middlewares, filters, and handlers that are used by
|
||||||
|
the bot.
|
||||||
|
6. Start the polling loop: The `start_polling()` method of the Dispatcher instance is called to start the main event loop
|
||||||
|
for the bot. This method listens for incoming messages and routes them to the appropriate handler.
|
||||||
|
|
||||||
|
### `tgbot/config.py`
|
||||||
|
|
||||||
|
The `config.py` script defines a data structure for storing configuration options for the bot, such as the Telegram bot
|
||||||
|
token, database credentials, and other parameters.
|
||||||
|
|
||||||
|
The config.py script also includes a `load_config` function for loading the configuration from a file using
|
||||||
|
the `environs` library.
|
||||||
|
|
||||||
|
The config.py file defines a `Config` class, which is used to store configuration settings for the bot.
|
||||||
|
|
||||||
|
The Config class has three nested classes, `TgBot`, `DbConfig`, and `Miscellaneous`, which are used to store
|
||||||
|
configuration settings for the Telegram bot, the database, and miscellaneous settings, respectively.
|
||||||
|
|
||||||
|
The `load_config` function is used to load the configuration settings from an environment file and create a `Config`
|
||||||
|
object.
|
||||||
|
|
||||||
|
### `tgbot/filters/admin.py`
|
||||||
|
|
||||||
|
The `admin.py` file defines an `AdminFilter` class, which is used to filter messages so that only messages from
|
||||||
|
authorized users **(i.e., users who are listed in the ADMINS configuration setting)** are processed by the bot.
|
||||||
|
|
||||||
|
The `AdminFilter` class is a subclass of `BoundFilter` from the **aiogram** library, and it defines a key property that
|
||||||
|
specifies the name of the filter. The `AdminFilter` class also defines an `__init__` method that takes a `is_admin`
|
||||||
|
parameter, which specifies whether the user who sent the message is an authorized user.
|
||||||
|
|
||||||
|
The `AdminFilter` class also defines a `check` method that checks whether the user who sent the message is an admin
|
||||||
|
user, and if so, it returns `True`, indicating that the message should be processed by the bot. Otherwise, it returns
|
||||||
|
`False`, indicating that the message should be ignored by the bot. The `check` method is called by the bot's dispatcher
|
||||||
|
when a message is received.
|
||||||
|
|
||||||
|
### `tgbot/handlers/admin.py`
|
||||||
|
|
||||||
|
The `admin.py` file defines a `register_admin` function, which is used to register event handlers for messages that are
|
||||||
|
sent by authorized users (**i.e., users who are listed in the ADMINS configuration setting**).
|
||||||
|
|
||||||
|
The `register_admin` function takes a `Dispatcher` object as its parameter, and it uses this object to register event
|
||||||
|
handlers that respond to different types of messages.
|
||||||
|
|
||||||
|
For example, it might register an event handler that responds to commands that are sent by authorized users, such as
|
||||||
|
the `/echo` command, which causes the bot to repeat the text of the message back to the user.
|
||||||
|
|
||||||
|
### `tgbot/handlers/echo.py`
|
||||||
|
|
||||||
|
The `echo.py` file defines a `register_echo` function, which is used to register an event handler for the `/echo`
|
||||||
|
command.
|
||||||
|
This event handler is responsible for repeating the text of the message back to the user. The `register_echo` function
|
||||||
|
takes a `Dispatcher` object as its parameter, and it uses this object to register the `/echo` command handler.
|
||||||
|
|
||||||
|
### `tgbot/handlers/user.py`
|
||||||
|
|
||||||
|
The `user.py` file defines a `register_user` function, which is used to register event handlers for messages that are
|
||||||
|
sent
|
||||||
|
by non-authorized users (i.e., users who are not listed in the ADMINS configuration setting).
|
||||||
|
|
||||||
|
The `register_user` function takes a `Dispatcher` object as its parameter, and it uses this object to register event
|
||||||
|
handlers that respond to different types of messages. For example, it might register an event handler that responds to
|
||||||
|
commands that are sent by non-authorized users, such as the `/help` command, which causes the bot to send a message with
|
||||||
|
a list of available commands.
|
||||||
|
|
||||||
|
### `tgbot/middlewares/environment.py`
|
||||||
|
|
||||||
|
`environment.py` is a file that contains the `EnvironmentMiddleware` class, which is a middleware used in the Telegram
|
||||||
|
bot.
|
||||||
|
|
||||||
|
A middleware is a piece of code that sits between the incoming request and the handler function. In this case, the
|
||||||
|
`EnvironmentMiddleware` class allows the bot to access the configuration data that was loaded by the `load_config`
|
||||||
|
function
|
||||||
|
in the `config.py` file. This configuration data can then be accessed by other parts of the bot, such as the handlers,
|
||||||
|
to
|
||||||
|
customize its behavior.
|
||||||
|
|
||||||
|
### `tgbot/keyboards/(inline|reply).py`
|
||||||
|
|
||||||
|
The `inline.py` and `reply.py` files define classes that are used to create inline and reply keyboards, respectively.
|
||||||
|
|
||||||
|
The `InlineKeyboard` class is a subclass of `InlineKeyboardMarkup` from the **aiogram** library, and it defines a
|
||||||
|
`__init__` method that takes a `inline_keyboard` parameter, which specifies the buttons that should be included in the
|
||||||
|
keyboard.
|
||||||
|
|
||||||
|
The `ReplyKeyboard` class is a subclass of `ReplyKeyboardMarkup` from the **aiogram** library, and it defines a
|
||||||
|
`__init__` method that takes a `keyboard` parameter, which specifies the buttons that should be included in the
|
||||||
|
keyboard.
|
||||||
|
|
||||||
|
### `tgbot/misc`
|
||||||
|
|
||||||
|
In general, a package called "misc" might be used to store miscellaneous code that doesn't fit into any of the other
|
||||||
|
packages or modules in a project. This could include utility functions, helper classes, or other types of code that are
|
||||||
|
used by multiple parts of the project.
|
||||||
|
|
||||||
|
In this case, the `misc` package contains a `states.py` file, which defines a `StateGroup` class that is used to define
|
||||||
|
the states that are used by the bot.
|
||||||
|
|
||||||
|
### `tgbot/models`
|
||||||
|
|
||||||
|
The `models` package can contain `users.py` file, which defines a `User` class that is used to represent a user in the
|
||||||
|
database. This can be used with combination of some ORM (Object Relational Mapper) to store and retrieve data from the
|
||||||
|
database.
|
||||||
|
|
||||||
|
### `tgbot/services`
|
||||||
|
|
||||||
|
This package can also be named `infrastructure`. It contains the code that is used to interact with external services.
|
||||||
|
|
||||||
|
A package called "services" could contain code that defines services that are used by an application. In software
|
||||||
|
development, a service is a self-contained piece of functionality that performs a specific task or provides a specific
|
||||||
|
capability. A service is typically defined as a class or a set of functions that implement the desired functionality.
|
||||||
|
|
||||||
|
Examples of services that might be included in a services package could include a **database access service, a caching
|
||||||
|
service, a messaging service**, or any other type of functionality that is used by the application. The exact contents
|
||||||
|
of
|
||||||
|
a services package would depend on the specific needs of the application and the services that it requires.
|
||||||
|
|
||||||
|
The `services` package can contain a `database.py` file, which defines a `Database` class that is used to connect to the
|
||||||
|
database and perform database operations.
|
||||||
|
|
||||||
|
## docker-compose.yml
|
||||||
|
|
||||||
|
The `docker-compose.yml` file defines the services that are used by the application, as well as the networks and volumes
|
||||||
|
that are needed by the application. The file begins by specifying the version of the Docker Compose file format that is
|
||||||
|
being used.
|
||||||
|
|
||||||
|
The `services` section of the file defines the containers that should be run as part of the application. In this example,
|
||||||
|
there is only one service, called `bot`, which is based on the `tg_bot-image` Docker image. The `container_name` specifies the
|
||||||
|
name that should be used for the container, and the `build` section specifies the location of the Dockerfile that should
|
||||||
|
be used to build the image.
|
||||||
|
|
||||||
|
The `working_dir` specifies the working directory that should be used by the container, and the `volumes` section specifies
|
||||||
|
the files and directories that should be mounted into the container. In this case, the entire project directory is
|
||||||
|
mounted into the container, which allows the application to access the files on the host machine.
|
||||||
|
|
||||||
|
The `command` specifies the command that should be run when the container is started, and the `restart` setting specifies
|
||||||
|
that the container should be automatically restarted if it exits.
|
||||||
|
|
||||||
|
The `env_file` setting specifies the location of the `.env` file, which contains the configuration settings for the application.
|
||||||
|
|
||||||
|
The `networks` section defines the networks that the container should be connected to. In this example, there is only one
|
||||||
|
network, called `tg_bot`, which is based on the bridge driver. This network allows the containers in the application to
|
||||||
|
communicate with each other.
|
||||||
|
|
||||||
|
## Dockerfile
|
||||||
|
|
||||||
|
The `Dockerfile` defines the instructions for building the Docker image that is used by the bot service. The file begins
|
||||||
|
by specifying the base image that should be used for the image, which in this case is `python:3.9-buster`. The `ENV`
|
||||||
|
instruction sets the value of the `BOT_NAME` environment variable, which is used by the `WORKDIR` instruction to specify the
|
||||||
|
working directory for the container.
|
||||||
|
|
||||||
|
The `COPY` instructions are used to copy the `requirements.txt` file and the entire project directory into the image. The
|
||||||
|
`RUN` instruction is used to install the Python dependencies from the `requirements.txt` file. This allows the application
|
||||||
|
to run in the container with all the necessary dependencies.
|
0
src/bot_aiogram/bin/__init__.py
Normal file
0
src/bot_aiogram/bin/__init__.py
Normal file
55
src/bot_aiogram/bin/__main__.py
Normal file
55
src/bot_aiogram/bin/__main__.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import aiogram
|
||||||
|
import aiogram.contrib.fsm_storage.memory as fsm_storage_memory
|
||||||
|
|
||||||
|
import tgbot.handlers as tgbot_handlers
|
||||||
|
import tgbot.middlewares as tgbot_middlewares
|
||||||
|
import tgbot.settings as tgbot_settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def register_all_middlewares(dp: aiogram.Dispatcher):
|
||||||
|
dp.setup_middleware(tgbot_middlewares.environment.EnvironmentMiddleware())
|
||||||
|
|
||||||
|
|
||||||
|
def register_all_handlers(dp: aiogram.Dispatcher):
|
||||||
|
tgbot_handlers.register_user(dp)
|
||||||
|
tgbot_handlers.register_echo(dp)
|
||||||
|
tgbot_handlers.register_voice_response(dp)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(filename)s:%(lineno)d #%(levelname)-8s [%(asctime)s] - %(name)s - %(message)s",
|
||||||
|
)
|
||||||
|
logger.info("Starting bot")
|
||||||
|
config = tgbot_settings.Settings()
|
||||||
|
|
||||||
|
storage = fsm_storage_memory.MemoryStorage()
|
||||||
|
bot = aiogram.Bot(token=config.tgbot.token.get_secret_value(), parse_mode="HTML")
|
||||||
|
dp = aiogram.Dispatcher(bot, storage=storage)
|
||||||
|
|
||||||
|
bot["config"] = config
|
||||||
|
|
||||||
|
register_all_middlewares(dp)
|
||||||
|
register_all_handlers(dp)
|
||||||
|
|
||||||
|
# start
|
||||||
|
try:
|
||||||
|
await dp.start_polling()
|
||||||
|
finally:
|
||||||
|
await dp.storage.close()
|
||||||
|
await dp.storage.wait_closed()
|
||||||
|
if bot.session:
|
||||||
|
await bot.session.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
logger.error("Bot stopped!")
|
17
src/bot_aiogram/docker-compose.yml
Executable file
17
src/bot_aiogram/docker-compose.yml
Executable file
|
@ -0,0 +1,17 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
bot:
|
||||||
|
image: "${BOT_IMAGE_NAME:-tg_bot-image}"
|
||||||
|
container_name: "${BOT_CONTAINER_NAME:-tg_bot-container}"
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
restart: always
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
networks:
|
||||||
|
- tg_bot_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
tg_bot_network:
|
||||||
|
driver: bridge
|
1364
src/bot_aiogram/poetry.lock
generated
Normal file
1364
src/bot_aiogram/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
src/bot_aiogram/poetry.toml
Normal file
3
src/bot_aiogram/poetry.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[virtualenvs]
|
||||||
|
create = true
|
||||||
|
in-project = true
|
143
src/bot_aiogram/pyproject.toml
Normal file
143
src/bot_aiogram/pyproject.toml
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
[build-system]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 120
|
||||||
|
target-version = ['py311']
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
known_first_party = ["backend", "tests"]
|
||||||
|
line_length = 120
|
||||||
|
profile = "black"
|
||||||
|
py_version = "311"
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
|
authors = ["jsdio@jsdio.ru"]
|
||||||
|
description = ""
|
||||||
|
name = "bot_aiogram"
|
||||||
|
readme = "README.md"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
aiogram = "2.18"
|
||||||
|
environs = "9.0"
|
||||||
|
pydantic-settings = "^2.0.3"
|
||||||
|
pytest-asyncio = "^0.21.1"
|
||||||
|
python = "^3.11"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
black = "^23.7.0"
|
||||||
|
isort = "^5.12.0"
|
||||||
|
pylint = "^2.17.5"
|
||||||
|
pylint-pydantic = "^0.2.4"
|
||||||
|
pylint-pytest = "^1.1.2"
|
||||||
|
pyright = "^1.1.318"
|
||||||
|
pyupgrade = "^3.10.1"
|
||||||
|
ruff = "^0.0.282"
|
||||||
|
sort-all = "^1.2.0"
|
||||||
|
toml-sort = "^0.23.1"
|
||||||
|
|
||||||
|
[tool.pylint]
|
||||||
|
disable = [
|
||||||
|
"broad-except",
|
||||||
|
"cannot-enumerate-pytest-fixtures",
|
||||||
|
"consider-using-from-import",
|
||||||
|
"consider-using-sys-exit",
|
||||||
|
"duplicate-code",
|
||||||
|
"fixme",
|
||||||
|
"missing-docstring",
|
||||||
|
"no-member",
|
||||||
|
"protected-access",
|
||||||
|
"too-few-public-methods",
|
||||||
|
"too-many-instance-attributes",
|
||||||
|
"too-many-locals",
|
||||||
|
"too-many-statements",
|
||||||
|
"unnecessary-ellipsis"
|
||||||
|
]
|
||||||
|
extension-pkg-allow-list = [
|
||||||
|
"orjson",
|
||||||
|
"pydantic"
|
||||||
|
]
|
||||||
|
ignore-path = [
|
||||||
|
"^.*venv/.*$"
|
||||||
|
]
|
||||||
|
load-plugins = [
|
||||||
|
"pylint_pydantic",
|
||||||
|
"pylint_pytest"
|
||||||
|
]
|
||||||
|
max-args = 15
|
||||||
|
max-line-length = 120
|
||||||
|
recursive = true
|
||||||
|
|
||||||
|
[tool.pylint.basic]
|
||||||
|
argument-rgx = "^_{0,2}[a-z][a-z0-9_]*$"
|
||||||
|
attr-rgx = "^_{0,2}[a-z][a-z0-9_]*$"
|
||||||
|
class-attribute-rgx = "^_{0,2}[a-zA-Z][a-zA-Z0-9_]*$"
|
||||||
|
variable-rgx = "^_{0,2}[a-z][a-z0-9_]*$"
|
||||||
|
|
||||||
|
[tool.pyright]
|
||||||
|
exclude = [
|
||||||
|
".pytest_cache",
|
||||||
|
".venv"
|
||||||
|
]
|
||||||
|
pythonPlatform = "All"
|
||||||
|
pythonVersion = "3.11"
|
||||||
|
reportConstantRedefenition = "none"
|
||||||
|
reportMissingTypeStubs = "none"
|
||||||
|
reportPrivateUsage = "information"
|
||||||
|
reportPropertyTypeMismatch = "warning"
|
||||||
|
reportUninitializedInstanceVariable = "warning"
|
||||||
|
reportUnknownMemberType = "none"
|
||||||
|
reportUnnecessaryTypeIgnoreComment = "warning"
|
||||||
|
reportUntypedFunctionDecorator = "warning"
|
||||||
|
typeCheckingMode = "strict"
|
||||||
|
useLibraryCodeForTypes = true
|
||||||
|
venv = ".venv"
|
||||||
|
venvPath = "."
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
ignore = [
|
||||||
|
# Pyright automatically infers the type of `self`
|
||||||
|
"ANN101",
|
||||||
|
# Pyright automatically infers the type of `cls`
|
||||||
|
"ANN102",
|
||||||
|
# In some cases actively detrimental; somewhat conflicts with black
|
||||||
|
"COM",
|
||||||
|
# Ignore missing docstrings
|
||||||
|
"D102",
|
||||||
|
# In combination with D213, this results in noisy diffs and inconsistencies
|
||||||
|
# See also <https://github.com/charliermarsh/ruff/issues/4174>.
|
||||||
|
"D200",
|
||||||
|
# This results inconsistencies between function and class docstrings
|
||||||
|
# See also <https://github.com/charliermarsh/ruff/issues/4175>.
|
||||||
|
"D202",
|
||||||
|
# D211 is preferred since the extra blank line isn't visually useful
|
||||||
|
"D203",
|
||||||
|
# D213 is preferred since it's more readable and allows more characters
|
||||||
|
"D212",
|
||||||
|
# Ignore missing docstrings
|
||||||
|
"D414",
|
||||||
|
# Covered by D401, which is more restrictive
|
||||||
|
"D415",
|
||||||
|
# Type-checkers interpret redundant `as` as exporting an item
|
||||||
|
"PLC0414",
|
||||||
|
# Permit using alias for 'import'
|
||||||
|
"PLR0402",
|
||||||
|
# Causes churn and awful looking import blocks for little gain
|
||||||
|
"TCH"
|
||||||
|
]
|
||||||
|
select = ["ALL"]
|
||||||
|
|
||||||
|
[tool.ruff.per-file-ignores]
|
||||||
|
"tests/*" = [
|
||||||
|
"D100",
|
||||||
|
"D103",
|
||||||
|
"D104",
|
||||||
|
"S101"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.tomlsort]
|
||||||
|
all = true
|
||||||
|
ignore_case = true
|
||||||
|
in_place = true
|
5
src/bot_aiogram/tgbot/__init__.py
Normal file
5
src/bot_aiogram/tgbot/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .settings import Settings
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Settings",
|
||||||
|
]
|
0
src/bot_aiogram/tgbot/filters/__init__.py
Normal file
0
src/bot_aiogram/tgbot/filters/__init__.py
Normal file
9
src/bot_aiogram/tgbot/handlers/__init__.py
Normal file
9
src/bot_aiogram/tgbot/handlers/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from .echo import *
|
||||||
|
from .user import *
|
||||||
|
from .voice import *
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"register_echo",
|
||||||
|
"register_user",
|
||||||
|
"register_voice_response",
|
||||||
|
]
|
11
src/bot_aiogram/tgbot/handlers/echo.py
Normal file
11
src/bot_aiogram/tgbot/handlers/echo.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import aiogram
|
||||||
|
|
||||||
|
|
||||||
|
async def bot_echo(message: aiogram.types.Message):
|
||||||
|
text = ["Эхо без состояния.", "Сообщение:", message.text]
|
||||||
|
|
||||||
|
await message.answer("\n".join(text))
|
||||||
|
|
||||||
|
|
||||||
|
def register_echo(dp: aiogram.Dispatcher):
|
||||||
|
dp.register_message_handler(bot_echo)
|
9
src/bot_aiogram/tgbot/handlers/user.py
Normal file
9
src/bot_aiogram/tgbot/handlers/user.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import aiogram
|
||||||
|
|
||||||
|
|
||||||
|
async def user_start(message: aiogram.types.Message):
|
||||||
|
await message.reply("Hello, user! Send me a voice message and I'll try to recognize it and answer you.")
|
||||||
|
|
||||||
|
|
||||||
|
def register_user(dp: aiogram.Dispatcher):
|
||||||
|
dp.register_message_handler(user_start, commands=["start"], state="*")
|
55
src/bot_aiogram/tgbot/handlers/voice.py
Normal file
55
src/bot_aiogram/tgbot/handlers/voice.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import aiogram
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
import tgbot.settings as tgbot_settings
|
||||||
|
|
||||||
|
|
||||||
|
async def voice_response(message_voice: aiogram.types.Message):
|
||||||
|
config = typing.cast(tgbot_settings.Settings, message_voice.bot.get("config"))
|
||||||
|
|
||||||
|
voice_file_id: str = message_voice.voice.file_id
|
||||||
|
file_info = await message_voice.bot.get_file(voice_file_id)
|
||||||
|
file_path: str = file_info.file_path
|
||||||
|
voice_data: io.BytesIO = io.BytesIO()
|
||||||
|
voice_data.name = "voice.ogg"
|
||||||
|
voice_data.seek(0)
|
||||||
|
|
||||||
|
await message_voice.bot.download_file(file_path, destination=voice_data)
|
||||||
|
await message_voice.bot.send_chat_action(message_voice.from_user.id, "typing")
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
f"{config.api.api_url}/api/v1/voice/",
|
||||||
|
data={"voice": voice_data},
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
voice_answer: bytes = await resp.read()
|
||||||
|
answer_io = io.BytesIO(voice_answer)
|
||||||
|
answer_io.name = "answer_io.ogg"
|
||||||
|
|
||||||
|
await message_voice.bot.send_chat_action(
|
||||||
|
message_voice.from_user.id, action=aiogram.types.ChatActions.RECORD_AUDIO
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await message_voice.answer_voice(voice=answer_io)
|
||||||
|
except aiogram.exceptions.BadRequest:
|
||||||
|
await message_voice.answer(
|
||||||
|
"We were unable to send you a voice message. Please check your privacy settings."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
error_text: str = await resp.text()
|
||||||
|
if error_text == "":
|
||||||
|
await message_voice.answer(f"Error: {resp.status}")
|
||||||
|
else:
|
||||||
|
await message_voice.answer(f"Error: {json.loads(error_text)['detail']}")
|
||||||
|
await session.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def register_voice_response(dp: aiogram.Dispatcher):
|
||||||
|
dp.register_message_handler(voice_response, content_types=aiogram.types.ContentType.VOICE)
|
0
src/bot_aiogram/tgbot/keyboards/__init__.py
Normal file
0
src/bot_aiogram/tgbot/keyboards/__init__.py
Normal file
0
src/bot_aiogram/tgbot/keyboards/inline.py
Normal file
0
src/bot_aiogram/tgbot/keyboards/inline.py
Normal file
0
src/bot_aiogram/tgbot/keyboards/reply.py
Normal file
0
src/bot_aiogram/tgbot/keyboards/reply.py
Normal file
5
src/bot_aiogram/tgbot/middlewares/__init__.py
Normal file
5
src/bot_aiogram/tgbot/middlewares/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .environment import *
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"EnvironmentMiddleware",
|
||||||
|
]
|
14
src/bot_aiogram/tgbot/middlewares/environment.py
Normal file
14
src/bot_aiogram/tgbot/middlewares/environment.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import aiogram.dispatcher.middlewares as dispatcher_middlewares
|
||||||
|
|
||||||
|
|
||||||
|
class EnvironmentMiddleware(dispatcher_middlewares.LifetimeControllerMiddleware):
|
||||||
|
skip_patterns = ["error", "update"]
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: typing.Any):
|
||||||
|
super().__init__()
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
async def pre_process(self, obj: typing.Any, data: dict[typing.Any, typing.Any], *args: typing.Any):
|
||||||
|
data.update(**self.kwargs)
|
0
src/bot_aiogram/tgbot/misc/__init__.py
Normal file
0
src/bot_aiogram/tgbot/misc/__init__.py
Normal file
0
src/bot_aiogram/tgbot/misc/states.py
Normal file
0
src/bot_aiogram/tgbot/misc/states.py
Normal file
4
src/bot_aiogram/tgbot/misc/utils.py
Normal file
4
src/bot_aiogram/tgbot/misc/utils.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
BASE_PATH = pathlib.Path(__file__).parent.parent.parent.parent.resolve()
|
||||||
|
ENV_PATH = BASE_PATH / ".env"
|
0
src/bot_aiogram/tgbot/models/__init__.py
Normal file
0
src/bot_aiogram/tgbot/models/__init__.py
Normal file
0
src/bot_aiogram/tgbot/services/__init__.py
Normal file
0
src/bot_aiogram/tgbot/services/__init__.py
Normal file
8
src/bot_aiogram/tgbot/settings.py
Normal file
8
src/bot_aiogram/tgbot/settings.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import pydantic_settings
|
||||||
|
|
||||||
|
import tgbot.split_settings as app_split_settings
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(pydantic_settings.BaseSettings):
|
||||||
|
api: app_split_settings.ApiSettings = app_split_settings.ApiSettings()
|
||||||
|
tgbot: app_split_settings.TgBotSettings = app_split_settings.TgBotSettings()
|
7
src/bot_aiogram/tgbot/split_settings/__init__.py
Normal file
7
src/bot_aiogram/tgbot/split_settings/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from .api import *
|
||||||
|
from .tgbot import *
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ApiSettings",
|
||||||
|
"TgBotSettings",
|
||||||
|
]
|
20
src/bot_aiogram/tgbot/split_settings/api.py
Normal file
20
src/bot_aiogram/tgbot/split_settings/api.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import pydantic_settings
|
||||||
|
|
||||||
|
import tgbot.split_settings.utils as split_settings_utils
|
||||||
|
|
||||||
|
|
||||||
|
class ApiSettings(pydantic_settings.BaseSettings):
|
||||||
|
model_config = pydantic_settings.SettingsConfigDict(
|
||||||
|
env_file=split_settings_utils.ENV_PATH,
|
||||||
|
env_prefix="API_",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
url: str = "127.0.0.1"
|
||||||
|
port: int = 8000
|
||||||
|
protocol: str = "http"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_url(self) -> str:
|
||||||
|
return f"{self.protocol}://{self.url}:{self.port}"
|
22
src/bot_aiogram/tgbot/split_settings/tgbot.py
Normal file
22
src/bot_aiogram/tgbot/split_settings/tgbot.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import pydantic
|
||||||
|
import pydantic_settings
|
||||||
|
|
||||||
|
import tgbot.split_settings.utils as split_settings_utils
|
||||||
|
|
||||||
|
|
||||||
|
class TgBotSettings(pydantic_settings.BaseSettings):
|
||||||
|
model_config = pydantic_settings.SettingsConfigDict(
|
||||||
|
env_file=split_settings_utils.ENV_PATH,
|
||||||
|
env_prefix="BOT_",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
token: pydantic.SecretStr = pydantic.Field(
|
||||||
|
default=..., validation_alias=pydantic.AliasChoices("token", "bot_token")
|
||||||
|
)
|
||||||
|
admins: str = pydantic.Field(default="")
|
||||||
|
|
||||||
|
@pydantic.field_validator("admins")
|
||||||
|
def validate_bot_admins(cls, v: str) -> list[int]:
|
||||||
|
return list(map(int, v.split(",")))
|
4
src/bot_aiogram/tgbot/split_settings/utils.py
Normal file
4
src/bot_aiogram/tgbot/split_settings/utils.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
BASE_PATH = pathlib.Path(__file__).parent.parent.parent.resolve()
|
||||||
|
ENV_PATH = BASE_PATH / ".env"
|
Loading…
Reference in New Issue
Block a user