diff --git a/docs/source/about.rst b/docs/source/about.rst index 367a488..9529263 100644 --- a/docs/source/about.rst +++ b/docs/source/about.rst @@ -1,6 +1,7 @@ О проекте =================================== + Зачем нужен Olgram ------------ diff --git a/docs/source/additional.rst b/docs/source/additional.rst index f742141..3a837f3 100644 --- a/docs/source/additional.rst +++ b/docs/source/additional.rst @@ -4,21 +4,23 @@ Донаты ---------------- -На рекламу проекта, аренду сервера и пиццу +На аренду сервера для этого проекта Bitcoin: ``bc1qlq7cm5chc8flr3fy8ewk967aknq3dwmxtwn9hl`` -Monero: - ``886AQ8tCVcQKp21xsuSLkfDdTAdtCFH1jR58Tw9MsaxFXoZ7YRHXx1cQcUfUnDX6hySzPsQEVt6RWPn3sXH9QUmwCr3oVqB`` +Litecoin: + ``LTC1QXAJSVZ0LW44AA5NYTUCH8CP2G8X7A4CDASE4Y7`` -Dash: - ``XqxetfWzr5n4Ms1TxMbdEEeHGe8CaMdmb6`` +Как убрать "Этот бот создан с помощью ..." +---------------- +Напишите нам на `@civsocit_feedback_bot `_. История изменений ---------------- +- `2024-01-12` Мультиязычность (стартовое сообщение и автоответчик) - `2022-08-01` Защита от флуда - `2022-07-23` Автоответчик не пишет сообщение лишний раз - `2022-07-04` Поддержка двух ботов в одном чате diff --git a/docs/source/conf.py b/docs/source/conf.py index b472451..9ef0397 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -3,7 +3,7 @@ # -- Project information project = 'Olgram' -copyright = '2022, Civsocit' +copyright = '2024, Civsocit' author = 'civsocit' release = '0.1' diff --git a/docs/source/quick_start.rst b/docs/source/quick_start.rst index f3d9132..9cd5c9e 100644 --- a/docs/source/quick_start.rst +++ b/docs/source/quick_start.rst @@ -68,6 +68,13 @@ BotFather - это официальный бот Telegram, создающий д Теперь просто отправьте новый текст приветствия. +.. note:: + + Чтобы настроить особый текст приветствия для, например, русскоязычных пользователей (т.е. тех пользователей, у + которых в настройках Telegram выставлена русская локализация), нажмите кнопку "Руссикй 🇷🇺" и только потом отправьте + текст приветствия. Чтобы отредактировать текст приветствия для всех остальных языков, нажмите "[все языки]". + + Как привязать бота к групповому чату ------------------------------------ diff --git a/locales/en/LC_MESSAGES/olgram.po b/locales/en/LC_MESSAGES/olgram.po index fa92b97..4d0ca73 100644 --- a/locales/en/LC_MESSAGES/olgram.po +++ b/locales/en/LC_MESSAGES/olgram.po @@ -455,7 +455,7 @@ msgid "" "отключено.\n" "\n" " Текущий текст:\n" -"
\n"
+"    
"
 "    {1}\n"
 "    
\n" " Отправьте сообщение, чтобы изменить текст.\n" diff --git a/locales/uk/LC_MESSAGES/olgram.po b/locales/uk/LC_MESSAGES/olgram.po index e17d1c9..303676e 100644 --- a/locales/uk/LC_MESSAGES/olgram.po +++ b/locales/uk/LC_MESSAGES/olgram.po @@ -407,9 +407,7 @@ msgid "" " команду /start\n" "\n" " Текущий текст:\n" -"
\n"
-"    {1}\n"
-"    
\n" +"
{1}
\n" " Отправьте сообщение, чтобы изменить текст.\n" " " msgstr "" @@ -419,9 +417,7 @@ msgstr "" " команду /start\n" "\n" " Поточний текст:\n" -"
\n"
-"    {1}\n"
-"    
\n" +"
{1}
\n" " Надішліть повідомлення, щоб змінити текст.\n" " \n" " " @@ -462,9 +458,7 @@ msgid "" "отключено.\n" "\n" " Текущий текст:\n" -"
\n"
-"    {1}\n"
-"    
\n" +"
{1}
\n" " Отправьте сообщение, чтобы изменить текст.\n" " " msgstr "" diff --git a/locales/zh/LC_MESSAGES/olgram.po b/locales/zh/LC_MESSAGES/olgram.po index e7764bd..d4b44b6 100644 --- a/locales/zh/LC_MESSAGES/olgram.po +++ b/locales/zh/LC_MESSAGES/olgram.po @@ -306,9 +306,7 @@ msgid "" " команду /start\n" "\n" " Текущий текст:\n" -"
\n"
-"    {1}\n"
-"    
\n" +"
{1}
\n" " Отправьте сообщение, чтобы изменить текст.\n" " " msgstr "" @@ -317,9 +315,7 @@ msgstr "" " /start\n" "\n" " 目前的文本。\n" -"
\n"
-"    {1}\n"
-"    
\n" +"
{1}
\n" " 发送消息,改变文本。\n" " " @@ -359,9 +355,7 @@ msgid "" "отключено.\n" "\n" " Текущий текст:\n" -"
\n"
-"    {1}\n"
-"    
\n" +"
{1}
\n" " Отправьте сообщение, чтобы изменить текст.\n" " " msgstr "" @@ -370,9 +364,7 @@ msgstr "" "默认情况下,它是禁用的。\n" "\n" " 目前的文本。\n" -"
\n"
-"    {1}\n"
-"    
。\n" +"
{1}
。\n" " 发送消息,改变文本。\n" " " diff --git a/olgram/commands/bot_actions.py b/olgram/commands/bot_actions.py index efb289f..ce37651 100644 --- a/olgram/commands/bot_actions.py +++ b/olgram/commands/bot_actions.py @@ -4,7 +4,7 @@ from aiogram import types from aiogram.utils.exceptions import TelegramAPIError, Unauthorized from aiogram import Bot as AioBot -from olgram.models.models import Bot +from olgram.models.models import Bot, BotStartMessage, BotSecondMessage from server.server import unregister_token from locales.locale import _ @@ -26,27 +26,39 @@ async def delete_bot(bot: Bot, call: types.CallbackQuery): pass -async def reset_bot_text(bot: Bot, call: types.CallbackQuery): +async def reset_bot_text(bot: Bot, call: types.CallbackQuery, state): """ Пользователь решил сбросить текст бота к default :param bot: :param call: :return: """ - bot.start_text = bot._meta.fields_map['start_text'].default - await bot.save() + async with state.proxy() as proxy: + lang = proxy.get("lang", "none") + if lang == "none": + await BotSecondMessage.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(_("Текст сброшен")) -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 бота :param bot: :param call: :return: """ - bot.second_text = bot._meta.fields_map['second_text'].default - await bot.save() + async with state.proxy() as proxy: + 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(_("Текст сброшен")) diff --git a/olgram/commands/info.py b/olgram/commands/info.py index fdce6ee..b1766dd 100644 --- a/olgram/commands/info.py +++ b/olgram/commands/info.py @@ -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)) diff --git a/olgram/commands/menu.py b/olgram/commands/menu.py index 9f1b095..8d09ba9 100644 --- a/olgram/commands/menu.py +++ b/olgram/commands/menu.py @@ -1,7 +1,7 @@ from olgram.router import dp 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.utils.callback_data import CallbackData from textwrap import dedent @@ -202,9 +202,26 @@ async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery): 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: 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.insert( types.InlineKeyboardButton(text=_("<< Завершить редактирование"), @@ -215,23 +232,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", chat=empty)) ) - keyboard.insert( + keyboard.row( types.InlineKeyboardButton(text=_("Сбросить текст"), callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="reset_text", 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(_(""" Сейчас вы редактируете текст, который отправляется после того, как пользователь отправит вашему боту @{0} команду /start - Текущий текст: -
-    {1}
-    
+ Текущий текст{2}: +
{1}
Отправьте сообщение, чтобы изменить текст. """)) - text = text.format(bot.name, bot.start_text) + text = text.format(bot.name, + prepared_languages.get(lang, bot.start_text), + _(" (для языка {0})").format(languages[lang]) if lang != "none" else "") if call: await edit_or_create(call, text, keyboard, parse_mode="HTML") else: @@ -264,9 +298,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, - chat_id: ty.Optional[int] = None): + chat_id: ty.Optional[int] = None, state=None): if call: 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.insert( types.InlineKeyboardButton(text=_("<< Завершить редактирование"), @@ -287,18 +327,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, 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(_(""" Сейчас вы редактируете текст автоответчика. Это сообщение отправляется в ответ на все входящие сообщения @{0} \ автоматически. По умолчанию оно отключено. - Текущий текст: -
-    {1}
-    
+ Текущий текст{2}: +
{1}
Отправьте сообщение, чтобы изменить текст. """)) - 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: await edit_or_create(call, text, keyboard, parse_mode="HTML") else: @@ -346,20 +403,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 with state.proxy() as proxy: bot_id = proxy.get("bot_id") + lang = proxy.get("lang", "none") + bot = await Bot.get_or_none(pk=bot_id) - bot.start_text = message.html_text - await bot.save() - await send_bot_text_menu(bot, chat_id=message.chat.id) + if lang == "none": + bot.start_text = message.html_text + await bot.save(update_fields=["start_text"]) + else: + obj, created = await BotStartMessage.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"]) + await send_bot_text_menu(bot, chat_id=message.chat.id, state=state) @dp.message_handler(state="wait_second_text", content_types="text", regexp="^[^/].+") # Not command async def second_text_received(message: types.Message, state: FSMContext): async with state.proxy() as proxy: bot_id = proxy.get("bot_id") + lang = proxy.get("lang", "none") + bot = await Bot.get_or_none(pk=bot_id) - bot.second_text = message.html_text - await bot.save() - await send_bot_second_text_menu(bot, chat_id=message.chat.id) + if lang == "none": + bot.second_text = message.html_text + 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 @@ -427,7 +507,7 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon await state.set_state("wait_start_text") async with state.proxy() as proxy: 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 operation == "delete_yes": @@ -447,16 +527,28 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon await bot_actions.olgram_text(bot, call) return await send_bot_settings_menu(bot, call) if operation == "reset_text": - await bot_actions.reset_bot_text(bot, call) - return await send_bot_text_menu(bot, call) + await bot_actions.reset_bot_text(bot, call, state) + 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": await state.set_state("wait_second_text") async with state.proxy() as proxy: proxy["bot_id"] = bot.id - return await send_bot_second_text_menu(bot, call) + return await send_bot_second_text_menu(bot, call, state=state) + if operation.startswith("alang_"): + async with state.proxy() as proxy: + lang = operation.replace("alang_", "") + if lang == "none" or lang in languages: + proxy["lang"] = lang + return await send_bot_second_text_menu(bot, call, state=state) if operation == "reset_second_text": - await bot_actions.reset_bot_second_text(bot, call) - return await send_bot_second_text_menu(bot, call) + await bot_actions.reset_bot_second_text(bot, call, state) + return await send_bot_second_text_menu(bot, call, state=state) if operation == "templates": await state.set_state("wait_template") async with state.proxy() as proxy: diff --git a/olgram/migrations/models/15_20240112035625_update.sql b/olgram/migrations/models/15_20240112035625_update.sql new file mode 100644 index 0000000..0038678 --- /dev/null +++ b/olgram/migrations/models/15_20240112035625_update.sql @@ -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"; diff --git a/olgram/migrations/models/16_20240112040146_update.sql b/olgram/migrations/models/16_20240112040146_update.sql new file mode 100644 index 0000000..df6f51a --- /dev/null +++ b/olgram/migrations/models/16_20240112040146_update.sql @@ -0,0 +1,4 @@ +-- upgrade -- +ALTER TABLE "bot_start_message" ADD "text" TEXT NOT NULL; +-- downgrade -- +ALTER TABLE "bot_start_message" DROP COLUMN "text"; diff --git a/olgram/migrations/models/17_20240112045126_update.sql b/olgram/migrations/models/17_20240112045126_update.sql new file mode 100644 index 0000000..16b7ffb --- /dev/null +++ b/olgram/migrations/models/17_20240112045126_update.sql @@ -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"; diff --git a/olgram/models/models.py b/olgram/models/models.py index 2209bd1..be1211e 100644 --- a/olgram/models/models.py +++ b/olgram/models/models.py @@ -70,6 +70,28 @@ class Bot(Model): table = 'bot' +class BotStartMessage(Model): + id = fields.IntField(pk=True) + bot = fields.ForeignKeyField("models.Bot", related_name="start_texts", on_delete=fields.CASCADE) + locale = fields.CharField(max_length=5) + text = fields.TextField() + + class Meta: + unique_together = ("bot", "locale") + 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): id = fields.IntField(pk=True) telegram_id = fields.BigIntField(index=True, unique=True) diff --git a/olgram/settings.py b/olgram/settings.py index fed833f..f6dbbda 100644 --- a/olgram/settings.py +++ b/olgram/settings.py @@ -41,7 +41,7 @@ class OlgramSettings(AbstractSettings): @classmethod def version(cls): - return "0.5.0" + return "0.6.1" @classmethod @lru_cache diff --git a/server/custom.py b/server/custom.py index 837cfd5..4bb6280 100644 --- a/server/custom.py +++ b/server/custom.py @@ -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, BotStartMessage, BotSecondMessage from locales.locale import _, translators from server.inlines import inline_handler @@ -78,26 +78,29 @@ async def send_user_message(message: types.Message, super_chat_id: int, bot): if message.from_user.username: user_info += " | @" + message.from_user.username 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) else: # Не добавлять информацию в конец текста, информация отдельным сообщением 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) await _redis.set(_message_unique_id(bot.pk, new_message_2.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, - pexpire=ServerSettings.redis_timeout_ms()) - return new_message else: try: new_message = await message.forward(super_chat_id) except exceptions.MessageCantBeForwarded: 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()) - return new_message + + await _redis.set(_message_unique_id(bot.pk, new_message.message_id), message.chat.id, + 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): @@ -158,7 +161,9 @@ async def handle_user_message(message: types.Message, super_chat_id: int, bot): 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) if send_auto: - 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): @@ -218,7 +223,8 @@ async def message_handler(message: types.Message, *args, **kwargs): if message.text and message.text == "/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: text += _(ServerSettings.append_text()) return SendMessage(chat_id=message.chat.id, text=text, parse_mode="HTML")