1
0
mirror of https://github.com/civsocit/olgram.git synced 2025-05-24 06:53:24 +00:00

Merge branch 'main' into stable

This commit is contained in:
er8dd 2024-03-02 20:05:23 +04:00
commit 44f39e4de0
17 changed files with 1539 additions and 865 deletions

25
.readthedocs.yaml Normal file
View File

@ -0,0 +1,25 @@
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.8"
jobs:
post_create_environment:
- python -m pip install sphinx_rtd_theme
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/source/conf.py

View File

@ -1,7 +1,7 @@
FROM python:3.8-buster
ENV PYTHONUNBUFFERED=1 \
POETRY_VERSION=1.1.12 \
POETRY_VERSION=1.5.1 \
POETRY_VIRTUALENVS_CREATE="false"
RUN apt-get update && \

View File

@ -20,6 +20,7 @@ Litecoin:
История изменений
----------------
- `2024-03-01` Непрерывные потоки сообщений (опция)
- `2024-02-17` Опция смены режима работы автоответчика: автоответчик отвечает на КАЖДОЕ сообщение
- `2024-01-12` Мультиязычность (стартовое сообщение и автоответчик)
- `2022-08-01` Защита от флуда

View File

@ -58,3 +58,26 @@ Olgram пересылает сообщения так, чтобы сообщен
По-умолчанию автоответчик отвечает только на первое сообщение в диалоге с пользователем. Чтобы автоответчик отвечал на
КАЖДОЕ входящее сообщение, включите эту опцию.
.. thread_interrupt:
Прерывать поток
--------------------------------
По-умолчанию поток сообщений от одного пользователя прерывается каждые 24 часа. Без этой опции поток сообщений не
прерывается никогда.
.. _mailing:
Рассылка
---------------
После включения этой опции ваш бот будет запоминать всех пользователей, которые пишут в ваш бот.
Вы сможете запустить рассылку по этим пользователям.
.. note::
Включение этой опции меняет текст политики конфиденциальности вашего feedback бота (команда /security_policy)
и может отпугнуть некоторых пользователей. Не включайте эту опцию без необходимости.

View File

@ -5,8 +5,8 @@
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"
"POT-Creation-Date: 2024-03-02 19:47+0400\n"
"PO-Revision-Date: 2024-03-02 19:48+0400\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: en_US\n"
@ -15,7 +15,7 @@ msgstr ""
"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"
"X-Generator: Poedit 3.4.2\n"
#: olgram/commands/admin.py:21 olgram/commands/info.py:21
#: olgram/commands/promo.py:23 olgram/commands/promo.py:39
@ -62,27 +62,27 @@ msgstr "Cancel"
msgid "Отправлено"
msgstr "Sent"
#: olgram/commands/bot_actions.py:22
#: olgram/commands/bot_actions.py:27
msgid "Бот удалён"
msgstr "Bot removed"
#: olgram/commands/bot_actions.py:38 olgram/commands/bot_actions.py:50
#: olgram/commands/bot_actions.py:49 olgram/commands/bot_actions.py:67
msgid "Текст сброшен"
msgstr "Text is reset"
#: olgram/commands/bot_actions.py:64
#: olgram/commands/bot_actions.py:81
msgid "Выбран личный чат"
msgstr "Personal chat selected"
#: olgram/commands/bot_actions.py:77
#: olgram/commands/bot_actions.py:94
msgid "Бот вышел из чатов"
msgstr "Bot leaved chats"
#: olgram/commands/bot_actions.py:83
#: olgram/commands/bot_actions.py:100
msgid "Нельзя привязать бота к этому чату"
msgstr "You can't bind a bot to this chat room"
#: olgram/commands/bot_actions.py:87
#: olgram/commands/bot_actions.py:104
msgid "Выбран чат {0}"
msgstr "Selected chat {0}"
@ -195,7 +195,11 @@ msgstr "All bots have outgoing messages: {0}\n"
msgid "Промо-кодов выдано: {0}\n"
msgstr "Promo codes issued: {0}\n"
#: olgram/commands/menu.py:31
#: olgram/commands/info.py:40
msgid "Рекламную плашку выключили: {0}\n"
msgstr "Ad disabled:: {0}\n"
#: olgram/commands/menu.py:33
msgid ""
"\n"
" У вас нет добавленных ботов.\n"
@ -209,25 +213,25 @@ msgstr ""
" Send the command /addbot to add a bot.\n"
" "
#: olgram/commands/menu.py:46
#: olgram/commands/menu.py:48
msgid "Ваши боты"
msgstr "Your bots"
#: olgram/commands/menu.py:67
#: olgram/commands/menu.py:69
msgid "Личные сообщения"
msgstr "Personal messages"
#: olgram/commands/menu.py:72
#: olgram/commands/menu.py:74
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
#: olgram/commands/menu.py:79 olgram/commands/menu.py:124
#: olgram/commands/menu.py:156 olgram/commands/menu.py:209
#: olgram/commands/menu.py:390
msgid "<< Назад"
msgstr "<< Back"
#: olgram/commands/menu.py:83
#: olgram/commands/menu.py:85
msgid ""
"\n"
" Этот бот не добавлен в чаты, поэтому все сообщения будут приходить "
@ -249,7 +253,7 @@ msgstr ""
" again.\n"
" "
#: olgram/commands/menu.py:90
#: olgram/commands/menu.py:92
msgid ""
"\n"
" В этом разделе вы можете привязать бота @{0} к чату.\n"
@ -261,27 +265,31 @@ msgstr ""
" Select the chat room where the bot will forward messages.\n"
" "
#: olgram/commands/menu.py:102
#: olgram/commands/menu.py:104
msgid "Текст"
msgstr "Text"
#: olgram/commands/menu.py:107
#: olgram/commands/menu.py:109
msgid "Чат"
msgstr "Chat"
#: olgram/commands/menu.py:112
#: olgram/commands/menu.py:114
msgid "Удалить бот"
msgstr "Delete bot"
#: olgram/commands/menu.py:117
#: olgram/commands/menu.py:119
msgid "Статистика"
msgstr "Statistics"
#: olgram/commands/menu.py:126
#: olgram/commands/menu.py:128
msgid "Опции"
msgstr "Options"
#: olgram/commands/menu.py:131
#: olgram/commands/menu.py:134 olgram/commands/menu.py:190
msgid "Рассылка"
msgstr "Mailing"
#: olgram/commands/menu.py:139
msgid ""
"\n"
" Управление ботом @{0}.\n"
@ -299,11 +307,11 @@ msgstr ""
" @civsocit_feedback_bot\n"
" "
#: olgram/commands/menu.py:143
#: olgram/commands/menu.py:151
msgid "Да, удалить бот"
msgstr "Yes, delete the bot"
#: olgram/commands/menu.py:152
#: olgram/commands/menu.py:160
msgid ""
"\n"
" Вы уверены, что хотите удалить бота @{0}?\n"
@ -313,43 +321,73 @@ msgstr ""
" Are you sure you want to delete the bot @{0}?\n"
" "
#: olgram/commands/menu.py:161
#: olgram/commands/menu.py:169
msgid "Потоки сообщений"
msgstr "Message threads"
#: olgram/commands/menu.py:166
#: olgram/commands/menu.py:174
msgid "Данные пользователя"
msgstr "User data"
#: olgram/commands/menu.py:171
#: olgram/commands/menu.py:179
msgid "Антифлуд"
msgstr "Antiflood"
#: olgram/commands/menu.py:178
#: olgram/commands/menu.py:184
msgid "Автоответчик всегда"
msgstr "Autorespond always"
#: olgram/commands/menu.py:195
msgid "Прерывать поток"
msgstr "Inteeupt thread"
#: olgram/commands/menu.py:203
msgid "Olgram подпись"
msgstr "Olgram signature"
#: olgram/commands/menu.py:189 olgram/commands/menu.py:190
#: olgram/commands/menu.py:214 olgram/commands/menu.py:215
msgid "включены"
msgstr "enabled"
#: olgram/commands/menu.py:189 olgram/commands/menu.py:190
#: olgram/commands/menu.py:214 olgram/commands/menu.py:215
msgid "выключены"
msgstr "disabled"
#: olgram/commands/menu.py:191
#: olgram/commands/menu.py:216
#, fuzzy
#| msgid "включены"
msgid "включен"
msgstr "enabled"
#: olgram/commands/menu.py:191
#: olgram/commands/menu.py:216 olgram/commands/menu.py:217
#, fuzzy
#| msgid "выключены"
msgid "выключен"
msgstr "disabled"
#: olgram/commands/menu.py:192
#: olgram/commands/menu.py:217
#, fuzzy
#| msgid "включены"
msgid "включён"
msgstr "enabled"
#: olgram/commands/menu.py:218
msgid "да"
msgstr "yes"
#: olgram/commands/menu.py:218
msgid "нет"
msgstr "no"
#: olgram/commands/menu.py:219 olgram/commands/menu.py:231
msgid "включена"
msgstr "enabled"
#: olgram/commands/menu.py:219 olgram/commands/menu.py:231
msgid "выключена"
msgstr "disabled"
#: olgram/commands/menu.py:220
msgid ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
@ -358,6 +396,12 @@ msgid ""
"info\">Данные пользователя</a>: <b>{1}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#antiflood\">Антифлуд</a>: <b>{2}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#always_second_message\">Автоответчик всегда</a>: <b>{3}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#thread_interrupt\">Прерывать поток</a>: <b>{4}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#mailing\">Рассылка</a>: <b>{5}</b>\n"
" "
msgstr ""
"\n"
@ -366,60 +410,86 @@ msgstr ""
" <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>"
"html#antiflood\">Anti-flood</a>: <b>{2}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#always_second_message\">Autorespond always</a>: <b>{3}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#thread_interrupt\">Interrupt threads</a>: <b>{4}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#mailing\">Mailing</a>: <b>{5}</b>\n"
" "
#: olgram/commands/menu.py:199
msgid "включена"
msgstr "enabled"
#: olgram/commands/menu.py:199
msgid "выключена"
msgstr "disabled"
#: olgram/commands/menu.py:200
#: olgram/commands/menu.py:232
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
#: olgram/commands/menu.py:259 olgram/commands/menu.py:421
#: olgram/commands/menu.py:480
msgid "<< Завершить редактирование"
msgstr "<< Finish editing"
#: olgram/commands/menu.py:214
#: olgram/commands/menu.py:263
msgid "Автоответчик"
msgstr "Autoresponder"
#: olgram/commands/menu.py:219 olgram/commands/menu.py:286
#: olgram/commands/menu.py:268 olgram/commands/menu.py:435
msgid "Сбросить текст"
msgstr "Reset text"
#: olgram/commands/menu.py:224
#: olgram/commands/menu.py:273 olgram/commands/menu.py:440
msgid "[все языки]"
msgstr "[all languages]"
#: olgram/commands/menu.py:290
msgid ""
"\n"
" Сейчас вы редактируете текст, который отправляется после того, как "
"пользователь отправит вашему боту @{0}\n"
" команду /start\n"
"\n"
" Текущий текст:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Текущий текст{2}:\n"
" <pre>{1}</pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
#: olgram/commands/menu.py:300 olgram/commands/menu.py:467
msgid " (для языка {0})"
msgstr " (for language {0})"
#: olgram/commands/menu.py:313
msgid "<< Отменить рассылку"
msgstr "<< Cancel"
#: olgram/commands/menu.py:317
msgid ""
"\n"
" You are now editing the text that is sent after the user sends your bot "
"@{0}\n"
" /start command.\n"
" Напишите сообщение, которое нужно разослать всем подписчикам вашего бота "
"@{0}. \n"
" У сообщения будет до {1} получателей. \n"
" Учтите, что\n"
" 1. Рассылается только одно сообщение за раз (в т.ч. только одна "
"картинка)\n"
" 2. Когда рассылка запущена, её нельзя отменить \n"
" "
msgstr ""
"\n"
" Current text:\n"
" <pre>\n"
" {1}\n"
" </pre>\n"
" Send a message to change the text.\n"
" Please send mailing message to send all @{0} subscribers. \n"
" Message will have up to {1} recipients. \n"
" Take note:\n"
" 1. Only one message per mailing\n"
" 2.Mailing cant be interrupted \n"
" "
#: olgram/commands/menu.py:251
#: olgram/commands/menu.py:367
msgid "Не удалось загрузить файл (слишком большой размер?)"
msgstr ""
#: olgram/commands/menu.py:374
msgid "Да, начать рассылку"
msgstr "Yes, start mailing"
#: olgram/commands/menu.py:394
msgid ""
"\n"
" Статистика по боту @{0}\n"
@ -439,45 +509,32 @@ msgstr ""
" Banned users: <b>{4}</b>\n"
" "
#: olgram/commands/menu.py:276
#: olgram/commands/menu.py:425
msgid "Предыдущий текст"
msgstr "Previous text"
#: olgram/commands/menu.py:281
#: olgram/commands/menu.py:430
msgid "Шаблоны ответов..."
msgstr "Answer templates..."
#: olgram/commands/menu.py:291
#: olgram/commands/menu.py:457
msgid ""
"\n"
" Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в "
"ответ на все входящие сообщения @{0} автоматически. По умолчанию оно "
"отключено.\n"
"\n"
" Текущий текст:\n"
" <pre>"
" {1}\n"
" </pre>\n"
" Текущий текст{2}:\n"
" <pre>{1}</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:466
msgid "отключено"
msgstr "disabled"
#: olgram/commands/menu.py:318
#: olgram/commands/menu.py:484
msgid ""
"\n"
" Сейчас вы редактируете шаблоны ответов для @{0}. Текущие шаблоны:\n"
@ -503,30 +560,50 @@ msgstr ""
" To remove a template from the list, send its number in the list (for "
"example, 4) "
#: olgram/commands/menu.py:337
#: olgram/commands/menu.py:503
msgid "(нет шаблонов)"
msgstr "(no templates)"
#: olgram/commands/menu.py:376
#: olgram/commands/menu.py:565
msgid "У вас нет шаблонов, чтобы их удалять"
msgstr "You don't have templates to delete them"
#: olgram/commands/menu.py:378
#: olgram/commands/menu.py:567
msgid "Неправильное число. Чтобы удалить шаблон, введите число от 0 до {0}"
msgstr "To delete a template, enter a number between 0 and {0}"
#: olgram/commands/menu.py:386
#: olgram/commands/menu.py:575
msgid "У вашего бота уже слишком много шаблонов"
msgstr "Your bot already has too many templates"
#: olgram/commands/menu.py:390
#: olgram/commands/menu.py:579
msgid "Такой текст уже есть в списке шаблонов"
msgstr "This text is already in the list of templates"
#: olgram/commands/menu.py:408
#: olgram/commands/menu.py:597
msgid "У вас нет прав на этого бота"
msgstr "You have no permissions to this bot"
#: olgram/commands/menu.py:617 olgram/commands/menu.py:643
msgid "Рассылка была совсем недавно, подождите немного"
msgstr "Mailing was recently, wait a bit please"
#: olgram/commands/menu.py:619 olgram/commands/menu.py:645
msgid "Нет пользователей для рассылки"
msgstr "No users for mailing"
#: olgram/commands/menu.py:647
msgid "Рассылка запущена"
msgstr "Mailing started"
#: olgram/commands/menu.py:649
msgid "Рассылка завершена, отправлено {0} сообщений"
msgstr "Mailing completed, {0} messages sent"
#: olgram/commands/menu.py:651
msgid "Устарело, создайте новую рассылку"
msgstr "Expired, please create new mailing"
#: olgram/commands/promo.py:27
msgid ""
"Новый промокод\n"
@ -551,8 +628,8 @@ msgstr "Promotion code withdrawn"
msgid ""
"Укажите аргумент: промокод. Например: <pre>/setpromo my-promo-code</pre>"
msgstr ""
"Specify the argument: promo code. For example: <pre>/setpromo my-promo-"
"code</pre>"
"Specify the argument: promo code. For example: <pre>/setpromo my-promo-code</"
"pre>"
#: olgram/commands/promo.py:78 olgram/commands/promo.py:82
msgid "Промокод не найден"
@ -619,87 +696,102 @@ msgstr ""
" Write your question and we will answer you shortly.\n"
" "
#: olgram/utils/permissions.py:40
#: olgram/utils/permissions.py:41
msgid "Владелец бота ограничил доступ к этому функционалу 😞"
msgstr "The bot owner has restricted access to this functionality 😞"
#: olgram/utils/permissions.py:52
#: olgram/utils/permissions.py:53
msgid "Владелец бота ограничил доступ к этому функционалу😞"
msgstr "The owner of the bot has restricted access to this function😞"
#: server/custom.py:55
#: server/custom.py:57
msgid ""
"<b>Политика конфиденциальности</b>\n"
"\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При "
"отправке сообщения (кроме команд /start и /security_policy) ваш "
"идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется только для общения с "
"оператором; боты Olgram не делают массовых рассылок.\n"
"удаляется из кеша. Этот идентификатор используется для общения с "
"оператором.\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"
"for a while and then deleted from the cache. This ID is used for "
"communicating with the operator\n"
"\n"
#: server/custom.py:61
#: server/custom.py:62
msgid ""
"При отправке сообщения (кроме команд /start и /security_policy) оператор "
"<b>видит</b> ваши имя пользователя, @username и идентификатор пользователя в "
"силу настроек, которые оператор указал при создании бота."
"силу настроек, которые оператор указал при создании бота.\n"
"\n"
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."
"that the operator specified when creating the bot.\n"
"\n"
#: server/custom.py:65
#: server/custom.py:66
msgid ""
"В зависимости от ваших настроек конфиденциальности Telegram, оператор может "
"видеть ваш username, имя пользователя и другую информацию."
"видеть ваш username, имя пользователя и другую информацию.\n"
"\n"
msgstr ""
"Depending on your Telegram privacy settings, the operator may see your "
"username, username and other information."
"username, username and other information.\n"
"\n"
#: server/custom.py:76
#: server/custom.py:70
msgid ""
"В этом боте включена массовая рассылка в силу настроек, которые оператор "
"указал при создании бота. Ваш идентификатор пользователя может быть записан "
"в базу данных на долгое время"
msgstr "Mailing enabled for this bot"
#: server/custom.py:73
msgid "В этом боте нет массовой рассылки сообщений"
msgstr "Mailing disabled for this bot"
#: server/custom.py:83
msgid "Сообщение от пользователя "
msgstr "Message from the user "
#: server/custom.py:135
#: server/custom.py:157
msgid "Вы заблокированы в этом боте"
msgstr "You are blocked in this bot"
#: server/custom.py:141
#: server/custom.py:163
msgid "Слишком много сообщений, подождите одну минуту"
msgstr "Too many messages, wait one minute"
#: server/custom.py:148
#: server/custom.py:170
msgid "Не удаётся связаться с владельцем бота"
msgstr "Cannot contact the owner of the bot"
#: server/custom.py:179
#: server/custom.py:202
msgid ""
"<i>Невозможно переслать сообщение: автор не найден (сообщение слишком "
"старое?)</i>"
msgstr ""
"<i>Cannot forward this message: author not found (message too old?)</i>"
#: server/custom.py:187
#: server/custom.py:210
msgid "Пользователь заблокирован"
msgstr "User is blocked"
#: server/custom.py:192
#: server/custom.py:215
msgid "Пользователь не был забанен"
msgstr "The user was not banned"
#: server/custom.py:195
#: server/custom.py:218
msgid "Пользователь разбанен"
msgstr "A user has been unlocked"
#: server/custom.py:200
#: server/custom.py:223
msgid "<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>"
msgstr "<i>Cannot forward the message (has the author blocked the bot?)</i>"
@ -711,11 +803,11 @@ msgstr "(Re)launch the bot"
msgid "Политика конфиденциальности"
msgstr "Privacy Policy"
msgid ""
"\n"
"\n"
"Этот бот создан с помощью @OlgramBot"
msgstr ""
"\n"
"\n"
"This bot was created using @OlgramBot"
#~ msgid ""
#~ "\n"
#~ "\n"
#~ "Этот бот создан с помощью @OlgramBot"
#~ msgstr ""
#~ "\n"
#~ "\n"
#~ "This bot was created using @OlgramBot"

View File

@ -5,8 +5,8 @@
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"
"POT-Creation-Date: 2024-03-02 19:47+0400\n"
"PO-Revision-Date: 2024-03-02 19:48+0400\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: uk_UA\n"
@ -16,7 +16,7 @@ msgstr ""
"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"
"X-Generator: Poedit 3.4.2\n"
#: olgram/commands/admin.py:21 olgram/commands/info.py:21
#: olgram/commands/promo.py:23 olgram/commands/promo.py:39
@ -63,27 +63,27 @@ msgstr "Скасувати"
msgid "Отправлено"
msgstr "Надіслано"
#: olgram/commands/bot_actions.py:22
#: olgram/commands/bot_actions.py:27
msgid "Бот удалён"
msgstr "Бот видалений"
#: olgram/commands/bot_actions.py:38 olgram/commands/bot_actions.py:50
#: olgram/commands/bot_actions.py:49 olgram/commands/bot_actions.py:67
msgid "Текст сброшен"
msgstr "Текст скинутий"
#: olgram/commands/bot_actions.py:64
#: olgram/commands/bot_actions.py:81
msgid "Выбран личный чат"
msgstr "Вибраний особистий чат"
#: olgram/commands/bot_actions.py:77
#: olgram/commands/bot_actions.py:94
msgid "Бот вышел из чатов"
msgstr "Бот вийшов із чатів"
#: olgram/commands/bot_actions.py:83
#: olgram/commands/bot_actions.py:100
msgid "Нельзя привязать бота к этому чату"
msgstr "Не можна прив'язати робота до цього чату"
#: olgram/commands/bot_actions.py:87
#: olgram/commands/bot_actions.py:104
msgid "Выбран чат {0}"
msgstr "Вибраний чат {0}"
@ -198,7 +198,11 @@ msgstr "Вихідних повідомлень у всіх роботів: {0}\
msgid "Промо-кодов выдано: {0}\n"
msgstr "Промо-кодів видано: {0}\n"
#: olgram/commands/menu.py:31
#: olgram/commands/info.py:40
msgid "Рекламную плашку выключили: {0}\n"
msgstr ""
#: olgram/commands/menu.py:33
msgid ""
"\n"
" У вас нет добавленных ботов.\n"
@ -213,25 +217,25 @@ msgstr ""
" \n"
" "
#: olgram/commands/menu.py:46
#: olgram/commands/menu.py:48
msgid "Ваши боты"
msgstr "Ваші боти"
#: olgram/commands/menu.py:67
#: olgram/commands/menu.py:69
msgid "Личные сообщения"
msgstr "Особисті повідомлення"
#: olgram/commands/menu.py:72
#: olgram/commands/menu.py:74
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
#: olgram/commands/menu.py:79 olgram/commands/menu.py:124
#: olgram/commands/menu.py:156 olgram/commands/menu.py:209
#: olgram/commands/menu.py:390
msgid "<< Назад"
msgstr "<< Назад"
#: olgram/commands/menu.py:83
#: olgram/commands/menu.py:85
msgid ""
"\n"
" Этот бот не добавлен в чаты, поэтому все сообщения будут приходить "
@ -254,7 +258,7 @@ msgstr ""
" \n"
" "
#: olgram/commands/menu.py:90
#: olgram/commands/menu.py:92
msgid ""
"\n"
" В этом разделе вы можете привязать бота @{0} к чату.\n"
@ -267,27 +271,31 @@ msgstr ""
" \n"
" "
#: olgram/commands/menu.py:102
#: olgram/commands/menu.py:104
msgid "Текст"
msgstr "Текст"
#: olgram/commands/menu.py:107
#: olgram/commands/menu.py:109
msgid "Чат"
msgstr "Чат"
#: olgram/commands/menu.py:112
#: olgram/commands/menu.py:114
msgid "Удалить бот"
msgstr "Видалити бот"
#: olgram/commands/menu.py:117
#: olgram/commands/menu.py:119
msgid "Статистика"
msgstr "Статистика"
#: olgram/commands/menu.py:126
#: olgram/commands/menu.py:128
msgid "Опции"
msgstr "Опції"
#: olgram/commands/menu.py:131
#: olgram/commands/menu.py:134 olgram/commands/menu.py:190
msgid "Рассылка"
msgstr ""
#: olgram/commands/menu.py:139
msgid ""
"\n"
" Управление ботом @{0}.\n"
@ -305,11 +313,11 @@ msgstr ""
" @civsocit_feedback_bot\n"
" "
#: olgram/commands/menu.py:143
#: olgram/commands/menu.py:151
msgid "Да, удалить бот"
msgstr "Так, видалити бот"
#: olgram/commands/menu.py:152
#: olgram/commands/menu.py:160
msgid ""
"\n"
" Вы уверены, что хотите удалить бота @{0}?\n"
@ -319,43 +327,75 @@ msgstr ""
" Ви впевнені, що хочете видалити бота @{0}?\n"
" "
#: olgram/commands/menu.py:161
#: olgram/commands/menu.py:169
msgid "Потоки сообщений"
msgstr "Потоки повідомлень"
#: olgram/commands/menu.py:166
#: olgram/commands/menu.py:174
msgid "Данные пользователя"
msgstr "Дані користувача"
#: olgram/commands/menu.py:171
#: olgram/commands/menu.py:179
msgid "Антифлуд"
msgstr "Антифлуд"
#: olgram/commands/menu.py:178
#: olgram/commands/menu.py:184
#, fuzzy
#| msgid "Автоответчик"
msgid "Автоответчик всегда"
msgstr "Автовідповідач"
#: olgram/commands/menu.py:195
msgid "Прерывать поток"
msgstr ""
#: olgram/commands/menu.py:203
msgid "Olgram подпись"
msgstr "Olgram підпис"
#: olgram/commands/menu.py:189 olgram/commands/menu.py:190
#: olgram/commands/menu.py:214 olgram/commands/menu.py:215
msgid "включены"
msgstr "включені"
#: olgram/commands/menu.py:189 olgram/commands/menu.py:190
#: olgram/commands/menu.py:214 olgram/commands/menu.py:215
msgid "выключены"
msgstr "вимкнені"
#: olgram/commands/menu.py:191
#: olgram/commands/menu.py:216
#, fuzzy
#| msgid "включены"
msgid "включен"
msgstr "включені"
#: olgram/commands/menu.py:191
#: olgram/commands/menu.py:216 olgram/commands/menu.py:217
#, fuzzy
#| msgid "выключены"
msgid "выключен"
msgstr "вимкнені"
#: olgram/commands/menu.py:192
#: olgram/commands/menu.py:217
#, fuzzy
#| msgid "включены"
msgid "включён"
msgstr "включені"
#: olgram/commands/menu.py:218
msgid "да"
msgstr "Ча"
#: olgram/commands/menu.py:218
msgid "нет"
msgstr ""
#: olgram/commands/menu.py:219 olgram/commands/menu.py:231
msgid "включена"
msgstr "включена"
#: olgram/commands/menu.py:219 olgram/commands/menu.py:231
msgid "выключена"
msgstr "вимкнена"
#: olgram/commands/menu.py:220
msgid ""
"\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
@ -364,65 +404,81 @@ msgid ""
"info\">Данные пользователя</a>: <b>{1}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#antiflood\">Антифлуд</a>: <b>{2}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#always_second_message\">Автоответчик всегда</a>: <b>{3}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#thread_interrupt\">Прерывать поток</a>: <b>{4}</b>\n"
" <a href=\"https://olgram.readthedocs.io/ru/latest/options."
"html#mailing\">Рассылка</a>: <b>{5}</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
#: olgram/commands/menu.py:232
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
#: olgram/commands/menu.py:259 olgram/commands/menu.py:421
#: olgram/commands/menu.py:480
msgid "<< Завершить редактирование"
msgstr "<< Завершити редагування"
#: olgram/commands/menu.py:214
#: olgram/commands/menu.py:263
msgid "Автоответчик"
msgstr "Автовідповідач"
#: olgram/commands/menu.py:219 olgram/commands/menu.py:286
#: olgram/commands/menu.py:268 olgram/commands/menu.py:435
msgid "Сбросить текст"
msgstr "Скинути текст"
#: olgram/commands/menu.py:224
#: olgram/commands/menu.py:273 olgram/commands/menu.py:440
msgid "[все языки]"
msgstr ""
#: olgram/commands/menu.py:290
msgid ""
"\n"
" Сейчас вы редактируете текст, который отправляется после того, как "
"пользователь отправит вашему боту @{0}\n"
" команду /start\n"
"\n"
" Текущий текст:\n"
" Текущий текст{2}:\n"
" <pre>{1}</pre>\n"
" Отправьте сообщение, чтобы изменить текст.\n"
" "
msgstr ""
"\n"
" Зараз ви редагуєте текст, який надсилається після того, як користувач "
"надішле вашому боту @{0}\n"
" команду /start\n"
"\n"
" Поточний текст:\n"
" <pre>{1}</pre>\n"
" Надішліть повідомлення, щоб змінити текст.\n"
" \n"
" "
#: olgram/commands/menu.py:251
#: olgram/commands/menu.py:300 olgram/commands/menu.py:467
msgid " (для языка {0})"
msgstr ""
#: olgram/commands/menu.py:313
#, fuzzy
#| msgid "Отменить"
msgid "<< Отменить рассылку"
msgstr "Скасувати"
#: olgram/commands/menu.py:317
msgid ""
"\n"
" Напишите сообщение, которое нужно разослать всем подписчикам вашего бота "
"@{0}. \n"
" У сообщения будет до {1} получателей. \n"
" Учтите, что\n"
" 1. Рассылается только одно сообщение за раз (в т.ч. только одна "
"картинка)\n"
" 2. Когда рассылка запущена, её нельзя отменить \n"
" "
msgstr ""
#: olgram/commands/menu.py:367
msgid "Не удалось загрузить файл (слишком большой размер?)"
msgstr ""
#: olgram/commands/menu.py:374
msgid "Да, начать рассылку"
msgstr ""
#: olgram/commands/menu.py:394
msgid ""
"\n"
" Статистика по боту @{0}\n"
@ -442,43 +498,32 @@ msgstr ""
" Забанено користувачів: <b>{4}</b>\n"
" "
#: olgram/commands/menu.py:276
#: olgram/commands/menu.py:425
msgid "Предыдущий текст"
msgstr "Попередній текст"
#: olgram/commands/menu.py:281
#: olgram/commands/menu.py:430
msgid "Шаблоны ответов..."
msgstr "Шаблони відповідей..."
#: olgram/commands/menu.py:291
#: olgram/commands/menu.py:457
msgid ""
"\n"
" Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в "
"ответ на все входящие сообщения @{0} автоматически. По умолчанию оно "
"отключено.\n"
"\n"
" Текущий текст:\n"
" Текущий текст{2}:\n"
" <pre>{1}</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:466
msgid "отключено"
msgstr "відключено"
#: olgram/commands/menu.py:318
#: olgram/commands/menu.py:484
msgid ""
"\n"
" Сейчас вы редактируете шаблоны ответов для @{0}. Текущие шаблоны:\n"
@ -504,30 +549,50 @@ msgstr ""
" \n"
" "
#: olgram/commands/menu.py:337
#: olgram/commands/menu.py:503
msgid "(нет шаблонов)"
msgstr "(Немає шаблонів)"
#: olgram/commands/menu.py:376
#: olgram/commands/menu.py:565
msgid "У вас нет шаблонов, чтобы их удалять"
msgstr "У вас немає шаблонів, щоб їх видаляти"
#: olgram/commands/menu.py:378
#: olgram/commands/menu.py:567
msgid "Неправильное число. Чтобы удалить шаблон, введите число от 0 до {0}"
msgstr "Неправильне число. Щоб видалити шаблон, введіть число від 0 до {0}"
#: olgram/commands/menu.py:386
#: olgram/commands/menu.py:575
msgid "У вашего бота уже слишком много шаблонов"
msgstr "У вашого бота вже дуже багато шаблонів"
#: olgram/commands/menu.py:390
#: olgram/commands/menu.py:579
msgid "Такой текст уже есть в списке шаблонов"
msgstr "Такий текст вже є у списку шаблонів"
#: olgram/commands/menu.py:408
#: olgram/commands/menu.py:597
msgid "У вас нет прав на этого бота"
msgstr "У вас немає прав на цього бота"
#: olgram/commands/menu.py:617 olgram/commands/menu.py:643
msgid "Рассылка была совсем недавно, подождите немного"
msgstr ""
#: olgram/commands/menu.py:619 olgram/commands/menu.py:645
msgid "Нет пользователей для рассылки"
msgstr ""
#: olgram/commands/menu.py:647
msgid "Рассылка запущена"
msgstr "Розсилка запущена"
#: olgram/commands/menu.py:649
msgid "Рассылка завершена, отправлено {0} сообщений"
msgstr "Розсилка завершена, надіслано {0} повідомлень"
#: olgram/commands/menu.py:651
msgid "Устарело, создайте новую рассылку"
msgstr "Застаріло, створіть нову розсилку"
#: olgram/commands/promo.py:27
msgid ""
"Новый промокод\n"
@ -623,23 +688,23 @@ msgstr ""
" \n"
" "
#: olgram/utils/permissions.py:40
#: olgram/utils/permissions.py:41
msgid "Владелец бота ограничил доступ к этому функционалу 😞"
msgstr "Власник бота обмежив доступ до цього функціоналу 😞"
#: olgram/utils/permissions.py:52
#: olgram/utils/permissions.py:53
msgid "Владелец бота ограничил доступ к этому функционалу😞"
msgstr "Власник бота обмежив доступ до цього функціоналу 😞"
#: server/custom.py:55
#: server/custom.py:57
msgid ""
"<b>Политика конфиденциальности</b>\n"
"\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При "
"отправке сообщения (кроме команд /start и /security_policy) ваш "
"идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется только для общения с "
"оператором; боты Olgram не делают массовых рассылок.\n"
"удаляется из кеша. Этот идентификатор используется для общения с "
"оператором.\n"
"\n"
msgstr ""
"<b>Політика конфіденційності</b>\n"
@ -647,45 +712,63 @@ msgstr ""
"Цей бот не зберігає ваші повідомлення, ім'я користувача та @username. При "
"надсиланні повідомлення (крім команд /start та /security_policy) ваш "
"ідентифікатор користувача записується в кеш на деякий час і потім "
"видаляється з кеша. Цей ідентифікатор використовується лише для спілкування "
"з оператором; боти Olgram не роблять масових розсилок.\n"
"видаляється з кеша. Цей ідентифікатор використовується для спілкування з "
"оператором.\n"
"\n"
#: server/custom.py:61
#: server/custom.py:62
msgid ""
"При отправке сообщения (кроме команд /start и /security_policy) оператор "
"<b>видит</b> ваши имя пользователя, @username и идентификатор пользователя в "
"силу настроек, которые оператор указал при создании бота."
"силу настроек, которые оператор указал при создании бота.\n"
"\n"
msgstr ""
"При надсиланні повідомлення (крім команд /start та /security_policy) "
"оператор <b>бачить</b> ваше ім'я користувача, @username та ідентифікатор "
"користувача через налаштування, які оператор вказав при створенні бота."
"користувача через налаштування, які оператор вказав при створенні бота.\n"
"\n"
#: server/custom.py:65
#: server/custom.py:66
msgid ""
"В зависимости от ваших настроек конфиденциальности Telegram, оператор может "
"видеть ваш username, имя пользователя и другую информацию."
"видеть ваш username, имя пользователя и другую информацию.\n"
"\n"
msgstr ""
"Залежно від ваших налаштувань конфіденційності Telegram оператор може бачити "
"ваш username, ім'я користувача та іншу інформацію."
"ваш username, ім'я користувача та іншу інформацію.\n"
"\n"
#: server/custom.py:76
#: server/custom.py:70
msgid ""
"В этом боте включена массовая рассылка в силу настроек, которые оператор "
"указал при создании бота. Ваш идентификатор пользователя может быть записан "
"в базу данных на долгое время"
msgstr ""
"У цьому роботі включено масове розсилання в силу налаштувань, які оператор "
"вказав при створенні робота. Ваш ідентифікатор користувача може бути "
"записаний до бази даних на довгий час"
#: server/custom.py:73
msgid "В этом боте нет массовой рассылки сообщений"
msgstr "У цьому роботі немає масової розсилки повідомлень"
#: server/custom.py:83
msgid "Сообщение от пользователя "
msgstr "Допис від користувача "
#: server/custom.py:135
#: server/custom.py:157
msgid "Вы заблокированы в этом боте"
msgstr "Ви заблоковані у цьому боті"
#: server/custom.py:141
#: server/custom.py:163
msgid "Слишком много сообщений, подождите одну минуту"
msgstr "Забагато повідомлень, зачекайте одну хвилину"
#: server/custom.py:148
#: server/custom.py:170
msgid "Не удаётся связаться с владельцем бота"
msgstr "Не вдається зв'язатися з власником бота"
#: server/custom.py:179
#: server/custom.py:202
msgid ""
"<i>Невозможно переслать сообщение: автор не найден (сообщение слишком "
"старое?)</i>"
@ -693,19 +776,19 @@ msgstr ""
"<i>Неможливо надіслати повідомлення: автора не знайдено (повідомлення "
"занадто старе?)</i>"
#: server/custom.py:187
#: server/custom.py:210
msgid "Пользователь заблокирован"
msgstr "Користувач заблоковано"
#: server/custom.py:192
#: server/custom.py:215
msgid "Пользователь не был забанен"
msgstr "Користувач не був забанений"
#: server/custom.py:195
#: server/custom.py:218
msgid "Пользователь разбанен"
msgstr "Користувач розбанений"
#: server/custom.py:200
#: server/custom.py:223
msgid "<i>Невозможно переслать сообщение (автор заблокировал бота?)</i>"
msgstr "<i>Неможливо надіслати повідомлення (автор заблокував робота?)</i>"
@ -717,11 +800,11 @@ msgstr "(Пере) запустити бота"
msgid "Политика конфиденциальности"
msgstr "Політика конфіденційності"
msgid ""
"\n"
"\n"
"Этот бот создан с помощью @OlgramBot"
msgstr ""
"\n"
"\n"
"Цей бот створено за допомогою @OlgramBot"
#~ msgid ""
#~ "\n"
#~ "\n"
#~ "Этот бот создан с помощью @OlgramBot"
#~ msgstr ""
#~ "\n"
#~ "\n"
#~ "Цей бот створено за допомогою @OlgramBot"

View File

@ -1,8 +1,13 @@
"""
Здесь работа с конкретным ботом
"""
import logging
from aiogram import types
from aiogram.utils.exceptions import TelegramAPIError, Unauthorized
from asyncio import sleep
from datetime import datetime
from olgram.utils.mix import send_stored_message
from aiogram.utils import exceptions
from aiogram import Bot as AioBot
from olgram.models.models import Bot, BotStartMessage, BotSecondMessage
from server.server import unregister_token
@ -15,14 +20,14 @@ async def delete_bot(bot: Bot, call: types.CallbackQuery):
"""
try:
await unregister_token(bot.decrypted_token())
except Unauthorized:
except exceptions.Unauthorized:
# Вероятно пользователь сбросил токен или удалил бот, это уже не наши проблемы
pass
await bot.delete()
await call.answer(_("Бот удалён"))
try:
await call.message.delete()
except TelegramAPIError:
except exceptions.TelegramAPIError:
pass
@ -84,7 +89,7 @@ async def select_chat(bot: Bot, call: types.CallbackQuery, chat: str):
try:
await chat.delete()
await a_bot.leave_chat(chat.chat_id)
except TelegramAPIError:
except exceptions.TelegramAPIError:
pass
await call.answer(_("Бот вышел из чатов"))
await a_bot.session.close()
@ -114,6 +119,11 @@ async def always_second_message(bot: Bot, call: types.CallbackQuery):
await bot.save(update_fields=["enable_always_second_message"])
async def thread_interrupt(bot: Bot, call: types.CallbackQuery):
bot.enable_thread_interrupt = not bot.enable_thread_interrupt
await bot.save(update_fields=["enable_thread_interrupt"])
async def olgram_text(bot: Bot, call: types.CallbackQuery):
if await bot.is_promo():
bot.enable_olgram_text = not bot.enable_olgram_text
@ -123,3 +133,38 @@ async def olgram_text(bot: Bot, call: types.CallbackQuery):
async def antiflood(bot: Bot, call: types.CallbackQuery):
bot.enable_antiflood = not bot.enable_antiflood
await bot.save(update_fields=["enable_antiflood"])
async def mailing(bot: Bot, call: types.CallbackQuery):
bot.enable_mailing = not bot.enable_mailing
await bot.save(update_fields=["enable_mailing"])
async def go_mailing(bot: Bot, context: dict) -> int:
users = await bot.mailing_users
a_bot = AioBot(bot.decrypted_token())
count = 0
for user in users:
bot.last_mailing_at = datetime.now()
await bot.save(update_fields=["last_mailing_at"])
try:
await sleep(0.05)
try:
file_id = await send_stored_message(context, a_bot, user.telegram_id)
except exceptions.RetryAfter as err:
await sleep(err.timeout)
file_id = await send_stored_message(context, a_bot, user.telegram_id)
if file_id:
context["mailing_id"] = file_id
count += 1
except (exceptions.ChatNotFound, exceptions.BotBlocked, exceptions.UserDeactivated):
await user.delete()
except Exception as err:
logging.error("mailing error")
logging.error(err, exc_info=True)
pass
return count

View File

@ -1,11 +1,13 @@
import logging
from io import BytesIO
from olgram.router import dp
from datetime import datetime, timedelta, timezone
from aiogram import types, Bot as AioBot
from olgram.models.models import Bot, User, DefaultAnswer, BotStartMessage, BotSecondMessage
from aiogram.dispatcher import FSMContext
from aiogram.utils.callback_data import CallbackData
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, wrap, send_stored_message
from olgram.commands import bot_actions
from locales.locale import _
@ -127,6 +129,12 @@ async def send_bot_menu(bot: Bot, call: types.CallbackQuery):
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="settings",
chat=empty))
)
if bot.enable_mailing:
keyboard.insert(
types.InlineKeyboardButton(text=_("Рассылка"),
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="go_mailing",
chat=empty))
)
await edit_or_create(call, dedent(_("""
Управление ботом @{0}.
@ -178,6 +186,17 @@ async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery):
operation="always_second_message",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Рассылка"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="mailing",
chat=empty))
)
keyboard.insert(
types.InlineKeyboardButton(text=_("Прерывать поток"),
callback_data=menu_callback.new(level=3, bot_id=bot.id,
operation="thread_interrupt",
chat=empty))
)
is_promo = await bot.is_promo()
if is_promo:
keyboard.insert(
@ -196,12 +215,17 @@ async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery):
info_turn = _("включены") if bot.enable_additional_info else _("выключены")
antiflood_turn = _("включен") if bot.enable_antiflood else _("выключен")
enable_always_second_message = _("включён") if bot.enable_always_second_message else _("выключен")
thread_interrupt = _("да") if bot.enable_thread_interrupt else _("нет")
mailing_turn = _("включена") if bot.enable_mailing 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>
<a href="https://olgram.readthedocs.io/ru/latest/options.html#always_second_message">Автоответчик всегда</a>: <b>{3}</b>
""")).format(thread_turn, info_turn, antiflood_turn, enable_always_second_message)
<a href="https://olgram.readthedocs.io/ru/latest/options.html#thread_interrupt">Прерывать поток</a>: <b>{4}</b>
<a href="https://olgram.readthedocs.io/ru/latest/options.html#mailing">Рассылка</a>: <b>{5}</b>
""")).format(thread_turn, info_turn, antiflood_turn, enable_always_second_message, thread_interrupt,
mailing_turn)
if is_promo:
olgram_turn = _("включена") if bot.enable_olgram_text else _("выключена")
@ -280,6 +304,83 @@ 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")
async def send_bot_mailing_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=1)
keyboard.insert(
types.InlineKeyboardButton(text=_("<< Отменить рассылку"),
callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty))
)
text = dedent(_("""
Напишите сообщение, которое нужно разослать всем подписчикам вашего бота @{0}.
У сообщения будет до {1} получателей.
Учтите, что
1. Рассылается только одно сообщение за раз (в т.ч. только одна картинка)
2. Когда рассылка запущена, её нельзя отменить
"""))
text = text.format(bot.name, len(await bot.mailing_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")
@dp.message_handler(state="wait_mailing_text",
content_types=[types.ContentType.TEXT,
types.ContentType.LOCATION,
types.ContentType.DOCUMENT,
types.ContentType.PHOTO,
types.ContentType.AUDIO,
types.ContentType.VIDEO]) # TODO: not command
async def mailing_text_received(message: types.Message, state: FSMContext):
async with state.proxy() as proxy:
bot_id = proxy["bot_id"]
proxy["mailing_content_type"] = message.content_type
buffer = BytesIO()
if message.content_type == types.ContentType.TEXT:
proxy["mailing_text"] = message.html_text
elif message.content_type == types.ContentType.LOCATION:
proxy["mailing_location"] = message.location
elif message.content_type in (types.ContentType.PHOTO, types.ContentType.DOCUMENT, types.ContentType.AUDIO,
types.ContentType.VIDEO):
proxy["mailing_caption"] = message.caption
if message.content_type == types.ContentType.PHOTO:
obj = message.photo[-1]
elif message.content_type == types.ContentType.DOCUMENT:
obj = message.document
elif message.content_type == types.ContentType.AUDIO:
obj = message.audio
elif message.content_type == types.ContentType.VIDEO:
obj = message.video
try:
await obj.download(buffer, timeout=5)
except Exception as err:
logging.error("Error downloading file")
logging.error(err, exc_info=True)
return await message.answer(_("Не удалось загрузить файл (слишком большой размер?)"))
proxy["mailing_data"] = buffer.getvalue()
proxy["mailing_file_name"] = getattr(obj, "file_name")
_message_id = await send_stored_message(proxy, AioBot.get_current(), message.chat.id)
keyboard = types.InlineKeyboardMarkup(row_width=1)
keyboard.insert(
types.InlineKeyboardButton(text=_("Да, начать рассылку"),
callback_data=menu_callback.new(level=3, bot_id=bot_id, operation="go_go_mailing",
chat=empty))
)
await AioBot.get_current().send_message(message.chat.id, reply_to_message_id=_message_id,
text="Вы уверены, что хотите разослать это сообщение всем пользователям?",
reply_markup=keyboard)
async def send_bot_statistic_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None,
chat_id: ty.Optional[int] = None):
if call:
@ -511,6 +612,15 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
return await send_bot_statistic_menu(bot, call)
if operation == "settings":
return await send_bot_settings_menu(bot, call)
if operation == "go_mailing":
if bot.last_mailing_at and bot.last_mailing_at >= datetime.now(tz=timezone.utc) - timedelta(minutes=5):
return await call.answer(_("Рассылка была совсем недавно, подождите немного"), show_alert=True)
if not await bot.mailing_users:
return await call.answer(_("Нет пользователей для рассылки"))
await state.set_state("wait_mailing_text")
async with state.proxy() as proxy:
proxy["bot_id"] = bot.id
return await send_bot_mailing_menu(bot, call)
if operation == "text":
await state.set_state("wait_start_text")
async with state.proxy() as proxy:
@ -520,6 +630,25 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
if level == "3":
if operation == "delete_yes":
return await bot_actions.delete_bot(bot, call)
if operation == "mailing":
await bot_actions.mailing(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "go_go_mailing":
if (await state.get_state()) == "wait_mailing_text":
async with state.proxy() as proxy:
mailing_data = dict(proxy)
await state.reset_state()
if bot.last_mailing_at and bot.last_mailing_at >= datetime.now(tz=timezone.utc) - timedelta(minutes=5):
return await call.answer(_("Рассылка была совсем недавно, подождите немного"), show_alert=True)
if not await bot.mailing_users:
return await call.answer(_("Нет пользователей для рассылки"))
await call.answer(_("Рассылка запущена"))
count = await bot_actions.go_mailing(bot, mailing_data)
return await call.message.answer(_("Рассылка завершена, отправлено {0} сообщений").format(count))
else:
return await call.answer(_("Устарело, создайте новую рассылку"))
if operation == "chat":
return await bot_actions.select_chat(bot, call, callback_data.get("chat"))
if operation == "threads":
@ -534,6 +663,12 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
if operation == "always_second_message":
await bot_actions.always_second_message(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "mailing":
await bot_actions.mailing(bot, call)
return await send_bot_settings_menu(bot, call)
if operation == "thread_interrupt":
await bot_actions.thread_interrupt(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)

View File

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

View File

@ -0,0 +1,6 @@
-- upgrade --
ALTER TABLE "bot" ADD "enable_mailing" BOOL NOT NULL DEFAULT False;
ALTER TABLE "bot" ADD "last_mailing_at" TIMESTAMPTZ;
-- downgrade --
ALTER TABLE "bot" DROP COLUMN "enable_mailing";
ALTER TABLE "bot" DROP COLUMN "last_mailing_at";

View File

@ -0,0 +1,10 @@
-- upgrade --
CREATE TABLE IF NOT EXISTS "mailinguser" (
"id" BIGSERIAL NOT NULL PRIMARY KEY,
"telegram_id" BIGINT NOT NULL,
"bot_id" INT NOT NULL REFERENCES "bot" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_mailinguser_bot_id_906a76" UNIQUE ("bot_id", "telegram_id")
);
CREATE INDEX IF NOT EXISTS "idx_mailinguser_telegra_55de60" ON "mailinguser" ("telegram_id");
-- downgrade --
DROP TABLE IF EXISTS "mailinguser";

View File

@ -47,6 +47,9 @@ class Bot(Model):
enable_olgram_text = fields.BooleanField(default=True)
enable_antiflood = fields.BooleanField(default=False)
enable_always_second_message = fields.BooleanField(default=False)
enable_thread_interrupt = fields.BooleanField(default=True)
enable_mailing = fields.BooleanField(default=False)
last_mailing_at = fields.DatetimeField(null=True, default=None)
def decrypted_token(self):
cryptor = DatabaseSettings.cryptor()
@ -105,6 +108,17 @@ class User(Model):
table = 'user'
class MailingUser(Model):
id = fields.BigIntField(pk=True)
telegram_id = fields.BigIntField(index=True)
bot = fields.ForeignKeyField("models.Bot", related_name="mailing_users", on_delete=fields.relational.CASCADE)
class Meta:
table = 'mailinguser'
unique_together = (("bot", "telegram_id"), )
class GroupChat(Model):
id = fields.IntField(pk=True)
chat_id = fields.BigIntField(index=True, unique=True)

View File

@ -7,7 +7,6 @@ from datetime import timedelta
import typing as ty
from olgram.utils.crypto import Cryptor
load_dotenv()
@ -41,7 +40,7 @@ class OlgramSettings(AbstractSettings):
@classmethod
def version(cls):
return "0.7.0"
return "0.7.1"
@classmethod
@lru_cache
@ -161,4 +160,6 @@ TORTOISE_ORM = {
"default_connection": "default",
},
},
"use_tz": False,
"timezone": "UTC"
}

View File

@ -1,5 +1,8 @@
import logging
from io import BytesIO
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup
from aiogram.utils.exceptions import TelegramAPIError
from aiogram import types, Bot as AioBot
from typing import Optional
@ -30,3 +33,31 @@ def wrap(data: str, max_len: int) -> str:
def button_text_limit(data: str) -> str:
return wrap(data, 30)
async def send_stored_message(storage: dict, bot: AioBot, chat_id: int):
content_type = storage["mailing_content_type"]
if content_type == types.ContentType.TEXT:
return await bot.send_message(chat_id, storage["mailing_text"], parse_mode="HTML")
if content_type == types.ContentType.LOCATION:
return await bot.send_location(chat_id, storage["mailing_location"][0], storage["mailing_location"][1])
if content_type in (types.ContentType.AUDIO, types.ContentType.VIDEO, types.ContentType.DOCUMENT,
types.ContentType.PHOTO):
caption = storage.get("mailing_caption")
if storage.get("mailing_id"):
logging.info("Mailing use file id")
obj = storage["mailing_id"]
else:
logging.info("Mailing upload file")
obj = types.InputFile(BytesIO(storage["mailing_data"]), filename=storage.get("mailing_file_name"))
if content_type == types.ContentType.AUDIO:
return (await bot.send_audio(chat_id, audio=obj, caption=caption)).audio.file_id
if content_type == types.ContentType.PHOTO:
return (await bot.send_photo(chat_id, photo=obj, caption=caption)).photo[-1].file_id
if content_type == types.ContentType.VIDEO:
return (await bot.send_video(chat_id, video=obj, caption=caption)).video.file_id
if content_type == types.ContentType.DOCUMENT:
return (await bot.send_document(chat_id, document=obj, caption=caption)).document.file_id
raise NotImplementedError("Mailing, unknown content type")

1378
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,3 +1,5 @@
import asyncio
from aiogram import Bot as AioBot, Dispatcher
from aiogram.dispatcher.webhook import WebhookRequestHandler
from aiogram.dispatcher.webhook import SendMessage
@ -11,7 +13,7 @@ from tortoise.expressions import F
import logging
import typing as ty
from olgram.settings import ServerSettings
from olgram.models.models import Bot, GroupChat, BannedUser, BotStartMessage, BotSecondMessage
from olgram.models.models import Bot, GroupChat, BannedUser, BotStartMessage, BotSecondMessage, MailingUser
from locales.locale import _, translators
from server.inlines import inline_handler
@ -55,15 +57,20 @@ def _on_security_policy(message: types.Message, bot):
text = _("<b>Политика конфиденциальности</b>\n\n"
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При отправке сообщения (кроме команд "
"/start и /security_policy) ваш идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется только для общения с оператором; боты Olgram "
"не делают массовых рассылок.\n\n")
"удаляется из кеша. Этот идентификатор используется для общения с оператором.\n\n")
if bot.enable_additional_info:
text += _("При отправке сообщения (кроме команд /start и /security_policy) оператор <b>видит</b> ваши имя "
"пользователя, @username и идентификатор пользователя в силу настроек, которые оператор указал при "
"создании бота.")
"создании бота.\n\n")
else:
text += _("В зависимости от ваших настроек конфиденциальности Telegram, оператор может видеть ваш username, "
"имя пользователя и другую информацию.")
"имя пользователя и другую информацию.\n\n")
if bot.enable_mailing:
text += _("В этом боте включена массовая рассылка в силу настроек, которые оператор указал при создании бота. "
"Ваш идентификатор пользователя может быть записан в базу данных на долгое время")
else:
text += _("В этом боте нет массовой рассылки сообщений")
return SendMessage(chat_id=message.chat.id,
text=text,
@ -106,31 +113,43 @@ async def send_user_message(message: types.Message, super_chat_id: int, bot):
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:
if bot.enable_thread_interrupt:
thread_timeout = ServerSettings.thread_timeout_ms()
else:
thread_timeout = ServerSettings.redis_timeout_ms()
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())
pexpire=thread_timeout)
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())
await _redis.set(
_thread_uniqie_id(bot.pk, message.chat.id), new_message.message_id, pexpire=thread_timeout)
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())
pexpire=thread_timeout)
else: # личные сообщения не поддерживают потоки сообщений: просто отправляем сообщение
await send_user_message(message, super_chat_id, bot)
async def _increase_count(_bot):
_bot.incoming_messages_count = F("incoming_messages_count") + 1
await _bot.save(update_fields=["incoming_messages_count"])
async def handle_user_message(message: types.Message, super_chat_id: int, bot):
"""Обычный пользователь прислал сообщение в бот, нужно переслать его операторам"""
_ = _get_translator(message)
is_super_group = super_chat_id < 0
if bot.enable_mailing:
asyncio.create_task(MailingUser.get_or_create(telegram_id=message.chat.id, bot=bot))
# Проверить, не забанен ли пользователь
banned = await bot.banned_users.filter(telegram_id=message.chat.id)
if banned:
@ -153,8 +172,7 @@ async def handle_user_message(message: types.Message, super_chat_id: int, bot):
_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"])
asyncio.create_task(_increase_count(bot))
# И отправить пользователю специальный текст, если он указан и если давно не отправляли
if bot.second_text:
@ -211,6 +229,8 @@ async def handle_operator_message(message: types.Message, super_chat_id: int, bo
elif super_chat_id > 0:
# в супер-чате кто-то пишет сообщение сам себе, только для личных сообщений
if bot.enable_mailing:
asyncio.create_task(MailingUser.get_or_create(telegram_id=message.chat.id, bot=bot))
await message.forward(super_chat_id)
# И отправить пользователю специальный текст, если он указан
if bot.second_text: