Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bdae028c3 | ||
|
|
942862f171 | ||
|
|
e95f21d413 | ||
|
|
1aeec0c9d8 | ||
|
|
5fcb5b8900 | ||
|
|
f0237ecb0b | ||
|
|
4790a21f60 | ||
|
|
ea8d251142 | ||
|
|
2e61640f5a | ||
|
|
188b58d8e2 | ||
|
|
0f84b67b49 | ||
|
|
8013c8c8e4 | ||
|
|
11f8004c55 | ||
|
|
0487838942 | ||
|
|
ddea5ba06c | ||
|
|
5c7ced1549 | ||
|
|
118b24df8f | ||
|
|
0d8081be35 | ||
|
|
c59fc4ebc1 | ||
|
|
64538aa17f | ||
|
|
7e3bc13f14 | ||
|
|
e4ec20a5c4 | ||
|
|
cbc3b586a7 | ||
|
|
5cf69e3ea9 | ||
|
|
04a3efd3fb | ||
|
|
29d7118833 | ||
|
|
7c6abd5558 | ||
|
|
d9d37f4a5f | ||
|
|
71e905b0be |
@@ -2,3 +2,4 @@
|
|||||||
venv
|
venv
|
||||||
config.json
|
config.json
|
||||||
*.yaml
|
*.yaml
|
||||||
|
docs/
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -5,4 +5,5 @@ venv
|
|||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
config.json
|
config.json
|
||||||
docker-compose-release.yaml
|
docker-compose-release.yaml
|
||||||
|
docs/build
|
||||||
|
|||||||
10
README.md
@@ -1,11 +1,12 @@
|
|||||||
# OLGram
|
# OLGram
|
||||||
|
|
||||||
[@olgram](https://t.me/olgrambot) - конструктор ботов обратной связи в Telegram
|
|
||||||
|
|
||||||
[](https://github.com/civsocit/olgram/actions?workflow=Linter)
|
[](https://github.com/civsocit/olgram/actions?workflow=Linter)
|
||||||
[](https://github.com/civsocit/olgram/actions?workflow=Deploy)
|
[](https://github.com/civsocit/olgram/actions?workflow=Deploy)
|
||||||
|
[](https://olgram.readthedocs.io)
|
||||||
|
|
||||||

|
[@olgram](https://t.me/olgrambot) - конструктор ботов обратной связи в Telegram
|
||||||
|
|
||||||
|
Документация: https://olgram.readthedocs.io
|
||||||
|
|
||||||
## Возможности и преимущества Olgram Bot
|
## Возможности и преимущества Olgram Bot
|
||||||
|
|
||||||
@@ -25,9 +26,10 @@
|
|||||||
### Для разработчиков: сборка и запуск проекта
|
### Для разработчиков: сборка и запуск проекта
|
||||||
|
|
||||||
Вам потребуется собственный VPS или любой хост со статическим адресом или доменом.
|
Вам потребуется собственный VPS или любой хост со статическим адресом или доменом.
|
||||||
* Создайте файл .env и заполните его по образцу example.env. Вам нужно заполнить переменные:
|
* Создайте файл .env по образцу example.env. Вам нужно заполнить переменные:
|
||||||
* BOT_TOKEN - токен нового бота, получить у [@botfather](https://t.me/botfather)
|
* BOT_TOKEN - токен нового бота, получить у [@botfather](https://t.me/botfather)
|
||||||
* POSTGRES_PASSWORD - любой случайный пароль
|
* POSTGRES_PASSWORD - любой случайный пароль
|
||||||
|
* TOKEN_ENCRYPTION_KEY - любой случайный пароль, отличный от POSTGRES_PASSWORD
|
||||||
* WEBHOOK_HOST - IP адрес или доменное имя сервера, на котором запускается проект
|
* WEBHOOK_HOST - IP адрес или доменное имя сервера, на котором запускается проект
|
||||||
* Сохраните файл docker-compose.yaml и соберите его:
|
* Сохраните файл docker-compose.yaml и соберите его:
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres
|
image: postgres:13.4
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- release.env
|
- release.env
|
||||||
@@ -9,6 +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:latest'
|
image: 'bitnami/redis:latest'
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -20,6 +22,8 @@ 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
|
||||||
@@ -71,6 +75,8 @@ 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:
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ fi
|
|||||||
|
|
||||||
sleep 10
|
sleep 10
|
||||||
aerich upgrade
|
aerich upgrade
|
||||||
|
python migrate.py
|
||||||
python main.py
|
python main.py
|
||||||
|
|||||||
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 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)
|
||||||
BIN
docs/images/addbot.jpg
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
docs/images/added.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/images/botfather.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
docs/images/botfathernew.png
Normal file
|
After Width: | Height: | Size: 560 KiB |
BIN
docs/images/botfathertoken.png
Normal file
|
After Width: | Height: | Size: 636 KiB |
BIN
docs/images/chat1.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
docs/images/chat2.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
BIN
docs/images/logo1_big.png
Normal file
|
After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
BIN
docs/images/start.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
docs/images/test.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
docs/images/test2.jpg
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
docs/images/text1.jpg
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
docs/images/text2.jpg
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
docs/images/text3.jpg
Normal file
|
After Width: | Height: | Size: 180 KiB |
35
docs/make.bat
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@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
|
||||||
40
docs/source/about.rst
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
О проекте
|
||||||
|
===================================
|
||||||
|
|
||||||
|
Зачем нужен 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>`_.
|
||||||
35
docs/source/conf.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
|
||||||
|
# -- Project information
|
||||||
|
|
||||||
|
project = 'Olgram'
|
||||||
|
copyright = '2021, 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'
|
||||||
52
docs/source/developer.rst
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
Для разработчиков
|
||||||
|
=================
|
||||||
|
|
||||||
|
Сборка и запуск
|
||||||
|
---------------
|
||||||
|
Вы можете развернуть 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 вы потеряете
|
||||||
|
токены всех ботов, которые пользователи зарегистрировали в вашем боте.
|
||||||
|
|
||||||
|
Дополнительно
|
||||||
|
-------------
|
||||||
|
|
||||||
|
В docker-compose.yaml приведена минимальная конфигурация. Для использования в серьёзных проектах мы советуем:
|
||||||
|
|
||||||
|
* Приобрести домен и настроить его на свой хост
|
||||||
|
* Наладить реверс-прокси и автоматическое обновление сертификатов - например, с помощью `Traefik <https://github.com/traefik/traefik>`_
|
||||||
|
* Скрыть IP сервера с помощью `Cloudflire <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.
|
||||||
22
docs/source/index.rst
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Добро пожаловать в документацию Olgram
|
||||||
|
===================================
|
||||||
|
|
||||||
|
**Olgram** `@olgrambot <https://t.me/olgrambot>`_ это конструктор, который позволяет создавать боты обратной связи
|
||||||
|
в Telegram. После подключения к Olgram пользователи вашего бота смогут писать сообщения, которые будут
|
||||||
|
пересылаться вам в чат, где вы сможете на них ответить. Читайте больше о проекте в главе :doc:`about`.
|
||||||
|
|
||||||
|
Откройте главу :doc:`quick_start` чтобы приступить к созданию своего первого бота!
|
||||||
|
|
||||||
|
Оглавление
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
about
|
||||||
|
quick_start
|
||||||
|
developer
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Если у вас возникли вопросы по использованию бота, или вы нашли ошибку - напишите
|
||||||
|
нам `@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.
|
||||||
96
docs/source/quick_start.rst
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
Быстрый старт
|
||||||
|
=============
|
||||||
|
|
||||||
|
Как создать бота
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Перейдите по ссылке `@Olgram <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 сможет пересылать туда сообщения.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Если у вас возникли вопросы по использованию бота, или вы нашли ошибку - напишите
|
||||||
|
нам `@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.
|
||||||
@@ -5,6 +5,10 @@ POSTGRES_PASSWORD=SOME_RANDOM_PASSWORD_HERE # example: x2y0n27ihiez93kmzj82
|
|||||||
POSTGRES_DB=olgram
|
POSTGRES_DB=olgram
|
||||||
POSTGRES_HOST=postgres
|
POSTGRES_HOST=postgres
|
||||||
|
|
||||||
|
TOKEN_ENCRYPTION_KEY=SOME_RANDOM_PASSWORD_HERE # example: i7flci0mx4z5patxnl6m
|
||||||
|
|
||||||
|
# ADMIN_ID=223453418 # use your user id or group chat id to restrict access to the bot
|
||||||
|
|
||||||
WEBHOOK_HOST=YOUR_HOST_HERE # example: 11.143.142.140 or my_domain.com
|
WEBHOOK_HOST=YOUR_HOST_HERE # example: 11.143.142.140 or my_domain.com
|
||||||
WEBHOOK_PORT=8443 # allowed: 80, 443, 8080, 8443
|
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
|
CUSTOM_CERT=true # use that if you don't set up your own domain and let's encrypt certificate
|
||||||
|
|||||||
15
main.py
@@ -1,8 +1,10 @@
|
|||||||
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
|
from olgram.settings import TORTOISE_ORM, OlgramSettings
|
||||||
|
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
|
||||||
@@ -21,7 +23,8 @@ async def init_database():
|
|||||||
|
|
||||||
|
|
||||||
async def init_olgram():
|
async def init_olgram():
|
||||||
from olgram.router import bot
|
from olgram.router import bot, dp
|
||||||
|
dp.setup_middleware(AccessMiddleware(OlgramSettings.admin_id()))
|
||||||
from aiogram.types import BotCommand
|
from aiogram.types import BotCommand
|
||||||
await bot.set_my_commands(
|
await bot.set_my_commands(
|
||||||
[
|
[
|
||||||
@@ -43,11 +46,17 @@ def main():
|
|||||||
"""
|
"""
|
||||||
Classic polling
|
Classic polling
|
||||||
"""
|
"""
|
||||||
|
parser = argparse.ArgumentParser("Olgram server")
|
||||||
|
parser.add_argument("--noserver", 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())
|
||||||
|
|
||||||
loop.create_task(dp.start_polling())
|
loop.create_task(dp.start_polling())
|
||||||
loop.create_task(server_main().start())
|
if not args.noserver:
|
||||||
|
loop.create_task(server_main().start())
|
||||||
|
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 202 KiB |
9
migrate.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from olgram.migrations.custom import migrate
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.get_event_loop().run_until_complete(migrate())
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
Здесь работа с конкретным ботом
|
Здесь работа с конкретным ботом
|
||||||
"""
|
"""
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
from aiogram.utils.exceptions import TelegramAPIError
|
from aiogram.utils.exceptions import TelegramAPIError, Unauthorized
|
||||||
from olgram.models.models import Bot
|
from olgram.models.models import Bot
|
||||||
from server.server import unregister_token
|
from server.server import unregister_token
|
||||||
|
|
||||||
@@ -11,7 +11,11 @@ async def delete_bot(bot: Bot, call: types.CallbackQuery):
|
|||||||
"""
|
"""
|
||||||
Пользователь решил удалить бота
|
Пользователь решил удалить бота
|
||||||
"""
|
"""
|
||||||
await unregister_token(bot.token)
|
try:
|
||||||
|
await unregister_token(bot.decrypted_token())
|
||||||
|
except Unauthorized:
|
||||||
|
# Вероятно пользователь сбросил токен или удалил бот, это уже не наши проблемы
|
||||||
|
pass
|
||||||
await bot.delete()
|
await bot.delete()
|
||||||
await call.answer("Бот удалён")
|
await call.answer("Бот удалён")
|
||||||
try:
|
try:
|
||||||
@@ -32,6 +36,18 @@ async def reset_bot_text(bot: Bot, call: types.CallbackQuery):
|
|||||||
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):
|
||||||
"""
|
"""
|
||||||
Пользователь выбрал чат, в который хочет получать сообщения от бота
|
Пользователь выбрал чат, в который хочет получать сообщения от бота
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ async def bot_added(message: types.Message, state: FSMContext):
|
|||||||
return await on_unknown_error()
|
return await on_unknown_error()
|
||||||
|
|
||||||
user, _ = await User.get_or_create(telegram_id=message.from_user.id)
|
user, _ = await User.get_or_create(telegram_id=message.from_user.id)
|
||||||
bot = Bot(token=token, owner=user, name=test_bot_info.username, super_chat_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:
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ async def send_chats_menu(bot: Bot, call: types.CallbackQuery):
|
|||||||
if not chats:
|
if not chats:
|
||||||
text = dedent(f"""
|
text = dedent(f"""
|
||||||
Этот бот не добавлен в чаты, поэтому все сообщения будут приходить вам в бот.
|
Этот бот не добавлен в чаты, поэтому все сообщения будут приходить вам в бот.
|
||||||
Чтобы подключить чат — просто добавьте бот @{bot.name} в чат.
|
Чтобы подключить чат — добавьте бот @{bot.name} в чат, откройте это меню ещё раз и выберите добавленный чат.
|
||||||
|
Если ваш бот состоял в групповом чате до того, как его добавили в Olgram - удалите бота из чата и добавьте
|
||||||
|
снова.
|
||||||
""")
|
""")
|
||||||
else:
|
else:
|
||||||
text = dedent(f"""
|
text = dedent(f"""
|
||||||
@@ -141,13 +143,18 @@ 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=3, bot_id=bot.id, operation="reset_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))
|
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=3, bot_id=bot.id, operation="reset_text",
|
||||||
|
chat=empty))
|
||||||
)
|
)
|
||||||
|
|
||||||
text = dedent("""
|
text = dedent("""
|
||||||
@@ -167,6 +174,43 @@ 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_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="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")
|
||||||
|
|
||||||
|
|
||||||
@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:
|
||||||
@@ -177,6 +221,16 @@ async def start_text_received(message: types.Message, state: FSMContext):
|
|||||||
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.text
|
||||||
|
await bot.save()
|
||||||
|
await send_bot_second_text_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")
|
||||||
@@ -214,3 +268,11 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
|
|||||||
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)
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ 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 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
|
||||||
@@ -20,7 +22,8 @@ async def start(message: types.Message, state: FSMContext):
|
|||||||
# TODO: locale
|
# TODO: locale
|
||||||
|
|
||||||
await message.answer(dedent("""
|
await message.answer(dedent("""
|
||||||
Olgram Bot — это конструктор ботов обратной связи в Telegram.
|
Olgram Bot — это конструктор ботов обратной связи в Telegram. Подробнее \
|
||||||
|
<a href="https://olgram.readthedocs.io">читайте здесь</a>.
|
||||||
|
|
||||||
Используйте эти команды, чтобы управлять этим ботом:
|
Используйте эти команды, чтобы управлять этим ботом:
|
||||||
|
|
||||||
@@ -28,20 +31,26 @@ async def start(message: types.Message, state: FSMContext):
|
|||||||
/mybots - управление ботами
|
/mybots - управление ботами
|
||||||
|
|
||||||
/help - помощь
|
/help - помощь
|
||||||
"""))
|
"""), parse_mode="html")
|
||||||
|
|
||||||
|
|
||||||
@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(f"""
|
await message.answer(dedent(f"""
|
||||||
О проекте https://telegra.ph/Olgram-09-15
|
Читайте инструкции на нашем сайте https://olgram.readthedocs.io
|
||||||
|
Техническая поддержка: @civsocit_feedback_bot
|
||||||
Репозиторий https://github.com/civsocit/olgram
|
|
||||||
|
|
||||||
Поддержка: @civsocit_feedback_bot
|
|
||||||
|
|
||||||
Версия {OlgramSettings.version()}
|
Версия {OlgramSettings.version()}
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message_handler(commands=["chatid"], state="*")
|
||||||
|
@public()
|
||||||
|
async def chat_id(message: types.Message, state: FSMContext):
|
||||||
|
"""
|
||||||
|
Команда /chatid
|
||||||
|
"""
|
||||||
|
await message.answer(message.chat.id)
|
||||||
|
|||||||
35
olgram/migrations/custom.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"""Наши собственные миграции, которые нельзя описать на языке SQL и с которыми не справится TortoiseORM/Aerich"""
|
||||||
|
|
||||||
|
from tortoise import transactions, Tortoise
|
||||||
|
from olgram.settings import TORTOISE_ORM
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Не забудь добавить миграцию в этот лист!
|
||||||
|
_migrations = [upgrade_1]
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
4
olgram/migrations/models/4_20210926165918_update.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- upgrade --
|
||||||
|
ALTER TABLE "bot" ADD "second_text" TEXT;
|
||||||
|
-- downgrade --
|
||||||
|
ALTER TABLE "bot" DROP COLUMN "second_text";
|
||||||
10
olgram/migrations/models/5_20210926185420_update.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- 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";
|
||||||
@@ -2,11 +2,27 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
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=50, unique=True)
|
token = fields.CharField(max_length=200, 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)
|
||||||
@@ -14,6 +30,7 @@ class Bot(Model):
|
|||||||
Здравствуйте!
|
Здравствуйте!
|
||||||
Напишите ваш вопрос и мы ответим вам в ближайшее время.
|
Напишите ваш вопрос и мы ответим вам в ближайшее время.
|
||||||
"""))
|
"""))
|
||||||
|
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)
|
||||||
@@ -21,6 +38,15 @@ class Bot(Model):
|
|||||||
on_delete=fields.relational.CASCADE,
|
on_delete=fields.relational.CASCADE,
|
||||||
null=True)
|
null=True)
|
||||||
|
|
||||||
|
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:
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
import os
|
import os
|
||||||
|
from olgram.utils.crypto import Cryptor
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
|
||||||
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 = os.getenv(parameter, None)
|
parameter_v = os.getenv(parameter, None)
|
||||||
if not parameter and not allow_none:
|
if not parameter_v and not allow_none:
|
||||||
raise ValueError(f"{parameter} not defined in ENV")
|
raise ValueError(f"{parameter} not defined in ENV")
|
||||||
return parameter
|
return parameter_v
|
||||||
|
|
||||||
|
|
||||||
class OlgramSettings(AbstractSettings):
|
class OlgramSettings(AbstractSettings):
|
||||||
@@ -22,11 +26,16 @@ class OlgramSettings(AbstractSettings):
|
|||||||
Максимальное количество ботов у одного пользователя
|
Максимальное количество ботов у одного пользователя
|
||||||
:return: int
|
:return: int
|
||||||
"""
|
"""
|
||||||
return 5
|
return 10
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def version(cls):
|
def version(cls):
|
||||||
return "0.0.3"
|
return "0.1.1"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@lru_cache
|
||||||
|
def admin_id(cls):
|
||||||
|
return cls._get_env("ADMIN_ID", True)
|
||||||
|
|
||||||
|
|
||||||
class ServerSettings(AbstractSettings):
|
class ServerSettings(AbstractSettings):
|
||||||
@@ -99,6 +108,12 @@ 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()}'
|
||||||
|
|||||||
16
olgram/utils/crypto.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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()
|
||||||
52
olgram/utils/permissions.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import aiogram.types as types
|
||||||
|
from aiogram.dispatcher.handler import CancelHandler, current_handler
|
||||||
|
from aiogram.dispatcher.middlewares import BaseMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
def public():
|
||||||
|
"""
|
||||||
|
Хендлеры с этим декоратором будут обрабатываться даже если пользователь не является владельцем бота
|
||||||
|
(например, команда /help)
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
setattr(func, "access_public", True)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class AccessMiddleware(BaseMiddleware):
|
||||||
|
def __init__(self, access_chat_id: int):
|
||||||
|
self._access_chat_id = access_chat_id
|
||||||
|
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_id = self._access_chat_id
|
||||||
|
if not admin_id:
|
||||||
|
return # Администратор бота вообще не указан
|
||||||
|
|
||||||
|
if self._is_public_command(): # Эта команда разрешена всем пользователям
|
||||||
|
return
|
||||||
|
|
||||||
|
if message.chat.id != admin_id:
|
||||||
|
await message.answer("Владелец бота ограничил доступ к этому функционалу 😞")
|
||||||
|
raise CancelHandler()
|
||||||
|
|
||||||
|
async def on_process_callback_query(self, call: types.CallbackQuery, data: dict):
|
||||||
|
admin_id = self._access_chat_id
|
||||||
|
if not admin_id:
|
||||||
|
return # Администратор бота вообще не указан
|
||||||
|
|
||||||
|
if self._is_public_command(): # Эта команда разрешена всем пользователям
|
||||||
|
return
|
||||||
|
|
||||||
|
if call.message.chat.id != admin_id:
|
||||||
|
await call.answer("Владелец бота ограничил доступ к этому функционалу😞")
|
||||||
|
raise CancelHandler()
|
||||||
8
pyproject.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["flit_core >=3.2,<4"]
|
||||||
|
build-backend = "flit_core.buildapi"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "olgram"
|
||||||
|
authors = [{name = "civsocit", email = "feedback@civsoc.it"}]
|
||||||
|
dynamic = ["version", "description"]
|
||||||
@@ -4,4 +4,5 @@ aerich==0.5.4
|
|||||||
python-dotenv~=0.17.1
|
python-dotenv~=0.17.1
|
||||||
aioredis==1.3.1
|
aioredis==1.3.1
|
||||||
aiocache
|
aiocache
|
||||||
aiohttp
|
aiohttp
|
||||||
|
pycrypto
|
||||||
@@ -45,10 +45,14 @@ async def message_handler(message, *args, **kwargs):
|
|||||||
# Это обычный чат: сообщение нужно переслать в супер-чат
|
# Это обычный чат: сообщение нужно переслать в супер-чат
|
||||||
new_message = await message.forward(super_chat_id)
|
new_message = await message.forward(super_chat_id)
|
||||||
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id)
|
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id)
|
||||||
|
|
||||||
|
# И отправить пользователю специальный текст, если он указан
|
||||||
|
if bot.second_text:
|
||||||
|
return SendMessage(chat_id=message.chat.id, text=bot.second_text)
|
||||||
else:
|
else:
|
||||||
# Это супер-чат
|
# Это супер-чат
|
||||||
if message.reply_to_message:
|
if message.reply_to_message:
|
||||||
# Ответ из супер-чата переслать тому пользователю,
|
# В супер-чате кто-то ответил на сообщение пользователя, нужно переслать тому пользователю
|
||||||
chat_id = await _redis.get(_message_unique_id(bot.pk, message.reply_to_message.message_id))
|
chat_id = await _redis.get(_message_unique_id(bot.pk, message.reply_to_message.message_id))
|
||||||
if not chat_id:
|
if not chat_id:
|
||||||
chat_id = message.reply_to_message.forward_from_chat
|
chat_id = message.reply_to_message.forward_from_chat
|
||||||
@@ -64,7 +68,11 @@ async def message_handler(message, *args, **kwargs):
|
|||||||
parse_mode="HTML")
|
parse_mode="HTML")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
# в супер-чате кто-то пишет сообщение сам себе
|
||||||
await message.forward(super_chat_id)
|
await message.forward(super_chat_id)
|
||||||
|
# И отправить пользователю специальный текст, если он указан
|
||||||
|
if bot.second_text:
|
||||||
|
return SendMessage(chat_id=message.chat.id, text=bot.second_text)
|
||||||
|
|
||||||
|
|
||||||
async def receive_invite(message: types.Message):
|
async def receive_invite(message: types.Message):
|
||||||
@@ -106,7 +114,7 @@ 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.token))
|
dp = Dispatcher(AioBot(bot.decrypted_token()))
|
||||||
|
|
||||||
dp.register_message_handler(message_handler, content_types=[types.ContentType.TEXT,
|
dp.register_message_handler(message_handler, content_types=[types.ContentType.TEXT,
|
||||||
types.ContentType.CONTACT,
|
types.ContentType.CONTACT,
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ async def register_token(bot: Bot) -> bool:
|
|||||||
:param bot: Бот
|
:param bot: Бот
|
||||||
:return: получилось ли
|
:return: получилось ли
|
||||||
"""
|
"""
|
||||||
await unregister_token(bot.token)
|
await unregister_token(bot.decrypted_token())
|
||||||
|
|
||||||
a_bot = AioBot(bot.token)
|
a_bot = AioBot(bot.decrypted_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')
|
||||||
|
|||||||