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

mailing first iteration

This commit is contained in:
er8dd 2024-03-01 22:12:05 +04:00
parent acb62fb644
commit f26958518c
7 changed files with 94 additions and 11 deletions

View File

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

View File

@ -128,3 +128,8 @@ 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"])

View File

@ -178,6 +178,11 @@ async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery):
operation="always_second_message", operation="always_second_message",
chat=empty)) chat=empty))
) )
keyboard.insert(
types.InlineKeyboardButton(text=_("Рассылка"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="mailing",
chat=empty))
)
keyboard.insert( keyboard.insert(
types.InlineKeyboardButton(text=_("Прерывать поток"), types.InlineKeyboardButton(text=_("Прерывать поток"),
callback_data=menu_callback.new(level=3, bot_id=bot.id, callback_data=menu_callback.new(level=3, bot_id=bot.id,
@ -203,14 +208,16 @@ async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery):
antiflood_turn = _("включен") if bot.enable_antiflood else _("выключен") antiflood_turn = _("включен") if bot.enable_antiflood else _("выключен")
enable_always_second_message = _("включён") if bot.enable_always_second_message else _("выключен") enable_always_second_message = _("включён") if bot.enable_always_second_message else _("выключен")
thread_interrupt = _("да") if bot.enable_thread_interrupt else _("нет") thread_interrupt = _("да") if bot.enable_thread_interrupt else _("нет")
mailing_turn = _("включена") if bot.enable_mailing 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#always_second_message">Автоответчик всегда</a>: <b>{3} <a href="https://olgram.readthedocs.io/ru/latest/options.html#always_second_message">Автоответчик всегда</a>: <b>{3}</b>
<a href="https://olgram.readthedocs.io/ru/latest/options.html#thread_interrupt">Прерывать поток</a>: <b>{4} <a href="https://olgram.readthedocs.io/ru/latest/options.html#thread_interrupt">Прерывать поток</a>: <b>{4}</b>
</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) """)).format(thread_turn, info_turn, antiflood_turn, enable_always_second_message, thread_interrupt,
mailing_turn)
if is_promo: if is_promo:
olgram_turn = _("включена") if bot.enable_olgram_text else _("выключена") olgram_turn = _("включена") if bot.enable_olgram_text else _("выключена")
@ -543,6 +550,9 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
if operation == "always_second_message": if operation == "always_second_message":
await bot_actions.always_second_message(bot, call) await bot_actions.always_second_message(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 == "thread_interrupt": if operation == "thread_interrupt":
await bot_actions.thread_interrupt(bot, call) await bot_actions.thread_interrupt(bot, call)
return await send_bot_settings_menu(bot, call) return await send_bot_settings_menu(bot, call)

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

@ -48,6 +48,8 @@ class Bot(Model):
enable_antiflood = fields.BooleanField(default=False) enable_antiflood = fields.BooleanField(default=False)
enable_always_second_message = fields.BooleanField(default=False) enable_always_second_message = fields.BooleanField(default=False)
enable_thread_interrupt = fields.BooleanField(default=True) 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): def decrypted_token(self):
cryptor = DatabaseSettings.cryptor() cryptor = DatabaseSettings.cryptor()
@ -106,6 +108,17 @@ class User(Model):
table = 'user' 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): class GroupChat(Model):
id = fields.IntField(pk=True) id = fields.IntField(pk=True)
chat_id = fields.BigIntField(index=True, unique=True) chat_id = fields.BigIntField(index=True, unique=True)

View File

@ -1,5 +1,6 @@
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup
from aiogram.utils.exceptions import TelegramAPIError from aiogram.utils.exceptions import TelegramAPIError
from aiogram import types, Bot as AioBot
from typing import Optional from typing import Optional
@ -30,3 +31,23 @@ 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")

View File

@ -1,3 +1,5 @@
import asyncio
from aiogram import Bot as AioBot, Dispatcher from aiogram import Bot as AioBot, Dispatcher
from aiogram.dispatcher.webhook import WebhookRequestHandler from aiogram.dispatcher.webhook import WebhookRequestHandler
from aiogram.dispatcher.webhook import SendMessage from aiogram.dispatcher.webhook import SendMessage
@ -11,7 +13,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, BotStartMessage, BotSecondMessage from olgram.models.models import Bot, GroupChat, BannedUser, BotStartMessage, BotSecondMessage, MailingUser
from locales.locale import _, translators from locales.locale import _, translators
from server.inlines import inline_handler from server.inlines import inline_handler
@ -55,15 +57,20 @@ 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) ваш идентификатор пользователя записывается в кеш на некоторое время и потом "
"удаляется из кеша. Этот идентификатор используется только для общения с оператором; боты Olgram " "удаляется из кеша. Этот идентификатор используется для общения с оператором.\n\n")
"не делают массовых рассылок.\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,
@ -130,11 +137,19 @@ async def send_to_superchat(is_super_group: bool, message: types.Message, super_
await send_user_message(message, super_chat_id, bot) 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): 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:
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) banned = await bot.banned_users.filter(telegram_id=message.chat.id)
if banned: if banned:
@ -157,8 +172,7 @@ async def handle_user_message(message: types.Message, super_chat_id: int, bot):
_logger.error(f"(exception on forwarding) {err}") _logger.error(f"(exception on forwarding) {err}")
return return
bot.incoming_messages_count = F("incoming_messages_count") + 1 asyncio.create_task(_increase_count(bot))
await bot.save(update_fields=["incoming_messages_count"])
# И отправить пользователю специальный текст, если он указан и если давно не отправляли # И отправить пользователю специальный текст, если он указан и если давно не отправляли
if bot.second_text: if bot.second_text: