mirror of
https://github.com/civsocit/olgram.git
synced 2025-12-17 13:36:17 +00:00
Compare commits
9 Commits
mailing
...
acb62fb644
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acb62fb644 | ||
|
|
ae45374490 | ||
|
|
0881c86349 | ||
|
|
82b68d1d9f | ||
|
|
a5e6fbce34 | ||
|
|
28ed36ffeb | ||
|
|
601c16622d | ||
|
|
9e46041d0f | ||
|
|
f41e17a15c |
25
.readthedocs.yaml
Normal file
25
.readthedocs.yaml
Normal 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
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
О проекте
|
О проекте
|
||||||
===================================
|
===================================
|
||||||
|
|
||||||
|
|
||||||
Зачем нужен Olgram
|
Зачем нужен Olgram
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,25 @@
|
|||||||
Донаты
|
Донаты
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
На рекламу проекта, аренду сервера и пиццу
|
На аренду сервера для этого проекта
|
||||||
|
|
||||||
Bitcoin:
|
Bitcoin:
|
||||||
``bc1qlq7cm5chc8flr3fy8ewk967aknq3dwmxtwn9hl``
|
``bc1qlq7cm5chc8flr3fy8ewk967aknq3dwmxtwn9hl``
|
||||||
|
|
||||||
Monero:
|
Litecoin:
|
||||||
``886AQ8tCVcQKp21xsuSLkfDdTAdtCFH1jR58Tw9MsaxFXoZ7YRHXx1cQcUfUnDX6hySzPsQEVt6RWPn3sXH9QUmwCr3oVqB``
|
``LTC1QXAJSVZ0LW44AA5NYTUCH8CP2G8X7A4CDASE4Y7``
|
||||||
|
|
||||||
Dash:
|
Как убрать "Этот бот создан с помощью ..."
|
||||||
``XqxetfWzr5n4Ms1TxMbdEEeHGe8CaMdmb6``
|
----------------
|
||||||
|
Напишите нам на `@civsocit_feedback_bot <https://t.me/civsocit_feedback_bot>`_.
|
||||||
|
|
||||||
|
|
||||||
История изменений
|
История изменений
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
- `2024-03-01` Непрерывные потоки сообщений (опция)
|
||||||
|
- `2024-02-17` Опция смены режима работы автоответчика: автоответчик отвечает на КАЖДОЕ сообщение
|
||||||
|
- `2024-01-12` Мультиязычность (стартовое сообщение и автоответчик)
|
||||||
- `2022-08-01` Защита от флуда
|
- `2022-08-01` Защита от флуда
|
||||||
- `2022-07-23` Автоответчик не пишет сообщение лишний раз
|
- `2022-07-23` Автоответчик не пишет сообщение лишний раз
|
||||||
- `2022-07-04` Поддержка двух ботов в одном чате
|
- `2022-07-04` Поддержка двух ботов в одном чате
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# -- Project information
|
# -- Project information
|
||||||
|
|
||||||
project = 'Olgram'
|
project = 'Olgram'
|
||||||
copyright = '2022, Civsocit'
|
copyright = '2024, Civsocit'
|
||||||
author = 'civsocit'
|
author = 'civsocit'
|
||||||
|
|
||||||
release = '0.1'
|
release = '0.1'
|
||||||
|
|||||||
@@ -50,15 +50,20 @@ Olgram пересылает сообщения так, чтобы сообщен
|
|||||||
При включении этой опции пользователю запрещается отправлять больше одного сообщения в минуту. Используйте её, если
|
При включении этой опции пользователю запрещается отправлять больше одного сообщения в минуту. Используйте её, если
|
||||||
не успеваете обрабатывать входящие сообщения.
|
не успеваете обрабатывать входящие сообщения.
|
||||||
|
|
||||||
.. _mailing:
|
|
||||||
|
|
||||||
Рассылка
|
.. _always_second_message:
|
||||||
---------------
|
|
||||||
|
|
||||||
После включения этой опции ваш бот будет запоминать всех пользователей, которые пишут в ваш бот.
|
Использовать автоответчик всегда
|
||||||
Вы сможете запустить рассылку по этим пользователям.
|
--------------------------------
|
||||||
|
|
||||||
.. note::
|
По-умолчанию автоответчик отвечает только на первое сообщение в диалоге с пользователем. Чтобы автоответчик отвечал на
|
||||||
|
КАЖДОЕ входящее сообщение, включите эту опцию.
|
||||||
|
|
||||||
Включение этой опции меняет текст политики конфиденциальности вашего feedback бота (команда /security_policy)
|
|
||||||
и может отпугнуть некоторых пользователей. Не включайте эту опцию без необходимости.
|
.. thread_interrupt:
|
||||||
|
|
||||||
|
Прерывать поток
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
По-умолчанию поток сообщений от одного пользователя прерывается каждые 24 часа. Без этой опции поток сообщений не
|
||||||
|
прерывается никогда.
|
||||||
|
|||||||
@@ -68,6 +68,13 @@ BotFather - это официальный бот Telegram, создающий д
|
|||||||
|
|
||||||
Теперь просто отправьте новый текст приветствия.
|
Теперь просто отправьте новый текст приветствия.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Чтобы настроить особый текст приветствия для, например, русскоязычных пользователей (т.е. тех пользователей, у
|
||||||
|
которых в настройках Telegram выставлена русская локализация), нажмите кнопку "Руссикй 🇷🇺" и только потом отправьте
|
||||||
|
текст приветствия. Чтобы отредактировать текст приветствия для всех остальных языков, нажмите "[все языки]".
|
||||||
|
|
||||||
|
|
||||||
Как привязать бота к групповому чату
|
Как привязать бота к групповому чату
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ msgid ""
|
|||||||
"отключено.\n"
|
"отключено.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Текущий текст:\n"
|
" Текущий текст:\n"
|
||||||
" <pre>\n"
|
" <pre>"
|
||||||
" {1}\n"
|
" {1}\n"
|
||||||
" </pre>\n"
|
" </pre>\n"
|
||||||
" Отправьте сообщение, чтобы изменить текст.\n"
|
" Отправьте сообщение, чтобы изменить текст.\n"
|
||||||
@@ -711,11 +711,11 @@ msgstr "(Re)launch the bot"
|
|||||||
msgid "Политика конфиденциальности"
|
msgid "Политика конфиденциальности"
|
||||||
msgstr "Privacy Policy"
|
msgstr "Privacy Policy"
|
||||||
|
|
||||||
#~ msgid ""
|
msgid ""
|
||||||
#~ "\n"
|
"\n"
|
||||||
#~ "\n"
|
"\n"
|
||||||
#~ "Этот бот создан с помощью @OlgramBot"
|
"Этот бот создан с помощью @OlgramBot"
|
||||||
#~ msgstr ""
|
msgstr ""
|
||||||
#~ "\n"
|
"\n"
|
||||||
#~ "\n"
|
"\n"
|
||||||
#~ "This bot was created using @OlgramBot"
|
"This bot was created using @OlgramBot"
|
||||||
|
|||||||
@@ -407,9 +407,7 @@ msgid ""
|
|||||||
" команду /start\n"
|
" команду /start\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Текущий текст:\n"
|
" Текущий текст:\n"
|
||||||
" <pre>\n"
|
" <pre>{1}</pre>\n"
|
||||||
" {1}\n"
|
|
||||||
" </pre>\n"
|
|
||||||
" Отправьте сообщение, чтобы изменить текст.\n"
|
" Отправьте сообщение, чтобы изменить текст.\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -419,9 +417,7 @@ msgstr ""
|
|||||||
" команду /start\n"
|
" команду /start\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Поточний текст:\n"
|
" Поточний текст:\n"
|
||||||
" <pre>\n"
|
" <pre>{1}</pre>\n"
|
||||||
" {1}\n"
|
|
||||||
" </pre>\n"
|
|
||||||
" Надішліть повідомлення, щоб змінити текст.\n"
|
" Надішліть повідомлення, щоб змінити текст.\n"
|
||||||
" \n"
|
" \n"
|
||||||
" "
|
" "
|
||||||
@@ -462,9 +458,7 @@ msgid ""
|
|||||||
"отключено.\n"
|
"отключено.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Текущий текст:\n"
|
" Текущий текст:\n"
|
||||||
" <pre>\n"
|
" <pre>{1}</pre>\n"
|
||||||
" {1}\n"
|
|
||||||
" </pre>\n"
|
|
||||||
" Отправьте сообщение, чтобы изменить текст.\n"
|
" Отправьте сообщение, чтобы изменить текст.\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -723,11 +717,11 @@ msgstr "(Пере) запустити бота"
|
|||||||
msgid "Политика конфиденциальности"
|
msgid "Политика конфиденциальности"
|
||||||
msgstr "Політика конфіденційності"
|
msgstr "Політика конфіденційності"
|
||||||
|
|
||||||
#~ msgid ""
|
msgid ""
|
||||||
#~ "\n"
|
"\n"
|
||||||
#~ "\n"
|
"\n"
|
||||||
#~ "Этот бот создан с помощью @OlgramBot"
|
"Этот бот создан с помощью @OlgramBot"
|
||||||
#~ msgstr ""
|
msgstr ""
|
||||||
#~ "\n"
|
"\n"
|
||||||
#~ "\n"
|
"\n"
|
||||||
#~ "Цей бот створено за допомогою @OlgramBot"
|
"Цей бот створено за допомогою @OlgramBot"
|
||||||
|
|||||||
@@ -306,9 +306,7 @@ msgid ""
|
|||||||
" команду /start\n"
|
" команду /start\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Текущий текст:\n"
|
" Текущий текст:\n"
|
||||||
" <pre>\n"
|
" <pre>{1}</pre>\n"
|
||||||
" {1}\n"
|
|
||||||
" </pre>\n"
|
|
||||||
" Отправьте сообщение, чтобы изменить текст.\n"
|
" Отправьте сообщение, чтобы изменить текст.\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -317,9 +315,7 @@ msgstr ""
|
|||||||
" /start\n"
|
" /start\n"
|
||||||
"\n"
|
"\n"
|
||||||
" 目前的文本。\n"
|
" 目前的文本。\n"
|
||||||
" <pre>\n"
|
" <pre>{1}</pre>\n"
|
||||||
" {1}\n"
|
|
||||||
" </pre>\n"
|
|
||||||
" 发送消息,改变文本。\n"
|
" 发送消息,改变文本。\n"
|
||||||
" "
|
" "
|
||||||
|
|
||||||
@@ -359,9 +355,7 @@ msgid ""
|
|||||||
"отключено.\n"
|
"отключено.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Текущий текст:\n"
|
" Текущий текст:\n"
|
||||||
" <pre>\n"
|
" <pre>{1}</pre>\n"
|
||||||
" {1}\n"
|
|
||||||
" </pre>\n"
|
|
||||||
" Отправьте сообщение, чтобы изменить текст.\n"
|
" Отправьте сообщение, чтобы изменить текст.\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -370,9 +364,7 @@ msgstr ""
|
|||||||
"默认情况下,它是禁用的。\n"
|
"默认情况下,它是禁用的。\n"
|
||||||
"\n"
|
"\n"
|
||||||
" 目前的文本。\n"
|
" 目前的文本。\n"
|
||||||
" <pre>\n"
|
" <pre>{1}</pre>。\n"
|
||||||
" {1}\n"
|
|
||||||
" </pre>。\n"
|
|
||||||
" 发送消息,改变文本。\n"
|
" 发送消息,改变文本。\n"
|
||||||
" "
|
" "
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
Здесь работа с конкретным ботом
|
Здесь работа с конкретным ботом
|
||||||
"""
|
"""
|
||||||
from asyncio import sleep
|
|
||||||
from datetime import datetime
|
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
from aiogram.utils import exceptions
|
from aiogram.utils.exceptions import TelegramAPIError, Unauthorized
|
||||||
from aiogram import Bot as AioBot
|
from aiogram import Bot as AioBot
|
||||||
from olgram.models.models import Bot
|
from olgram.models.models import Bot, BotStartMessage, BotSecondMessage
|
||||||
from olgram.utils.mix import send_stored_message
|
|
||||||
from server.server import unregister_token
|
from server.server import unregister_token
|
||||||
from locales.locale import _
|
from locales.locale import _
|
||||||
|
|
||||||
@@ -18,38 +15,50 @@ async def delete_bot(bot: Bot, call: types.CallbackQuery):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await unregister_token(bot.decrypted_token())
|
await unregister_token(bot.decrypted_token())
|
||||||
except exceptions.Unauthorized:
|
except Unauthorized:
|
||||||
# Вероятно пользователь сбросил токен или удалил бот, это уже не наши проблемы
|
# Вероятно пользователь сбросил токен или удалил бот, это уже не наши проблемы
|
||||||
pass
|
pass
|
||||||
await bot.delete()
|
await bot.delete()
|
||||||
await call.answer(_("Бот удалён"))
|
await call.answer(_("Бот удалён"))
|
||||||
try:
|
try:
|
||||||
await call.message.delete()
|
await call.message.delete()
|
||||||
except exceptions.TelegramAPIError:
|
except TelegramAPIError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def reset_bot_text(bot: Bot, call: types.CallbackQuery):
|
async def reset_bot_text(bot: Bot, call: types.CallbackQuery, state):
|
||||||
"""
|
"""
|
||||||
Пользователь решил сбросить текст бота к default
|
Пользователь решил сбросить текст бота к default
|
||||||
:param bot:
|
:param bot:
|
||||||
:param call:
|
:param call:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
bot.start_text = bot._meta.fields_map['start_text'].default
|
async with state.proxy() as proxy:
|
||||||
await bot.save()
|
lang = proxy.get("lang", "none")
|
||||||
|
if lang == "none":
|
||||||
|
await BotStartMessage.filter(bot=bot).delete()
|
||||||
|
bot.start_text = bot._meta.fields_map['start_text'].default
|
||||||
|
await bot.save(update_fields=["start_text"])
|
||||||
|
else:
|
||||||
|
await BotStartMessage.filter(bot=bot, locale=lang).delete()
|
||||||
await call.answer(_("Текст сброшен"))
|
await call.answer(_("Текст сброшен"))
|
||||||
|
|
||||||
|
|
||||||
async def reset_bot_second_text(bot: Bot, call: types.CallbackQuery):
|
async def reset_bot_second_text(bot: Bot, call: types.CallbackQuery, state):
|
||||||
"""
|
"""
|
||||||
Пользователь решил сбросить second text бота
|
Пользователь решил сбросить second text бота
|
||||||
:param bot:
|
:param bot:
|
||||||
:param call:
|
:param call:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
bot.second_text = bot._meta.fields_map['second_text'].default
|
async with state.proxy() as proxy:
|
||||||
await bot.save()
|
lang = proxy.get("lang", "none")
|
||||||
|
if lang == "none":
|
||||||
|
await BotSecondMessage.filter(bot=bot).delete()
|
||||||
|
bot.second_text = bot._meta.fields_map['second_text'].default
|
||||||
|
await bot.save(update_fields=["second_text"])
|
||||||
|
else:
|
||||||
|
await BotSecondMessage.filter(bot=bot, locale=lang).delete()
|
||||||
await call.answer(_("Текст сброшен"))
|
await call.answer(_("Текст сброшен"))
|
||||||
|
|
||||||
|
|
||||||
@@ -75,7 +84,7 @@ async def select_chat(bot: Bot, call: types.CallbackQuery, chat: str):
|
|||||||
try:
|
try:
|
||||||
await chat.delete()
|
await chat.delete()
|
||||||
await a_bot.leave_chat(chat.chat_id)
|
await a_bot.leave_chat(chat.chat_id)
|
||||||
except exceptions.TelegramAPIError:
|
except TelegramAPIError:
|
||||||
pass
|
pass
|
||||||
await call.answer(_("Бот вышел из чатов"))
|
await call.answer(_("Бот вышел из чатов"))
|
||||||
await a_bot.session.close()
|
await a_bot.session.close()
|
||||||
@@ -100,6 +109,16 @@ async def additional_info(bot: Bot, call: types.CallbackQuery):
|
|||||||
await bot.save(update_fields=["enable_additional_info"])
|
await bot.save(update_fields=["enable_additional_info"])
|
||||||
|
|
||||||
|
|
||||||
|
async def always_second_message(bot: Bot, call: types.CallbackQuery):
|
||||||
|
bot.enable_always_second_message = not bot.enable_always_second_message
|
||||||
|
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):
|
async def olgram_text(bot: Bot, call: types.CallbackQuery):
|
||||||
if await bot.is_promo():
|
if await bot.is_promo():
|
||||||
bot.enable_olgram_text = not bot.enable_olgram_text
|
bot.enable_olgram_text = not bot.enable_olgram_text
|
||||||
@@ -109,35 +128,3 @@ async def olgram_text(bot: Bot, call: types.CallbackQuery):
|
|||||||
async def antiflood(bot: Bot, call: types.CallbackQuery):
|
async def antiflood(bot: Bot, call: types.CallbackQuery):
|
||||||
bot.enable_antiflood = not bot.enable_antiflood
|
bot.enable_antiflood = not bot.enable_antiflood
|
||||||
await bot.save(update_fields=["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) -> int:
|
|
||||||
users = await bot.mailing_users
|
|
||||||
a_bot = AioBot(bot.decrypted_token())
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
|
|
||||||
print(f"start mailing {context}")
|
|
||||||
|
|
||||||
for user in users:
|
|
||||||
bot.last_mailing_at = datetime.now()
|
|
||||||
await bot.save(update_fields=["last_mailing_at"])
|
|
||||||
try:
|
|
||||||
await sleep(0.05)
|
|
||||||
try:
|
|
||||||
await send_stored_message(context, a_bot, user.telegram_id)
|
|
||||||
except exceptions.RetryAfter as err:
|
|
||||||
await sleep(err.timeout)
|
|
||||||
await send_stored_message(context, a_bot, user.telegram_id)
|
|
||||||
count += 1
|
|
||||||
except (exceptions.ChatNotFound, exceptions.BotBlocked, exceptions.UserDeactivated):
|
|
||||||
await user.delete()
|
|
||||||
except exceptions.TelegramAPIError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return count
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
from olgram.router import dp
|
from olgram.router import dp
|
||||||
|
|
||||||
from aiogram import types, Bot as AioBot
|
from aiogram import types, Bot as AioBot
|
||||||
from olgram.models.models import Bot, User, DefaultAnswer
|
from olgram.models.models import Bot, User, DefaultAnswer, BotStartMessage, BotSecondMessage
|
||||||
from aiogram.dispatcher import FSMContext
|
from aiogram.dispatcher import FSMContext
|
||||||
from aiogram.utils.callback_data import CallbackData
|
from aiogram.utils.callback_data import CallbackData
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from olgram.utils.mix import edit_or_create, button_text_limit, wrap, send_stored_message
|
from olgram.utils.mix import edit_or_create, button_text_limit, wrap
|
||||||
from olgram.commands import bot_actions
|
from olgram.commands import bot_actions
|
||||||
from locales.locale import _
|
from locales.locale import _
|
||||||
|
|
||||||
import typing as ty
|
import typing as ty
|
||||||
|
|
||||||
|
|
||||||
menu_callback = CallbackData('menu', 'level', 'bot_id', 'operation', 'chat')
|
menu_callback = CallbackData('menu', 'level', 'bot_id', 'operation', 'chat')
|
||||||
|
|
||||||
empty = "0"
|
empty = "0"
|
||||||
@@ -127,12 +127,6 @@ async def send_bot_menu(bot: Bot, call: types.CallbackQuery):
|
|||||||
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="settings",
|
callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="settings",
|
||||||
chat=empty))
|
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(_("""
|
await edit_or_create(call, dedent(_("""
|
||||||
Управление ботом @{0}.
|
Управление ботом @{0}.
|
||||||
@@ -179,8 +173,15 @@ async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery):
|
|||||||
chat=empty))
|
chat=empty))
|
||||||
)
|
)
|
||||||
keyboard.insert(
|
keyboard.insert(
|
||||||
types.InlineKeyboardButton(text=_("Рассылка"),
|
types.InlineKeyboardButton(text=_("Автоответчик всегда"),
|
||||||
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="mailing",
|
callback_data=menu_callback.new(level=3, bot_id=bot.id,
|
||||||
|
operation="always_second_message",
|
||||||
|
chat=empty))
|
||||||
|
)
|
||||||
|
keyboard.insert(
|
||||||
|
types.InlineKeyboardButton(text=_("Прерывать поток"),
|
||||||
|
callback_data=menu_callback.new(level=3, bot_id=bot.id,
|
||||||
|
operation="thread_interrupt",
|
||||||
chat=empty))
|
chat=empty))
|
||||||
)
|
)
|
||||||
is_promo = await bot.is_promo()
|
is_promo = await bot.is_promo()
|
||||||
@@ -200,13 +201,16 @@ async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery):
|
|||||||
thread_turn = _("включены") if bot.enable_threads else _("выключены")
|
thread_turn = _("включены") if bot.enable_threads else _("выключены")
|
||||||
info_turn = _("включены") if bot.enable_additional_info else _("выключены")
|
info_turn = _("включены") if bot.enable_additional_info else _("выключены")
|
||||||
antiflood_turn = _("включен") if bot.enable_antiflood else _("выключен")
|
antiflood_turn = _("включен") if bot.enable_antiflood else _("выключен")
|
||||||
mailing_turn = _("включена") if bot.enable_mailing else _("выключена")
|
enable_always_second_message = _("включён") if bot.enable_always_second_message else _("выключен")
|
||||||
|
thread_interrupt = _("да") if bot.enable_thread_interrupt else _("нет")
|
||||||
text = dedent(_("""
|
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#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#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#antiflood">Антифлуд</a>: <b>{2}</b>
|
||||||
<a href="https://olgram.readthedocs.io/ru/latest/options.html#mailing">Рассылка</a>: <b>{3}</b>
|
<a href="https://olgram.readthedocs.io/ru/latest/options.html#always_second_message">Автоответчик всегда</a>: <b>{3}
|
||||||
""")).format(thread_turn, info_turn, antiflood_turn, mailing_turn)
|
<a href="https://olgram.readthedocs.io/ru/latest/options.html#thread_interrupt">Прерывать поток</a>: <b>{4}
|
||||||
|
</b>
|
||||||
|
""")).format(thread_turn, info_turn, antiflood_turn, enable_always_second_message, thread_interrupt)
|
||||||
|
|
||||||
if is_promo:
|
if is_promo:
|
||||||
olgram_turn = _("включена") if bot.enable_olgram_text else _("выключена")
|
olgram_turn = _("включена") if bot.enable_olgram_text else _("выключена")
|
||||||
@@ -215,9 +219,26 @@ async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery):
|
|||||||
await edit_or_create(call, text, reply_markup=keyboard, parse_mode="HTML")
|
await edit_or_create(call, text, reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None, chat_id: ty.Optional[int] = None):
|
languages = {
|
||||||
|
"en": "English 🇺🇸",
|
||||||
|
"ru": "Русский 🇷🇺",
|
||||||
|
"uk": "Український 🇺🇦",
|
||||||
|
"tr": "Türkçe 🇹🇷",
|
||||||
|
"hy": "հայերեն 🇦🇲",
|
||||||
|
"ka": "ქართული ენა 🇬🇪"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None, chat_id: ty.Optional[int] = None,
|
||||||
|
state=None):
|
||||||
if call:
|
if call:
|
||||||
await call.answer()
|
await call.answer()
|
||||||
|
|
||||||
|
async with state.proxy() as proxy:
|
||||||
|
lang = proxy.get("lang", "none")
|
||||||
|
|
||||||
|
prepared_languages = {ln.locale: ln.text for ln in await bot.start_texts}
|
||||||
|
|
||||||
keyboard = types.InlineKeyboardMarkup(row_width=2)
|
keyboard = types.InlineKeyboardMarkup(row_width=2)
|
||||||
keyboard.insert(
|
keyboard.insert(
|
||||||
types.InlineKeyboardButton(text=_("<< Завершить редактирование"),
|
types.InlineKeyboardButton(text=_("<< Завершить редактирование"),
|
||||||
@@ -228,47 +249,40 @@ async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] =
|
|||||||
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="next_text",
|
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="next_text",
|
||||||
chat=empty))
|
chat=empty))
|
||||||
)
|
)
|
||||||
keyboard.insert(
|
keyboard.row(
|
||||||
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=3, bot_id=bot.id, operation="reset_text",
|
||||||
chat=empty))
|
chat=empty))
|
||||||
)
|
)
|
||||||
|
keyboard.add(
|
||||||
|
types.InlineKeyboardButton(text=("🟢 " if lang == "none" else "") + _("[все языки]"),
|
||||||
|
callback_data=menu_callback.new(level=3, bot_id=bot.id,
|
||||||
|
operation="slang_none", chat=empty))
|
||||||
|
)
|
||||||
|
for code, name in languages.items():
|
||||||
|
prefix = ""
|
||||||
|
if code == lang:
|
||||||
|
prefix = "🟢 "
|
||||||
|
elif code in prepared_languages:
|
||||||
|
prefix = "✔️ "
|
||||||
|
keyboard.insert(
|
||||||
|
types.InlineKeyboardButton(text=prefix + name,
|
||||||
|
callback_data=menu_callback.new(level=3, bot_id=bot.id,
|
||||||
|
operation=f"slang_{code}",
|
||||||
|
chat=empty))
|
||||||
|
)
|
||||||
|
|
||||||
text = dedent(_("""
|
text = dedent(_("""
|
||||||
Сейчас вы редактируете текст, который отправляется после того, как пользователь отправит вашему боту @{0}
|
Сейчас вы редактируете текст, который отправляется после того, как пользователь отправит вашему боту @{0}
|
||||||
команду /start
|
команду /start
|
||||||
|
|
||||||
Текущий текст:
|
Текущий текст{2}:
|
||||||
<pre>
|
<pre>{1}</pre>
|
||||||
{1}
|
|
||||||
</pre>
|
|
||||||
Отправьте сообщение, чтобы изменить текст.
|
Отправьте сообщение, чтобы изменить текст.
|
||||||
"""))
|
"""))
|
||||||
text = text.format(bot.name, bot.start_text)
|
text = text.format(bot.name,
|
||||||
if call:
|
prepared_languages.get(lang, bot.start_text),
|
||||||
await edit_or_create(call, text, keyboard, parse_mode="HTML")
|
_(" (для языка {0})").format(languages[lang]) if lang != "none" else "")
|
||||||
else:
|
|
||||||
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:
|
if call:
|
||||||
await edit_or_create(call, text, keyboard, parse_mode="HTML")
|
await edit_or_create(call, text, keyboard, parse_mode="HTML")
|
||||||
else:
|
else:
|
||||||
@@ -301,9 +315,15 @@ async def send_bot_statistic_menu(bot: Bot, call: ty.Optional[types.CallbackQuer
|
|||||||
|
|
||||||
|
|
||||||
async def send_bot_second_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None,
|
async def send_bot_second_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None,
|
||||||
chat_id: ty.Optional[int] = None):
|
chat_id: ty.Optional[int] = None, state=None):
|
||||||
if call:
|
if call:
|
||||||
await call.answer()
|
await call.answer()
|
||||||
|
|
||||||
|
async with state.proxy() as proxy:
|
||||||
|
lang = proxy.get("lang", "none")
|
||||||
|
|
||||||
|
prepared_languages = {ln.locale: ln.text for ln in await bot.second_texts}
|
||||||
|
|
||||||
keyboard = types.InlineKeyboardMarkup(row_width=2)
|
keyboard = types.InlineKeyboardMarkup(row_width=2)
|
||||||
keyboard.insert(
|
keyboard.insert(
|
||||||
types.InlineKeyboardButton(text=_("<< Завершить редактирование"),
|
types.InlineKeyboardButton(text=_("<< Завершить редактирование"),
|
||||||
@@ -324,18 +344,35 @@ async def send_bot_second_text_menu(bot: Bot, call: ty.Optional[types.CallbackQu
|
|||||||
callback_data=menu_callback.new(level=3, bot_id=bot.id,
|
callback_data=menu_callback.new(level=3, bot_id=bot.id,
|
||||||
operation="reset_second_text", chat=empty))
|
operation="reset_second_text", chat=empty))
|
||||||
)
|
)
|
||||||
|
keyboard.add(
|
||||||
|
types.InlineKeyboardButton(text=("🟢 " if lang == "none" else "") + _("[все языки]"),
|
||||||
|
callback_data=menu_callback.new(level=3, bot_id=bot.id,
|
||||||
|
operation="alang_none", chat=empty))
|
||||||
|
)
|
||||||
|
for code, name in languages.items():
|
||||||
|
prefix = ""
|
||||||
|
if code == lang:
|
||||||
|
prefix = "🟢 "
|
||||||
|
elif code in prepared_languages:
|
||||||
|
prefix = "✔️ "
|
||||||
|
keyboard.insert(
|
||||||
|
types.InlineKeyboardButton(text=prefix + name,
|
||||||
|
callback_data=menu_callback.new(level=3, bot_id=bot.id,
|
||||||
|
operation=f"alang_{code}",
|
||||||
|
chat=empty))
|
||||||
|
)
|
||||||
|
|
||||||
text = dedent(_("""
|
text = dedent(_("""
|
||||||
Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в ответ на все входящие сообщения @{0} \
|
Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в ответ на все входящие сообщения @{0} \
|
||||||
автоматически. По умолчанию оно отключено.
|
автоматически. По умолчанию оно отключено.
|
||||||
|
|
||||||
Текущий текст:
|
Текущий текст{2}:
|
||||||
<pre>
|
<pre>{1}</pre>
|
||||||
{1}
|
|
||||||
</pre>
|
|
||||||
Отправьте сообщение, чтобы изменить текст.
|
Отправьте сообщение, чтобы изменить текст.
|
||||||
"""))
|
"""))
|
||||||
text = text.format(bot.name, bot.second_text if bot.second_text else _("(отключено)"))
|
text = text.format(bot.name,
|
||||||
|
prepared_languages.get(lang, bot.second_text or _("отключено")),
|
||||||
|
_(" (для языка {0})").format(languages[lang]) if lang != "none" else "")
|
||||||
if call:
|
if call:
|
||||||
await edit_or_create(call, text, keyboard, parse_mode="HTML")
|
await edit_or_create(call, text, keyboard, parse_mode="HTML")
|
||||||
else:
|
else:
|
||||||
@@ -383,63 +420,43 @@ async def send_bot_templates_menu(bot: Bot, call: ty.Optional[types.CallbackQuer
|
|||||||
async def start_text_received(message: types.Message, state: FSMContext):
|
async def start_text_received(message: types.Message, state: FSMContext):
|
||||||
async with state.proxy() as proxy:
|
async with state.proxy() as proxy:
|
||||||
bot_id = proxy.get("bot_id")
|
bot_id = proxy.get("bot_id")
|
||||||
|
lang = proxy.get("lang", "none")
|
||||||
|
|
||||||
bot = await Bot.get_or_none(pk=bot_id)
|
bot = await Bot.get_or_none(pk=bot_id)
|
||||||
bot.start_text = message.html_text
|
if lang == "none":
|
||||||
await bot.save()
|
bot.start_text = message.html_text
|
||||||
await send_bot_text_menu(bot, chat_id=message.chat.id)
|
await bot.save(update_fields=["start_text"])
|
||||||
|
else:
|
||||||
|
obj, created = await BotStartMessage.get_or_create(bot=bot,
|
||||||
@dp.message_handler(state="wait_mailing_text",
|
locale=lang,
|
||||||
content_types=[types.ContentType.TEXT,
|
defaults={"text": message.html_text})
|
||||||
types.ContentType.LOCATION,
|
if not created:
|
||||||
types.ContentType.DOCUMENT,
|
obj.text = message.html_text
|
||||||
types.ContentType.PHOTO,
|
await obj.save(update_fields=["text"])
|
||||||
types.ContentType.AUDIO,
|
await send_bot_text_menu(bot, chat_id=message.chat.id, state=state)
|
||||||
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
|
|
||||||
|
|
||||||
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 == types.ContentType.PHOTO:
|
|
||||||
proxy["mailing_photo"] = message.photo[0].file_id
|
|
||||||
proxy["mailing_caption"] = message.caption
|
|
||||||
elif message.content_type == types.ContentType.DOCUMENT:
|
|
||||||
proxy["mailing_document"] = message.document.file_id
|
|
||||||
proxy["mailing_caption"] = message.caption
|
|
||||||
elif message.content_type == types.ContentType.AUDIO:
|
|
||||||
proxy["mailing_audio"] = message.audio.file_id
|
|
||||||
proxy["mailing_caption"] = message.caption
|
|
||||||
elif message.content_type == types.ContentType.VIDEO:
|
|
||||||
proxy["mailing_video"] = message.video.file_id
|
|
||||||
proxy["mailing_video"] = message.caption
|
|
||||||
|
|
||||||
_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.message_id,
|
|
||||||
text="Вы уверены, что хотите разослать это сообщение всем пользователям?",
|
|
||||||
reply_markup=keyboard)
|
|
||||||
|
|
||||||
|
|
||||||
@dp.message_handler(state="wait_second_text", content_types="text", regexp="^[^/].+") # Not command
|
@dp.message_handler(state="wait_second_text", content_types="text", regexp="^[^/].+") # Not command
|
||||||
async def second_text_received(message: types.Message, state: FSMContext):
|
async def second_text_received(message: types.Message, state: FSMContext):
|
||||||
async with state.proxy() as proxy:
|
async with state.proxy() as proxy:
|
||||||
bot_id = proxy.get("bot_id")
|
bot_id = proxy.get("bot_id")
|
||||||
|
lang = proxy.get("lang", "none")
|
||||||
|
|
||||||
bot = await Bot.get_or_none(pk=bot_id)
|
bot = await Bot.get_or_none(pk=bot_id)
|
||||||
bot.second_text = message.html_text
|
if lang == "none":
|
||||||
await bot.save()
|
bot.second_text = message.html_text
|
||||||
await send_bot_second_text_menu(bot, chat_id=message.chat.id)
|
await bot.save(update_fields=["second_text"])
|
||||||
|
else:
|
||||||
|
obj, created = await BotSecondMessage.get_or_create(bot=bot,
|
||||||
|
locale=lang,
|
||||||
|
defaults={"text": message.html_text})
|
||||||
|
if not created:
|
||||||
|
obj.text = message.html_text
|
||||||
|
await obj.save(update_fields=["text"])
|
||||||
|
if not bot.second_text:
|
||||||
|
bot.second_text = message.html_text
|
||||||
|
await bot.save(update_fields=["second_text"])
|
||||||
|
await send_bot_second_text_menu(bot, chat_id=message.chat.id, state=state)
|
||||||
|
|
||||||
|
|
||||||
@dp.message_handler(state="wait_template", content_types="text", regexp="^[^/](.+)?") # Not command
|
@dp.message_handler(state="wait_template", content_types="text", regexp="^[^/](.+)?") # Not command
|
||||||
@@ -503,20 +520,11 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
|
|||||||
return await send_bot_statistic_menu(bot, call)
|
return await send_bot_statistic_menu(bot, call)
|
||||||
if operation == "settings":
|
if operation == "settings":
|
||||||
return await send_bot_settings_menu(bot, call)
|
return await send_bot_settings_menu(bot, call)
|
||||||
if operation == "go_mailing":
|
|
||||||
if bot.last_mailing_at and bot.last_mailing_at >= datetime.now() - 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":
|
if operation == "text":
|
||||||
await state.set_state("wait_start_text")
|
await state.set_state("wait_start_text")
|
||||||
async with state.proxy() as proxy:
|
async with state.proxy() as proxy:
|
||||||
proxy["bot_id"] = bot.id
|
proxy["bot_id"] = bot.id
|
||||||
return await send_bot_text_menu(bot, call)
|
return await send_bot_text_menu(bot, call, state=state)
|
||||||
|
|
||||||
if level == "3":
|
if level == "3":
|
||||||
if operation == "delete_yes":
|
if operation == "delete_yes":
|
||||||
@@ -529,40 +537,41 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
|
|||||||
if operation == "antiflood":
|
if operation == "antiflood":
|
||||||
await bot_actions.antiflood(bot, call)
|
await bot_actions.antiflood(bot, call)
|
||||||
return await send_bot_settings_menu(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 == "additional_info":
|
if operation == "additional_info":
|
||||||
await bot_actions.additional_info(bot, call)
|
await bot_actions.additional_info(bot, call)
|
||||||
return await send_bot_settings_menu(bot, call)
|
return await send_bot_settings_menu(bot, call)
|
||||||
|
if operation == "always_second_message":
|
||||||
|
await bot_actions.always_second_message(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":
|
if operation == "olgram_text":
|
||||||
await bot_actions.olgram_text(bot, call)
|
await bot_actions.olgram_text(bot, call)
|
||||||
return await send_bot_settings_menu(bot, call)
|
return await send_bot_settings_menu(bot, call)
|
||||||
if operation == "reset_text":
|
if operation == "reset_text":
|
||||||
await bot_actions.reset_bot_text(bot, call)
|
await bot_actions.reset_bot_text(bot, call, state)
|
||||||
return await send_bot_text_menu(bot, call)
|
return await send_bot_text_menu(bot, call, state=state)
|
||||||
|
if operation.startswith("slang_"):
|
||||||
|
async with state.proxy() as proxy:
|
||||||
|
lang = operation.replace("slang_", "")
|
||||||
|
if lang == "none" or lang in languages:
|
||||||
|
proxy["lang"] = lang
|
||||||
|
return await send_bot_text_menu(bot, call, state=state)
|
||||||
if operation == "next_text":
|
if operation == "next_text":
|
||||||
await state.set_state("wait_second_text")
|
await state.set_state("wait_second_text")
|
||||||
async with state.proxy() as proxy:
|
async with state.proxy() as proxy:
|
||||||
proxy["bot_id"] = bot.id
|
proxy["bot_id"] = bot.id
|
||||||
return await send_bot_second_text_menu(bot, call)
|
return await send_bot_second_text_menu(bot, call, state=state)
|
||||||
if operation == "go_go_mailing":
|
if operation.startswith("alang_"):
|
||||||
if (await state.get_state()) == "wait_mailing_text":
|
async with state.proxy() as proxy:
|
||||||
async with state.proxy() as proxy:
|
lang = operation.replace("alang_", "")
|
||||||
mailing_data = dict(proxy)
|
if lang == "none" or lang in languages:
|
||||||
await state.reset_state()
|
proxy["lang"] = lang
|
||||||
|
return await send_bot_second_text_menu(bot, call, state=state)
|
||||||
if bot.last_mailing_at and bot.last_mailing_at >= datetime.now() - 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)
|
|
||||||
await call.message.answer(_("Рассылка завершена, отправлено {0} сообщений").format(count))
|
|
||||||
if operation == "reset_second_text":
|
if operation == "reset_second_text":
|
||||||
await bot_actions.reset_bot_second_text(bot, call)
|
await bot_actions.reset_bot_second_text(bot, call, state)
|
||||||
return await send_bot_second_text_menu(bot, call)
|
return await send_bot_second_text_menu(bot, call, state=state)
|
||||||
if operation == "templates":
|
if operation == "templates":
|
||||||
await state.set_state("wait_template")
|
await state.set_state("wait_template")
|
||||||
async with state.proxy() as proxy:
|
async with state.proxy() as proxy:
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
-- upgrade --
|
|
||||||
ALTER TABLE "bot" ADD "last_mailing_at" TIMESTAMPTZ;
|
|
||||||
ALTER TABLE "bot" ADD "enable_mailing" BOOL NOT NULL DEFAULT False;
|
|
||||||
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 --
|
|
||||||
ALTER TABLE "bot" DROP COLUMN "last_mailing_at";
|
|
||||||
ALTER TABLE "bot" DROP COLUMN "enable_mailing";
|
|
||||||
DROP TABLE IF EXISTS "mailinguser";
|
|
||||||
9
olgram/migrations/models/15_20240112035625_update.sql
Normal file
9
olgram/migrations/models/15_20240112035625_update.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- upgrade --
|
||||||
|
CREATE TABLE IF NOT EXISTS "bot_start_message" (
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"locale" VARCHAR(5) NOT NULL,
|
||||||
|
"bot_id" INT NOT NULL REFERENCES "bot" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_bot_start_m_bot_id_871cd1" UNIQUE ("bot_id", "locale")
|
||||||
|
);
|
||||||
|
-- downgrade --
|
||||||
|
DROP TABLE IF EXISTS "bot_start_message";
|
||||||
4
olgram/migrations/models/16_20240112040146_update.sql
Normal file
4
olgram/migrations/models/16_20240112040146_update.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- upgrade --
|
||||||
|
ALTER TABLE "bot_start_message" ADD "text" TEXT NOT NULL;
|
||||||
|
-- downgrade --
|
||||||
|
ALTER TABLE "bot_start_message" DROP COLUMN "text";
|
||||||
10
olgram/migrations/models/17_20240112045126_update.sql
Normal file
10
olgram/migrations/models/17_20240112045126_update.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- upgrade --
|
||||||
|
CREATE TABLE IF NOT EXISTS "bot_second_message" (
|
||||||
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
|
"locale" VARCHAR(5) NOT NULL,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"bot_id" INT NOT NULL REFERENCES "bot" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_bot_second__bot_id_432892" UNIQUE ("bot_id", "locale")
|
||||||
|
);
|
||||||
|
-- downgrade --
|
||||||
|
DROP TABLE IF EXISTS "bot_second_message";
|
||||||
4
olgram/migrations/models/18_20240217035146_update.sql
Normal file
4
olgram/migrations/models/18_20240217035146_update.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- upgrade --
|
||||||
|
ALTER TABLE "bot" ADD "enable_always_second_message" BOOL NOT NULL DEFAULT False;
|
||||||
|
-- downgrade --
|
||||||
|
ALTER TABLE "bot" DROP COLUMN "enable_always_second_message";
|
||||||
4
olgram/migrations/models/19_20240301193152_update.sql
Normal file
4
olgram/migrations/models/19_20240301193152_update.sql
Normal 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";
|
||||||
@@ -46,8 +46,8 @@ class Bot(Model):
|
|||||||
enable_additional_info = fields.BooleanField(default=False)
|
enable_additional_info = fields.BooleanField(default=False)
|
||||||
enable_olgram_text = fields.BooleanField(default=True)
|
enable_olgram_text = fields.BooleanField(default=True)
|
||||||
enable_antiflood = fields.BooleanField(default=False)
|
enable_antiflood = fields.BooleanField(default=False)
|
||||||
enable_mailing = fields.BooleanField(default=False)
|
enable_always_second_message = fields.BooleanField(default=False)
|
||||||
last_mailing_at = fields.DatetimeField(null=True, default=None)
|
enable_thread_interrupt = fields.BooleanField(default=True)
|
||||||
|
|
||||||
def decrypted_token(self):
|
def decrypted_token(self):
|
||||||
cryptor = DatabaseSettings.cryptor()
|
cryptor = DatabaseSettings.cryptor()
|
||||||
@@ -72,15 +72,26 @@ class Bot(Model):
|
|||||||
table = 'bot'
|
table = 'bot'
|
||||||
|
|
||||||
|
|
||||||
class MailingUser(Model):
|
class BotStartMessage(Model):
|
||||||
id = fields.BigIntField(pk=True)
|
id = fields.IntField(pk=True)
|
||||||
telegram_id = fields.BigIntField(index=True)
|
bot = fields.ForeignKeyField("models.Bot", related_name="start_texts", on_delete=fields.CASCADE)
|
||||||
|
locale = fields.CharField(max_length=5)
|
||||||
bot = fields.ForeignKeyField("models.Bot", related_name="mailing_users", on_delete=fields.relational.CASCADE)
|
text = fields.TextField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'mailinguser'
|
unique_together = ("bot", "locale")
|
||||||
unique_together = (("bot", "telegram_id"), )
|
table = 'bot_start_message'
|
||||||
|
|
||||||
|
|
||||||
|
class BotSecondMessage(Model):
|
||||||
|
id = fields.IntField(pk=True)
|
||||||
|
bot = fields.ForeignKeyField("models.Bot", related_name="second_texts", on_delete=fields.CASCADE)
|
||||||
|
locale = fields.CharField(max_length=5)
|
||||||
|
text = fields.TextField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("bot", "locale")
|
||||||
|
table = 'bot_second_message'
|
||||||
|
|
||||||
|
|
||||||
class User(Model):
|
class User(Model):
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class OlgramSettings(AbstractSettings):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def version(cls):
|
def version(cls):
|
||||||
return "0.5.0"
|
return "0.7.1"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@lru_cache
|
@lru_cache
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup
|
||||||
from aiogram import types, Bot as AioBot
|
|
||||||
from aiogram.utils.exceptions import TelegramAPIError
|
from aiogram.utils.exceptions import TelegramAPIError
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -31,23 +30,3 @@ def wrap(data: str, max_len: int) -> str:
|
|||||||
|
|
||||||
def button_text_limit(data: str) -> str:
|
def button_text_limit(data: str) -> str:
|
||||||
return wrap(data, 30)
|
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 == types.ContentType.AUDIO:
|
|
||||||
return await bot.send_audio(chat_id, audio=storage["mailing_audio"], caption=storage.get("mailing_caption"))
|
|
||||||
if content_type == types.ContentType.DOCUMENT:
|
|
||||||
return await bot.send_document(chat_id, document=storage["mailing_document"],
|
|
||||||
caption=storage.get("mailing_caption"))
|
|
||||||
if content_type == types.ContentType.PHOTO:
|
|
||||||
return await bot.send_photo(chat_id, photo=storage["mailing_photo"],
|
|
||||||
caption=storage.get("mailing_caption"))
|
|
||||||
if content_type == types.ContentType.VIDEO:
|
|
||||||
return await bot.send_video(chat_id, video=storage["mailing_video"],
|
|
||||||
caption=storage.get("mailing_caption"))
|
|
||||||
raise NotImplementedError("Mailing, unknown content type")
|
|
||||||
1378
poetry.lock
generated
1378
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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/
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from tortoise.expressions import F
|
|||||||
import logging
|
import logging
|
||||||
import typing as ty
|
import typing as ty
|
||||||
from olgram.settings import ServerSettings
|
from olgram.settings import ServerSettings
|
||||||
from olgram.models.models import Bot, GroupChat, BannedUser, MailingUser
|
from olgram.models.models import Bot, GroupChat, BannedUser, BotStartMessage, BotSecondMessage
|
||||||
from locales.locale import _, translators
|
from locales.locale import _, translators
|
||||||
from server.inlines import inline_handler
|
from server.inlines import inline_handler
|
||||||
|
|
||||||
@@ -55,20 +55,15 @@ def _on_security_policy(message: types.Message, bot):
|
|||||||
text = _("<b>Политика конфиденциальности</b>\n\n"
|
text = _("<b>Политика конфиденциальности</b>\n\n"
|
||||||
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При отправке сообщения (кроме команд "
|
"Этот бот не хранит ваши сообщения, имя пользователя и @username. При отправке сообщения (кроме команд "
|
||||||
"/start и /security_policy) ваш идентификатор пользователя записывается в кеш на некоторое время и потом "
|
"/start и /security_policy) ваш идентификатор пользователя записывается в кеш на некоторое время и потом "
|
||||||
"удаляется из кеша. Этот идентификатор используется для общения с оператором.\n\n")
|
"удаляется из кеша. Этот идентификатор используется только для общения с оператором; боты Olgram "
|
||||||
|
"не делают массовых рассылок.\n\n")
|
||||||
if bot.enable_additional_info:
|
if bot.enable_additional_info:
|
||||||
text += _("При отправке сообщения (кроме команд /start и /security_policy) оператор <b>видит</b> ваши имя "
|
text += _("При отправке сообщения (кроме команд /start и /security_policy) оператор <b>видит</b> ваши имя "
|
||||||
"пользователя, @username и идентификатор пользователя в силу настроек, которые оператор указал при "
|
"пользователя, @username и идентификатор пользователя в силу настроек, которые оператор указал при "
|
||||||
"создании бота.\n\n")
|
"создании бота.")
|
||||||
else:
|
else:
|
||||||
text += _("В зависимости от ваших настроек конфиденциальности Telegram, оператор может видеть ваш username, "
|
text += _("В зависимости от ваших настроек конфиденциальности Telegram, оператор может видеть ваш username, "
|
||||||
"имя пользователя и другую информацию.\n\n")
|
"имя пользователя и другую информацию.")
|
||||||
|
|
||||||
if bot.enable_mailing:
|
|
||||||
text += _("В этом боте включена массовая рассылка в силу настроек, которые оператор указал при создании бота. "
|
|
||||||
"Ваш идентификатор пользователя может быть записан в базу данных на долгое время")
|
|
||||||
else:
|
|
||||||
text += _("В этом боте нет массовой рассылки сообщений")
|
|
||||||
|
|
||||||
return SendMessage(chat_id=message.chat.id,
|
return SendMessage(chat_id=message.chat.id,
|
||||||
text=text,
|
text=text,
|
||||||
@@ -83,47 +78,54 @@ async def send_user_message(message: types.Message, super_chat_id: int, bot):
|
|||||||
if message.from_user.username:
|
if message.from_user.username:
|
||||||
user_info += " | @" + message.from_user.username
|
user_info += " | @" + message.from_user.username
|
||||||
user_info += f" | #ID{message.from_user.id}"
|
user_info += f" | #ID{message.from_user.id}"
|
||||||
|
if message.from_user.locale:
|
||||||
|
user_info += f" | lang: {message.from_user.locale}"
|
||||||
|
if message.forward_sender_name:
|
||||||
|
user_info += f" | fwd: {message.forward_sender_name}"
|
||||||
|
|
||||||
# Добавлять информацию в конец текста
|
# Добавлять информацию в конец текста
|
||||||
if message.content_type == types.ContentType.TEXT and len(message.text) + len(user_info) < 4093: # noqa:E721
|
if message.content_type == types.ContentType.TEXT \
|
||||||
|
and len(message.text) + len(user_info) < 4093: # noqa:E721
|
||||||
new_message = await message.bot.send_message(super_chat_id, message.text + "\n\n" + user_info)
|
new_message = await message.bot.send_message(super_chat_id, message.text + "\n\n" + user_info)
|
||||||
else: # Не добавлять информацию в конец текста, информация отдельным сообщением
|
else: # Не добавлять информацию в конец текста, информация отдельным сообщением
|
||||||
new_message = await message.bot.send_message(super_chat_id, text=user_info)
|
new_message = await message.bot.send_message(super_chat_id, text=user_info)
|
||||||
new_message_2 = await message.copy_to(super_chat_id, reply_to_message_id=new_message.message_id)
|
new_message_2 = await message.copy_to(super_chat_id, reply_to_message_id=new_message.message_id)
|
||||||
await _redis.set(_message_unique_id(bot.pk, new_message_2.message_id), message.chat.id,
|
await _redis.set(_message_unique_id(bot.pk, new_message_2.message_id), message.chat.id,
|
||||||
pexpire=ServerSettings.redis_timeout_ms())
|
pexpire=ServerSettings.redis_timeout_ms())
|
||||||
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id,
|
|
||||||
pexpire=ServerSettings.redis_timeout_ms())
|
|
||||||
return new_message
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
new_message = await message.forward(super_chat_id)
|
new_message = await message.forward(super_chat_id)
|
||||||
except exceptions.MessageCantBeForwarded:
|
except exceptions.MessageCantBeForwarded:
|
||||||
new_message = await message.copy_to(super_chat_id)
|
new_message = await message.copy_to(super_chat_id)
|
||||||
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id,
|
|
||||||
pexpire=ServerSettings.redis_timeout_ms())
|
await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id,
|
||||||
return new_message
|
pexpire=ServerSettings.redis_timeout_ms())
|
||||||
|
return new_message
|
||||||
|
|
||||||
|
|
||||||
async def send_to_superchat(is_super_group: bool, message: types.Message, super_chat_id: int, bot):
|
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 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))
|
thread_first_message = await _redis.get(_thread_uniqie_id(bot.pk, message.chat.id))
|
||||||
if thread_first_message:
|
if thread_first_message:
|
||||||
# переслать в супер-чат, отвечая на предыдущее сообщение
|
# переслать в супер-чат, отвечая на предыдущее сообщение
|
||||||
try:
|
try:
|
||||||
new_message = await message.copy_to(super_chat_id, reply_to_message_id=int(thread_first_message))
|
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,
|
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:
|
except exceptions.BadRequest:
|
||||||
new_message = await send_user_message(message, super_chat_id, bot)
|
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,
|
await _redis.set(
|
||||||
pexpire=ServerSettings.thread_timeout_ms())
|
_thread_uniqie_id(bot.pk, message.chat.id), new_message.message_id, pexpire=thread_timeout)
|
||||||
else:
|
else:
|
||||||
# переслать супер-чат
|
# переслать супер-чат
|
||||||
new_message = await send_user_message(message, super_chat_id, bot)
|
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,
|
await _redis.set(_thread_uniqie_id(bot.pk, message.chat.id), new_message.message_id,
|
||||||
pexpire=ServerSettings.thread_timeout_ms())
|
pexpire=thread_timeout)
|
||||||
else: # личные сообщения не поддерживают потоки сообщений: просто отправляем сообщение
|
else: # личные сообщения не поддерживают потоки сообщений: просто отправляем сообщение
|
||||||
await send_user_message(message, super_chat_id, bot)
|
await send_user_message(message, super_chat_id, bot)
|
||||||
|
|
||||||
@@ -133,10 +135,6 @@ async def handle_user_message(message: types.Message, super_chat_id: int, bot):
|
|||||||
_ = _get_translator(message)
|
_ = _get_translator(message)
|
||||||
is_super_group = super_chat_id < 0
|
is_super_group = super_chat_id < 0
|
||||||
|
|
||||||
# Записать пользователя для рассылки, если она включена
|
|
||||||
if bot.enable_mailing:
|
|
||||||
_, __ = await MailingUser.get_or_create(telegram_id=message.chat.id, bot=bot)
|
|
||||||
|
|
||||||
# Проверить, не забанен ли пользователь
|
# Проверить, не забанен ли пользователь
|
||||||
banned = await bot.banned_users.filter(telegram_id=message.chat.id)
|
banned = await bot.banned_users.filter(telegram_id=message.chat.id)
|
||||||
if banned:
|
if banned:
|
||||||
@@ -166,8 +164,10 @@ async def handle_user_message(message: types.Message, super_chat_id: int, bot):
|
|||||||
if bot.second_text:
|
if bot.second_text:
|
||||||
send_auto = not await _redis.get(_last_message_uid(bot.pk, message.chat.id))
|
send_auto = not await _redis.get(_last_message_uid(bot.pk, message.chat.id))
|
||||||
await _redis.setex(_last_message_uid(bot.pk, message.chat.id), 60 * 60 * 3, 1)
|
await _redis.setex(_last_message_uid(bot.pk, message.chat.id), 60 * 60 * 3, 1)
|
||||||
if send_auto:
|
if send_auto or bot.enable_always_second_message:
|
||||||
return SendMessage(chat_id=message.chat.id, text=bot.second_text, parse_mode="HTML")
|
text_obj = await BotSecondMessage.get_or_none(bot=bot, locale=str(message.from_user.locale))
|
||||||
|
return SendMessage(chat_id=message.chat.id, text=text_obj.text if text_obj else bot.second_text,
|
||||||
|
parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
async def handle_operator_message(message: types.Message, super_chat_id: int, bot):
|
async def handle_operator_message(message: types.Message, super_chat_id: int, bot):
|
||||||
@@ -227,7 +227,8 @@ async def message_handler(message: types.Message, *args, **kwargs):
|
|||||||
|
|
||||||
if message.text and message.text == "/start":
|
if message.text and message.text == "/start":
|
||||||
# На команду start нужно ответить, не пересылая сообщение никуда
|
# На команду start нужно ответить, не пересылая сообщение никуда
|
||||||
text = bot.start_text
|
text_obj = await BotStartMessage.get_or_none(bot=bot, locale=str(message.from_user.locale))
|
||||||
|
text = text_obj.text if text_obj else bot.start_text
|
||||||
if bot.enable_olgram_text:
|
if bot.enable_olgram_text:
|
||||||
text += _(ServerSettings.append_text())
|
text += _(ServerSettings.append_text())
|
||||||
return SendMessage(chat_id=message.chat.id, text=text, parse_mode="HTML")
|
return SendMessage(chat_id=message.chat.id, text=text, parse_mode="HTML")
|
||||||
|
|||||||
Reference in New Issue
Block a user