From d738da2fa9dead31cced8c03320000ba9c4f7179 Mon Sep 17 00:00:00 2001 From: mihalin Date: Tue, 29 Jun 2021 15:29:41 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=BC=D0=B5=D1=80=20Instance?= =?UTF-8?q?=20=D0=B1=D0=BE=D1=82=D0=B0,=20=D1=80=D0=B0=D0=B7=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +-- bot.py | 3 +- docker-compose.yaml | 11 ++++- instance/Dockerfile | 10 ++++ instance/__init__.py | 0 instance/bot.py | 77 +++++++++++++++++++++++++++++ olgram/bot/bot.py | 46 +++++++++++++++++ olgram/bot/bots.py | 48 +++++++++++++----- olgram/bot/start.py | 10 ++-- olgram/models/{bot.py => models.py} | 8 +++ olgram/models/user.py | 9 ---- settings.py => olgram/settings.py | 10 ++++ olgram/utils/database.py | 4 +- olgram/utils/mix.py | 9 ++++ requirements.txt | 1 + 15 files changed, 221 insertions(+), 33 deletions(-) create mode 100644 instance/Dockerfile create mode 100644 instance/__init__.py create mode 100644 instance/bot.py create mode 100644 olgram/bot/bot.py rename olgram/models/{bot.py => models.py} (67%) delete mode 100644 olgram/models/user.py rename settings.py => olgram/settings.py (74%) create mode 100644 olgram/utils/mix.py diff --git a/README.md b/README.md index b3242b6..58a12ef 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Open-source self-hosted Livegram alternative ##### -таблицы - -Боты: токен, имя, владелец -Пользователи: идентификатор +instance поведение +Кто-то написал сообщение в любом чате - переслать в супер-чат +Кто-то ответил на сообщение в супер-чате - переслать автору сообщения +Кто-то написал /start - отправить стартовое сообщение \ No newline at end of file diff --git a/bot.py b/bot.py index 65ea281..7d65eb8 100644 --- a/bot.py +++ b/bot.py @@ -6,7 +6,7 @@ from settings import BotSettings from olgram.bot.bots import router as bots_router from olgram.bot.start import router as start_router - +from olgram.bot.bot import router as bot_router from olgram.utils.database import init_database @@ -21,6 +21,7 @@ def main(): start_router.setup(dp) bots_router.setup(dp) + bot_router.setup(dp) executor.start_polling(dp, skip_updates=True) diff --git a/docker-compose.yaml b/docker-compose.yaml index 0ce7388..609d98a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -7,9 +7,18 @@ services: - POSTGRES_PASSWORD=test_passwd - POSTGRES_DB=olgram ports: - - '5431:5432' + - '5430:5432' volumes: - database:/var/lib/postgresql/data + redis: + image: 'bitnami/redis:latest' + environment: + - ALLOW_EMPTY_PASSWORD=yes + volumes: + - redis-db:/bitnami/redis/data + ports: + - '6370:6379' volumes: database: + redis-db: diff --git a/instance/Dockerfile b/instance/Dockerfile new file mode 100644 index 0000000..9d36986 --- /dev/null +++ b/instance/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.8-buster + +COPY . /app + +WORKDIR /app + +RUN pip install --upgrade pip && \ + pip install -r requirements.txt && \ + +CMD ["python", "instance"] diff --git a/instance/__init__.py b/instance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/instance/bot.py b/instance/bot.py new file mode 100644 index 0000000..5ad70c8 --- /dev/null +++ b/instance/bot.py @@ -0,0 +1,77 @@ +import asyncio +import aioredis +import typing as ty +from aiogram import Bot, Dispatcher, executor, types, exceptions +from aiogram.contrib.fsm_storage.memory import MemoryStorage + +from olgram.utils.router import Router + +token = "(token)" +bot_id = token.split(":")[0] +start_text = 'Здравствуйте! Напишите тут что-то' +super_chat_id = -1 # ID чата здесь + +router = Router() +redis: ty.Optional[aioredis.Redis] = None + + +def message_unique_id(message_id) -> str: + return bot_id + "-" + str(message_id) + + +@router.message_handler(content_types=[types.ContentType.ANY]) +async def receive_text(message: types.Message): + """ + Some text received + :param message: + :return: + """ + if message.text and message.text.startswith("/start"): + await message.answer(start_text) + return + + if message.chat.id != super_chat_id: + # Это обычный чат + new_message = await message.forward(super_chat_id) + await redis.set(message_unique_id(new_message.message_id), message.chat.id) + else: + # Это чат, в который бот должен пересылать сообщения + if message.reply_to_message: + chat_id = await redis.get(message_unique_id(message.reply_to_message.message_id)) + if not chat_id: + chat_id = message.reply_to_message.forward_from_chat + if not chat_id: + await message.reply("Невозможно ответить, автор сообщения не найден") + return + chat_id = int(chat_id) + try: + await message.copy_to(chat_id) + except exceptions.MessageError: + await message.reply("Невозможно отправить сообщение пользователю: возможно, он заблокировал бота") + return + + else: + await message.forward(super_chat_id) + + +async def init_redis(): + global redis + redis = await aioredis.create_redis_pool('redis://localhost:6370') + + +def main(): + """ + Classic polling + """ + + asyncio.get_event_loop().run_until_complete(init_redis()) + + bot = Bot(token) + dp = Dispatcher(bot, storage=MemoryStorage()) + router.setup(dp) + + executor.start_polling(dp, skip_updates=True) + + +if __name__ == '__main__': + main() diff --git a/olgram/bot/bot.py b/olgram/bot/bot.py new file mode 100644 index 0000000..5cad383 --- /dev/null +++ b/olgram/bot/bot.py @@ -0,0 +1,46 @@ +from aiogram import types, Bot as AioBot +from aiogram.dispatcher import FSMContext +from aiogram.utils.callback_data import CallbackData +from textwrap import dedent + +from olgram.utils.router import Router +from olgram.utils.mix import try_delete_message +from olgram.models.models import Bot, User + +router = Router() + +# Пользователь выбрал бота +select_bot = CallbackData('bot_select', 'bot_id') +# Пользователь выбрал, что хочет сделать со своим ботом +bot_operation = CallbackData('bot_operation', 'operation') + + +@router.callback_query_handler(select_bot.filter()) +async def select_bot_callback(call: types.CallbackQuery, callback_data: dict, state: FSMContext): + """ + Пользователь выбрал бота для редактирования + """ + bot_id = callback_data["bot_id"] + bot = await Bot.get_or_none(id=bot_id) + if not bot or (await bot.owner).telegram_id != call.from_user.id: + await call.answer("Такого бота нет", show_alert=True) + return + + async with state.proxy() as proxy: + proxy["bot"] = bot + + await try_delete_message(call.message) + + keyboard = types.InlineKeyboardMarkup(row_width=2) + keyboard.insert(types.InlineKeyboardButton(text="Текст", callback_data=bot_operation.new(operation="text"))) + keyboard.insert(types.InlineKeyboardButton(text="Чат", callback_data=bot_operation.new(operation="chat"))) + keyboard.insert(types.InlineKeyboardButton(text="Удалить бот", callback_data=bot_operation.new(operation="delete"))) + keyboard.insert(types.InlineKeyboardButton(text="<<Вернуться к списку ботов", + callback_data=bot_operation.new(operation="back"))) + + await AioBot.get_current().send_message(call.message.chat.id, dedent(f""" + Управление ботом @{bot.name}. + + Если у вас возникли вопросы по настройке бота, то посмотрите нашу справку /help. + """), reply_markup=keyboard) + diff --git a/olgram/bot/bots.py b/olgram/bot/bots.py index 0e42163..2d70dcb 100644 --- a/olgram/bot/bots.py +++ b/olgram/bot/bots.py @@ -5,40 +5,65 @@ import re from textwrap import dedent from ..utils.router import Router -from olgram.models.bot import Bot -from olgram.models.user import User +from .bot import select_bot +from olgram.models.models import Bot, User +from olgram.settings import OlgramSettings router = Router() token_pattern = r'[0-9]{8,10}:[a-zA-Z0-9_-]{35}' -@router.message_handler(commands=["my_bots"]) +@router.message_handler(commands=["mybots"], state="*") async def my_bots(message: types.Message, state: FSMContext): + """ + Команда /mybots (список ботов) + """ user = await User.get_or_none(telegram_id=message.from_user.id) bots = await Bot.filter(owner=user) if not bots: await message.answer(dedent(""" У вас нет добавленных ботов. - Отправьте команду /add_bot, чтобы добавить бот. + Отправьте команду /addbot, чтобы добавить бот. """)) return - bots_str = "\n".join(["@" + bot.name for bot in bots]) - await message.answer(dedent(f""" - Ваши боты: - {bots_str} - """)) + keyboard = types.InlineKeyboardMarkup(row_width=2) + for bot in bots: + keyboard.insert(types.InlineKeyboardButton(text="@" + bot.name, callback_data=select_bot.new(bot_id=bot.id))) + + await message.answer("Ваши боты", reply_markup=keyboard) -@router.message_handler(commands=["add_bot"]) +@router.message_handler(commands=["addbot"], state="*") async def add_bot(message: types.Message, state: FSMContext): - await message.answer("Окей, пришли тогда токен plz") + """ + Команда /addbot (добавить бота) + """ + bot_count = await Bot.filter(user__telegram_id=message.from_user.id).count() + if bot_count > OlgramSettings.max_bots_per_user(): + await message.answer("У вас уже слишком много ботов") + return + + await message.answer(dedent(""" + Чтобы подключить бот, вам нужно выполнить три действия: + + 1. Перейдите в бот @BotFather, нажмите START и отправьте команду /newbot + 2. Введите название бота, а потом username бота. + 3. После создания бота перешлите ответное сообщение в этот бот или скопируйте и пришлите token бота. + + Важно: не подключайте боты, которые используются в других сервисах (Manybot, Chatfuel, Livegram и других). + + Подробную инструкцию по созданию бота читайте здесь /help) + """)) await state.set_state("add_bot") @router.message_handler(state="add_bot", content_types="text", regexp="^[^/].+") # Not command async def bot_added(message: types.Message, state: FSMContext): + """ + Пользователь добавляет бота и мы ждём от него токен + """ token = re.findall(token_pattern, message.text) async def on_invalid_token(): @@ -66,6 +91,7 @@ async def bot_added(message: types.Message, state: FSMContext): try: test_bot = AioBot(token) test_bot_info = await test_bot.get_me() + await test_bot.session.close() except ValueError: return await on_invalid_token() except Unauthorized: diff --git a/olgram/bot/start.py b/olgram/bot/start.py index 52fda17..6271ab9 100644 --- a/olgram/bot/start.py +++ b/olgram/bot/start.py @@ -1,4 +1,4 @@ -from aiogram import Bot, Dispatcher, executor, types +from aiogram import types from aiogram.dispatcher import FSMContext from textwrap import dedent from ..utils.router import Router @@ -9,7 +9,7 @@ router = Router() @router.message_handler(commands=["start"], state="*") async def start(message: types.Message, state: FSMContext): """ - Start command handler + Команда /start """ await state.reset_state() @@ -20,8 +20,8 @@ async def start(message: types.Message, state: FSMContext): Используйте эти команды, чтобы управлять этим ботом: - /add_bot - добавить бот - /my_bots - управление ботами + /addbot - добавить бот + /mybots - управление ботами /help - помощь @@ -32,7 +32,7 @@ async def start(message: types.Message, state: FSMContext): @router.message_handler(commands=["help"], state="*") async def help(message: types.Message, state: FSMContext): """ - Help command handler + Команда /help """ await message.answer(dedent(""" diff --git a/olgram/models/bot.py b/olgram/models/models.py similarity index 67% rename from olgram/models/bot.py rename to olgram/models/models.py index 0f85a32..dee8349 100644 --- a/olgram/models/bot.py +++ b/olgram/models/models.py @@ -10,3 +10,11 @@ class Bot(Model): class Meta: table = 'bot' + + +class User(Model): + id = fields.IntField(pk=True) + telegram_id = fields.IntField(index=True, unique=True) + + class Meta: + table = 'user' diff --git a/olgram/models/user.py b/olgram/models/user.py deleted file mode 100644 index 2c7b351..0000000 --- a/olgram/models/user.py +++ /dev/null @@ -1,9 +0,0 @@ -from tortoise import Model, fields - - -class User(Model): - id = fields.IntField(pk=True) - telegram_id = fields.IntField(index=True, unique=True) - - class Meta: - table = 'user' diff --git a/settings.py b/olgram/settings.py similarity index 74% rename from settings.py rename to olgram/settings.py index fcd3f8d..6d0f373 100644 --- a/settings.py +++ b/olgram/settings.py @@ -15,6 +15,16 @@ class _Settings(ABC): return parameter +class OlgramSettings(_Settings): + @classmethod + def max_bots_per_user(cls) -> int: + """ + Максимальное количество ботов у одного пользователя + :return: int + """ + return 5 + + class BotSettings(_Settings): @classmethod def token(cls) -> str: diff --git a/olgram/utils/database.py b/olgram/utils/database.py index 585b5df..4c81d85 100644 --- a/olgram/utils/database.py +++ b/olgram/utils/database.py @@ -8,8 +8,8 @@ async def init_database(): # which contain models from "app.models" await Tortoise.init( db_url=f'postgres://{DatabaseSettings.user()}:{DatabaseSettings.password()}' - f'@localhost:5431/{DatabaseSettings.database_name()}', - modules={'models': ['olgram.models.bot', 'olgram.models.user']} + f'@localhost:5430/{DatabaseSettings.database_name()}', + modules={'models': ['olgram.models.models']} ) # Generate the schema await Tortoise.generate_schemas() diff --git a/olgram/utils/mix.py b/olgram/utils/mix.py new file mode 100644 index 0000000..57b4802 --- /dev/null +++ b/olgram/utils/mix.py @@ -0,0 +1,9 @@ +from aiogram.types import Message +from aiogram.utils.exceptions import TelegramAPIError + + +async def try_delete_message(message: Message): + try: + await message.delete() + except TelegramAPIError: + pass diff --git a/requirements.txt b/requirements.txt index f07d7da..e32cd67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ aiogram tortoise-orm[asyncpg] aerich python-dotenv +aioredis \ No newline at end of file