Compare commits

..

No commits in common. "main" and "v0.0.3" have entirely different histories.
main ... v0.0.3

82 changed files with 201 additions and 4876 deletions

View File

@ -2,4 +2,3 @@
venv venv
config.json config.json
*.yaml *.yaml
docs/

View File

@ -1,3 +1,3 @@
[flake8] [flake8]
exclude = .git,__pycache__,venv,.venv exclude = .git,__pycache__,venv
max-line-length = 120 max-line-length = 120

View File

@ -4,8 +4,6 @@ on: push
env: env:
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
POETRY_VERSION: 1.1.2
POETRY_VIRTUALENVS_CREATE: "false"
jobs: jobs:
lint: lint:
@ -19,7 +17,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install poetry pip install -r requirements.txt
poetry install pip install flake8
- name: Check flake8 - name: Check flake8
run: flake8 . run: python -m flake8 .

9
.gitignore vendored
View File

@ -5,11 +5,4 @@ venv
__pycache__ __pycache__
*.pyc *.pyc
config.json config.json
docker-compose-release.yaml docker-compose-release.yaml
docs/build
ad.md
release.env
test.py
backup
*.mo
*.pot

View File

@ -1,26 +1,11 @@
FROM python:3.8-buster FROM python:3.8-buster
ENV PYTHONUNBUFFERED=1 \
POETRY_VERSION=1.1.12 \
POETRY_VIRTUALENVS_CREATE="false"
RUN apt-get update && \
apt-get install -y gettext build-essential && \
apt-get clean && rm -rf /var/cache/apt/* && rm -rf /var/lib/apt/lists/* && rm -rf /tmp/*
RUN pip install "poetry==$POETRY_VERSION"
WORKDIR /app
COPY pyproject.toml poetry.lock docker-entrypoint.sh ./
RUN poetry install --no-interaction --no-ansi --no-dev
COPY . /app COPY . /app
RUN msgfmt locales/zh/LC_MESSAGES/olgram.po -o locales/zh/LC_MESSAGES/olgram.mo --use-fuzzy WORKDIR /app
RUN msgfmt locales/uk/LC_MESSAGES/olgram.po -o locales/uk/LC_MESSAGES/olgram.mo --use-fuzzy
RUN msgfmt locales/en/LC_MESSAGES/olgram.po -o locales/en/LC_MESSAGES/olgram.mo --use-fuzzy RUN pip install --upgrade pip && \
pip install -r requirements.txt
EXPOSE 80 EXPOSE 80

View File

@ -1,26 +1,44 @@
# OLGram # OLGram
[@olgram](https://t.me/olgrambot) - конструктор ботов обратной связи в Telegram
[![Static Analysis Status](https://github.com/civsocit/olgram/workflows/Linter/badge.svg)](https://github.com/civsocit/olgram/actions?workflow=Linter) [![Static Analysis Status](https://github.com/civsocit/olgram/workflows/Linter/badge.svg)](https://github.com/civsocit/olgram/actions?workflow=Linter)
[![Deploy Status](https://github.com/civsocit/olgram/workflows/Deploy/badge.svg)](https://github.com/civsocit/olgram/actions?workflow=Deploy) [![Deploy Status](https://github.com/civsocit/olgram/workflows/Deploy/badge.svg)](https://github.com/civsocit/olgram/actions?workflow=Deploy)
[![Documentation](https://readthedocs.org/projects/olgram/badge/?version=latest)](https://olgram.readthedocs.io)
[@OlgramBot](https://t.me/olgrambot) - конструктор ботов обратной связи в Telegram ![Logo](media/logo1_big.png)
Документация: https://olgram.readthedocs.io ## Возможности и преимущества Olgram Bot
* **Общение с клиентами**. После подключения бота, вы сможете общаться с вашими пользователями бота через диалог с
ботом, либо подключенный отдельно чат, где может находиться ваш колл-центр.
* **Все типы сообщений**. Livegram боты поддерживают все типы сообщений — текст, фото, видео, голосовые сообщения и
стикеры.
* **Open-source**. В отличие от известного проекта Livegram код нашего конструктора полностью открыт.
* **Self-hosted**. Вы можете развернуть свой собственный конструктор, если не доверяете нашему.
* **Безопасность**. В отличие от Livegram, мы не храним сообщения, которые вы отправляете в бот. А наши сервера
располагаются в Германии, что делает проект неподконтрольным российским властям.
**Olgram** [@OlgramBot](https://t.me/olgrambot) это конструктор, который позволяет создавать боты обратной связи По любым вопросам, связанным с Olgram, пишите в наш бот обратной связи
в Telegram. После подключения к Olgram пользователи вашего бота смогут писать сообщения, которые будут [@civsocit_feedback_bot](https://t.me/civsocit_feedback_bot)
пересылаться вам в чат, где вы сможете на них ответить.
Такие боты могут вам пригодиться, например: ### Для разработчиков: сборка и запуск проекта
*Пример 1.* Вы администрируете Telegram-канал и хотите дать своим подписчикам возможность связаться с вами, Вам потребуется собственный VPS или любой хост со статическим адресом или доменом.
но не хотите оставлять свои личные контакты. Тогда вы можете создать бота обратной связи: подписчики будут писать * Создайте файл .env и заполните его по образцу example.env. Вам нужно заполнить переменные:
боту, вы будете отвечать через бота анонимно. * BOT_TOKEN - токен нового бота, получить у [@botfather](https://t.me/botfather)
* POSTGRES_PASSWORD - любой случайный пароль
* WEBHOOK_HOST - IP адрес или доменное имя сервера, на котором запускается проект
* Сохраните файл docker-compose.yaml и соберите его:
```
sudo docker-compose up -d
```
*Пример 2.* Вы организуете небольшой call-центр в Telegram или группу технической поддержки. С помощью бота обратной В docker-compose.yaml минимальная конфигурация. Для использования в серьёзных проектах мы советуем:
связи вы можете принимать заявки от пользователей в общий чат ваших специалистов, обсуждать эти заявки и отвечать * Приобрести домен и настроить его на свой хост
пользователям прямо из этого чата. * Наладить реверс-прокси и автоматическое обновление сертификатов - например, с помощью
[Traefik](https://github.com/traefik/traefik)
* Скрыть IP сервера с помощью [Cloudflire](https://www.cloudflare.com), чтобы пользователи ботов не могли найти IP адрес
хоста по Webhook бота.
Читайте больше: https://olgram.readthedocs.io Пример более сложной конфигурации есть в файле docker-compose-full.yaml

View File

@ -1,9 +1,7 @@
# Конфигурация, удобная для разработки в PyCharm: бот запускается без docker, порты postgres и redis открыты на localhost
# Не используйте её в production!
version: '3' version: '3'
services: services:
postgres: postgres:
image: postgres:14 image: kartoza/postgis
environment: environment:
- POSTGRES_USER=test_user - POSTGRES_USER=test_user
- POSTGRES_PASSWORD=test_passwd - POSTGRES_PASSWORD=test_passwd
@ -13,7 +11,7 @@ services:
volumes: volumes:
- database:/var/lib/postgresql/data - database:/var/lib/postgresql/data
redis: redis:
image: 'bitnami/redis:6.2.7' image: 'bitnami/redis:latest'
environment: environment:
- ALLOW_EMPTY_PASSWORD=yes - ALLOW_EMPTY_PASSWORD=yes
volumes: volumes:

View File

@ -1,8 +1,7 @@
# Пример сложной конфигурации сервера: реверс-прокси, автоматическое обновление github
version: '3' version: '3'
services: services:
postgres: postgres:
image: postgres:13.4 image: postgres
restart: unless-stopped restart: unless-stopped
env_file: env_file:
- release.env - release.env
@ -10,10 +9,8 @@ services:
- database:/var/lib/postgresql/data - database:/var/lib/postgresql/data
networks: networks:
- traefik - traefik
labels:
- 'com.centurylinklabs.watchtower.enable="false"'
redis: redis:
image: 'bitnami/redis:6.2.7' image: 'bitnami/redis:latest'
restart: unless-stopped restart: unless-stopped
environment: environment:
- ALLOW_EMPTY_PASSWORD=yes - ALLOW_EMPTY_PASSWORD=yes
@ -23,8 +20,6 @@ services:
- release.env - release.env
networks: networks:
- traefik - traefik
labels:
- 'com.centurylinklabs.watchtower.enable="false"'
olgram: olgram:
image: ghcr.io/civsocit/olgram/bot:stable image: ghcr.io/civsocit/olgram/bot:stable
restart: unless-stopped restart: unless-stopped
@ -76,8 +71,6 @@ services:
- --certificatesresolvers.le.acme.tlschallenge=false - --certificatesresolvers.le.acme.tlschallenge=false
- --certificatesresolvers.le.acme.httpchallenge=true - --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
labels:
- 'com.centurylinklabs.watchtower.enable="false"'
volumes: volumes:
database: database:

View File

@ -1,36 +0,0 @@
# Минимальная конфигурация сервера, код собирается из текущей директории
version: '3'
services:
postgres:
image: postgres:13.4
restart: unless-stopped
env_file:
- .env
volumes:
- database:/var/lib/postgresql/data
redis:
image: 'bitnami/redis:latest'
restart: unless-stopped
environment:
- ALLOW_EMPTY_PASSWORD=yes
volumes:
- redis-db:/bitnami/redis/data
env_file:
- .env
olgram:
build: .
restart: unless-stopped
env_file:
- .env
volumes:
- olgram-cert:/cert
ports:
- "${WEBHOOK_PORT}:80"
depends_on:
- postgres
- redis
volumes:
database:
redis-db:
olgram-cert:

View File

@ -1,4 +1,3 @@
# Минимальная конфигурация сервера
version: '3' version: '3'
services: services:
postgres: postgres:
@ -9,7 +8,7 @@ services:
volumes: volumes:
- database:/var/lib/postgresql/data - database:/var/lib/postgresql/data
redis: redis:
image: 'bitnami/redis:6.2.7' image: 'bitnami/redis:latest'
restart: unless-stopped restart: unless-stopped
environment: environment:
- ALLOW_EMPTY_PASSWORD=yes - ALLOW_EMPTY_PASSWORD=yes

View File

@ -5,11 +5,10 @@ if [ ! -z "${CUSTOM_CERT}" ]; then
echo "Use custom certificate" echo "Use custom certificate"
if [ ! -f /cert/private.key ]; then if [ ! -f /cert/private.key ]; then
echo "Generate new certificate" echo "Generate new certificate"
openssl req -newkey rsa:2048 -sha256 -nodes -keyout /cert/private.key -x509 -days 10000 -out /cert/public.pem -subj "/C=US/ST=Berlin/L=Berlin/O=my_org/CN=${WEBHOOK_HOST}" openssl req -newkey rsa:2048 -sha256 -nodes -keyout /cert/private.key -x509 -days 1000 -out /cert/public.pem -subj "/C=US/ST=Berlin/L=Berlin/O=my_org/CN=${WEBHOOK_HOST}"
fi fi
fi fi
sleep 10 sleep 10
aerich upgrade aerich upgrade
python migrate.py python main.py
python main.py $@

View File

@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

View File

@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@ -1,41 +0,0 @@
О проекте
===================================
Зачем нужен Olgram
------------
Olgram - это конструктор ботов обратной связи. Такие боты могут вам пригодиться, например:
*Пример 1.* Вы администрируете Telegram-канал и хотите дать своим подписчикам возможность связаться с вами,
но не хотите оставлять свои личные контакты. Тогда вы можете создать бота обратной связи: подписчики будут писать
боту, вы будете отвечать через бота анонимно.
*Пример 2.* Вы организуете небольшой call-центр в Telegram или группу технической поддержки. С помощью бота обратной
связи вы можете принимать заявки от пользователей в общий чат ваших специалистов, обсуждать эти заявки и отвечать
пользователям прямо из этого чата.
.. note::
Olgram - молодой развивающийся проект. Мы готовы расширить функционал бота под ваш сценарий использования, если он
не является слишком узкоспециализированным и пригодится другим пользователям. Напишите нам по этому вопросу
`@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.
Почему не Livegram?
------------
Наше принципиальное отличие от `Livegram <https://t.me/LivegramBot>`_ это открытость и безопасность.
* Olgram не хранит сообщения, которые вы пересылаете через него
* Код нашего проекта `полностью открыт <https://github.com/civsocit/olgram>`_
* Вы можете развернуть Olgram на своём собственном сервере (читайте :doc:`developer`)
* Наши сервера находятся в Германии, мы не подконтрольны российскому или белорусскому правительству
Всё это позволяет вам использовать Olgram (в отличие от Livegram) в политических и других серьёзных проектах.
Чтобы приступить к созданию своего первого бота, откройте главу :doc:`quick_start`
.. note::
Если у вас возникли вопросы по использованию бота, или вы нашли ошибку - напишите
нам `@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.

View File

@ -1,44 +0,0 @@
Дополнительно
=============
Донаты
----------------
На рекламу проекта, аренду сервера и пиццу
Bitcoin:
``bc1qlq7cm5chc8flr3fy8ewk967aknq3dwmxtwn9hl``
Litecoin:
``LTC1QXAJSVZ0LW44AA5NYTUCH8CP2G8X7A4CDASE4Y7``
Как убрать "Этот бот создан с помощью ..."
----------------
Напишите нам на `@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.
История изменений
----------------
- `2022-08-01` Защита от флуда
- `2022-07-23` Автоответчик не пишет сообщение лишний раз
- `2022-07-04` Поддержка двух ботов в одном чате
- `2022-06-25` Поддержка HTML\Markdown в стартовом сообщении и автоответчике
- `2022-06-25` Пересылка отредактированных сообщений
- `2022-06-16` User info по возможности отправляется в том же сообщении, что и сообщение пользователя
- `2022-05-26` Возможность отвечать на более старые сообщения (1/2 года)
- `2022-04-11` Частичная поддержка украинского, английского языка
- `2022-04-09` 'Этот бот создан с помощью...' возможность выключать по промо
- `2022-03-17` Политика конфиденциальности
- `2022-03-17` Дополнительная информация о пользователях (имя пользователя и тд)
- `2022-02-19` Статистика использования бота
- `2022-02-16` Потоки сообщений
- `2022-02-16` Очистка Redis по timeout
- `2022-02-12` Шаблоны ответов
- `2022-01-27` Настройки логирования
- `2022-01-18` Команды /ban и /unban (возможность банить пользователей)
- `2021-12-14` Bugfix обработка изменения ID чата
- `2021-10-01` Возможность ограничивать права на бота (ADMIN_ID)
- `2021-09-26` Шифрование токенов
- `2021-09-26` Добавлен автоответчик
- `2021-09-24` Initial

View File

@ -1,35 +0,0 @@
# Configuration file for the Sphinx documentation builder.
# -- Project information
project = 'Olgram'
copyright = '2023, Civsocit'
author = 'civsocit'
release = '0.1'
version = '0.1.0'
# -- General configuration
extensions = [
'sphinx.ext.duration',
'sphinx.ext.doctest',
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
'sphinx.ext.intersphinx',
]
intersphinx_mapping = {
'python': ('https://docs.python.org/3/', None),
'sphinx': ('https://www.sphinx-doc.org/en/master/', None),
}
intersphinx_disabled_domains = ['std']
templates_path = ['_templates']
# -- Options for HTML output
html_theme = 'sphinx_rtd_theme'
# -- Options for EPUB output
epub_show_urls = 'footnote'

View File

@ -1,80 +0,0 @@
Для разработчиков
=================
.. _run:
Сборка и запуск
---------------
Вы можете развернуть Olgram на своём сервере. Вам потребуется собственный VPS или любой хост со статическим адресом
или доменом.
1. Создайте файл .env и заполните его по образцу `example.env <https://github.com/civsocit/olgram/blob/main/example.env>`_
Вам нужно заполнить переменные:
* ``BOT_TOKEN`` - токен нового бота, получить у `@botfather <https://t.me/botfather>`_
* ``POSTGRES_PASSWORD`` - любой случайный пароль
* ``TOKEN_ENCRYPTION_KEY`` - любой случайный пароль, отличный от POSTGRES_PASSWORD
* ``WEBHOOK_HOST`` - IP адрес или доменное имя сервера, на котором запускается проект
2. Рядом с файлом .env сохраните файл
`docker-compose.yaml <https://github.com/civsocit/olgram/blob/main/docker-compose.yaml>`_ и соберите его:
.. code-block:: console
(bash) $ sudo docker-compose up -d
Готово, ваш собственный Olgram запущен!
.. warning::
Не потеряйте TOKEN_ENCRYPTION_KEY! Его нельзя восстановить. В случае утери TOKEN_ENCRYPTION_KEY вы потеряете
токены всех ботов, которые пользователи зарегистрировали в вашем боте.
Возможно, вы захотите внести изменения в проект и запустить бот с этими изменениями. Тогда:
1. Склонируйте репозиторий
.. code-block:: console
(bash) $ git clone https://github.com/civsocit/olgram
2. Внесите в код все изменения, которые хотите внести
3. В каталоге с репозиторием (рядом с файлами .yaml) создайте файл .env и заполните его, как в инструкции выше
4. Соберите и запустите сервер:
.. code-block:: console
(bash) $ sudo docker-compose -f docker-compose-src.yaml up -d
Дополнительно
-------------
В docker-compose.yaml приведена минимальная конфигурация. Для использования в серьёзных проектах мы советуем:
* Приобрести домен и настроить его на свой хост
* Наладить реверс-прокси и автоматическое обновление сертификатов - например, с помощью `Traefik <https://github.com/traefik/traefik>`_
* Скрыть IP сервера с помощью `Cloudflare <https://www.cloudflare.com>`_, чтобы пользователи ботов не могли найти IP адрес хоста по Webhook бота.
Пример более сложной конфигурации есть в файле `docker-compose-full.yaml <https://github.com/civsocit/olgram/blob/main/docker-compose-full.yaml>`_
Как ограничить доступ к своему боту
-----------------------------------
По-умолчанию все пользователи Telegram могут писать в ваш Olgram и регистрировать там своих ботов. Чтобы ограничить
доступ к боту, укажите в переменных окружения (файл .env):
``ADMIN_ID=<идентификатор чата>``
Идентификатор чата это либо ваш Telegram ID, либо ID группового чата Telegram. Идентификатор можно посмотреть
командой /chatid.
Настройка языка
---------------
Язык по-умолчанию - русский. Поддержку другого языка можно добавлять по образцу китайского в папку locales/
(китайский - zh). Код языка указать в настройках .env
``O_LANG=<идентификатор языка>``

View File

@ -1,25 +0,0 @@
Добро пожаловать в документацию Olgram
===================================
**Olgram** `@olgrambot <https://t.me/olgrambot>`_ это конструктор, который позволяет создавать боты обратной связи
в Telegram. После подключения к Olgram пользователи вашего бота смогут писать сообщения, которые будут
пересылаться вам в чат, где вы сможете на них ответить. Читайте больше о проекте в главе :doc:`about`.
Откройте главу :doc:`quick_start` чтобы приступить к созданию своего первого бота!
Оглавление
--------
.. toctree::
about
quick_start
templates
options
developer
additional
.. note::
Если у вас возникли вопросы по использованию бота, или вы нашли ошибку - напишите
нам `@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.

View File

@ -1,51 +0,0 @@
Опции
=============
.. _threads:
Потоки сообщений
----------------
Olgram пересылает сообщения так, чтобы сообщения от одного и того же пользователя оставались в одном и том же
потоке сообщений. Тогда по кнопке View Replies можно увидеть диалог с этим пользователем, а все остальные сообщения из
чата скрываются:
.. image:: ../images/thread.gif
:width: 300
**Как настроить потоки сообщений**
Привяжите вашего feedback бота к групповому чату :doc:`quick_start`. В настройках группового чата откройте историю
чата для новых участников чата ("Chat history for new members -> Visible"). Изменение этой настройки превращает чат в
`супергруппу <https://telegram.org/blog/supergroups5k>`_: потоки сообщений работают только в таких группах
Включите потоки в настройках бота Olgram Опции->Потоки сообщений
.. _user_info:
Данные пользователя
-------------------
При получении входящего сообщения Olgram может пересылать дополнительную информацию об отправителе. Имя, username и
идентификатор пользователя. Например так:
.. image:: ../images/user_info.jpg
:width: 300
Эта функция может быть полезной, чтобы отличить одного пользователя от другого. Имя и username можно сменить, но
идентификатор #id остаётся неизменным для одного и того же аккаунта.
Включить эту функцию можно в настройках бота Olgram Опции->Данные пользователя
.. note::
Включение этой опции меняет текст политики конфиденциальности вашего feedback бота (команда /security_policy)
и может отпугнуть некоторых пользователей. Не включайте эту опцию без необходимости.
.. _antiflood:
Защита от флуда
---------------
При включении этой опции пользователю запрещается отправлять больше одного сообщения в минуту. Используйте её, если
не успеваете обрабатывать входящие сообщения.

View File

@ -1,110 +0,0 @@
Быстрый старт
=============
Как создать бота
----------------
Перейдите по ссылке `@OlgramBot <https://t.me/olgrambot>`_ и нажмите Запустить:
.. image:: ../images/start.jpg
:width: 300
Нажмите ссылку "addbot":
.. image:: ../images/addbot.jpg
:width: 300
И перейдите по ссылке создания бота:
.. image:: ../images/botfather.jpg
:width: 300
BotFather - это официальный бот Telegram, создающий другие боты, которые и будут помогать вам управлять каналами.
Запустите его как и Olgram bot и кликните по ссылке "/newbot":
.. image:: ../images/botfathernew.png
:width: 300
В диалоге прописываете, как вы хотите что бы назывался бот, и название ссылки, ведущей к этому боту. В итоге вы
получаете токен:
.. image:: ../images/botfathertoken.png
:width: 300
Скопируйте этот токен и отправьте в Olgram:
.. image:: ../images/added.jpg
:width: 300
Готово! Бот добавлен в Olgram. Теперь для человека,желающего что-то спросить, бот будет выглядеть примерно так:
.. image:: ../images/test.jpg
:width: 300
Для вас же это будет выглядеть так:
.. image:: ../images/test2.jpg
:width: 300
Как изменить текст приветствия
------------------------------
По-умолчанию ваш бот после запуска отправляет приветственное сообщение:
Здравствуйте! Напишите свой вопрос, и мы ответим вам в ближайшее время
Вы можете настроить этот текст. Для этого откройте список ботов командой /mybots и выберите нужного бота:
.. image:: ../images/text1.jpg
:width: 300
В появившемся меню выберите "Текст"
.. image:: ../images/text2.jpg
:width: 300
Теперь просто отправьте новый текст приветствия.
Как привязать бота к групповому чату
------------------------------------
По-умолчанию ваш бот пересылает сообщения от пользователей вам в личные сообщения. Бота можно привязать к групповому
чату. Для этого добавьте его в групповой чат. Затем откройте список ботов, как в примере выше, выберите нужного бота
и нажмите кнопку "Чат":
.. image:: ../images/chat1.jpg
:width: 300
Затем выберите в списке тот чат, в который добавили бота
.. image:: ../images/chat2.jpg
:width: 300
Готово. Теперь сообщения от пользователей будут пересылаться в групповой чат.
.. note::
Нужно сначала зарегистрировать своего бота в Olgram, и только потом добавить в групповой чат. Если бот уже
добавлен в групповой чат, удалите его оттуда и добавьте заново - тогда Olgram сможет пересылать туда сообщения.
Как блокировать и разблокировать пользователей
------------------------------------
Вы можете отправлять в бан пользователей feedback бота. Для этого есть команды /ban и /unban.
Например так:
.. image:: ../images/ban2.png
:width: 300
Со стороны пользователя этот диалог будет выглядеть так:
.. image:: ../images/ban1.png
:width: 300
.. note::
Если у вас возникли вопросы по использованию бота, или вы нашли ошибку - напишите
нам `@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.

View File

@ -1,32 +0,0 @@
Шаблоны ответов
=============
Иногда в поддержке приходится отвечать на однотипные вопросы однотипными ответами. Например:
Q. ``Здравствуйте! Когда будет доставлен мой заказ?``
A. ``Добрый день. Ваш заказ принят в обработку. Среднее время доставки 2-4 дня. Мы уведомим вас об изменении статуса заказа``
Чтобы не печатать каждый раз одинаковые тексты, в Olgram можно задать список шаблонных ответов. Тогда диалог с
пользователем может выглядеть так:
.. image:: ../images/inline.gif
:width: 300
Заметьте, чтобы увидеть список вариантов ответов, нужно упомянуть вашего feedback бота и нажать пробел
Как настроить шаблоны
---------------------
Шаблоны можно задать в меню Olgram бота Текст -> Автоответчик -> Шаблоны ответов.
.. image:: ../images/settemplates.jpg
:width: 300
Обязательно включите inline mode в вашем feedback боте. Для этого отправьте @BotFather команду ``/setinline``
и следуйте инструкциям
.. note::
Может пройти несколько минут, прежде чем добавленные в OlgramBot шаблоны появятся в списке вашего feedback бота

View File

@ -1,35 +1,12 @@
# example: 123456789:AAAA-abc123_AbcdEFghijKLMnopqrstu12 (without quotes!) BOT_TOKEN=YOUR_BOT_TOKEN_HERE # example: 123456789:AAAA-abc123_AbcdEFghijKLMnopqrstu12
BOT_TOKEN=YOUR_BOT_TOKEN_HERE
POSTGRES_USER=olgram POSTGRES_USER=olgram
POSTGRES_PASSWORD=SOME_RANDOM_PASSWORD_HERE # example: x2y0n27ihiez93kmzj82
# example: x2y0n27ihiez93kmzj82 (without quotes!)
POSTGRES_PASSWORD=SOME_RANDOM_PASSWORD_HERE
POSTGRES_DB=olgram POSTGRES_DB=olgram
POSTGRES_HOST=postgres POSTGRES_HOST=postgres
# example: i7flci0mx4z5patxnl6m (without quotes!) WEBHOOK_HOST=YOUR_HOST_HERE # example: 11.143.142.140 or my_domain.com
TOKEN_ENCRYPTION_KEY=SOME_RANDOM_PASSWORD_HERE WEBHOOK_PORT=8443 # allowed: 80, 443, 8080, 8443
CUSTOM_CERT=true # use that if you don't set up your own domain and let's encrypt certificate
# use your user id or group chat id to restrict access to the bot
# ADMIN_ID=223453418
# use your user id or group chat id to give selected users access to the bot's general statistics (/info command)
# SUPERVISOR_ID=223453419
# example: 11.143.142.140 or my_domain.com (without quotes, without 'https://' prefix!)
WEBHOOK_HOST=YOUR_HOST_HERE
# allowed: 80, 443, 8080, 8443
WEBHOOK_PORT=8443
# use that if you don't set up your own domain and let's encrypt certificate
CUSTOM_CERT=true
REDIS_PATH=redis://redis REDIS_PATH=redis://redis
# Set log level, can be CRITICAL, ERROR, WARNING, INFO, DEBUG. By default it set to WARNING.
LOGLEVEL=
# Uncomment this to switch bot language to English
# O_LANG=en

View File

@ -1,721 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2022-09-02 05:02+0400\n"
"PO-Revision-Date: 2022-09-02 05:07+0400\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 3.1\n"
#: olgram/commands/admin.py:21 olgram/commands/info.py:21
#: olgram/commands/promo.py:23 olgram/commands/promo.py:39
msgid "Недостаточно прав"
msgstr "Not enough permissions"
#: olgram/commands/admin.py:27
msgid "Нужно указать имя бота"
msgstr "You need to specify the bot's name"
#: olgram/commands/admin.py:33
msgid "Такого бота нет в системе"
msgstr "There is no such bot"
#: olgram/commands/admin.py:39 olgram/commands/admin.py:53
msgid "Пропустить"
msgstr "Skip"
#: olgram/commands/admin.py:42
msgid ""
"Введите текст, который будет отправлен владельцу бота {0}. Напишите "
"'Пропустить' чтобы отменить"
msgstr ""
"Enter the text that will be sent to the owner of the bot {0}. Write 'Skip' "
"to cancel"
#: olgram/commands/admin.py:50
msgid "Поддерживается только текст"
msgstr "Only text is supported"
#: olgram/commands/admin.py:55 olgram/commands/admin.py:71
msgid "Отменено"
msgstr "Cancelled"
#: olgram/commands/admin.py:61 olgram/commands/admin.py:69
msgid "Отправить"
msgstr "Send"
#: olgram/commands/admin.py:62
msgid "Отменить"
msgstr "Cancel"
#: olgram/commands/admin.py:81
msgid "Отправлено"
msgstr "Sent"
#: olgram/commands/bot_actions.py:22
msgid "Бот удалён"
msgstr "Bot removed"
#: olgram/commands/bot_actions.py:38 olgram/commands/bot_actions.py:50
msgid "Текст сброшен"
msgstr "Text is reset"
#: olgram/commands/bot_actions.py:64
msgid "Выбран личный чат"
msgstr "Personal chat selected"
#: olgram/commands/bot_actions.py:77
msgid "Бот вышел из чатов"
msgstr "Bot leaved chats"
#: olgram/commands/bot_actions.py:83
msgid "Нельзя привязать бота к этому чату"
msgstr "You can't bind a bot to this chat room"
#: olgram/commands/bot_actions.py:87
msgid "Выбран чат {0}"
msgstr "Selected chat {0}"
#: olgram/commands/bots.py:46
msgid ""
"У вас уже слишком много ботов. Удалите какой-нибудь свой бот из Olgram(/"
"mybots -> (Выбрать бота) -> Удалить бот)"
msgstr ""
"You already have too many bots. Remove any of your bots from Olgram(/mybots -"
"> (Select bot) -> Remove bot)"
#: olgram/commands/bots.py:50
msgid ""
"\n"
" Чтобы подключить бот, вам нужно выполнить три действия:\n"
"\n"
" 1. Перейдите в бот @BotFather, нажмите START и отправьте команду /"
"newbot\n"
" 2. Введите название бота, а потом username бота.\n"
" 3. После создания бота перешлите ответное сообщение в этот бот или "
"скопируйте и пришлите token бота.\n"
"\n"
" Важно: не подключайте боты, которые используются в других сервисах "
"(Manybot, Chatfuel, Livegram и других).\n"
" "
msgstr ""
"\n"
" To connect the bot, you need to follow three steps:\n"
"\n"
" 1. Go to bot @BotFather, press START and send command /newbot\n"
" 2. Enter the bot's name and then the bot's username.\n"
" 3. Once the bot is created, forward a reply message to this bot or copy "
"and send the bot's token.\n"
"\n"
" Important: do not connect bots that are used in other services (Manybot, "
"Chatfuel, Livegram and others).\n"
" "
#: olgram/commands/bots.py:70
msgid ""
"\n"
" Это не токен бота.\n"
"\n"
" Токен выглядит вот так: 123456789:AAAA-"
"abc123_AbcdEFghijKLMnopqrstu12\n"
" "
msgstr ""
"\n"
" This is not a bot token.\n"
"\n"
" The token looks like this: 123456789:AAAA-"
"abc123_AbcdEFghijKLMnopqrstu12\n"
" "
#: olgram/commands/bots.py:77
msgid ""
"\n"
" Не удалось запустить этого бота: неверный токен\n"
" "
msgstr ""
"\n"
" Failed to start this bot: Wrong token\n"
" "
#: olgram/commands/bots.py:82
msgid ""
"\n"
" Не удалось запустить этого бота: непредвиденная ошибка\n"
" "
msgstr ""
"\n"
" Failed to start this bot: unexpected error\n"
" "
#: olgram/commands/bots.py:87
msgid ""
"\n"
" Такой бот уже есть в базе данных\n"
" "
msgstr ""
"\n"
" Such a bot is already in the database\n"
" "
#: olgram/commands/bots.py:122
msgid "Бот добавлен! Список ваших ботов: /mybots"
msgstr "Bot added! List of your bots: /mybots"
#: olgram/commands/info.py:34
msgid "Количество ботов: {0}\n"
msgstr "Number of bots: {0}\n"
#: olgram/commands/info.py:35
msgid "Количество пользователей (у конструктора): {0}\n"
msgstr "Number of users (at the constructor): {0}\n"
#: olgram/commands/info.py:36
msgid "Шаблонов ответов: {0}\n"
msgstr "Answer templates: {0}\n"
#: olgram/commands/info.py:37
msgid "Входящих сообщений у всех ботов: {0}\n"
msgstr "Incoming messages from all bots: {0}\n"
#: olgram/commands/info.py:38
msgid "Исходящих сообщений у всех ботов: {0}\n"
msgstr "All bots have outgoing messages: {0}\n"
#: olgram/commands/info.py:39
msgid "Промо-кодов выдано: {0}\n"
msgstr "Promo codes issued: {0}\n"
#: olgram/commands/menu.py:31
msgid ""
"\n"
" У вас нет добавленных ботов.\n"
"\n"
" Отправьте команду /addbot, чтобы добавить бот.\n"
" "
msgstr ""
"\n"
" You do not have any bots added.\n"
"\n"
" Send the command /addbot to add a bot.\n"
" "
#: olgram/commands/menu.py:46
msgid "Ваши боты"
msgstr "Your bots"
#: olgram/commands/menu.py:67
msgid "Личные сообщения"
msgstr "Personal messages"
#: olgram/commands/menu.py:72
msgid "❗️ Выйти из всех чатов"
msgstr "❗️ Leave all chats"
#: olgram/commands/menu.py:77 olgram/commands/menu.py:122
#: olgram/commands/menu.py:148 olgram/commands/menu.py:184
#: olgram/commands/menu.py:247
msgid "<< Назад"
msgstr "<< Back"
#: olgram/commands/menu.py:83
msgid ""
"\n"
" Этот бот не добавлен в чаты, поэтому все сообщения будут приходить "
"вам в бот.\n"
" Чтобы подключить чат — добавьте бот @{0} в чат, откройте это меню "
"ещё раз и выберите добавленный чат.\n"
" Если ваш бот состоял в групповом чате до того, как его добавили в "
"Olgram - удалите бота из чата и добавьте\n"
" снова.\n"
" "
msgstr ""
"\n"
" This bot is not added to the chats, so all messages will come to you "
"in the bot.\n"
" To add a chat - add the bot @{0} to the chat, open this menu again "
"and select the added chat.\n"
" If your bot was in a group chat before you added it to Olgram - "
"remove the bot from the chat and add\n"
" again.\n"
" "
#: olgram/commands/menu.py:90
msgid ""
"\n"
" В этом разделе вы можете привязать бота @{0} к чату.\n"
" Выберите чат, куда бот будет пересылать сообщения.\n"
" "
msgstr ""
"\n"
" In this section you can bind the @{0} bot to a chat room.\n"
" Select the chat room where the bot will forward messages.\n"
" "
#: olgram/commands/menu.py:102
msgid "Текст"
msgstr "Text"
#: olgram/commands/menu.py:107
msgid "Чат"
msgstr "Chat"
#: olgram/commands/menu.py:112
msgid "Удалить бот"
msgstr "Delete bot"
#: olgram/commands/menu.py:117
msgid "Статистика"
msgstr "Statistics"
#: olgram/commands/menu.py:126
msgid "Опции"
msgstr "Options"
#: olgram/commands/menu.py:131
msgid ""
"\n"
" Управление ботом @{0}.\n"
"\n"
" Если у вас возникли вопросы по настройке бота, то посмотрите нашу "
"справку /help или напишите нам\n"
" @civsocit_feedback_bot\n"
" "
msgstr ""
"\n"
" Bot management @{0}.\n"
"\n"
" If you have any questions about configuring the bot, see our help /help "
"or email us\n"
" @civsocit_feedback_bot\n"
" "
#: olgram/commands/menu.py:143
msgid "Да, удалить бот"
msgstr "Yes, delete the bot"
#: olgram/commands/menu.py:152
msgid ""
"\n"
" Вы уверены, что хотите удалить бота @{0}?\n"
" "
msgstr ""
"\n"
" Are you sure you want to delete the bot @{0}?\n"
" "
#: olgram/commands/menu.py:161
msgid "Потоки сообщений"
msgstr "Message threads"
#: olgram/commands/menu.py:166
msgid "Данные пользователя"
msgstr "User data"
#: olgram/commands/menu.py:171
msgid "Антифлуд"
msgstr "Antiflood"
#: olgram/commands/menu.py:178
msgid "Olgram подпись"
msgstr "Olgram signature"
#: olgram/commands/menu.py:189 olgram/commands/menu.py:190
msgid "включены"
msgstr "enabled"
#: olgram/commands/menu.py:189 olgram/commands/menu.py:190
msgid "выключены"
msgstr "disabled"
#: olgram/commands/menu.py:191
#, fuzzy
#| msgid "включены"
msgid "включен"
msgstr "enabled"
#: olgram/commands/menu.py:191
#, fuzzy
#| msgid "выключены"
msgid "выключен"
msgstr "disabled"
#: olgram/commands/menu.py:192
msgid ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#threads\">Потоки сообщений</a>: <b>{0}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#user-"
"info\">Данные пользователя</a>: <b>{1}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#antiflood\">Антифлуд</a>: <b>{2}</b>\n"
" "
msgstr ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#threads\">Threads</a>: <b>{0}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#user-"
"info\">User data</a>: <b>{1}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#antiflood\">Antiflood</a>: <b>{2}</b>"
#: olgram/commands/menu.py:199
msgid "включена"
msgstr "enabled"
#: olgram/commands/menu.py:199
msgid "выключена"
msgstr "disabled"
#: olgram/commands/menu.py:200
msgid "Olgram подпись: <b>{0}</b>"
msgstr "Olgram signature: <b>{0}</b>"
#: olgram/commands/menu.py:210 olgram/commands/menu.py:272
#: olgram/commands/menu.py:314
msgid "<< Завершить редактирование"
msgstr "<< Finish editing"
#: olgram/commands/menu.py:214
msgid "Автоответчик"
msgstr "Autoresponder"
#: olgram/commands/menu.py:219 olgram/commands/menu.py:286
msgid "Сбросить текст"
msgstr "Reset text"
#: olgram/commands/menu.py:224
msgid ""
"\n"
" Сейчас вы редактируете текст, который отправляется после того, как "
"пользователь отправит вашему боту @{0}\n"
" команду /start\n"
"\n"
" Текущий текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" You are now editing the text that is sent after the user sends your bot "
"@{0}\n"
" /start command.\n"
"\n"
" Current text:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Send a message to change the text.\n"
" "
#: olgram/commands/menu.py:251
msgid ""
"\n"
" Статистика по боту @{0}\n"
"\n"
" Входящих сообщений: <b>{1}</b>\n"
" Ответных сообщений: <b>{2}</b>\n"
" Шаблоны ответов: <b>{3}</b>\n"
" Забанено пользователей: <b>{4}</b>\n"
" "
msgstr ""
"\n"
" Statistics @{0}\n"
"\n"
" Income messages: <b>{1}</b>\n"
" Response messages: <b>{2}</b>\n"
" Tempaltes: <b>{3}</b>\n"
" Banned users: <b>{4}</b>\n"
" "
#: olgram/commands/menu.py:276
msgid "Предыдущий текст"
msgstr "Previous text"
#: olgram/commands/menu.py:281
msgid "Шаблоны ответов..."
msgstr "Answer templates..."
#: olgram/commands/menu.py:291
msgid ""
"\n"
" Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в "
"ответ на все входящие сообщения @{0} автоматически. По умолчанию оно "
"отключено.\n"
"\n"
" Текущий текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" You are now editing the autoresponder text. This message is sent in "
"response to all incoming @{0} messages automatically. It is disabled by "
"default.\n"
"\n"
" Current text:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Send a message to change the text.\n"
" "
#: olgram/commands/menu.py:301
msgid "(отключено)"
msgstr "(disabled)"
#: olgram/commands/menu.py:318
msgid ""
"\n"
" Сейчас вы редактируете шаблоны ответов для @{0}. Текущие шаблоны:\n"
"\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте какую-нибудь фразу (например: \"Ваш заказ готов, ожидайте!\"), "
"чтобы добавить её в шаблон.\n"
" Чтобы удалить шаблон из списка, отправьте его номер в списке (например, "
"4)\n"
" "
msgstr ""
"\n"
" You are currently editing the answer templates for @{0}. Current "
"templates:\n"
"\n"
" <pre>\n"
" {1}\n"
" </pre>.\n"
" Send some phrase (e.g., \"Your order is ready, wait!\") to add to the "
"template.\n"
" To remove a template from the list, send its number in the list (for "
"example, 4) "
#: olgram/commands/menu.py:337
msgid "(нет шаблонов)"
msgstr "(no templates)"
#: olgram/commands/menu.py:376
msgid "У вас нет шаблонов, чтобы их удалять"
msgstr "You don't have templates to delete them"
#: olgram/commands/menu.py:378
msgid "Неправильное число. Чтобы удалить шаблон, введите число от 0 до {0}"
msgstr "To delete a template, enter a number between 0 and {0}"
#: olgram/commands/menu.py:386
msgid "У вашего бота уже слишком много шаблонов"
msgstr "Your bot already has too many templates"
#: olgram/commands/menu.py:390
msgid "Такой текст уже есть в списке шаблонов"
msgstr "This text is already in the list of templates"
#: olgram/commands/menu.py:408
msgid "У вас нет прав на этого бота"
msgstr "You have no permissions to this bot"
#: olgram/commands/promo.py:27
msgid ""
"Новый промокод\n"
"```{0}```"
msgstr ""
"New promo code\n"
"```{0}```"
#: olgram/commands/promo.py:46
msgid "Неправильный токен"
msgstr "Incorrect token"
#: olgram/commands/promo.py:49
msgid "Такого кода не существует"
msgstr "There is no such code"
#: olgram/commands/promo.py:59
msgid "Промокод отозван"
msgstr "Promotion code withdrawn"
#: olgram/commands/promo.py:70
msgid ""
"Укажите аргумент: промокод. Например: <pre>/setpromo my-promo-code</pre>"
msgstr ""
"Specify the argument: promo code. For example: <pre>/setpromo my-promo-"
"code</pre>"
#: olgram/commands/promo.py:78 olgram/commands/promo.py:82
msgid "Промокод не найден"
msgstr "Promo code not found"
#: olgram/commands/promo.py:85
msgid "Промокод уже использован"
msgstr "Promo code has already been used"
#: olgram/commands/promo.py:91
msgid "Промокод активирован! Спасибо 🙌"
msgstr "Promo code activated! Thank you 🙌"
#: olgram/commands/start.py:23
msgid ""
"\n"
" Olgram Bot — это конструктор ботов обратной связи в Telegram. Подробнее "
"<a href=\"https://olgram.readthedocs.io\">читайте здесь</a>. Следите за "
"обновлениями <a href=\"https://t.me/civsoc_it\">здесь</a>.\n"
"\n"
" Используйте эти команды, чтобы управлять этим ботом:\n"
"\n"
" /addbot - добавить бот\n"
" /mybots - управление ботами\n"
"\n"
" /help - помощь\n"
" "
msgstr ""
"\n"
" Olgram Bot is a feedback bot contructor for Telegram. More info <a "
"href=\"https://olgram.readthedocs.io\">here</a>.\n"
"\n"
" Use that commands to control bot:\n"
"\n"
" /addbot - add bot\n"
" /mybots - bot control\n"
"\n"
" /help - help\n"
" "
#: olgram/commands/start.py:43
msgid ""
"\n"
" Читайте инструкции на нашем сайте https://olgram.readthedocs.io\n"
" Техническая поддержка: @civsocit_feedback_bot\n"
" Версия {0}\n"
" "
msgstr ""
"\n"
" Read the instructions on our website at https://olgram.readthedocs.io\n"
" Technical support: @civsocit_feedback_bot\n"
" Version {0}\n"
" "
#: olgram/models/models.py:30
msgid ""
"\n"
" Здравствуйте!\n"
" Напишите ваш вопрос и мы ответим вам в ближайшее время.\n"
" "
msgstr ""
"\n"
" Hello!\n"
" Write your question and we will answer you shortly.\n"
" "
#: olgram/utils/permissions.py:40
msgid "Владелец бота ограничил доступ к этому функционалу 😞"
msgstr "The bot owner has restricted access to this functionality 😞"
#: olgram/utils/permissions.py:52
msgid "Владелец бота ограничил доступ к этому функционалу😞"
msgstr "The owner of the bot has restricted access to this function😞"
#: server/custom.py:55
msgid ""
"<b>Политика конфиденциальности</b>\n"
"\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При "
"отправке сообщения (кроме команд /start и /security_policy) ваш "
"идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется только для общения с "
"оператором; боты Olgram не делают массовых рассылок.\n"
"\n"
msgstr ""
"<b>Privacy Policy</b>.\n"
"\n"
"This bot does not store your messages, username and @username. When you send "
"a message (except for /start and /security_policy), your username is cached "
"for a while and then deleted from the cache. This ID is only used for "
"communicating with the operator; Olgram bots do not do mass mailings.\n"
"\n"
#: server/custom.py:61
msgid ""
"При отправке сообщения (кроме команд /start и /security_policy) оператор "
"<b>видит</b> ваши имя пользователя, @username и идентификатор пользователя в "
"силу настроек, которые оператор указал при создании бота."
msgstr ""
"When sending a message (except /start and /security_policy), the operator "
"<b>sees</b> your username, @username and user ID by virtue of the settings "
"that the operator specified when creating the bot."
#: server/custom.py:65
msgid ""
"В зависимости от ваших настроек конфиденциальности Telegram, оператор может "
"видеть ваш username, имя пользователя и другую информацию."
msgstr ""
"Depending on your Telegram privacy settings, the operator may see your "
"username, username and other information."
#: server/custom.py:76
msgid "Сообщение от пользователя "
msgstr "Message from the user "
#: server/custom.py:135
msgid "Вы заблокированы в этом боте"
msgstr "You are blocked in this bot"
#: server/custom.py:141
msgid "Слишком много сообщений, подождите одну минуту"
msgstr "Too many messages, wait one minute"
#: server/custom.py:148
msgid "Не удаётся связаться с владельцем бота"
msgstr "Cannot contact the owner of the bot"
#: server/custom.py:179
msgid ""
"<i>Невозможно переслать сообщение: автор не найден (сообщение слишком "
"старое?)</i>"
msgstr ""
"<i>Cannot forward this message: author not found (message too old?)</i>"
#: server/custom.py:187
msgid "Пользователь заблокирован"
msgstr "User is blocked"
#: server/custom.py:192
msgid "Пользователь не был забанен"
msgstr "The user was not banned"
#: server/custom.py:195
msgid "Пользователь разбанен"
msgstr "A user has been unlocked"
#: server/custom.py:200
msgid "<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>"
msgstr "<i>Cannot forward the message (has the author blocked the bot?)</i>"
#: server/server.py:41
msgid "(Пере)запустить бота"
msgstr "(Re)launch the bot"
#: server/server.py:42
msgid "Политика конфиденциальности"
msgstr "Privacy Policy"
#~ msgid ""
#~ "\n"
#~ "\n"
#~ "Этот бот создан с помощью @OlgramBot"
#~ msgstr ""
#~ "\n"
#~ "\n"
#~ "This bot was created using @OlgramBot"

View File

@ -1,25 +0,0 @@
import gettext
from olgram.settings import BotSettings
from os.path import dirname
locales_dir = dirname(__file__)
def dummy_translator(x: str) -> str:
return x
lang = BotSettings.language()
if lang == "ru":
_ = dummy_translator
else:
t = gettext.translation("olgram", localedir=locales_dir, languages=[lang])
_ = t.gettext
translators = {
"ru": dummy_translator,
"uk": gettext.translation("olgram", localedir=locales_dir, languages=["uk"]).gettext,
"zh": gettext.translation("olgram", localedir=locales_dir, languages=["zh"]).gettext,
"en": gettext.translation("olgram", localedir=locales_dir, languages=["en"]).gettext,
}

View File

@ -1,733 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2022-09-02 05:07+0400\n"
"PO-Revision-Date: 2022-09-02 05:12+0400\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: uk_UA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 3.1\n"
#: olgram/commands/admin.py:21 olgram/commands/info.py:21
#: olgram/commands/promo.py:23 olgram/commands/promo.py:39
msgid "Недостаточно прав"
msgstr "Недостатньо прав"
#: olgram/commands/admin.py:27
msgid "Нужно указать имя бота"
msgstr "Потрібно вказати ім'я бота"
#: olgram/commands/admin.py:33
msgid "Такого бота нет в системе"
msgstr "Такого бота немає в системі"
#: olgram/commands/admin.py:39 olgram/commands/admin.py:53
msgid "Пропустить"
msgstr "Пропустити"
#: olgram/commands/admin.py:42
msgid ""
"Введите текст, который будет отправлен владельцу бота {0}. Напишите "
"'Пропустить' чтобы отменить"
msgstr ""
"Введіть текст, який буде надіслано власнику бота {0}. Напишіть 'Пропустити', "
"щоб скасувати"
#: olgram/commands/admin.py:50
msgid "Поддерживается только текст"
msgstr "Підтримується лише текст"
#: olgram/commands/admin.py:55 olgram/commands/admin.py:71
msgid "Отменено"
msgstr "Скасовано"
#: olgram/commands/admin.py:61 olgram/commands/admin.py:69
msgid "Отправить"
msgstr "Надіслати"
#: olgram/commands/admin.py:62
msgid "Отменить"
msgstr "Скасувати"
#: olgram/commands/admin.py:81
msgid "Отправлено"
msgstr "Надіслано"
#: olgram/commands/bot_actions.py:22
msgid "Бот удалён"
msgstr "Бот видалений"
#: olgram/commands/bot_actions.py:38 olgram/commands/bot_actions.py:50
msgid "Текст сброшен"
msgstr "Текст скинутий"
#: olgram/commands/bot_actions.py:64
msgid "Выбран личный чат"
msgstr "Вибраний особистий чат"
#: olgram/commands/bot_actions.py:77
msgid "Бот вышел из чатов"
msgstr "Бот вийшов із чатів"
#: olgram/commands/bot_actions.py:83
msgid "Нельзя привязать бота к этому чату"
msgstr "Не можна прив'язати робота до цього чату"
#: olgram/commands/bot_actions.py:87
msgid "Выбран чат {0}"
msgstr "Вибраний чат {0}"
#: olgram/commands/bots.py:46
msgid ""
"У вас уже слишком много ботов. Удалите какой-нибудь свой бот из Olgram(/"
"mybots -> (Выбрать бота) -> Удалить бот)"
msgstr ""
"У вас вже надто багато роботів. Видаліть якийсь свій бот з Olgram(/mybots -> "
"(Вибрати бота) -> Видалити бот)"
#: olgram/commands/bots.py:50
msgid ""
"\n"
" Чтобы подключить бот, вам нужно выполнить три действия:\n"
"\n"
" 1. Перейдите в бот @BotFather, нажмите START и отправьте команду /"
"newbot\n"
" 2. Введите название бота, а потом username бота.\n"
" 3. После создания бота перешлите ответное сообщение в этот бот или "
"скопируйте и пришлите token бота.\n"
"\n"
" Важно: не подключайте боты, которые используются в других сервисах "
"(Manybot, Chatfuel, Livegram и других).\n"
" "
msgstr ""
"\n"
" Щоб підключити бот, вам потрібно виконати три дії:\n"
"\n"
" 1. Перейдіть до бот @BotFather, натисніть START і надішліть команду /"
"newbot\n"
" 2. Введіть назву бота, а потім username бота.\n"
" 3. Після створення бота перешліть повідомлення у цей бот або скопіюйте "
"і надішліть token бота.\n"
"\n"
" Важливо: не підключайте роботи, які використовуються в інших сервісах "
"(Manybot, Chatfuel, Livegram та інших).\n"
" \n"
" "
#: olgram/commands/bots.py:70
msgid ""
"\n"
" Это не токен бота.\n"
"\n"
" Токен выглядит вот так: 123456789:AAAA-"
"abc123_AbcdEFghijKLMnopqrstu12\n"
" "
msgstr ""
"\n"
" Це не токен робота.\n"
"\n"
" Токен виглядає ось так: 123456789:AAAA-"
"abc123_AbcdEFghijKLMnopqrstu12\n"
" "
#: olgram/commands/bots.py:77
msgid ""
"\n"
" Не удалось запустить этого бота: неверный токен\n"
" "
msgstr ""
"\n"
" Не вдалося запустити цього бота: неправильний токен\n"
" "
#: olgram/commands/bots.py:82
msgid ""
"\n"
" Не удалось запустить этого бота: непредвиденная ошибка\n"
" "
msgstr ""
"\n"
" Не вдалося запустити цього бота: непередбачена помилка\n"
" "
#: olgram/commands/bots.py:87
msgid ""
"\n"
" Такой бот уже есть в базе данных\n"
" "
msgstr ""
"\n"
" Такий бот вже є у базі даних\n"
" "
#: olgram/commands/bots.py:122
msgid "Бот добавлен! Список ваших ботов: /mybots"
msgstr "Бот доданий! Список ваших роботів: /mybots"
#: olgram/commands/info.py:34
msgid "Количество ботов: {0}\n"
msgstr "Кількість ботів: {0}\n"
#: olgram/commands/info.py:35
msgid "Количество пользователей (у конструктора): {0}\n"
msgstr "Кількість користувачів (у конструктора): {0}\n"
#: olgram/commands/info.py:36
msgid "Шаблонов ответов: {0}\n"
msgstr "Шаблонів відповідей: {0}\n"
#: olgram/commands/info.py:37
msgid "Входящих сообщений у всех ботов: {0}\n"
msgstr "Вхідних повідомлень у всіх роботів: {0}\n"
#: olgram/commands/info.py:38
msgid "Исходящих сообщений у всех ботов: {0}\n"
msgstr "Вихідних повідомлень у всіх роботів: {0}\n"
#: olgram/commands/info.py:39
msgid "Промо-кодов выдано: {0}\n"
msgstr "Промо-кодів видано: {0}\n"
#: olgram/commands/menu.py:31
msgid ""
"\n"
" У вас нет добавленных ботов.\n"
"\n"
" Отправьте команду /addbot, чтобы добавить бот.\n"
" "
msgstr ""
"\n"
" У вас немає доданих роботів.\n"
"\n"
" Надішліть команду /addbot, щоб додати бот.\n"
" \n"
" "
#: olgram/commands/menu.py:46
msgid "Ваши боты"
msgstr "Ваші боти"
#: olgram/commands/menu.py:67
msgid "Личные сообщения"
msgstr "Особисті повідомлення"
#: olgram/commands/menu.py:72
msgid "❗️ Выйти из всех чатов"
msgstr "❗️ Вийти зі всіх чатів"
#: olgram/commands/menu.py:77 olgram/commands/menu.py:122
#: olgram/commands/menu.py:148 olgram/commands/menu.py:184
#: olgram/commands/menu.py:247
msgid "<< Назад"
msgstr "<< Назад"
#: olgram/commands/menu.py:83
msgid ""
"\n"
" Этот бот не добавлен в чаты, поэтому все сообщения будут приходить "
"вам в бот.\n"
" Чтобы подключить чат — добавьте бот @{0} в чат, откройте это меню "
"ещё раз и выберите добавленный чат.\n"
" Если ваш бот состоял в групповом чате до того, как его добавили в "
"Olgram - удалите бота из чата и добавьте\n"
" снова.\n"
" "
msgstr ""
"\n"
" Цей бот не доданий до чатів, тому всі повідомлення будуть приходити "
"вам у бот.\n"
" Щоб підключити чат — додайте бот @{0} до чату, відкрийте це меню ще "
"раз і виберіть доданий чат.\n"
" Якщо ваш бот перебував у груповому чаті до того, як його додали до "
"Olgram - видаліть бота з чату та додайте\n"
" знову.\n"
" \n"
" "
#: olgram/commands/menu.py:90
msgid ""
"\n"
" В этом разделе вы можете привязать бота @{0} к чату.\n"
" Выберите чат, куда бот будет пересылать сообщения.\n"
" "
msgstr ""
"\n"
" У цьому розділі ви можете прив'язати робота @{0} до чату.\n"
" Виберіть чат, куди бот пересилатиме повідомлення.\n"
" \n"
" "
#: olgram/commands/menu.py:102
msgid "Текст"
msgstr "Текст"
#: olgram/commands/menu.py:107
msgid "Чат"
msgstr "Чат"
#: olgram/commands/menu.py:112
msgid "Удалить бот"
msgstr "Видалити бот"
#: olgram/commands/menu.py:117
msgid "Статистика"
msgstr "Статистика"
#: olgram/commands/menu.py:126
msgid "Опции"
msgstr "Опції"
#: olgram/commands/menu.py:131
msgid ""
"\n"
" Управление ботом @{0}.\n"
"\n"
" Если у вас возникли вопросы по настройке бота, то посмотрите нашу "
"справку /help или напишите нам\n"
" @civsocit_feedback_bot\n"
" "
msgstr ""
"\n"
" Управління роботом @{0}.\n"
"\n"
" Якщо у вас виникли питання з налаштування бота, подивіться нашу довідку /"
"help або напишіть нам\n"
" @civsocit_feedback_bot\n"
" "
#: olgram/commands/menu.py:143
msgid "Да, удалить бот"
msgstr "Так, видалити бот"
#: olgram/commands/menu.py:152
msgid ""
"\n"
" Вы уверены, что хотите удалить бота @{0}?\n"
" "
msgstr ""
"\n"
" Ви впевнені, що хочете видалити бота @{0}?\n"
" "
#: olgram/commands/menu.py:161
msgid "Потоки сообщений"
msgstr "Потоки повідомлень"
#: olgram/commands/menu.py:166
msgid "Данные пользователя"
msgstr "Дані користувача"
#: olgram/commands/menu.py:171
msgid "Антифлуд"
msgstr "Антифлуд"
#: olgram/commands/menu.py:178
msgid "Olgram подпись"
msgstr "Olgram підпис"
#: olgram/commands/menu.py:189 olgram/commands/menu.py:190
msgid "включены"
msgstr "включені"
#: olgram/commands/menu.py:189 olgram/commands/menu.py:190
msgid "выключены"
msgstr "вимкнені"
#: olgram/commands/menu.py:191
#, fuzzy
#| msgid "включены"
msgid "включен"
msgstr "включені"
#: olgram/commands/menu.py:191
#, fuzzy
#| msgid "выключены"
msgid "выключен"
msgstr "вимкнені"
#: olgram/commands/menu.py:192
msgid ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#threads\">Потоки сообщений</a>: <b>{0}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#user-"
"info\">Данные пользователя</a>: <b>{1}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#antiflood\">Антифлуд</a>: <b>{2}</b>\n"
" "
msgstr ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#threads\">Потоки повідомлень</a>: <b>{0}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#user-"
"info\">Дані користувача</a>: <b>{1}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#antiflood\">Anti-flood</a>: <b>{2}</b>"
#: olgram/commands/menu.py:199
msgid "включена"
msgstr "включена"
#: olgram/commands/menu.py:199
msgid "выключена"
msgstr "вимкнена"
#: olgram/commands/menu.py:200
msgid "Olgram подпись: <b>{0}</b>"
msgstr "Olgram підпис: <b>{0}</b>"
#: olgram/commands/menu.py:210 olgram/commands/menu.py:272
#: olgram/commands/menu.py:314
msgid "<< Завершить редактирование"
msgstr "<< Завершити редагування"
#: olgram/commands/menu.py:214
msgid "Автоответчик"
msgstr "Автовідповідач"
#: olgram/commands/menu.py:219 olgram/commands/menu.py:286
msgid "Сбросить текст"
msgstr "Скинути текст"
#: olgram/commands/menu.py:224
msgid ""
"\n"
" Сейчас вы редактируете текст, который отправляется после того, как "
"пользователь отправит вашему боту @{0}\n"
" команду /start\n"
"\n"
" Текущий текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" Зараз ви редагуєте текст, який надсилається після того, як користувач "
"надішле вашому боту @{0}\n"
" команду /start\n"
"\n"
" Поточний текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Надішліть повідомлення, щоб змінити текст.\n"
" \n"
" "
#: olgram/commands/menu.py:251
msgid ""
"\n"
" Статистика по боту @{0}\n"
"\n"
" Входящих сообщений: <b>{1}</b>\n"
" Ответных сообщений: <b>{2}</b>\n"
" Шаблоны ответов: <b>{3}</b>\n"
" Забанено пользователей: <b>{4}</b>\n"
" "
msgstr ""
"\n"
" Статистика з роботи @{0}\n"
"\n"
" Вхідних повідомлень: <b>{1}</b>\n"
" Повідомлень у відповідь: <b>{2}</b>\n"
" Шаблони відповідей: <b>{3}</b>\n"
" Забанено користувачів: <b>{4}</b>\n"
" "
#: olgram/commands/menu.py:276
msgid "Предыдущий текст"
msgstr "Попередній текст"
#: olgram/commands/menu.py:281
msgid "Шаблоны ответов..."
msgstr "Шаблони відповідей..."
#: olgram/commands/menu.py:291
msgid ""
"\n"
" Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в "
"ответ на все входящие сообщения @{0} автоматически. По умолчанию оно "
"отключено.\n"
"\n"
" Текущий текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" Зараз ви редагуєте текст автовідповідача. Це повідомлення надсилається у "
"відповідь на всі вхідні повідомлення @{0} автоматично. За замовчуванням його "
"вимкнено.\n"
"\n"
" Поточний текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Надішліть повідомлення, щоб змінити текст.\n"
" "
#: olgram/commands/menu.py:301
msgid "(отключено)"
msgstr "(відключено)"
#: olgram/commands/menu.py:318
msgid ""
"\n"
" Сейчас вы редактируете шаблоны ответов для @{0}. Текущие шаблоны:\n"
"\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте какую-нибудь фразу (например: \"Ваш заказ готов, ожидайте!\"), "
"чтобы добавить её в шаблон.\n"
" Чтобы удалить шаблон из списка, отправьте его номер в списке (например, "
"4)\n"
" "
msgstr ""
"\n"
" Зараз ви редагуєте шаблони для @{0}. Поточні шаблони:\n"
"\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Надішліть якусь фразу (наприклад: \"Ваше замовлення готове, чекайте!\"), "
"щоб додати її до шаблону.\n"
" Щоб видалити шаблон зі списку, відправте його у списку (наприклад, 4)\n"
" \n"
" "
#: olgram/commands/menu.py:337
msgid "(нет шаблонов)"
msgstr "(Немає шаблонів)"
#: olgram/commands/menu.py:376
msgid "У вас нет шаблонов, чтобы их удалять"
msgstr "У вас немає шаблонів, щоб їх видаляти"
#: olgram/commands/menu.py:378
msgid "Неправильное число. Чтобы удалить шаблон, введите число от 0 до {0}"
msgstr "Неправильне число. Щоб видалити шаблон, введіть число від 0 до {0}"
#: olgram/commands/menu.py:386
msgid "У вашего бота уже слишком много шаблонов"
msgstr "У вашого бота вже дуже багато шаблонів"
#: olgram/commands/menu.py:390
msgid "Такой текст уже есть в списке шаблонов"
msgstr "Такий текст вже є у списку шаблонів"
#: olgram/commands/menu.py:408
msgid "У вас нет прав на этого бота"
msgstr "У вас немає прав на цього бота"
#: olgram/commands/promo.py:27
msgid ""
"Новый промокод\n"
"```{0}```"
msgstr ""
"Новий промокод\n"
"```{0}```"
#: olgram/commands/promo.py:46
msgid "Неправильный токен"
msgstr "Неправильний токен"
#: olgram/commands/promo.py:49
msgid "Такого кода не существует"
msgstr "Такого коду не існує"
#: olgram/commands/promo.py:59
msgid "Промокод отозван"
msgstr "Промокод відкликаний"
#: olgram/commands/promo.py:70
msgid ""
"Укажите аргумент: промокод. Например: <pre>/setpromo my-promo-code</pre>"
msgstr ""
"Зазначте аргумент: промокод. Наприклад: <pre>/setpromo my-promo-code</pre>"
#: olgram/commands/promo.py:78 olgram/commands/promo.py:82
msgid "Промокод не найден"
msgstr "Промокод не знайдено"
#: olgram/commands/promo.py:85
msgid "Промокод уже использован"
msgstr "Промокод уже використаний"
#: olgram/commands/promo.py:91
msgid "Промокод активирован! Спасибо 🙌"
msgstr "Промокод активовано! Дякую 🙌"
#: olgram/commands/start.py:23
msgid ""
"\n"
" Olgram Bot — это конструктор ботов обратной связи в Telegram. Подробнее "
"<a href=\"https://olgram.readthedocs.io\">читайте здесь</a>. Следите за "
"обновлениями <a href=\"https://t.me/civsoc_it\">здесь</a>.\n"
"\n"
" Используйте эти команды, чтобы управлять этим ботом:\n"
"\n"
" /addbot - добавить бот\n"
" /mybots - управление ботами\n"
"\n"
" /help - помощь\n"
" "
msgstr ""
"\n"
" Olgram Bot - це конструктор роботів зворотного зв'язку в Telegram. "
"Докладніше <a href=\"https://olgram.readthedocs.io\">читайте тут</a>. "
"Слідкуйте за оновленнями <a href=\"https://t.me/civsoc_it\">тут</a>.\n"
"\n"
" Використовуйте ці команди, щоб керувати цим ботом:\n"
"\n"
" /addbot - додати бот\n"
" /mybots - керування ботами\n"
"\n"
" /help - допомога\n"
" \n"
" "
#: olgram/commands/start.py:43
msgid ""
"\n"
" Читайте инструкции на нашем сайте https://olgram.readthedocs.io\n"
" Техническая поддержка: @civsocit_feedback_bot\n"
" Версия {0}\n"
" "
msgstr ""
"\n"
" Читайте інструкції на нашому сайті https://olgram.readthedocs.io\n"
" Технічна підтримка: @civsocit_feedback_bot\n"
" Версія {0}\n"
" \n"
" "
#: olgram/models/models.py:30
msgid ""
"\n"
" Здравствуйте!\n"
" Напишите ваш вопрос и мы ответим вам в ближайшее время.\n"
" "
msgstr ""
"\n"
" Доброго дня!\n"
" Напишіть ваше запитання, і ми відповімо вам найближчим часом.\n"
" \n"
" "
#: olgram/utils/permissions.py:40
msgid "Владелец бота ограничил доступ к этому функционалу 😞"
msgstr "Власник бота обмежив доступ до цього функціоналу 😞"
#: olgram/utils/permissions.py:52
msgid "Владелец бота ограничил доступ к этому функционалу😞"
msgstr "Власник бота обмежив доступ до цього функціоналу 😞"
#: server/custom.py:55
msgid ""
"<b>Политика конфиденциальности</b>\n"
"\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При "
"отправке сообщения (кроме команд /start и /security_policy) ваш "
"идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется только для общения с "
"оператором; боты Olgram не делают массовых рассылок.\n"
"\n"
msgstr ""
"<b>Політика конфіденційності</b>\n"
"\n"
"Цей бот не зберігає ваші повідомлення, ім'я користувача та @username. При "
"надсиланні повідомлення (крім команд /start та /security_policy) ваш "
"ідентифікатор користувача записується в кеш на деякий час і потім "
"видаляється з кеша. Цей ідентифікатор використовується лише для спілкування "
"з оператором; боти Olgram не роблять масових розсилок.\n"
"\n"
#: server/custom.py:61
msgid ""
"При отправке сообщения (кроме команд /start и /security_policy) оператор "
"<b>видит</b> ваши имя пользователя, @username и идентификатор пользователя в "
"силу настроек, которые оператор указал при создании бота."
msgstr ""
"При надсиланні повідомлення (крім команд /start та /security_policy) "
"оператор <b>бачить</b> ваше ім'я користувача, @username та ідентифікатор "
"користувача через налаштування, які оператор вказав при створенні бота."
#: server/custom.py:65
msgid ""
"В зависимости от ваших настроек конфиденциальности Telegram, оператор может "
"видеть ваш username, имя пользователя и другую информацию."
msgstr ""
"Залежно від ваших налаштувань конфіденційності Telegram оператор може бачити "
"ваш username, ім'я користувача та іншу інформацію."
#: server/custom.py:76
msgid "Сообщение от пользователя "
msgstr "Допис від користувача "
#: server/custom.py:135
msgid "Вы заблокированы в этом боте"
msgstr "Ви заблоковані у цьому боті"
#: server/custom.py:141
msgid "Слишком много сообщений, подождите одну минуту"
msgstr "Забагато повідомлень, зачекайте одну хвилину"
#: server/custom.py:148
msgid "Не удаётся связаться с владельцем бота"
msgstr "Не вдається зв'язатися з власником бота"
#: server/custom.py:179
msgid ""
"<i>Невозможно переслать сообщение: автор не найден (сообщение слишком "
"старое?)</i>"
msgstr ""
"<i>Неможливо надіслати повідомлення: автора не знайдено (повідомлення "
"занадто старе?)</i>"
#: server/custom.py:187
msgid "Пользователь заблокирован"
msgstr "Користувач заблоковано"
#: server/custom.py:192
msgid "Пользователь не был забанен"
msgstr "Користувач не був забанений"
#: server/custom.py:195
msgid "Пользователь разбанен"
msgstr "Користувач розбанений"
#: server/custom.py:200
msgid "<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>"
msgstr "<i>Неможливо надіслати повідомлення (автор заблокував робота?)</i>"
#: server/server.py:41
msgid "(Пере)запустить бота"
msgstr "(Пере) запустити бота"
#: server/server.py:42
msgid "Политика конфиденциальности"
msgstr "Політика конфіденційності"
#~ msgid ""
#~ "\n"
#~ "\n"
#~ "Этот бот создан с помощью @OlgramBot"
#~ msgstr ""
#~ "\n"
#~ "\n"
#~ "Цей бот створено за допомогою @OlgramBot"

View File

@ -1,565 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2022-03-22 04:36+0300\n"
"PO-Revision-Date: 2022-03-22 04:55+0300\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 3.0\n"
"Last-Translator: \n"
"Plural-Forms: nplurals=1; plural=0;\n"
"Language: zh_CN\n"
#: olgram/commands/bot_actions.py:21
msgid "Бот удалён"
msgstr "移除机器人"
#: olgram/commands/bot_actions.py:37 olgram/commands/bot_actions.py:49
msgid "Текст сброшен"
msgstr "重置文本"
#: olgram/commands/bot_actions.py:63
msgid "Выбран личный чат"
msgstr "选择了私聊"
#: olgram/commands/bot_actions.py:68
msgid "Нельзя привязать бота к этому чату"
msgstr "你不能将机器人链接到这个群聊"
#: olgram/commands/bot_actions.py:72
msgid "Выбран чат {0}"
msgstr "聊天选择 {0}"
#: olgram/commands/bots.py:42
msgid "У вас уже слишком много ботов."
msgstr "你已经有太多的机器人了。"
#: olgram/commands/bots.py:45
msgid ""
"\n"
" Чтобы подключить бот, вам нужно выполнить три действия:\n"
"\n"
" 1. Перейдите в бот @BotFather, нажмите START и отправьте команду /"
"newbot\n"
" 2. Введите название бота, а потом username бота.\n"
" 3. После создания бота перешлите ответное сообщение в этот бот или "
"скопируйте и пришлите token бота.\n"
"\n"
" Важно: не подключайте боты, которые используются в других сервисах "
"(Manybot, Chatfuel, Livegram и других).\n"
" "
msgstr ""
"\n"
" 要连接机器人,你需要遵循三个步骤。\n"
"\n"
" 1. 转到机器人@BotFather按/START键并发送/newbot\n"
" 2. 输入机器人的名字,然后输入机器人的用户名。\n"
" 3. 一旦创建了机器人,就向这个机器人转发一条回复信息,或者复制并发送机器人"
"的令牌。\n"
"\n"
" 重要不要连接用于其他服务的机器人Manybot、Chatfuel、Livegram和其"
"他)。\n"
" "
#: olgram/commands/bots.py:65
msgid ""
"\n"
" Это не токен бота.\n"
"\n"
" Токен выглядит вот так: 123456789:AAAA-"
"abc123_AbcdEFghijKLMnopqrstu12\n"
" "
msgstr ""
"\n"
" 这不是一个机器人令牌。\n"
"\n"
" 该令牌看起来像这样123456789:AAAA-abc123_AbcdEFghijKLMnopqrstu12\n"
" "
#: olgram/commands/bots.py:72
msgid ""
"\n"
" Не удалось запустить этого бота: неверный токен\n"
" "
msgstr ""
"\n"
" 运行此机器人失败:错误的令牌\n"
" "
#: olgram/commands/bots.py:77
msgid ""
"\n"
" Не удалось запустить этого бота: непредвиденная ошибка\n"
" "
msgstr ""
"\n"
" 该机器人无法启动:意外错误\n"
" "
#: olgram/commands/bots.py:82
msgid ""
"\n"
" Такой бот уже есть в базе данных\n"
" "
msgstr ""
"\n"
" 这个机器人已经在数据库中出现了\n"
" "
#: olgram/commands/bots.py:114
msgid "Бот добавлен! Список ваших ботов: /mybots"
msgstr "机器人已加入! 你的机器人列表:/mybots"
#: olgram/commands/info.py:21
msgid "Недостаточно прав"
msgstr "没有足够的权利"
#: olgram/commands/info.py:32
msgid "Количество ботов: {0}\n"
msgstr "机器人的数量。{0}\n"
#: olgram/commands/info.py:33
msgid "Количество пользователей (у конструктора): {0}\n"
msgstr "用户的数量(在构造器处)。{0}\n"
#: olgram/commands/info.py:34
msgid "Шаблонов ответов: {0}\n"
msgstr "回复模板。{0}\n"
#: olgram/commands/info.py:35
msgid "Входящих сообщений у всех ботов: {0}\n"
msgstr "所有的机器人都有传入的信息。{0}\n"
#: olgram/commands/info.py:36
msgid "Исходящих сообщений у всех ботов: {0}\n"
msgstr "所有的机器人都有外发信息。{0}\n"
#: olgram/commands/menu.py:31
msgid ""
"\n"
" У вас нет добавленных ботов.\n"
"\n"
" Отправьте команду /addbot, чтобы добавить бот.\n"
" "
msgstr ""
"\n"
" 你没有添加任何机器人。\n"
"\n"
" 发送命令/addbot来添加一个机器人。\n"
" "
#: olgram/commands/menu.py:46
msgid "Ваши боты"
msgstr "你的机器人"
#: olgram/commands/menu.py:67
msgid "Личные сообщения"
msgstr "个人留言"
#: olgram/commands/menu.py:72 olgram/commands/menu.py:117
#: olgram/commands/menu.py:143 olgram/commands/menu.py:166
#: olgram/commands/menu.py:222
msgid "<< Назад"
msgstr "<< 返回"
#: olgram/commands/menu.py:78
msgid ""
"\n"
" Этот бот не добавлен в чаты, поэтому все сообщения будут приходить "
"вам в бот.\n"
" Чтобы подключить чат — добавьте бот @{0} в чат, откройте это меню "
"ещё раз и выберите добавленный чат.\n"
" Если ваш бот состоял в групповом чате до того, как его добавили в "
"Olgram - удалите бота из чата и добавьте\n"
" снова.\n"
" "
msgstr ""
"\n"
" 这个机器人没有被添加到聊天记录中,所以所有的信息都会在机器人中找到"
"你。\n"
" 要连接群聊--将机器人@{0}添加到群聊中,再次打开此菜单并选择添加的群"
"聊。\n"
" 如果你的机器人在添加到Olgram之前是在群组中请将其从群聊中删"
"除,然后添加到群组中。\n"
" 再次。\n"
" "
#: olgram/commands/menu.py:85
msgid ""
"\n"
" В этом разделе вы можете привязать бота @{0} к чату.\n"
" Выберите чат, куда бот будет пересылать сообщения.\n"
" "
msgstr ""
"\n"
" 在本节中,您可以将@{0}机器人绑定到一个群聊中。\n"
" 选择机器人将转发消息的群聊。\n"
" "
#: olgram/commands/menu.py:97
msgid "Текст"
msgstr "自动回复"
#: olgram/commands/menu.py:102
msgid "Чат"
msgstr "群聊"
#: olgram/commands/menu.py:107
msgid "Удалить бот"
msgstr "删除机器人"
#: olgram/commands/menu.py:112
msgid "Статистика"
msgstr "统计数据"
#: olgram/commands/menu.py:121
msgid "Опции"
msgstr "选择"
#: olgram/commands/menu.py:126
msgid ""
"\n"
" Управление ботом @{0}.\n"
"\n"
" Если у вас возникли вопросы по настройке бота, то посмотрите нашу "
"справку /help или напишите нам\n"
" @civsocit_feedback_bot\n"
" "
msgstr ""
"\n"
" 机器人管理@{0}。\n"
"\n"
" 如果你有任何关于机器人配置的问题,请参阅我们的帮助/help\n"
" "
#: olgram/commands/menu.py:138
msgid "Да, удалить бот"
msgstr "是的,删除该机器人"
#: olgram/commands/menu.py:147
msgid ""
"\n"
" Вы уверены, что хотите удалить бота @{0}?\n"
" "
msgstr ""
"\n"
" 你确定要删除机器人@{0}吗?\n"
" "
#: olgram/commands/menu.py:156
msgid "Потоки сообщений"
msgstr "信息流"
#: olgram/commands/menu.py:161
msgid "Данные пользователя"
msgstr "用户数据"
#: olgram/commands/menu.py:171 olgram/commands/menu.py:172
msgid "включены"
msgstr "包括"
#: olgram/commands/menu.py:171 olgram/commands/menu.py:172
msgid "выключены"
msgstr "关闭"
#: olgram/commands/menu.py:173
msgid ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#threads"
"\">Потоки сообщений</a>: <b>{0}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#user-info"
"\">Данные пользователя</a>: <b>{1}</b>\n"
" "
msgstr ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#threads\">"
"信息流</a>: <b>{0}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options.html#user-info"
"\">用户数据</a>: <b>{1}</b>\n"
" "
#: olgram/commands/menu.py:185 olgram/commands/menu.py:247
#: olgram/commands/menu.py:289
msgid "<< Завершить редактирование"
msgstr "<< 完成编辑"
#: olgram/commands/menu.py:189
msgid "Автоответчик"
msgstr "自动回复"
#: olgram/commands/menu.py:194 olgram/commands/menu.py:261
msgid "Сбросить текст"
msgstr "重置文本"
#: olgram/commands/menu.py:199
msgid ""
"\n"
" Сейчас вы редактируете текст, который отправляется после того, как "
"пользователь отправит вашему боту @{0}\n"
" команду /start\n"
"\n"
" Текущий текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" 你现在正在编辑用户向你的机器人发送@{0}之后的文本。\n"
" /start\n"
"\n"
" 目前的文本。\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" 发送消息,改变文本。\n"
" "
#: olgram/commands/menu.py:226
msgid ""
"\n"
" Статистика по боту @{0}\n"
"\n"
" Входящих сообщений: <b>{1}</b>\n"
" Ответных сообщений: <b>{2}</b>\n"
" Шаблоны ответов: <b>{3}</b>\n"
" Забанено пользователей: <b>{4}</b>\n"
" "
msgstr ""
"\n"
" 机器人统计 @{0}\n"
"\n"
" 收到的信息: <b>{1}</b>\n"
" 回复信息: <b>{2}</b>\n"
" 回复模板: <b>{3}</b>\n"
" 被禁止的用户: <b>{4}</b>\n"
" "
#: olgram/commands/menu.py:251
msgid "Предыдущий текст"
msgstr ""
#: olgram/commands/menu.py:256
msgid "Шаблоны ответов..."
msgstr "回复模板..."
#: olgram/commands/menu.py:266
msgid ""
"\n"
" Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в "
"ответ на все входящие сообщения @{0} автоматически. По умолчанию оно "
"отключено.\n"
"\n"
" Текущий текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" 你现在正在编辑自动回复的文本。该信息会自动响应所有收到的@{0}信息而发送。"
"默认情况下,它是禁用的。\n"
"\n"
" 目前的文本。\n"
" <pre>\n"
" {1}\n"
" </pre>。\n"
" 发送消息,改变文本。\n"
" "
#: olgram/commands/menu.py:276
msgid "(отключено)"
msgstr "(关闭)"
#: olgram/commands/menu.py:293
msgid ""
"\n"
" Сейчас вы редактируете шаблоны ответов для @{0}. Текущие шаблоны:\n"
"\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Отправьте какую-нибудь фразу (например: \"Ваш заказ готов, ожидайте!\"), "
"чтобы добавить её в шаблон.\n"
" Чтобы удалить шаблон из списка, отправьте его номер в списке (например, "
"4)\n"
" "
msgstr ""
"\n"
" 你现在正在编辑@{0}的回复模板。目前的模板。\n"
"\n"
" <pre>\n"
" {1}\n"
" </pre>。\n"
" 发送一个短语(例如:\"您的订单已准备好,请等待!\"),将其添加到模板"
"中。\n"
" 要从列表中删除一个模板请发送它在列表中的编号如4。\n"
" "
#: olgram/commands/menu.py:312
msgid "(нет шаблонов)"
msgstr "(没有模板)"
#: olgram/commands/menu.py:351
msgid "У вас нет шаблонов, чтобы их удалять"
msgstr "你没有模板来删除它们"
#: olgram/commands/menu.py:353
msgid "Неправильное число. Чтобы удалить шаблон, введите число от 0 до {0}"
msgstr "不正确的数字。要删除一个模式请在0和{0}之间输入一个数字。"
#: olgram/commands/menu.py:361
msgid "У вашего бота уже слишком много шаблонов"
msgstr "你的机器人已经有太多的模式了"
#: olgram/commands/menu.py:365
msgid "Такой текст уже есть в списке шаблонов"
msgstr "此文本已在模板列表中"
#: olgram/commands/menu.py:383
msgid "У вас нет прав на этого бота"
msgstr "你对这个机器人没有任何权利"
#: olgram/commands/start.py:23
msgid ""
"\n"
" Olgram Bot — это конструктор ботов обратной связи в Telegram. Подробнее "
"<a href=\"https://olgram.readthedocs.io\">читайте здесь</a>.\n"
"\n"
" Используйте эти команды, чтобы управлять этим ботом:\n"
"\n"
" /addbot - добавить бот\n"
" /mybots - управление ботами\n"
"\n"
" /help - помощь\n"
" "
msgstr ""
"\n"
" Olgram Bot — 是一个Telegram反馈机器人的构建者。阅读更多 <a href="
"\"https://olgram.readthedocs.io\">在此阅读</a>.\n"
"\n"
" 使用这些命令来控制这个机器人:\n"
"\n"
" /addbot - 绑定机器人\n"
" /mybots - 机器人控制\n"
"\n"
" /help - 帮助\n"
" "
#: olgram/commands/start.py:42
msgid ""
"\n"
" Читайте инструкции на нашем сайте https://olgram.readthedocs.io\n"
" Техническая поддержка: @civsocit_feedback_bot\n"
" Версия {0}\n"
" "
msgstr ""
"\n"
" 请阅读我们网站上的说明 https://olgram.readthedocs.io\n"
" 版本{0}\n"
" "
#: olgram/models/models.py:30
msgid ""
"\n"
" Здравствуйте!\n"
" Напишите ваш вопрос и мы ответим вам в ближайшее время.\n"
" "
msgstr ""
"\n"
" 你好!\n"
" 请写下您的问题,我们将很快给您答复。\n"
" "
#: olgram/utils/permissions.py:40
msgid "Владелец бота ограничил доступ к этому функционалу 😞"
msgstr "机器人所有者已经限制了对该功能的访问 😞"
#: olgram/utils/permissions.py:52
msgid "Владелец бота ограничил доступ к этому функционалу😞"
msgstr "机器人主人限制了对该功能的访问😞。"
#: server/custom.py:40
msgid ""
"<b>Политика конфиденциальности</b>\n"
"\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При "
"отправке сообщения (кроме команд /start и /security_policy) ваш "
"идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется только для общения с "
"оператором; боты Olgram не делают массовых рассылок.\n"
"\n"
msgstr ""
"<b>隐私政策</b\n"
"\n"
"这个机器人不存储你的信息、用户名或@用户名。当你发送消息时(除/start和/"
"security_policy外你的用户名会被缓存一段时间然后从缓存中删除。这个ID只用"
"于与运营商沟通Olgram机器人不做批量信息发送。\n"
"\n"
#: server/custom.py:46
msgid ""
"При отправке сообщения (кроме команд /start и /security_policy) оператор "
"<b>видит</b> ваши имя пользователя, @username и идентификатор пользователя в "
"силу настроек, которые оператор указал при создании бота."
msgstr ""
"当发送消息时(除了/start和/security_policy操作者<b>看到</b>你的用户名、@"
"用户名和用户ID凭借的是操作者在创建机器人时指定的设置。"
#: server/custom.py:50
msgid ""
"В зависимости от ваших настроек конфиденциальности Telegram, оператор может "
"видеть ваш username, имя пользователя и другую информацию."
msgstr ""
"根据你的Telegram隐私设置运营商可能会看到你的用户名用户名和其他信息。"
#: server/custom.py:61
msgid "Сообщение от пользователя "
msgstr "用户的信息 "
#: server/custom.py:88
msgid "Вы заблокированы в этом боте"
msgstr "你在这个机器人中被封锁了"
#: server/custom.py:128
msgid ""
"<i>Невозможно переслать сообщение: автор не найден (сообщение слишком "
"старое?)</i>"
msgstr "无法转发信息:找不到作者(信息太旧?)"
#: server/custom.py:136
msgid "Пользователь заблокирован"
msgstr "用户被封锁了"
#: server/custom.py:141
msgid "Пользователь не был забанен"
msgstr "该用户没有被禁止"
#: server/custom.py:144
msgid "Пользователь разбанен"
msgstr "解禁用户"
#: server/custom.py:149
msgid "<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>"
msgstr "无法转发该信息(作者已经屏蔽了机器人?)"
#: server/server.py:41
msgid "(Пере)запустить бота"
msgstr "(重新)启动机器人"
#: server/server.py:42
msgid "Политика конфиденциальности"
msgstr "隐私政策"
msgid "\n\nЭтот бот создан с помощью @OlgramBot"
msgstr "\n\n "

39
main.py
View File

@ -1,38 +1,34 @@
import asyncio import asyncio
import argparse
from tortoise import Tortoise from tortoise import Tortoise
from olgram.router import dp from olgram.router import dp
from olgram.settings import TORTOISE_ORM, OlgramSettings from olgram.settings import TORTOISE_ORM
from olgram.utils.permissions import AccessMiddleware
from server.custom import init_redis from server.custom import init_redis
import olgram.commands.bots # noqa: F401 import olgram.commands.bots # noqa: F401
import olgram.commands.start # noqa: F401 import olgram.commands.start # noqa: F401
import olgram.commands.menu # noqa: F401 import olgram.commands.menu # noqa: F401
import olgram.commands.bot_actions # noqa: F401 import olgram.commands.bot_actions # noqa: F401
import olgram.commands.info # noqa: F401
import olgram.commands.promo # noqa: F401
import olgram.commands.admin # noqa: F401
from locales.locale import _
from server.server import main as server_main from server.server import main as server_main
import logging
logging.basicConfig(level=logging.INFO)
async def init_database(): async def init_database():
await Tortoise.init(config=TORTOISE_ORM) await Tortoise.init(config=TORTOISE_ORM)
async def init_olgram(): async def init_olgram():
from olgram.router import bot, dp from olgram.router import bot
dp.setup_middleware(AccessMiddleware(OlgramSettings.admin_ids()))
from aiogram.types import BotCommand from aiogram.types import BotCommand
await bot.set_my_commands( await bot.set_my_commands(
[ [
BotCommand("start", _("Запустить бота")), BotCommand("start", "Запустить бота"),
BotCommand("addbot", _("Добавить бот")), BotCommand("addbot", "Добавить бот"),
BotCommand("mybots", _("Управление ботами")), BotCommand("mybots", "Управление ботами"),
BotCommand("help", _("Справка")) BotCommand("help", "Справка")
] ]
) )
@ -44,21 +40,14 @@ async def initialization():
def main(): def main():
parser = argparse.ArgumentParser("Olgram bot and feedback server") """
group = parser.add_mutually_exclusive_group() Classic polling
group.add_argument("--noserver", help="Не запускать сервер обратной связи, только сам Olgram", action="store_true") """
group.add_argument("--onlyserver", help="Запустить только сервер обратной связи, без Olgram", action="store_true")
args = parser.parse_args()
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(initialization()) loop.run_until_complete(initialization())
if not args.onlyserver: loop.create_task(dp.start_polling())
print("Run olgram polling") loop.create_task(server_main().start())
loop.create_task(dp.start_polling())
if not args.noserver:
print("Run olgram server")
loop.create_task(server_main().start())
loop.run_forever() loop.run_forever()

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

BIN
media/logo1_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1,6 +0,0 @@
import asyncio
from olgram.migrations.custom import migrate
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(migrate())

View File

@ -1,81 +0,0 @@
"""
Здесь некоторые команды администратора
"""
from aiogram import types
from aiogram.dispatcher import FSMContext
from olgram.models import models
from olgram.router import dp
from olgram.settings import OlgramSettings
from locales.locale import _
@dp.message_handler(commands=["notifyowner"], state="*")
async def notify(message: types.Message, state: FSMContext):
"""
Команда /notify-owner
"""
if message.chat.id != OlgramSettings.supervisor_id():
await message.answer(_("Недостаточно прав"))
return
bot_name = message.get_args()
if not bot_name:
await message.answer(_("Нужно указать имя бота"))
return
bot = await models.Bot.filter(name=bot_name.replace("@", "")).first()
if not bot:
await message.answer(_("Такого бота нет в системе"))
return
await state.set_state("wait_owner_notify_message")
await state.update_data({"notify_to_bot": bot.id})
markup = types.ReplyKeyboardMarkup([[types.KeyboardButton(text=_("Пропустить"))]],
resize_keyboard=True)
await message.answer(_("Введите текст, который будет отправлен владельцу бота {0}. "
"Напишите 'Пропустить' чтобы отменить").format(bot_name), reply_markup=markup)
@dp.message_handler(state="wait_owner_notify_message")
async def on_notify_text(message: types.Message, state: FSMContext):
if not message.text:
await state.reset_state(with_data=True)
await message.answer(_("Поддерживается только текст"), reply_markup=types.ReplyKeyboardRemove())
return
if message.text == _("Пропустить"):
await state.reset_state(with_data=True)
await message.answer(_("Отменено"), reply_markup=types.ReplyKeyboardRemove())
return
await state.update_data({"notify_text": message.text})
await state.set_state("wait_owner_notify_message_confirm")
markup = types.ReplyKeyboardMarkup([[types.KeyboardButton(text=_("Отправить")),
types.KeyboardButton(text=_("Отменить"))]], resize_keyboard=True)
await message.answer("Точно отправить?", reply_markup=markup)
@dp.message_handler(state="wait_owner_notify_message_confirm")
async def on_notify_message_confirm(message: types.Message, state: FSMContext):
if not message.text or (message.text != _("Отправить")):
await state.reset_state(with_data=True)
await message.answer(_("Отменено"), reply_markup=types.ReplyKeyboardRemove())
return
data = await state.get_data()
bot = await models.Bot.get(pk=data["notify_to_bot"])
text = data["notify_text"]
chat_id = (await bot.owner).telegram_id
await state.reset_state(with_data=True)
await message.bot.send_message(chat_id, text=text)
await message.answer(_("Отправлено"), reply_markup=types.ReplyKeyboardRemove())

View File

@ -2,24 +2,18 @@
Здесь работа с конкретным ботом Здесь работа с конкретным ботом
""" """
from aiogram import types from aiogram import types
from aiogram.utils.exceptions import TelegramAPIError, Unauthorized from aiogram.utils.exceptions import TelegramAPIError
from aiogram import Bot as AioBot
from olgram.models.models import Bot from olgram.models.models import Bot
from server.server import unregister_token from server.server import unregister_token
from locales.locale import _
async def delete_bot(bot: Bot, call: types.CallbackQuery): async def delete_bot(bot: Bot, call: types.CallbackQuery):
""" """
Пользователь решил удалить бота Пользователь решил удалить бота
""" """
try: await unregister_token(bot.token)
await unregister_token(bot.decrypted_token())
except Unauthorized:
# Вероятно пользователь сбросил токен или удалил бот, это уже не наши проблемы
pass
await bot.delete() await bot.delete()
await call.answer(_("Бот удалён")) await call.answer("Бот удалён")
try: try:
await call.message.delete() await call.message.delete()
except TelegramAPIError: except TelegramAPIError:
@ -35,19 +29,7 @@ async def reset_bot_text(bot: Bot, call: types.CallbackQuery):
""" """
bot.start_text = bot._meta.fields_map['start_text'].default bot.start_text = bot._meta.fields_map['start_text'].default
await bot.save() await bot.save()
await call.answer(_("Текст сброшен")) await call.answer("Текст сброшен")
async def reset_bot_second_text(bot: Bot, call: types.CallbackQuery):
"""
Пользователь решил сбросить second text бота
:param bot:
:param call:
:return:
"""
bot.second_text = bot._meta.fields_map['second_text'].default
await bot.save()
await call.answer(_("Текст сброшен"))
async def select_chat(bot: Bot, call: types.CallbackQuery, chat: str): async def select_chat(bot: Bot, call: types.CallbackQuery, chat: str):
@ -61,48 +43,13 @@ async def select_chat(bot: Bot, call: types.CallbackQuery, chat: str):
if chat == "personal": if chat == "personal":
bot.group_chat = None bot.group_chat = None
await bot.save() await bot.save()
await call.answer(_("Выбран личный чат")) await call.answer("Выбран личный чат")
return
if chat == "leave":
bot.group_chat = None
await bot.save()
chats = await bot.group_chats.all()
a_bot = AioBot(bot.decrypted_token())
for chat in chats:
try:
await chat.delete()
await a_bot.leave_chat(chat.chat_id)
except TelegramAPIError:
pass
await call.answer(_("Бот вышел из чатов"))
await a_bot.session.close()
return return
chat_obj = await bot.group_chats.filter(id=chat).first() chat_obj = await bot.group_chats.filter(id=chat).first()
if not chat_obj: if not chat_obj:
await call.answer(_("Нельзя привязать бота к этому чату")) await call.answer("Нельзя привязать бота к этому чату")
return return
bot.group_chat = chat_obj bot.group_chat = chat_obj
await bot.save() await bot.save()
await call.answer(_("Выбран чат {0}").format(chat_obj.name)) await call.answer(f"Выбран чат {chat_obj.name}")
async def threads(bot: Bot, call: types.CallbackQuery):
bot.enable_threads = not bot.enable_threads
await bot.save(update_fields=["enable_threads"])
async def additional_info(bot: Bot, call: types.CallbackQuery):
bot.enable_additional_info = not bot.enable_additional_info
await bot.save(update_fields=["enable_additional_info"])
async def olgram_text(bot: Bot, call: types.CallbackQuery):
if await bot.is_promo():
bot.enable_olgram_text = not bot.enable_olgram_text
await bot.save(update_fields=["enable_olgram_text"])
async def antiflood(bot: Bot, call: types.CallbackQuery):
bot.enable_antiflood = not bot.enable_antiflood
await bot.save(update_fields=["enable_antiflood"])

View File

@ -9,10 +9,9 @@ import re
from textwrap import dedent from textwrap import dedent
from olgram.models.models import Bot, User from olgram.models.models import Bot, User
from olgram.settings import OlgramSettings, BotSettings from olgram.settings import OlgramSettings
from olgram.commands.menu import send_bots_menu from olgram.commands.menu import send_bots_menu
from server.server import register_token from server.server import register_token
from locales.locale import _
from olgram.router import dp from olgram.router import dp
@ -37,17 +36,12 @@ async def add_bot(message: types.Message, state: FSMContext):
""" """
Команда /addbot (добавить бота) Команда /addbot (добавить бота)
""" """
user = await User.get_or_none(telegram_id=message.from_user.id)
max_bot_count = OlgramSettings.max_bots_per_user()
if user and await user.is_promo():
max_bot_count = OlgramSettings.max_bots_per_user_promo()
bot_count = await Bot.filter(owner__telegram_id=message.from_user.id).count() bot_count = await Bot.filter(owner__telegram_id=message.from_user.id).count()
if bot_count >= max_bot_count: if bot_count >= OlgramSettings.max_bots_per_user():
await message.answer(_("У вас уже слишком много ботов. Удалите какой-нибудь свой бот из Olgram" await message.answer("У вас уже слишком много ботов.")
"(/mybots -> (Выбрать бота) -> Удалить бот)"))
return return
await message.answer(dedent(_(""" await message.answer(dedent("""
Чтобы подключить бот, вам нужно выполнить три действия: Чтобы подключить бот, вам нужно выполнить три действия:
1. Перейдите в бот @BotFather, нажмите START и отправьте команду /newbot 1. Перейдите в бот @BotFather, нажмите START и отправьте команду /newbot
@ -55,7 +49,7 @@ async def add_bot(message: types.Message, state: FSMContext):
3. После создания бота перешлите ответное сообщение в этот бот или скопируйте и пришлите token бота. 3. После создания бота перешлите ответное сообщение в этот бот или скопируйте и пришлите token бота.
Важно: не подключайте боты, которые используются в других сервисах (Manybot, Chatfuel, Livegram и других). Важно: не подключайте боты, которые используются в других сервисах (Manybot, Chatfuel, Livegram и других).
"""))) """))
await state.set_state("add_bot") await state.set_state("add_bot")
@ -67,26 +61,26 @@ async def bot_added(message: types.Message, state: FSMContext):
token = re.findall(token_pattern, message.text) token = re.findall(token_pattern, message.text)
async def on_invalid_token(): async def on_invalid_token():
await message.answer(dedent(_(""" await message.answer(dedent("""
Это не токен бота. Это не токен бота.
Токен выглядит вот так: 123456789:AAAA-abc123_AbcdEFghijKLMnopqrstu12 Токен выглядит вот так: 123456789:AAAA-abc123_AbcdEFghijKLMnopqrstu12
"""))) """))
async def on_dummy_token(): async def on_dummy_token():
await message.answer(dedent(_(""" await message.answer(dedent("""
Не удалось запустить этого бота: неверный токен Не удалось запустить этого бота: неверный токен
"""))) """))
async def on_unknown_error(): async def on_unknown_error():
await message.answer(dedent(_(""" await message.answer(dedent("""
Не удалось запустить этого бота: непредвиденная ошибка Не удалось запустить этого бота: непредвиденная ошибка
"""))) """))
async def on_duplication_bot(): async def on_duplication_bot():
await message.answer(dedent(_(""" await message.answer(dedent("""
Такой бот уже есть в базе данных Такой бот уже есть в базе данных
"""))) """))
if not token: if not token:
return await on_invalid_token() return await on_invalid_token()
@ -104,12 +98,8 @@ async def bot_added(message: types.Message, state: FSMContext):
except TelegramAPIError: except TelegramAPIError:
return await on_unknown_error() return await on_unknown_error()
if token == BotSettings.token(): user, _ = await User.get_or_create(telegram_id=message.from_user.id)
return await on_duplication_bot() bot = Bot(token=token, owner=user, name=test_bot_info.username, super_chat_id=message.from_user.id)
user, created = await User.get_or_create(telegram_id=message.from_user.id)
bot = Bot(token=Bot.encrypted_token(token), owner=user, name=test_bot_info.username,
super_chat_id=message.from_user.id)
try: try:
await bot.save() await bot.save()
except IntegrityError: except IntegrityError:
@ -119,5 +109,5 @@ async def bot_added(message: types.Message, state: FSMContext):
await bot.delete() await bot.delete()
return await on_unknown_error() return await on_unknown_error()
await message.answer(_("Бот добавлен! Список ваших ботов: /mybots")) await message.answer("Бот добавлен! Список ваших ботов: /mybots")
await state.reset_state() await state.reset_state()

View File

@ -1,40 +0,0 @@
"""
Здесь метрики
"""
from aiogram import types
from aiogram.dispatcher import FSMContext
from olgram.models import models
from olgram.router import dp
from olgram.settings import OlgramSettings
from locales.locale import _
@dp.message_handler(commands=["info"], state="*")
async def info(message: types.Message, state: FSMContext):
"""
Команда /info
"""
if message.chat.id != OlgramSettings.supervisor_id():
await message.answer(_("Недостаточно прав"))
return
bots = await models.Bot.all()
bots_count = len(bots)
user_count = len(await models.User.all())
templates_count = len(await models.DefaultAnswer.all())
promo_count = len(await models.Promo.all())
olgram_text_disabled = len(await models.Bot.filter(enable_olgram_text=False))
income_messages = sum([bot.incoming_messages_count for bot in bots])
outgoing_messages = sum([bot.outgoing_messages_count for bot in bots])
await message.answer(_("Количество ботов: {0}\n").format(bots_count) +
_("Количество пользователей (у конструктора): {0}\n").format(user_count) +
_("Шаблонов ответов: {0}\n").format(templates_count) +
_("Входящих сообщений у всех ботов: {0}\n").format(income_messages) +
_("Исходящих сообщений у всех ботов: {0}\n").format(outgoing_messages) +
_("Промо-кодов выдано: {0}\n").format(promo_count) +
_("Рекламную плашку выключили: {0}\n".format(olgram_text_disabled)))

View File

@ -1,13 +1,12 @@
from olgram.router import dp from olgram.router import dp
from aiogram import types, Bot as AioBot from aiogram import types, Bot as AioBot
from olgram.models.models import Bot, User, DefaultAnswer from olgram.models.models import Bot, User
from aiogram.dispatcher import FSMContext from aiogram.dispatcher import FSMContext
from aiogram.utils.callback_data import CallbackData from aiogram.utils.callback_data import CallbackData
from textwrap import dedent from textwrap import dedent
from olgram.utils.mix import edit_or_create, button_text_limit, wrap from olgram.utils.mix import edit_or_create, button_text_limit
from olgram.commands import bot_actions from olgram.commands import bot_actions
from locales.locale import _
import typing as ty import typing as ty
@ -28,11 +27,11 @@ async def send_bots_menu(chat_id: int, user_id: int, call=None):
user = await User.get_or_none(telegram_id=user_id) user = await User.get_or_none(telegram_id=user_id)
bots = await Bot.filter(owner=user) bots = await Bot.filter(owner=user)
if not bots: if not bots:
await AioBot.get_current().send_message(chat_id, dedent(_(""" await AioBot.get_current().send_message(chat_id, dedent("""
У вас нет добавленных ботов. У вас нет добавленных ботов.
Отправьте команду /addbot, чтобы добавить бот. Отправьте команду /addbot, чтобы добавить бот.
"""))) """))
return return
keyboard = types.InlineKeyboardMarkup(row_width=2) keyboard = types.InlineKeyboardMarkup(row_width=2)
@ -43,7 +42,7 @@ async def send_bots_menu(chat_id: int, user_id: int, call=None):
chat=empty)) chat=empty))
) )
text = _("Ваши боты") text = "Ваши боты"
if call: if call:
await edit_or_create(call, text, keyboard) await edit_or_create(call, text, keyboard)
else: else:
@ -64,33 +63,26 @@ async def send_chats_menu(bot: Bot, call: types.CallbackQuery):
) )
if chats: if chats:
keyboard.insert( keyboard.insert(
types.InlineKeyboardButton(text=_("Личные сообщения"), types.InlineKeyboardButton(text="Личные сообщения",
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="chat", callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="chat",
chat="personal")) chat="personal"))
) )
keyboard.insert(
types.InlineKeyboardButton(text=_("❗️ Выйти из всех чатов"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="chat",
chat="leave"))
)
keyboard.insert( keyboard.insert(
types.InlineKeyboardButton(text=_("<< Назад"), types.InlineKeyboardButton(text="<< Назад",
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty,
chat=empty)) chat=empty))
) )
if not chats: if not chats:
text = dedent(_(""" text = dedent(f"""
Этот бот не добавлен в чаты, поэтому все сообщения будут приходить вам в бот. Этот бот не добавлен в чаты, поэтому все сообщения будут приходить вам в бот.
Чтобы подключить чат добавьте бот @{0} в чат, откройте это меню ещё раз и выберите добавленный чат. Чтобы подключить чат просто добавьте бот @{bot.name} в чат.
Если ваш бот состоял в групповом чате до того, как его добавили в Olgram - удалите бота из чата и добавьте """)
снова.
""")).format(bot.name)
else: else:
text = dedent(_(""" text = dedent(f"""
В этом разделе вы можете привязать бота @{0} к чату. В этом разделе вы можете привязать бота @{bot.name} к чату.
Выберите чат, куда бот будет пересылать сообщения. Выберите чат, куда бот будет пересылать сообщения.
""")).format(bot.name) """)
await edit_or_create(call, text, keyboard) await edit_or_create(call, text, keyboard)
@ -99,107 +91,49 @@ async def send_bot_menu(bot: Bot, call: types.CallbackQuery):
await call.answer() await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=2) keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert( keyboard.insert(
types.InlineKeyboardButton(text=_("Текст"), types.InlineKeyboardButton(text="Текст",
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="text", callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="text",
chat=empty)) chat=empty))
) )
keyboard.insert( keyboard.insert(
types.InlineKeyboardButton(text=_("Чат"), types.InlineKeyboardButton(text="Чат",
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="chat", callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="chat",
chat=empty)) chat=empty))
) )
keyboard.insert( keyboard.insert(
types.InlineKeyboardButton(text=_("Удалить бот"), types.InlineKeyboardButton(text="Удалить бот",
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="delete", callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="delete",
chat=empty)) chat=empty))
) )
keyboard.insert( keyboard.insert(
types.InlineKeyboardButton(text=_("Статистика"), types.InlineKeyboardButton(text="<< Назад",
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="stat",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Назад"),
callback_data=menu_callback.new(level=0, bot_id=empty, operation=empty, chat=empty)) callback_data=menu_callback.new(level=0, bot_id=empty, operation=empty, chat=empty))
) )
keyboard.insert(
types.InlineKeyboardButton(text=_("Опции"),
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="settings",
chat=empty))
)
await edit_or_create(call, dedent(_(""" await edit_or_create(call, dedent(f"""
Управление ботом @{0}. Управление ботом @{bot.name}.
Если у вас возникли вопросы по настройке бота, то посмотрите нашу справку /help или напишите нам Если у вас возникли вопросы по настройке бота, то посмотрите нашу справку /help или напишите нам
@civsocit_feedback_bot @civsocit_feedback_bot
""")).format(bot.name), reply_markup=keyboard) """), reply_markup=keyboard)
async def send_bot_delete_menu(bot: Bot, call: types.CallbackQuery): async def send_bot_delete_menu(bot: Bot, call: types.CallbackQuery):
await call.answer() await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=2) keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert( keyboard.insert(
types.InlineKeyboardButton(text=_("Да, удалить бот"), types.InlineKeyboardButton(text="Да, удалить бот",
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="delete_yes", callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="delete_yes",
chat=empty)) chat=empty))
) )
keyboard.insert( keyboard.insert(
types.InlineKeyboardButton(text=_("<< Назад"), types.InlineKeyboardButton(text="<< Назад",
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty)) callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
) )
await edit_or_create(call, dedent(_(""" await edit_or_create(call, dedent(f"""
Вы уверены, что хотите удалить бота @{0}? Вы уверены, что хотите удалить бота @{bot.name}?
""")).format(bot.name), reply_markup=keyboard) """), reply_markup=keyboard)
async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery):
await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert(
types.InlineKeyboardButton(text=_("Потоки сообщений"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="threads",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Данные пользователя"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="additional_info",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Антифлуд"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="antiflood",
chat=empty))
)
is_promo = await bot.is_promo()
if is_promo:
keyboard.insert(
types.InlineKeyboardButton(text=_("Olgram подпись"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="olgram_text",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Назад"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty,
chat=empty))
)
thread_turn = _("включены") if bot.enable_threads else _("выключены")
info_turn = _("включены") if bot.enable_additional_info else _("выключены")
antiflood_turn = _("включен") if bot.enable_antiflood else _("выключен")
text = dedent(_("""
<a href="https://olgram.readthedocs.io/ru/latest/options.html#threads">Потоки сообщений</a>: <b>{0}</b>
<a href="https://olgram.readthedocs.io/ru/latest/options.html#user-info">Данные пользователя</a>: <b>{1}</b>
<a href="https://olgram.readthedocs.io/ru/latest/options.html#antiflood">Антифлуд</a>: <b>{2}</b>
""")).format(thread_turn, info_turn, antiflood_turn)
if is_promo:
olgram_turn = _("включена") if bot.enable_olgram_text else _("выключена")
text += _("Olgram подпись: <b>{0}</b>").format(olgram_turn)
await edit_or_create(call, text, reply_markup=keyboard, parse_mode="HTML")
async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None, chat_id: ty.Optional[int] = None): async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None, chat_id: ty.Optional[int] = None):
@ -207,22 +141,17 @@ async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] =
await call.answer() await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=2) keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert( keyboard.insert(
types.InlineKeyboardButton(text=_("<< Завершить редактирование"), types.InlineKeyboardButton(text="Сбросить текст",
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Автоответчик"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="next_text",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Сбросить текст"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="reset_text", callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="reset_text",
chat=empty)) chat=empty))
) )
keyboard.insert(
types.InlineKeyboardButton(text="<< Завершить редактирование",
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
)
text = dedent(_(""" text = dedent("""
Сейчас вы редактируете текст, который отправляется после того, как пользователь отправит вашему боту @{0} Сейчас вы редактируете текст, который отправляется после того, как пользователь отправит вашему боту {0}
команду /start команду /start
Текущий текст: Текущий текст:
@ -230,7 +159,7 @@ async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] =
{1} {1}
</pre> </pre>
Отправьте сообщение, чтобы изменить текст. Отправьте сообщение, чтобы изменить текст.
""")) """)
text = text.format(bot.name, bot.start_text) text = text.format(bot.name, bot.start_text)
if call: if call:
await edit_or_create(call, text, keyboard, parse_mode="HTML") await edit_or_create(call, text, keyboard, parse_mode="HTML")
@ -238,163 +167,16 @@ async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] =
await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML") await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML")
async def send_bot_statistic_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None,
chat_id: ty.Optional[int] = None):
if call:
await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Назад"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
)
text = dedent(_("""
Статистика по боту @{0}
Входящих сообщений: <b>{1}</b>
Ответных сообщений: <b>{2}</b>
Шаблоны ответов: <b>{3}</b>
Забанено пользователей: <b>{4}</b>
""")).format(bot.name, bot.incoming_messages_count, bot.outgoing_messages_count, len(await bot.answers),
len(await bot.banned_users))
if call:
await edit_or_create(call, text, keyboard, parse_mode="HTML")
else:
await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML")
async def send_bot_second_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None,
chat_id: ty.Optional[int] = None):
if call:
await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Завершить редактирование"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Предыдущий текст"),
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="text",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Шаблоны ответов..."),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="templates",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Сбросить текст"),
callback_data=menu_callback.new(level=3, bot_id=bot.id,
operation="reset_second_text", chat=empty))
)
text = dedent(_("""
Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в ответ на все входящие сообщения @{0} \
автоматически. По умолчанию оно отключено.
Текущий текст:
<pre>
{1}
</pre>
Отправьте сообщение, чтобы изменить текст.
"""))
text = text.format(bot.name, bot.second_text if bot.second_text else _("(отключено)"))
if call:
await edit_or_create(call, text, keyboard, parse_mode="HTML")
else:
await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML")
async def send_bot_templates_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None,
chat_id: ty.Optional[int] = None):
if call:
await call.answer()
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Завершить редактирование"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
)
text = dedent(_("""
Сейчас вы редактируете шаблоны ответов для @{0}. Текущие шаблоны:
<pre>
{1}
</pre>
Отправьте какую-нибудь фразу (например: "Ваш заказ готов, ожидайте!"), чтобы добавить её в шаблон.
Чтобы удалить шаблон из списка, отправьте его номер в списке (например, 4)
"""))
templates = await bot.answers
total_text_len = sum(len(t.text) for t in templates) + len(text) # примерная длина текста
max_len = 1000
if total_text_len > 4000:
max_len = 100
templates_text = "\n".join(f"{n}. {wrap(template.text, max_len)}" for n, template in enumerate(templates))
if not templates_text:
templates_text = _("(нет шаблонов)")
text = text.format(bot.name, templates_text)
if call:
await edit_or_create(call, text, keyboard, parse_mode="HTML")
else:
await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML")
@dp.message_handler(state="wait_start_text", content_types="text", regexp="^[^/].+") # Not command @dp.message_handler(state="wait_start_text", content_types="text", regexp="^[^/].+") # Not command
async def start_text_received(message: types.Message, state: FSMContext): async def start_text_received(message: types.Message, state: FSMContext):
async with state.proxy() as proxy: async with state.proxy() as proxy:
bot_id = proxy.get("bot_id") bot_id = proxy.get("bot_id")
bot = await Bot.get_or_none(pk=bot_id) bot = await Bot.get_or_none(pk=bot_id)
bot.start_text = message.html_text bot.start_text = message.text
await bot.save() await bot.save()
await send_bot_text_menu(bot, chat_id=message.chat.id) await send_bot_text_menu(bot, chat_id=message.chat.id)
@dp.message_handler(state="wait_second_text", content_types="text", regexp="^[^/].+") # Not command
async def second_text_received(message: types.Message, state: FSMContext):
async with state.proxy() as proxy:
bot_id = proxy.get("bot_id")
bot = await Bot.get_or_none(pk=bot_id)
bot.second_text = message.html_text
await bot.save()
await send_bot_second_text_menu(bot, chat_id=message.chat.id)
@dp.message_handler(state="wait_template", content_types="text", regexp="^[^/](.+)?") # Not command
async def template_received(message: types.Message, state: FSMContext):
async with state.proxy() as proxy:
bot_id = proxy.get("bot_id")
bot = await Bot.get_or_none(pk=bot_id)
if message.text.isdigit():
# Delete template
number = int(message.text)
templates = await bot.answers
if not templates:
await message.answer(_("У вас нет шаблонов, чтобы их удалять"))
if number < 0 or number >= len(templates):
await message.answer(_("Неправильное число. Чтобы удалить шаблон, введите число от 0 до {0}").format(
len(templates)))
return
await templates[number].delete()
else:
# Add template
total_templates = len(await bot.answers)
if total_templates > 30:
await message.answer(_("У вашего бота уже слишком много шаблонов"))
else:
answers = await bot.answers.filter(text=message.text)
if answers:
await message.answer(_("Такой текст уже есть в списке шаблонов"))
else:
template = DefaultAnswer(text=message.text, bot=bot)
await template.save()
await send_bot_templates_menu(bot, chat_id=message.chat.id)
@dp.callback_query_handler(menu_callback.filter(), state="*") @dp.callback_query_handler(menu_callback.filter(), state="*")
async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMContext): async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMContext):
level = callback_data.get("level") level = callback_data.get("level")
@ -405,11 +187,10 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
bot_id = callback_data.get("bot_id") bot_id = callback_data.get("bot_id")
bot = await Bot.get_or_none(id=bot_id) bot = await Bot.get_or_none(id=bot_id)
if not bot or (await bot.owner).telegram_id != call.from_user.id: if not bot or (await bot.owner).telegram_id != call.from_user.id:
await call.answer(_("У вас нет прав на этого бота"), show_alert=True) await call.answer("У вас нет прав на этого бота", show_alert=True)
return return
if level == "1": if level == "1":
await state.reset_state()
return await send_bot_menu(bot, call) return await send_bot_menu(bot, call)
operation = callback_data.get("operation") operation = callback_data.get("operation")
@ -419,10 +200,6 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
return await send_chats_menu(bot, call) return await send_chats_menu(bot, call)
if operation == "delete": if operation == "delete":
return await send_bot_delete_menu(bot, call) return await send_bot_delete_menu(bot, call)
if operation == "stat":
return await send_bot_statistic_menu(bot, call)
if operation == "settings":
return await send_bot_settings_menu(bot, call)
if operation == "text": if operation == "text":
await state.set_state("wait_start_text") await state.set_state("wait_start_text")
async with state.proxy() as proxy: async with state.proxy() as proxy:
@ -434,31 +211,6 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
return await bot_actions.delete_bot(bot, call) return await bot_actions.delete_bot(bot, call)
if operation == "chat": if operation == "chat":
return await bot_actions.select_chat(bot, call, callback_data.get("chat")) return await bot_actions.select_chat(bot, call, callback_data.get("chat"))
if operation == "threads":
await bot_actions.threads(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "antiflood":
await bot_actions.antiflood(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "additional_info":
await bot_actions.additional_info(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "olgram_text":
await bot_actions.olgram_text(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "reset_text": if operation == "reset_text":
await bot_actions.reset_bot_text(bot, call) await bot_actions.reset_bot_text(bot, call)
return await send_bot_text_menu(bot, call) return await send_bot_text_menu(bot, call)
if operation == "next_text":
await state.set_state("wait_second_text")
async with state.proxy() as proxy:
proxy["bot_id"] = bot.id
return await send_bot_second_text_menu(bot, call)
if operation == "reset_second_text":
await bot_actions.reset_bot_second_text(bot, call)
return await send_bot_second_text_menu(bot, call)
if operation == "templates":
await state.set_state("wait_template")
async with state.proxy() as proxy:
proxy["bot_id"] = bot.id
return await send_bot_templates_menu(bot, call)

View File

@ -1,91 +0,0 @@
"""
Здесь промокоды
"""
from aiogram import types
from aiogram.dispatcher import FSMContext
from olgram.models import models
from uuid import UUID
from olgram.router import dp
from olgram.settings import OlgramSettings
from locales.locale import _
@dp.message_handler(commands=["newpromo"], state="*")
async def new_promo(message: types.Message, state: FSMContext):
"""
Команда /newpromo
"""
if message.chat.id != OlgramSettings.supervisor_id():
await message.answer(_("Недостаточно прав"))
return
promo = await models.Promo()
await message.answer(_("Новый промокод\n```{0}```").format(promo.code), parse_mode="Markdown")
await promo.save()
@dp.message_handler(commands=["delpromo"], state="*")
async def del_promo(message: types.Message, state: FSMContext):
"""
Команда /delpromo
"""
if message.chat.id != OlgramSettings.supervisor_id():
await message.answer(_("Недостаточно прав"))
return
try:
uuid = UUID(message.get_args().strip())
promo = await models.Promo.get_or_none(code=uuid)
except ValueError:
return await message.answer(_("Неправильный токен"))
if not promo:
return await message.answer(_("Такого кода не существует"))
user = await models.User.filter(promo=promo)
bots = await user.bots()
for bot in bots:
bot.enable_olgram_text = True
await bot.save(update_fields=["enable_olgram_text"])
await promo.delete()
await message.answer(_("Промокод отозван"))
@dp.message_handler(commands=["setpromo"], state="*")
async def setpromo(message: types.Message, state: FSMContext):
"""
Команда /setpromo
"""
arg = message.get_args()
if not arg:
return await message.answer(_("Укажите аргумент: промокод. Например: <pre>/setpromo my-promo-code</pre>"),
parse_mode="HTML")
arg = arg.strip()
try:
UUID(arg)
except ValueError:
return await message.answer(_("Промокод не найден"))
promo = await models.Promo.get_or_none(code=arg)
if not promo:
return await message.answer(_("Промокод не найден"))
if promo.owner:
return await message.answer(_("Промокод уже использован"))
user, created = await models.User.get_or_create(telegram_id=message.from_user.id)
promo.owner = user
await promo.save(update_fields=["owner_id"])
await message.answer(_("Промокод активирован! Спасибо 🙌"))

View File

@ -6,24 +6,21 @@ from aiogram import types
from aiogram.dispatcher import FSMContext from aiogram.dispatcher import FSMContext
from textwrap import dedent from textwrap import dedent
from olgram.settings import OlgramSettings from olgram.settings import OlgramSettings
from olgram.utils.permissions import public
from locales.locale import _
from olgram.router import dp from olgram.router import dp
@dp.message_handler(commands=["start"], state="*") @dp.message_handler(commands=["start"], state="*")
@public()
async def start(message: types.Message, state: FSMContext): async def start(message: types.Message, state: FSMContext):
""" """
Команда /start Команда /start
""" """
await state.reset_state() await state.reset_state()
await message.answer(dedent(_(""" # TODO: locale
Olgram Bot это конструктор ботов обратной связи в Telegram. Подробнее \
<a href="https://olgram.readthedocs.io">читайте здесь</a>. Следите за обновлениями \ await message.answer(dedent("""
<a href="https://t.me/civsoc_it">здесь</a>. Olgram Bot это конструктор ботов обратной связи в Telegram.
Используйте эти команды, чтобы управлять этим ботом: Используйте эти команды, чтобы управлять этим ботом:
@ -31,26 +28,20 @@ async def start(message: types.Message, state: FSMContext):
/mybots - управление ботами /mybots - управление ботами
/help - помощь /help - помощь
""")), parse_mode="html", disable_web_page_preview=True) """))
@dp.message_handler(commands=["help"], state="*") @dp.message_handler(commands=["help"], state="*")
@public()
async def help(message: types.Message, state: FSMContext): async def help(message: types.Message, state: FSMContext):
""" """
Команда /help Команда /help
""" """
await message.answer(dedent(_(""" await message.answer(dedent(f"""
Читайте инструкции на нашем сайте https://olgram.readthedocs.io О проекте https://telegra.ph/Olgram-09-15
Техническая поддержка: @civsocit_feedback_bot
Версия {0}
""")).format(OlgramSettings.version()))
Репозиторий https://github.com/civsocit/olgram
@dp.message_handler(commands=["chatid"], state="*") Поддержка: @civsocit_feedback_bot
@public()
async def chat_id(message: types.Message, state: FSMContext): Версия {OlgramSettings.version()}
""" """))
Команда /chatid
"""
await message.answer(message.chat.id)

View File

@ -1,79 +0,0 @@
"""Наши собственные миграции, которые нельзя описать на языке SQL и с которыми не справится TortoiseORM/Aerich"""
import aioredis
from tortoise import transactions, Tortoise
from olgram.settings import TORTOISE_ORM, ServerSettings
from olgram.models.models import MetaInfo, Bot
import logging
async def upgrade_1():
"""Шифруем токены"""
meta_info = await MetaInfo.first()
if meta_info.version != 0:
logging.info("skip")
return
async with transactions.in_transaction():
bots = await Bot.all()
for bot in bots:
bot.token = bot.encrypted_token(bot.token)
await bot.save()
meta_info.version = 1
await meta_info.save()
logging.info("done")
async def upgrade_2():
"""Отменяем малый TTL для старых сообщений"""
meta_info = await MetaInfo.first()
if meta_info.version != 1:
logging.info("skip")
return
con = await aioredis.create_connection(ServerSettings.redis_path())
client = aioredis.Redis(con)
i, keys = await client.scan()
for key in keys:
if not key.startswith(b"thread"):
await client.pexpire(key, ServerSettings.redis_timeout_ms())
meta_info.version = 2
await meta_info.save()
logging.info("done")
async def upgrade_3():
"""start_text и second_text должны быть валидными HTML"""
import html
meta_info = await MetaInfo.first()
if meta_info.version != 2:
logging.info("skip")
return
async with transactions.in_transaction():
bots = await Bot.all()
for bot in bots:
if bot.start_text:
bot.start_text = html.escape(bot.start_text)
if bot.second_text:
bot.second_text = html.escape(bot.second_text)
await bot.save(update_fields=["start_text", "second_text"])
meta_info.version = 3
await meta_info.save()
logging.info("done")
# Не забудь добавить миграцию в этот лист!
_migrations = [upgrade_1, upgrade_2, upgrade_3]
async def migrate():
logging.info("Run custom migrations...")
await Tortoise.init(config=TORTOISE_ORM)
for migration in _migrations:
logging.info(f"Migration {migration.__name__}...")
await migration()

View File

@ -1,4 +0,0 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_threads" BOOL NOT NULL DEFAULT False;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_threads";

View File

@ -1,4 +0,0 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_additional_info" BOOL NOT NULL DEFAULT False;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_additional_info";

View File

@ -1,10 +0,0 @@
-- upgrade --
CREATE TABLE IF NOT EXISTS "promo" (
"id" BIGSERIAL NOT NULL PRIMARY KEY,
"code" UUID NOT NULL,
"date" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"owner_id" INT REFERENCES "user" ("id") ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS "idx_promo_code_9b981a" ON "promo" ("code");
-- downgrade --
DROP TABLE IF EXISTS "promo";

View File

@ -1,4 +0,0 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_olgram_text" BOOL NOT NULL DEFAULT True;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_olgram_text";

View File

@ -1,4 +0,0 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_antiflood" BOOL NOT NULL DEFAULT False;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_antiflood";

View File

@ -1,4 +0,0 @@
-- upgrade --
ALTER TABLE "bot" ADD "second_text" TEXT;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "second_text";

View File

@ -1,10 +0,0 @@
-- upgrade --
ALTER TABLE "bot" ALTER COLUMN "token" TYPE VARCHAR(200) USING "token"::VARCHAR(200);
CREATE TABLE IF NOT EXISTS "_custom_meta_info" (
"id" SERIAL NOT NULL PRIMARY KEY,
"version" INT NOT NULL DEFAULT 0
);;
INSERT INTO _custom_meta_info (id, version) VALUES (0, 0) ON CONFLICT DO NOTHING;
-- downgrade --
ALTER TABLE "bot" ALTER COLUMN "token" TYPE VARCHAR(50) USING "token"::VARCHAR(50);
DROP TABLE IF EXISTS "_custom_meta_info";

View File

@ -1,11 +0,0 @@
-- upgrade --
CREATE TABLE IF NOT EXISTS "bot_banned_user" (
"id" BIGSERIAL NOT NULL PRIMARY KEY,
"telegram_id" BIGINT NOT NULL,
"username" VARCHAR(100),
"bot_id" INT NOT NULL REFERENCES "bot" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_bot_banned__telegra_915aca" ON "bot_banned_user" ("telegram_id");
-- downgrade --
DROP TABLE IF EXISTS "bot_banned_user";
DROP INDEX IF EXISTS "idx_bot_banned__telegra_915aca";

View File

@ -1,7 +0,0 @@
-- upgrade --
CREATE TABLE IF NOT EXISTS "defaultanswer" (
"id" BIGSERIAL NOT NULL PRIMARY KEY,
"bot_id" INT NOT NULL REFERENCES "bot" ("id") ON DELETE CASCADE
);
-- downgrade --
DROP TABLE IF EXISTS "defaultanswer";

View File

@ -1,4 +0,0 @@
-- upgrade --
ALTER TABLE "defaultanswer" ADD "text" TEXT NOT NULL;
-- downgrade --
ALTER TABLE "defaultanswer" DROP COLUMN "text";

View File

@ -1,6 +0,0 @@
-- upgrade --
ALTER TABLE "bot" ADD "outgoing_messages_count" BIGINT NOT NULL DEFAULT 0;
ALTER TABLE "bot" ADD "incoming_messages_count" BIGINT NOT NULL DEFAULT 0;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "outgoing_messages_count";
ALTER TABLE "bot" DROP COLUMN "incoming_messages_count";

View File

@ -2,36 +2,18 @@ from tortoise.models import Model
from tortoise import fields from tortoise import fields
from uuid import uuid4 from uuid import uuid4
from textwrap import dedent from textwrap import dedent
from olgram.settings import DatabaseSettings
from locales.locale import _
class MetaInfo(Model):
id = fields.IntField(pk=True)
version = fields.IntField(default=0)
def __init__(self, **kwargs):
# Кажется это единственный способ сделать single-instance модель в TortoiseORM :(
if "id" in kwargs:
kwargs["id"] = 0
self.id = 0
super(MetaInfo, self).__init__(**kwargs)
class Meta:
table = '_custom_meta_info'
class Bot(Model): class Bot(Model):
id = fields.IntField(pk=True) id = fields.IntField(pk=True)
token = fields.CharField(max_length=200, unique=True) token = fields.CharField(max_length=50, unique=True)
owner = fields.ForeignKeyField("models.User", related_name="bots") owner = fields.ForeignKeyField("models.User", related_name="bots")
name = fields.CharField(max_length=33) name = fields.CharField(max_length=33)
code = fields.UUIDField(default=uuid4, index=True) code = fields.UUIDField(default=uuid4, index=True)
start_text = fields.TextField(default=dedent(_(""" start_text = fields.TextField(default=dedent("""
Здравствуйте! Здравствуйте!
Напишите ваш вопрос и мы ответим вам в ближайшее время. Напишите ваш вопрос и мы ответим вам в ближайшее время.
"""))) """))
second_text = fields.TextField(null=True, default=None)
group_chats = fields.ManyToManyField("models.GroupChat", related_name="bots", on_delete=fields.relational.CASCADE, group_chats = fields.ManyToManyField("models.GroupChat", related_name="bots", on_delete=fields.relational.CASCADE,
null=True) null=True)
@ -39,33 +21,12 @@ class Bot(Model):
on_delete=fields.relational.CASCADE, on_delete=fields.relational.CASCADE,
null=True) null=True)
incoming_messages_count = fields.BigIntField(default=0)
outgoing_messages_count = fields.BigIntField(default=0)
enable_threads = fields.BooleanField(default=False)
enable_additional_info = fields.BooleanField(default=False)
enable_olgram_text = fields.BooleanField(default=True)
enable_antiflood = fields.BooleanField(default=False)
def decrypted_token(self):
cryptor = DatabaseSettings.cryptor()
return cryptor.decrypt(self.token)
@classmethod
def encrypted_token(cls, token: str):
cryptor = DatabaseSettings.cryptor()
return cryptor.encrypt(token)
async def super_chat_id(self): async def super_chat_id(self):
group_chat = await self.group_chat group_chat = await self.group_chat
if group_chat: if group_chat:
return group_chat.chat_id return group_chat.chat_id
return (await self.owner).telegram_id return (await self.owner).telegram_id
async def is_promo(self):
await self.fetch_related("owner")
return await self.owner.is_promo()
class Meta: class Meta:
table = 'bot' table = 'bot'
@ -74,10 +35,6 @@ class User(Model):
id = fields.IntField(pk=True) id = fields.IntField(pk=True)
telegram_id = fields.BigIntField(index=True, unique=True) telegram_id = fields.BigIntField(index=True, unique=True)
async def is_promo(self):
await self.fetch_related("promo")
return bool(self.promo)
class Meta: class Meta:
table = 'user' table = 'user'
@ -89,29 +46,3 @@ class GroupChat(Model):
class Meta: class Meta:
table = 'group_chat' table = 'group_chat'
class BannedUser(Model):
id = fields.BigIntField(pk=True)
telegram_id = fields.BigIntField(index=True)
username = fields.CharField(max_length=100, default=None, null=True)
bot = fields.ForeignKeyField("models.Bot", related_name="banned_users", on_delete=fields.relational.CASCADE)
class Meta:
table = "bot_banned_user"
class DefaultAnswer(Model):
id = fields.BigIntField(pk=True)
bot = fields.ForeignKeyField("models.Bot", related_name="answers", on_delete=fields.relational.CASCADE)
text = fields.TextField()
class Promo(Model):
id = fields.BigIntField(pk=True)
code = fields.UUIDField(default=uuid4, index=True)
date = fields.DatetimeField(auto_now_add=True)
owner = fields.ForeignKeyField("models.User", related_name="promo", on_delete=fields.relational.SET_NULL,
null=True, default=None)

View File

@ -1,25 +1,18 @@
from dotenv import load_dotenv from dotenv import load_dotenv
from abc import ABC from abc import ABC
import os import os
import logging
from functools import lru_cache
from datetime import timedelta
import typing as ty
from olgram.utils.crypto import Cryptor
load_dotenv() load_dotenv()
# TODO: рефакторинг, использовать какой-нибудь lazy-config вместо своих костылей
class AbstractSettings(ABC): class AbstractSettings(ABC):
@classmethod @classmethod
def _get_env(cls, parameter: str, allow_none: bool = False) -> str: def _get_env(cls, parameter: str, allow_none: bool = False) -> str:
parameter_v = os.getenv(parameter, None) parameter = os.getenv(parameter, None)
if not parameter_v and not allow_none: if not parameter and not allow_none:
raise ValueError(f"{parameter} not defined in ENV") raise ValueError(f"{parameter} not defined in ENV")
return parameter_v return parameter
class OlgramSettings(AbstractSettings): class OlgramSettings(AbstractSettings):
@ -29,31 +22,11 @@ class OlgramSettings(AbstractSettings):
Максимальное количество ботов у одного пользователя Максимальное количество ботов у одного пользователя
:return: int :return: int
""" """
return 10 return 5
@classmethod
def max_bots_per_user_promo(cls) -> int:
"""
Максимальное количество ботов у одного пользователя с промо-доступом
:return: int
"""
return 25
@classmethod @classmethod
def version(cls): def version(cls):
return "0.5.0" return "0.0.3"
@classmethod
@lru_cache
def admin_ids(cls):
_ids = cls._get_env("ADMIN_ID", True)
return set(map(int, _ids.split(","))) if _ids else None
@classmethod
@lru_cache
def supervisor_id(cls):
_id = cls._get_env("SUPERVISOR_ID", True)
return int(_id) if _id else None
class ServerSettings(AbstractSettings): class ServerSettings(AbstractSettings):
@ -65,6 +38,10 @@ class ServerSettings(AbstractSettings):
def hook_port(cls) -> int: def hook_port(cls) -> int:
return int(cls._get_env("WEBHOOK_PORT")) return int(cls._get_env("WEBHOOK_PORT"))
@classmethod
def app_host(cls) -> str:
return "olgram"
@classmethod @classmethod
def app_port(cls) -> int: def app_port(cls) -> int:
return 80 return 80
@ -94,24 +71,9 @@ class ServerSettings(AbstractSettings):
def append_text(cls) -> str: def append_text(cls) -> str:
return "\n\nЭтот бот создан с помощью @OlgramBot" return "\n\nЭтот бот создан с помощью @OlgramBot"
@classmethod
@lru_cache
def redis_timeout_ms(cls) -> ty.Optional[int]:
return int(timedelta(days=180).total_seconds() * 1000.0)
@classmethod
@lru_cache
def thread_timeout_ms(cls) -> int:
return int(timedelta(days=1).total_seconds() * 1000.0)
logging.basicConfig(level=os.environ.get("LOGLEVEL") or "WARNING",
format='%(asctime)s %(levelname)-8s %(message)s')
class BotSettings(AbstractSettings): class BotSettings(AbstractSettings):
@classmethod @classmethod
@lru_cache
def token(cls) -> str: def token(cls) -> str:
""" """
Токен olgram бота Токен olgram бота
@ -119,14 +81,6 @@ class BotSettings(AbstractSettings):
""" """
return cls._get_env("BOT_TOKEN") return cls._get_env("BOT_TOKEN")
@classmethod
def language(cls) -> str:
"""
Язык
"""
lang = cls._get_env("O_LANG", allow_none=True)
return lang.lower() if lang else "ru"
class DatabaseSettings(AbstractSettings): class DatabaseSettings(AbstractSettings):
@classmethod @classmethod
@ -145,12 +99,6 @@ class DatabaseSettings(AbstractSettings):
def host(cls) -> str: def host(cls) -> str:
return cls._get_env("POSTGRES_HOST") return cls._get_env("POSTGRES_HOST")
@classmethod
@lru_cache
def cryptor(cls) -> Cryptor:
password = cls._get_env("TOKEN_ENCRYPTION_KEY")
return Cryptor(password)
TORTOISE_ORM = { TORTOISE_ORM = {
"connections": {"default": f'postgres://{DatabaseSettings.user()}:{DatabaseSettings.password()}' "connections": {"default": f'postgres://{DatabaseSettings.user()}:{DatabaseSettings.password()}'

View File

@ -1,16 +0,0 @@
import base64
from Crypto.Cipher import AES
class Cryptor:
def __init__(self, password: str):
password = password.rjust(32)[:32]
self._cipher = AES.new(password.encode("utf-8"), AES.MODE_ECB)
def encrypt(self, data: str) -> str:
if data.startswith(" "):
raise ValueError("Data should not start with space!")
return base64.b64encode(self._cipher.encrypt(data.encode("utf-8").rjust(64))).decode("utf-8")
def decrypt(self, data: str) -> str:
return self._cipher.decrypt(base64.b64decode(data.encode("utf-8"))).decode("utf-8").lstrip()

View File

@ -22,11 +22,8 @@ async def edit_or_create(call: CallbackQuery, message: str,
parse_mode=parse_mode) parse_mode=parse_mode)
def wrap(data: str, max_len: int) -> str: def button_text_limit(data: str) -> str:
max_len = 30
if len(data) > max_len: if len(data) > max_len:
data = data[:max_len-4] + "..." data = data[:max_len-4] + "..."
return data return data
def button_text_limit(data: str) -> str:
return wrap(data, 30)

View File

@ -1,54 +0,0 @@
import aiogram.types as types
from aiogram.dispatcher.handler import CancelHandler, current_handler
from aiogram.dispatcher.middlewares import BaseMiddleware
import typing as ty
from locales.locale import _
def public():
"""
Хендлеры с этим декоратором будут обрабатываться даже если пользователь не является владельцем бота
(например, команда /help)
:return:
"""
def decorator(func):
setattr(func, "access_public", True)
return func
return decorator
class AccessMiddleware(BaseMiddleware):
def __init__(self, access_chat_ids: ty.Iterable[int]):
self._access_chat_ids = access_chat_ids
super(AccessMiddleware, self).__init__()
@classmethod
def _is_public_command(cls) -> bool:
handler = current_handler.get()
return handler and getattr(handler, "access_public", False)
async def on_process_message(self, message: types.Message, data: dict):
admin_ids = self._access_chat_ids
if not admin_ids:
return # Администраторы бота вообще не указаны
if self._is_public_command(): # Эта команда разрешена всем пользователям
return
if message.chat.id not in admin_ids:
await message.answer(_("Владелец бота ограничил доступ к этому функционалу 😞"))
raise CancelHandler()
async def on_process_callback_query(self, call: types.CallbackQuery, data: dict):
admin_ids = self._access_chat_ids
if not admin_ids:
return # Администраторы бота вообще не указаны
if self._is_public_command(): # Эта команда разрешена всем пользователям
return
if call.message.chat.id not in admin_ids:
await call.answer(_("Владелец бота ограничил доступ к этому функционалу😞"))
raise CancelHandler()

898
poetry.lock generated
View File

@ -1,898 +0,0 @@
[[package]]
name = "aerich"
version = "0.5.7"
description = "A database migrations tool for Tortoise ORM."
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
[package.dependencies]
click = "*"
ddlparse = "*"
dictdiffer = "*"
pydantic = "*"
tortoise-orm = "*"
[package.extras]
aiomysql = ["aiomysql"]
asyncpg = ["asyncpg"]
[[package]]
name = "aiocache"
version = "0.11.1"
description = "multi backend asyncio cache"
category = "main"
optional = false
python-versions = "*"
[package.extras]
dev = ["asynctest (>=0.11.0)", "codecov", "coverage", "flake8", "ipdb", "marshmallow", "pystache", "pytest", "pytest-asyncio", "pytest-mock", "sphinx", "sphinx-autobuild", "sphinx-rtd-theme", "black"]
memcached = ["aiomcache (>=0.5.2)"]
msgpack = ["msgpack (>=0.5.5)"]
redis = ["aioredis (>=0.3.3)", "aioredis (>=1.0.0)"]
[[package]]
name = "aiogram"
version = "2.13"
description = "Is a pretty simple and fully asynchronous framework for Telegram Bot API"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
aiohttp = ">=3.7.2,<4.0.0"
Babel = ">=2.8.0"
certifi = ">=2020.6.20"
[package.extras]
fast = ["uvloop (>=0.14.0,<0.15.0)", "ujson (>=1.35)"]
proxy = ["aiohttp-socks (>=0.5.3,<0.6.0)"]
[[package]]
name = "aiohttp"
version = "3.8.1"
description = "Async http client/server framework (asyncio)"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
aiosignal = ">=1.1.2"
async-timeout = ">=4.0.0a3,<5.0"
attrs = ">=17.3.0"
charset-normalizer = ">=2.0,<3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
yarl = ">=1.0,<2.0"
[package.extras]
speedups = ["aiodns", "brotli", "cchardet"]
[[package]]
name = "aioredis"
version = "1.3.0"
description = "asyncio (PEP 3156) Redis support"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
async-timeout = "*"
hiredis = "*"
[[package]]
name = "aiosignal"
version = "1.2.0"
description = "aiosignal: a list of registered asynchronous callbacks"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]]
name = "aiosqlite"
version = "0.17.0"
description = "asyncio bridge to the standard sqlite3 module"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
typing_extensions = ">=3.7.2"
[[package]]
name = "async-timeout"
version = "4.0.2"
description = "Timeout context manager for asyncio programs"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "asyncpg"
version = "0.25.0"
description = "An asyncio PostgreSQL driver"
category = "main"
optional = false
python-versions = ">=3.6.0"
[package.extras]
dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"]
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"]
test = ["pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"]
[[package]]
name = "attrs"
version = "21.4.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
[[package]]
name = "babel"
version = "2.10.3"
description = "Internationalization utilities"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pytz = ">=2015.7"
[[package]]
name = "certifi"
version = "2022.6.15"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "charset-normalizer"
version = "2.1.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=3.6.0"
[package.extras]
unicode_backport = ["unicodedata2"]
[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.5"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "ddlparse"
version = "1.10.0"
description = "DDL parase and Convert to BigQuery JSON schema"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
pyparsing = "*"
[[package]]
name = "dictdiffer"
version = "0.9.0"
description = "Dictdiffer is a library that helps you to diff and patch dictionaries."
category = "main"
optional = false
python-versions = "*"
[package.extras]
all = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)", "check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "sphinx (>=3)", "tox (>=3.7.0)", "numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "pytest (==5.4.3)", "pytest-pycodestyle (>=2)", "pytest-pydocstyle (>=2)", "pytest (>=6)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2.2.0)", "numpy (>=1.20.0)"]
docs = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)"]
numpy = ["numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)"]
tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "sphinx (>=3)", "tox (>=3.7.0)", "pytest (==5.4.3)", "pytest-pycodestyle (>=2)", "pytest-pydocstyle (>=2)", "pytest (>=6)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2.2.0)"]
[[package]]
name = "flake8"
version = "4.0.1"
description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
mccabe = ">=0.6.0,<0.7.0"
pycodestyle = ">=2.8.0,<2.9.0"
pyflakes = ">=2.4.0,<2.5.0"
[[package]]
name = "frozenlist"
version = "1.3.0"
description = "A list-like structure which implements collections.abc.MutableSequence"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "hiredis"
version = "2.0.0"
description = "Python wrapper for hiredis"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "idna"
version = "3.3"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "iso8601"
version = "0.1.16"
description = "Simple module to parse ISO 8601 dates"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "mccabe"
version = "0.6.1"
description = "McCabe checker, plugin for flake8"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "multidict"
version = "6.0.2"
description = "multidict implementation"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "pycodestyle"
version = "2.8.0"
description = "Python style guide checker"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pycrypto"
version = "2.6.1"
description = "Cryptographic modules for Python."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pydantic"
version = "1.9.1"
description = "Data validation and settings management using python type hints"
category = "main"
optional = false
python-versions = ">=3.6.1"
[package.dependencies]
typing-extensions = ">=3.7.4.3"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
[[package]]
name = "pyflakes"
version = "2.4.0"
description = "passive checker of Python programs"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "main"
optional = false
python-versions = ">=3.6.8"
[package.extras]
diagrams = ["railroad-diagrams", "jinja2"]
[[package]]
name = "pypika-tortoise"
version = "0.1.5"
description = "Forked from pypika and streamline just for tortoise-orm"
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
[[package]]
name = "python-dotenv"
version = "0.19.2"
description = "Read key-value pairs from a .env file and set them as environment variables"
category = "main"
optional = false
python-versions = ">=3.5"
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-gettext"
version = "4.0"
description = "Python Gettext po to mo file compiler."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pytz"
version = "2022.1"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "tortoise-orm"
version = "0.18.1"
description = "Easy async ORM for python, built with relations in mind"
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
[package.dependencies]
aiosqlite = ">=0.16.0,<0.18.0"
asyncpg = {version = "*", optional = true, markers = "extra == \"asyncpg\""}
iso8601 = ">=0.1.13,<0.2.0"
pypika-tortoise = ">=0.1.3,<0.2.0"
pytz = "*"
[package.extras]
aiomysql = ["aiomysql"]
asyncmy = ["asyncmy"]
asyncpg = ["asyncpg"]
accel = ["ciso8601", "orjson", "uvloop"]
[[package]]
name = "typing-extensions"
version = "4.2.0"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "yarl"
version = "1.7.2"
description = "Yet another URL library"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "9151864c69f349148a36cc7551bb54fd240229eb2533e92b82a0f85a251a56f3"
[metadata.files]
aerich = [
{file = "aerich-0.5.7-py3-none-any.whl", hash = "sha256:0684eb3d631c7c6e14caf2b2c3b9dad1c15ce8ade5771773c015a302f54ff4f6"},
{file = "aerich-0.5.7.tar.gz", hash = "sha256:f9ef8796f7a13ba9965eda0aa6840033bbd42b2e4e52c24d8f0dbdb85e4a5187"},
]
aiocache = [
{file = "aiocache-0.11.1-py2.py3-none-any.whl", hash = "sha256:e55c7caaa5753794fd301c3a2e592737fa1d036db9f8d04ae154facdfb48a157"},
{file = "aiocache-0.11.1.tar.gz", hash = "sha256:f2ebe0b05cec45782e7b5ea0bb74640f157dd4bb1028b4565364dda9fe33be7f"},
]
aiogram = [
{file = "aiogram-2.13-py3-none-any.whl", hash = "sha256:e1923ad789bb8d90a7b1edc5fce7dc33df982d1fedc6528c65cc38d2d88f1ae0"},
{file = "aiogram-2.13.tar.gz", hash = "sha256:f656f57580fd8e1ad0f8fe645f59cf3ad40e9899050dd6ac999a8f0cdbf1b116"},
]
aiohttp = [
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"},
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"},
{file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"},
{file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"},
{file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"},
{file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"},
{file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"},
{file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"},
{file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"},
{file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"},
{file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"},
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"},
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"},
{file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"},
{file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"},
{file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"},
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"},
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"},
{file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"},
{file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"},
{file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"},
{file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"},
]
aioredis = [
{file = "aioredis-1.3.0-py3-none-any.whl", hash = "sha256:71302cebeb7add86f1fe660b469068760ca4364504e75ee83dd6f6b7118bfe28"},
{file = "aioredis-1.3.0.tar.gz", hash = "sha256:86da2748fb0652625a8346f413167f078ec72bdc76e217db7e605a059cd56e86"},
]
aiosignal = [
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
]
aiosqlite = [
{file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"},
{file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"},
]
async-timeout = [
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
]
asyncpg = [
{file = "asyncpg-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf5e3408a14a17d480f36ebaf0401a12ff6ae5457fdf45e4e2775c51cc9517d3"},
{file = "asyncpg-0.25.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bc197fc4aca2fd24f60241057998124012469d2e414aed3f992579db0c88e3a"},
{file = "asyncpg-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a70783f6ffa34cc7dd2de20a873181414a34fd35a4a208a1f1a7f9f695e4ec4"},
{file = "asyncpg-0.25.0-cp310-cp310-win32.whl", hash = "sha256:43cde84e996a3afe75f325a68300093425c2f47d340c0fc8912765cf24a1c095"},
{file = "asyncpg-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:56d88d7ef4341412cd9c68efba323a4519c916979ba91b95d4c08799d2ff0c09"},
{file = "asyncpg-0.25.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a84d30e6f850bac0876990bcd207362778e2208df0bee8be8da9f1558255e634"},
{file = "asyncpg-0.25.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:beaecc52ad39614f6ca2e48c3ca15d56e24a2c15cbfdcb764a4320cc45f02fd5"},
{file = "asyncpg-0.25.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:6f8f5fc975246eda83da8031a14004b9197f510c41511018e7b1bedde6968e92"},
{file = "asyncpg-0.25.0-cp36-cp36m-win32.whl", hash = "sha256:ddb4c3263a8d63dcde3d2c4ac1c25206bfeb31fa83bd70fd539e10f87739dee4"},
{file = "asyncpg-0.25.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bf6dc9b55b9113f39eaa2057337ce3f9ef7de99a053b8a16360395ce588925cd"},
{file = "asyncpg-0.25.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:acb311722352152936e58a8ee3c5b8e791b24e84cd7d777c414ff05b3530ca68"},
{file = "asyncpg-0.25.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a61fb196ce4dae2f2fa26eb20a778db21bbee484d2e798cb3cc988de13bdd1b"},
{file = "asyncpg-0.25.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2633331cbc8429030b4f20f712f8d0fbba57fa8555ee9b2f45f981b81328b256"},
{file = "asyncpg-0.25.0-cp37-cp37m-win32.whl", hash = "sha256:863d36eba4a7caa853fd7d83fad5fd5306f050cc2fe6e54fbe10cdb30420e5e9"},
{file = "asyncpg-0.25.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fe471ccd915b739ca65e2e4dbd92a11b44a5b37f2e38f70827a1c147dafe0fa8"},
{file = "asyncpg-0.25.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:72a1e12ea0cf7c1e02794b697e3ca967b2360eaa2ce5d4bfdd8604ec2d6b774b"},
{file = "asyncpg-0.25.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4327f691b1bdb222df27841938b3e04c14068166b3a97491bec2cb982f49f03e"},
{file = "asyncpg-0.25.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:739bbd7f89a2b2f6bc44cb8bf967dab12c5bc714fcbe96e68d512be45ecdf962"},
{file = "asyncpg-0.25.0-cp38-cp38-win32.whl", hash = "sha256:18d49e2d93a7139a2fdbd113e320cc47075049997268a61bfbe0dde680c55471"},
{file = "asyncpg-0.25.0-cp38-cp38-win_amd64.whl", hash = "sha256:191fe6341385b7fdea7dbdcf47fd6db3fd198827dcc1f2b228476d13c05a03c6"},
{file = "asyncpg-0.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:52fab7f1b2c29e187dd8781fce896249500cf055b63471ad66332e537e9b5f7e"},
{file = "asyncpg-0.25.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a738f1b2876f30d710d3dc1e7858160a0afe1603ba16bf5f391f5316eb0ed855"},
{file = "asyncpg-0.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4105f57ad1e8fbc8b1e535d8fcefa6ce6c71081228f08680c6dea24384ff0e"},
{file = "asyncpg-0.25.0-cp39-cp39-win32.whl", hash = "sha256:f55918ded7b85723a5eaeb34e86e7b9280d4474be67df853ab5a7fa0cc7c6bf2"},
{file = "asyncpg-0.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:649e2966d98cc48d0646d9a4e29abecd8b59d38d55c256d5c857f6b27b7407ac"},
{file = "asyncpg-0.25.0.tar.gz", hash = "sha256:63f8e6a69733b285497c2855464a34de657f2cccd25aeaeeb5071872e9382540"},
]
attrs = [
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
]
babel = [
{file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"},
{file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"},
]
certifi = [
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
{file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
]
charset-normalizer = [
{file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"},
{file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"},
]
click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
ddlparse = [
{file = "ddlparse-1.10.0-py3-none-any.whl", hash = "sha256:71761b3457c8720853af3aeef266e2da1b6edef50936969492d586d7046a2ac2"},
{file = "ddlparse-1.10.0.tar.gz", hash = "sha256:6418681baa848eb01251ab79eb3d0ad7e140e6ab1deaae5a019353ddb3a908da"},
]
dictdiffer = [
{file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"},
{file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"},
]
flake8 = [
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
{file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
]
frozenlist = [
{file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"},
{file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"},
{file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"},
{file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"},
{file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"},
{file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"},
{file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"},
{file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"},
{file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"},
{file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"},
{file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"},
{file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"},
{file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"},
{file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"},
{file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"},
{file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"},
{file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"},
{file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"},
{file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"},
]
hiredis = [
{file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a"},
{file = "hiredis-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63"},
{file = "hiredis-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6"},
{file = "hiredis-2.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79"},
{file = "hiredis-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc"},
{file = "hiredis-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"},
{file = "hiredis-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048"},
{file = "hiredis-2.0.0-cp38-cp38-win32.whl", hash = "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426"},
{file = "hiredis-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581"},
{file = "hiredis-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e"},
{file = "hiredis-2.0.0-cp39-cp39-win32.whl", hash = "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d"},
{file = "hiredis-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9"},
{file = "hiredis-2.0.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54"},
{file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27"},
{file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d"},
{file = "hiredis-2.0.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163"},
{file = "hiredis-2.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a"},
{file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87"},
{file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41"},
{file = "hiredis-2.0.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0"},
{file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
]
iso8601 = [
{file = "iso8601-0.1.16-py2.py3-none-any.whl", hash = "sha256:906714829fedbc89955d52806c903f2332e3948ed94e31e85037f9e0226b8376"},
{file = "iso8601-0.1.16.tar.gz", hash = "sha256:36532f77cc800594e8f16641edae7f1baf7932f05d8e508545b95fc53c6dc85b"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
multidict = [
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
{file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"},
{file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"},
{file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"},
{file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"},
{file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"},
{file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"},
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"},
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"},
{file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"},
{file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"},
{file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"},
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"},
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"},
{file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"},
{file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"},
{file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
{file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
]
pycodestyle = [
{file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
{file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
]
pycrypto = [
{file = "pycrypto-2.6.1.tar.gz", hash = "sha256:f2ce1e989b272cfcb677616763e0a2e7ec659effa67a88aa92b3a65528f60a3c"},
]
pydantic = [
{file = "pydantic-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193"},
{file = "pydantic-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11"},
{file = "pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"},
{file = "pydantic-1.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131"},
{file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580"},
{file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd"},
{file = "pydantic-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd"},
{file = "pydantic-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761"},
{file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918"},
{file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74"},
{file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a"},
{file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166"},
{file = "pydantic-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b"},
{file = "pydantic-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892"},
{file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e"},
{file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608"},
{file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537"},
{file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380"},
{file = "pydantic-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728"},
{file = "pydantic-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a"},
{file = "pydantic-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1"},
{file = "pydantic-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195"},
{file = "pydantic-1.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b"},
{file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49"},
{file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6"},
{file = "pydantic-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0"},
{file = "pydantic-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6"},
{file = "pydantic-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810"},
{file = "pydantic-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f"},
{file = "pydantic-1.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee"},
{file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761"},
{file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd"},
{file = "pydantic-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1"},
{file = "pydantic-1.9.1-py3-none-any.whl", hash = "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58"},
{file = "pydantic-1.9.1.tar.gz", hash = "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a"},
]
pyflakes = [
{file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
{file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pypika-tortoise = [
{file = "pypika-tortoise-0.1.5.tar.gz", hash = "sha256:2e2f747bfc645a25e097485651278cd7d920b709686ce54e43387ba1c9294048"},
{file = "pypika_tortoise-0.1.5-py3-none-any.whl", hash = "sha256:23d993558e3005ac7f7f2865d9add3d8168097f45246f8844fa46d6682a99d90"},
]
python-dotenv = [
{file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"},
{file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"},
]
python-gettext = [
{file = "python-gettext-4.0.tar.gz", hash = "sha256:626b501a51ac892fc3460cf550e60dca121f544eaa46eb69c90ce4682fc7ec02"},
]
pytz = [
{file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
{file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
]
tortoise-orm = [
{file = "tortoise-orm-0.18.1.tar.gz", hash = "sha256:537361ce2d0829741afd43afd9bc9413a314a176cb58747d88047c20ccc01db1"},
{file = "tortoise_orm-0.18.1-py3-none-any.whl", hash = "sha256:edc9f3b49635b1dd74f73de38f54e031377e4f02b3698322502047f2e031af8b"},
]
typing-extensions = [
{file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"},
{file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
]
yarl = [
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"},
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"},
{file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"},
{file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"},
{file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"},
{file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"},
{file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"},
{file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"},
{file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"},
{file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"},
{file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"},
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"},
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"},
{file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"},
{file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"},
{file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"},
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"},
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"},
{file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"},
{file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"},
{file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"},
{file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"},
]

View File

@ -1,25 +0,0 @@
[tool.poetry]
name = "olgram"
version = "0.2.0"
description = ""
authors = ["Civ Soc <feedback@civsoc.it>"]
license = "CC0"
[tool.poetry.dependencies]
python = "^3.8"
aiogram = "2.13"
python-dotenv = "^0.19.2"
aiocache = "^0.11.1"
aiohttp = "^3.8.1"
pycrypto = "^2.6.1"
aioredis = "1.3"
aerich = "0.5.x"
tortoise-orm = {extras = ["asyncpg"], version = "^0.18.1"}
python-gettext = "^4.0"
[tool.poetry.dev-dependencies]
flake8 = "^4.0.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@ -1 +0,0 @@
/usr/lib/python3.10/Tools/i18n/pygettext.py -d chinese -o locales/olgram.pot olgram/ server/

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
aiogram~=2.13
tortoise-orm[asyncpg]
aerich==0.5.4
python-dotenv~=0.17.1
aioredis==1.3.1
aiocache
aiohttp

View File

@ -7,28 +7,20 @@ from contextvars import ContextVar
from aiohttp.web_exceptions import HTTPNotFound from aiohttp.web_exceptions import HTTPNotFound
from aioredis.commands import create_redis_pool from aioredis.commands import create_redis_pool
from aioredis import Redis from aioredis import Redis
from tortoise.expressions import F
import logging import logging
import typing as ty import typing as ty
from olgram.settings import ServerSettings from olgram.settings import ServerSettings
from olgram.models.models import Bot, GroupChat, BannedUser from olgram.models.models import Bot, GroupChat
from locales.locale import _, translators
from server.inlines import inline_handler
logging.basicConfig(level=logging.INFO)
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
_logger.setLevel(logging.INFO)
db_bot_instance: ContextVar[Bot] = ContextVar('db_bot_instance') db_bot_instance: ContextVar[Bot] = ContextVar('db_bot_instance')
_redis: ty.Optional[Redis] = None _redis: ty.Optional[Redis] = None
def _get_translator(message: types.Message) -> ty.Callable:
if not message.from_user.locale:
return _
return translators.get(message.from_user.locale.language, _)
async def init_redis(): async def init_redis():
global _redis global _redis
_redis = await create_redis_pool(ServerSettings.redis_path()) _redis = await create_redis_pool(ServerSettings.redis_path())
@ -38,207 +30,41 @@ def _message_unique_id(bot_id: int, message_id: int) -> str:
return f"{bot_id}_{message_id}" return f"{bot_id}_{message_id}"
def _thread_uniqie_id(bot_id: int, chat_id: int) -> str: async def message_handler(message, *args, **kwargs):
return f"thread_{bot_id}_{chat_id}" _logger.info("message handler")
def _last_message_uid(bot_id: int, chat_id: int) -> str:
return f"lm_{bot_id}_{chat_id}"
def _antiflood_marker_uid(bot_id: int, chat_id: int) -> str:
return f"af_{bot_id}_{chat_id}"
def _on_security_policy(message: types.Message, bot):
_ = _get_translator(message)
text = _("<b>Политика конфиденциальности</b>\n\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При отправке сообщения (кроме команд "
"/start и /security_policy) ваш идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется только для общения с оператором; боты Olgram "
"не делают массовых рассылок.\n\n")
if bot.enable_additional_info:
text += _("При отправке сообщения (кроме команд /start и /security_policy) оператор <b>видит</b> ваши имя "
"пользователя, @username и идентификатор пользователя в силу настроек, которые оператор указал при "
"создании бота.")
else:
text += _("В зависимости от ваших настроек конфиденциальности Telegram, оператор может видеть ваш username, "
"имя пользователя и другую информацию.")
return SendMessage(chat_id=message.chat.id,
text=text,
parse_mode="HTML")
async def send_user_message(message: types.Message, super_chat_id: int, bot):
"""Переслать сообщение от пользователя, добавлять к нему user info при необходимости"""
if bot.enable_additional_info:
user_info = _("Сообщение от пользователя ")
user_info += message.from_user.full_name
if message.from_user.username:
user_info += " | @" + message.from_user.username
user_info += f" | #ID{message.from_user.id}"
# Добавлять информацию в конец текста
if message.content_type == types.ContentType.TEXT and len(message.text) + len(user_info) < 4093: # noqa:E721
new_message = await message.bot.send_message(super_chat_id, message.text + "\n\n" + user_info)
else: # Не добавлять информацию в конец текста, информация отдельным сообщением
new_message = await message.bot.send_message(super_chat_id, text=user_info)
new_message_2 = await message.copy_to(super_chat_id, reply_to_message_id=new_message.message_id)
await _redis.set(_message_unique_id(bot.pk, new_message_2.message_id), message.chat.id,
pexpire=ServerSettings.redis_timeout_ms())
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id,
pexpire=ServerSettings.redis_timeout_ms())
return new_message
else:
try:
new_message = await message.forward(super_chat_id)
except exceptions.MessageCantBeForwarded:
new_message = await message.copy_to(super_chat_id)
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id,
pexpire=ServerSettings.redis_timeout_ms())
return new_message
async def send_to_superchat(is_super_group: bool, message: types.Message, super_chat_id: int, bot):
"""Пересылка сообщения от пользователя оператору (логика потоков сообщений)"""
if is_super_group and bot.enable_threads:
thread_first_message = await _redis.get(_thread_uniqie_id(bot.pk, message.chat.id))
if thread_first_message:
# переслать в супер-чат, отвечая на предыдущее сообщение
try:
new_message = await message.copy_to(super_chat_id, reply_to_message_id=int(thread_first_message))
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id,
pexpire=ServerSettings.redis_timeout_ms())
except exceptions.BadRequest:
new_message = await send_user_message(message, super_chat_id, bot)
await _redis.set(_thread_uniqie_id(bot.pk, message.chat.id), new_message.message_id,
pexpire=ServerSettings.thread_timeout_ms())
else:
# переслать супер-чат
new_message = await send_user_message(message, super_chat_id, bot)
await _redis.set(_thread_uniqie_id(bot.pk, message.chat.id), new_message.message_id,
pexpire=ServerSettings.thread_timeout_ms())
else: # личные сообщения не поддерживают потоки сообщений: просто отправляем сообщение
await send_user_message(message, super_chat_id, bot)
async def handle_user_message(message: types.Message, super_chat_id: int, bot):
"""Обычный пользователь прислал сообщение в бот, нужно переслать его операторам"""
_ = _get_translator(message)
is_super_group = super_chat_id < 0
# Проверить, не забанен ли пользователь
banned = await bot.banned_users.filter(telegram_id=message.chat.id)
if banned:
return SendMessage(chat_id=message.chat.id,
text=_("Вы заблокированы в этом боте"))
# Проверить анти-флуд
if bot.enable_antiflood:
if await _redis.get(_antiflood_marker_uid(bot.pk, message.chat.id)):
return SendMessage(chat_id=message.chat.id,
text=_("Слишком много сообщений, подождите одну минуту"))
await _redis.setex(_antiflood_marker_uid(bot.pk, message.chat.id), 60, 1)
# Пересылаем сообщение в супер-чат
try:
await send_to_superchat(is_super_group, message, super_chat_id, bot)
except (exceptions.Unauthorized, exceptions.ChatNotFound):
return SendMessage(chat_id=message.chat.id, text=_("Не удаётся связаться с владельцем бота"))
except exceptions.TelegramAPIError as err:
_logger.error(f"(exception on forwarding) {err}")
return
bot.incoming_messages_count = F("incoming_messages_count") + 1
await bot.save(update_fields=["incoming_messages_count"])
# И отправить пользователю специальный текст, если он указан и если давно не отправляли
if bot.second_text:
send_auto = not await _redis.get(_last_message_uid(bot.pk, message.chat.id))
await _redis.setex(_last_message_uid(bot.pk, message.chat.id), 60 * 60 * 3, 1)
if send_auto:
return SendMessage(chat_id=message.chat.id, text=bot.second_text, parse_mode="HTML")
async def handle_operator_message(message: types.Message, super_chat_id: int, bot):
"""Оператор написал что-то, нужно переслать сообщение обратно пользователю, или забанить его и т.д."""
_ = _get_translator(message)
if message.reply_to_message:
if message.reply_to_message.from_user.id != message.bot.id:
return # нас интересуют только ответы на сообщения бота
# В супер-чате кто-то ответил на сообщение пользователя, нужно переслать тому пользователю
chat_id = await _redis.get(_message_unique_id(bot.pk, message.reply_to_message.message_id))
if not chat_id:
chat_id = message.reply_to_message.forward_from_chat
if not chat_id:
return SendMessage(chat_id=message.chat.id,
text=_("<i>Невозможно переслать сообщение: автор не найден (сообщение слишком "
"старое?)</i>"),
parse_mode="HTML")
chat_id = int(chat_id)
if message.text == "/ban":
user, create = await BannedUser.get_or_create(telegram_id=chat_id, bot=bot)
await user.save()
return SendMessage(chat_id=message.chat.id, text=_("Пользователь заблокирован"))
if message.text == "/unban":
banned_user = await bot.banned_users.filter(telegram_id=chat_id).first()
if not banned_user:
return SendMessage(chat_id=message.chat.id, text=_("Пользователь не был забанен"))
else:
await banned_user.delete()
return SendMessage(chat_id=message.chat.id, text=_("Пользователь разбанен"))
try:
await message.copy_to(chat_id)
except (exceptions.MessageError, exceptions.Unauthorized):
await message.reply(_("<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>"),
parse_mode="HTML")
return
bot.outgoing_messages_count = F("outgoing_messages_count") + 1
await bot.save(update_fields=["outgoing_messages_count"])
elif super_chat_id > 0:
# в супер-чате кто-то пишет сообщение сам себе, только для личных сообщений
await message.forward(super_chat_id)
# И отправить пользователю специальный текст, если он указан
if bot.second_text:
return SendMessage(chat_id=message.chat.id, text=bot.second_text, parse_mode="HTML")
async def message_handler(message: types.Message, *args, **kwargs):
_ = _get_translator(message)
bot = db_bot_instance.get() bot = db_bot_instance.get()
if message.text and message.text == "/start": if message.text and message.text.startswith("/start"):
# На команду start нужно ответить, не пересылая сообщение никуда # На команду start нужно ответить, не пересылая сообщение никуда
text = bot.start_text return SendMessage(chat_id=message.chat.id,
if bot.enable_olgram_text: text=bot.start_text + ServerSettings.append_text())
text += _(ServerSettings.append_text())
return SendMessage(chat_id=message.chat.id, text=text, parse_mode="HTML")
if message.text and message.text == "/security_policy":
# На команду security_policy нужно ответить, не пересылая сообщение никуда
return _on_security_policy(message, bot)
super_chat_id = await bot.super_chat_id() super_chat_id = await bot.super_chat_id()
if message.chat.id != super_chat_id: if message.chat.id != super_chat_id:
# Это обычный чат # Это обычный чат: сообщение нужно переслать в супер-чат
return await handle_user_message(message, super_chat_id, bot) new_message = await message.forward(super_chat_id)
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id)
else: else:
# Это супер-чат # Это супер-чат
return await handle_operator_message(message, super_chat_id, bot) if message.reply_to_message:
# Ответ из супер-чата переслать тому пользователю,
chat_id = await _redis.get(_message_unique_id(bot.pk, message.reply_to_message.message_id))
async def edited_message_handler(message: types.Message, *args, **kwargs): if not chat_id:
return await message_handler(message, *args, **kwargs, is_edited=True) chat_id = message.reply_to_message.forward_from_chat
if not chat_id:
return SendMessage(chat_id=message.chat.id,
text="<i>Невозможно переслать сообщение: автор не найден</i>",
parse_mode="HTML")
chat_id = int(chat_id)
try:
await message.copy_to(chat_id)
except (exceptions.MessageError, exceptions.BotBlocked):
await message.reply("<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>",
parse_mode="HTML")
return
else:
await message.forward(super_chat_id)
async def receive_invite(message: types.Message): async def receive_invite(message: types.Message):
@ -255,18 +81,6 @@ async def receive_invite(message: types.Message):
break break
async def receive_group_create(message: types.Message):
bot = db_bot_instance.get()
chat, _ = await GroupChat.get_or_create(chat_id=message.chat.id,
defaults={"name": message.chat.full_name})
chat.name = message.chat.full_name
await chat.save()
if chat not in await bot.group_chats.all():
await bot.group_chats.add(chat)
await bot.save()
async def receive_left(message: types.Message): async def receive_left(message: types.Message):
bot = db_bot_instance.get() bot = db_bot_instance.get()
if message.left_chat_member.id == message.bot.id: if message.left_chat_member.id == message.bot.id:
@ -279,23 +93,6 @@ async def receive_left(message: types.Message):
await bot.save() await bot.save()
async def receive_inline(inline_query):
_logger.info("inline handler")
bot = db_bot_instance.get()
return await inline_handler(inline_query, bot)
async def receive_migrate(message: types.Message):
bot = db_bot_instance.get()
from_id = message.chat.id
to_id = message.migrate_to_chat_id
chats = await bot.group_chats.filter(chat_id=from_id)
for chat in chats:
chat.chat_id = to_id
await chat.save(update_fields=["chat_id"])
class CustomRequestHandler(WebhookRequestHandler): class CustomRequestHandler(WebhookRequestHandler):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -309,26 +106,19 @@ class CustomRequestHandler(WebhookRequestHandler):
if not bot: if not bot:
return None return None
db_bot_instance.set(bot) db_bot_instance.set(bot)
dp = Dispatcher(AioBot(bot.decrypted_token())) dp = Dispatcher(AioBot(bot.token))
supported_messages = [types.ContentType.TEXT,
types.ContentType.CONTACT,
types.ContentType.ANIMATION,
types.ContentType.AUDIO,
types.ContentType.DOCUMENT,
types.ContentType.PHOTO,
types.ContentType.STICKER,
types.ContentType.VIDEO,
types.ContentType.VOICE,
types.ContentType.LOCATION]
dp.register_message_handler(message_handler, content_types=supported_messages)
dp.register_edited_message_handler(edited_message_handler, content_types=supported_messages)
dp.register_message_handler(message_handler, content_types=[types.ContentType.TEXT,
types.ContentType.CONTACT,
types.ContentType.ANIMATION,
types.ContentType.AUDIO,
types.ContentType.DOCUMENT,
types.ContentType.PHOTO,
types.ContentType.STICKER,
types.ContentType.VIDEO,
types.ContentType.VOICE])
dp.register_message_handler(receive_invite, content_types=[types.ContentType.NEW_CHAT_MEMBERS]) dp.register_message_handler(receive_invite, content_types=[types.ContentType.NEW_CHAT_MEMBERS])
dp.register_message_handler(receive_left, content_types=[types.ContentType.LEFT_CHAT_MEMBER]) dp.register_message_handler(receive_left, content_types=[types.ContentType.LEFT_CHAT_MEMBER])
dp.register_message_handler(receive_migrate, content_types=[types.ContentType.MIGRATE_TO_CHAT_ID])
dp.register_message_handler(receive_group_create, content_types=[types.ContentType.GROUP_CHAT_CREATED])
dp.register_inline_handler(receive_inline)
return dp return dp

View File

@ -1,56 +0,0 @@
from aiocache import cached
import hashlib
from aiogram.types import InlineQuery, InputTextMessageContent, InlineQueryResultArticle
from aiogram.bot import Bot as AioBot
from olgram.models.models import Bot
import typing as ty
@cached(ttl=60)
async def get_phrases(bot: Bot) -> ty.List:
objects = await bot.answers
return [obj.text for obj in objects]
async def check_chat_member(chat_id: int, user_id: int, bot: AioBot) -> bool:
member = await bot.get_chat_member(chat_id, user_id)
return member.is_chat_member()
@cached(ttl=60)
async def check_permissions(inline_query: InlineQuery, bot: Bot):
user_id = inline_query.from_user.id
super_chat_id = await bot.super_chat_id()
if super_chat_id == user_id:
return True
if super_chat_id < 0: # Group chat
is_member = await check_chat_member(super_chat_id, user_id, inline_query.bot)
return is_member
return False
async def inline_handler(inline_query: InlineQuery, bot: Bot):
# Check permissions at first
allow = await check_permissions(inline_query, bot)
if not allow:
return await inline_query.answer([]) # forbidden
all_phrases = await get_phrases(bot)
phrases = [phrase for phrase in all_phrases if inline_query.query.lower() in phrase.lower()]
items = []
for phrase in phrases:
input_content = InputTextMessageContent(phrase)
result_id: str = hashlib.md5(phrase.encode()).hexdigest()
item = InlineQueryResultArticle(
id=result_id,
title=phrase,
input_message_content=input_content,
)
items.append(item)
await inline_query.answer(results=items)

View File

@ -1,11 +1,9 @@
from aiogram import Bot as AioBot from aiogram import Bot as AioBot
from aiogram.types import BotCommand
from olgram.models.models import Bot from olgram.models.models import Bot
from aiohttp import web from aiohttp import web
from asyncio import get_event_loop from asyncio import get_event_loop
import ssl import ssl
from olgram.settings import ServerSettings from olgram.settings import ServerSettings
from locales.locale import _
from .custom import CustomRequestHandler from .custom import CustomRequestHandler
import logging import logging
@ -28,20 +26,14 @@ async def register_token(bot: Bot) -> bool:
:param bot: Бот :param bot: Бот
:return: получилось ли :return: получилось ли
""" """
await unregister_token(bot.decrypted_token()) await unregister_token(bot.token)
a_bot = AioBot(bot.decrypted_token()) a_bot = AioBot(bot.token)
certificate = None certificate = None
if ServerSettings.use_custom_cert(): if ServerSettings.use_custom_cert():
certificate = open(ServerSettings.public_path(), 'rb') certificate = open(ServerSettings.public_path(), 'rb')
res = await a_bot.set_webhook(url_for_bot(bot), certificate=certificate, drop_pending_updates=True, res = await a_bot.set_webhook(url_for_bot(bot), certificate=certificate, drop_pending_updates=True)
max_connections=10)
await a_bot.set_my_commands([
BotCommand("/start", _("(Пере)запустить бота")),
BotCommand("/security_policy", _("Политика конфиденциальности"))
])
await a_bot.session.close() await a_bot.session.close()
del a_bot del a_bot
return res return res
@ -73,5 +65,5 @@ def main():
runner = web.AppRunner(app) runner = web.AppRunner(app)
loop.run_until_complete(runner.setup()) loop.run_until_complete(runner.setup())
logger.info("Server initialization done") logger.info("Server initialization done")
site = web.TCPSite(runner, port=ServerSettings.app_port(), ssl_context=context) site = web.TCPSite(runner, host=ServerSettings.app_host(), port=ServerSettings.app_port(), ssl_context=context)
return site return site