mirror of
https://github.com/civsocit/olgram.git
synced 2025-12-16 23:46:17 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58f551c77d | ||
|
|
696bc5368b | ||
|
|
340246b937 |
@@ -49,3 +49,16 @@ Olgram пересылает сообщения так, чтобы сообщен
|
||||
|
||||
При включении этой опции пользователю запрещается отправлять больше одного сообщения в минуту. Используйте её, если
|
||||
не успеваете обрабатывать входящие сообщения.
|
||||
|
||||
.. _mailing:
|
||||
|
||||
Рассылка
|
||||
---------------
|
||||
|
||||
После включения этой опции ваш бот будет запоминать всех пользователей, которые пишут в ваш бот.
|
||||
Вы сможете запустить рассылку по этим пользователям.
|
||||
|
||||
.. note::
|
||||
|
||||
Включение этой опции меняет текст политики конфиденциальности вашего feedback бота (команда /security_policy)
|
||||
и может отпугнуть некоторых пользователей. Не включайте эту опцию без необходимости.
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"""
|
||||
Здесь работа с конкретным ботом
|
||||
"""
|
||||
from asyncio import sleep
|
||||
from datetime import datetime
|
||||
from aiogram import types
|
||||
from aiogram.utils.exceptions import TelegramAPIError, Unauthorized
|
||||
from aiogram.utils import exceptions
|
||||
from aiogram import Bot as AioBot
|
||||
from olgram.models.models import Bot
|
||||
from olgram.utils.mix import send_stored_message
|
||||
from server.server import unregister_token
|
||||
from locales.locale import _
|
||||
|
||||
@@ -15,14 +18,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
|
||||
|
||||
|
||||
@@ -72,7 +75,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()
|
||||
@@ -106,3 +109,35 @@ 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) -> 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
|
||||
|
||||
@@ -37,4 +37,4 @@ async def info(message: types.Message, state: FSMContext):
|
||||
_("Входящих сообщений у всех ботов: {0}\n").format(income_messages) +
|
||||
_("Исходящих сообщений у всех ботов: {0}\n").format(outgoing_messages) +
|
||||
_("Промо-кодов выдано: {0}\n").format(promo_count) +
|
||||
_("Рекламную плашку выключили: {0}\n".format(olgram_text_disabled)))
|
||||
_("Рекламную плашку выключили: {0}\n").format(olgram_text_disabled))
|
||||
|
||||
@@ -4,14 +4,14 @@ from aiogram import types, Bot as AioBot
|
||||
from olgram.models.models import Bot, User, DefaultAnswer
|
||||
from aiogram.dispatcher import FSMContext
|
||||
from aiogram.utils.callback_data import CallbackData
|
||||
from datetime import datetime, timedelta
|
||||
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 _
|
||||
|
||||
import typing as ty
|
||||
|
||||
|
||||
menu_callback = CallbackData('menu', 'level', 'bot_id', 'operation', 'chat')
|
||||
|
||||
empty = "0"
|
||||
@@ -127,6 +127,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}.
|
||||
@@ -172,6 +178,11 @@ async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery):
|
||||
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="antiflood",
|
||||
chat=empty))
|
||||
)
|
||||
keyboard.insert(
|
||||
types.InlineKeyboardButton(text=_("Рассылка"),
|
||||
callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="mailing",
|
||||
chat=empty))
|
||||
)
|
||||
is_promo = await bot.is_promo()
|
||||
if is_promo:
|
||||
keyboard.insert(
|
||||
@@ -189,11 +200,13 @@ async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery):
|
||||
thread_turn = _("включены") if bot.enable_threads else _("выключены")
|
||||
info_turn = _("включены") if bot.enable_additional_info else _("выключены")
|
||||
antiflood_turn = _("включен") if bot.enable_antiflood 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>
|
||||
""")).format(thread_turn, info_turn, antiflood_turn)
|
||||
<a href="https://olgram.readthedocs.io/ru/latest/options.html#mailing">Рассылка</a>: <b>{3}</b>
|
||||
""")).format(thread_turn, info_turn, antiflood_turn, mailing_turn)
|
||||
|
||||
if is_promo:
|
||||
olgram_turn = _("включена") if bot.enable_olgram_text else _("выключена")
|
||||
@@ -238,6 +251,30 @@ 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")
|
||||
|
||||
|
||||
async def send_bot_statistic_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None,
|
||||
chat_id: ty.Optional[int] = None):
|
||||
if call:
|
||||
@@ -352,6 +389,49 @@ async def start_text_received(message: types.Message, state: FSMContext):
|
||||
await send_bot_text_menu(bot, chat_id=message.chat.id)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
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
|
||||
async def second_text_received(message: types.Message, state: FSMContext):
|
||||
async with state.proxy() as proxy:
|
||||
@@ -423,6 +503,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() - 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:
|
||||
@@ -440,6 +529,9 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
|
||||
if operation == "antiflood":
|
||||
await bot_actions.antiflood(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":
|
||||
await bot_actions.additional_info(bot, call)
|
||||
return await send_bot_settings_menu(bot, call)
|
||||
@@ -454,6 +546,20 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon
|
||||
async with state.proxy() as proxy:
|
||||
proxy["bot_id"] = bot.id
|
||||
return await send_bot_second_text_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() - 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":
|
||||
await bot_actions.reset_bot_second_text(bot, call)
|
||||
return await send_bot_second_text_menu(bot, call)
|
||||
|
||||
14
olgram/migrations/models/15_20221106042712_update.sql
Normal file
14
olgram/migrations/models/15_20221106042712_update.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- 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";
|
||||
@@ -46,6 +46,8 @@ class Bot(Model):
|
||||
enable_additional_info = fields.BooleanField(default=False)
|
||||
enable_olgram_text = fields.BooleanField(default=True)
|
||||
enable_antiflood = fields.BooleanField(default=False)
|
||||
enable_mailing = fields.BooleanField(default=False)
|
||||
last_mailing_at = fields.DatetimeField(null=True, default=None)
|
||||
|
||||
def decrypted_token(self):
|
||||
cryptor = DatabaseSettings.cryptor()
|
||||
@@ -70,6 +72,17 @@ class Bot(Model):
|
||||
table = 'bot'
|
||||
|
||||
|
||||
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 User(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
telegram_id = fields.BigIntField(index=True, unique=True)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup
|
||||
from aiogram import types, Bot as AioBot
|
||||
from aiogram.utils.exceptions import TelegramAPIError
|
||||
|
||||
from typing import Optional
|
||||
@@ -30,3 +31,23 @@ 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 == 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")
|
||||
@@ -11,7 +11,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
|
||||
from olgram.models.models import Bot, GroupChat, BannedUser, MailingUser
|
||||
from locales.locale import _, translators
|
||||
from server.inlines import inline_handler
|
||||
|
||||
@@ -55,15 +55,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,
|
||||
@@ -128,6 +133,10 @@ 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:
|
||||
_, __ = await MailingUser.get_or_create(telegram_id=message.chat.id, bot=bot)
|
||||
|
||||
# Проверить, не забанен ли пользователь
|
||||
banned = await bot.banned_users.filter(telegram_id=message.chat.id)
|
||||
if banned:
|
||||
|
||||
Reference in New Issue
Block a user