mirror of
				https://github.com/civsocit/olgram.git
				synced 2025-10-29 11:23:25 +00:00 
			
		
		
		
	Миграции
This commit is contained in:
		
							parent
							
								
									c5e0192d24
								
							
						
					
					
						commit
						415ec12b2f
					
				
							
								
								
									
										2
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | |||
| .idea | ||||
| venv | ||||
							
								
								
									
										10
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| FROM python:3.8-buster | ||||
| 
 | ||||
| COPY . /app | ||||
| 
 | ||||
| WORKDIR /app | ||||
| 
 | ||||
| RUN pip install --upgrade pip && \ | ||||
|     pip install -r requirements.txt | ||||
| 
 | ||||
| ENTRYPOINT ["./docker-entrypoint.sh"] | ||||
|  | @ -5,7 +5,4 @@ Open-source self-hosted Livegram alternative | |||
| 
 | ||||
| ##### | ||||
| 
 | ||||
| instance поведение | ||||
| Кто-то написал сообщение в любом чате - переслать в супер-чат | ||||
| Кто-то ответил на сообщение в супер-чате - переслать автору сообщения | ||||
| Кто-то написал /start - отправить стартовое сообщение | ||||
| (TODO: readme) | ||||
							
								
								
									
										4
									
								
								aerich.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								aerich.ini
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| [aerich] | ||||
| tortoise_orm = olgram.settings.TORTOISE_ORM | ||||
| location = ./olgram/migrations | ||||
| src_folder = . | ||||
							
								
								
									
										74
									
								
								bot.py
									
									
									
									
									
								
							
							
						
						
									
										74
									
								
								bot.py
									
									
									
									
									
								
							|  | @ -1,74 +0,0 @@ | |||
| import asyncio | ||||
| 
 | ||||
| import aiogram.types | ||||
| from aiogram import Bot as AioBot, Dispatcher | ||||
| from aiogram.contrib.fsm_storage.memory import MemoryStorage | ||||
| 
 | ||||
| from olgram.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 | ||||
| 
 | ||||
| from olgram.models.models import Bot, GroupChat | ||||
| 
 | ||||
| from instance.bot import BotInstance | ||||
| 
 | ||||
| import typing as ty | ||||
| 
 | ||||
| 
 | ||||
| async def invite_callback(identify: int, message: aiogram.types.Message): | ||||
|     bot = await Bot.get(id=identify) | ||||
|     chat, _ = await GroupChat.get_or_create(chat_id=message.chat.id, | ||||
|                                             defaults={"name": message.chat.full_name}) | ||||
|     if chat not in await bot.group_chats.all(): | ||||
|         await bot.group_chats.add(chat) | ||||
| 
 | ||||
| 
 | ||||
| async def left_callback(identify: int, message: aiogram.types.Message): | ||||
|     bot = await Bot.get(id=identify) | ||||
| 
 | ||||
|     chat = await bot.group_chats.get_or_none(chat_id=message.chat.id) | ||||
|     if chat: | ||||
|         await bot.group_chats.remove(chat) | ||||
| 
 | ||||
| 
 | ||||
| def run_bot(bot: BotInstance, loop: ty.Optional[asyncio.AbstractEventLoop] = None): | ||||
|     loop = loop or asyncio.get_event_loop() | ||||
|     loop.create_task(bot.start_polling()) | ||||
| 
 | ||||
| 
 | ||||
| async def run_all_bots(loop: asyncio.AbstractEventLoop): | ||||
|     bots = await Bot.all() | ||||
|     for bot in bots: | ||||
|         run_bot(BotInstance(bot.token, | ||||
|                             bot.super_chat_id, | ||||
|                             bot.start_text, | ||||
|                             invite_callback=invite_callback, | ||||
|                             left_callback=left_callback, | ||||
|                             identify=bot.id), loop) | ||||
| 
 | ||||
| 
 | ||||
| def main(): | ||||
|     """ | ||||
|     Classic polling | ||||
|     """ | ||||
|     loop = asyncio.get_event_loop() | ||||
|     loop.run_until_complete(init_database()) | ||||
| 
 | ||||
|     bot = AioBot(BotSettings.token()) | ||||
|     dp = Dispatcher(bot, storage=MemoryStorage()) | ||||
| 
 | ||||
|     start_router.setup(dp) | ||||
|     bots_router.setup(dp) | ||||
|     bot_router.setup(dp) | ||||
| 
 | ||||
|     loop.run_until_complete(run_all_bots(loop)) | ||||
|     loop.create_task(dp.start_polling()) | ||||
| 
 | ||||
|     loop.run_forever() | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     main() | ||||
							
								
								
									
										30
									
								
								docker-compose-release.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								docker-compose-release.yaml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| version: '3' | ||||
| services: | ||||
|   postgres: | ||||
|     image: kartoza/postgis | ||||
|     restart: unless-stopped | ||||
|     env_file: | ||||
|       - release.env | ||||
|     volumes: | ||||
|       - database:/var/lib/postgresql/data | ||||
|   redis: | ||||
|     image: 'bitnami/redis:latest' | ||||
|     restart: unless-stopped | ||||
|     environment: | ||||
|       - ALLOW_EMPTY_PASSWORD=yes | ||||
|     volumes: | ||||
|       - redis-db:/bitnami/redis/data | ||||
|     env_file: | ||||
|       - release.env | ||||
|   bot: | ||||
|     build: . | ||||
|     restart: unless-stopped | ||||
|     env_file: | ||||
|       - release.env | ||||
|     depends_on: | ||||
|       - postgres | ||||
|       - redis | ||||
| 
 | ||||
| volumes: | ||||
|   database: | ||||
|   redis-db: | ||||
							
								
								
									
										5
									
								
								docker-entrypoint.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								docker-entrypoint.sh
									
									
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| #!/bin/sh | ||||
| set -e | ||||
| 
 | ||||
| aerich upgrade | ||||
| python main.py | ||||
							
								
								
									
										65
									
								
								extendedinstance/bot.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								extendedinstance/bot.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,65 @@ | |||
| from aiogram import types | ||||
| import asyncio | ||||
| import aiocache | ||||
| import typing as ty | ||||
| from instance.bot import BotInstance, BotProperties | ||||
| from olgram.models.models import Bot, GroupChat | ||||
| 
 | ||||
| 
 | ||||
| class BotInstanceDatabase(BotInstance): | ||||
|     _instances: ty.Dict[int, "BotInstanceDatabase"] = dict() | ||||
| 
 | ||||
|     def __init__(self, bot: Bot): | ||||
|         self._bot = bot | ||||
|         super().__init__() | ||||
| 
 | ||||
|     @classmethod | ||||
|     async def run_all(cls): | ||||
|         bots = await Bot.all() | ||||
|         for bot in bots: | ||||
|             cls._instances[bot.id] = BotInstanceDatabase(bot) | ||||
|             # Polling только для отладки | ||||
|             asyncio.get_event_loop().create_task(cls._instances[bot.id].start_polling()) | ||||
| 
 | ||||
|     @classmethod | ||||
|     async def on_delete(cls, instance: Bot): | ||||
|         # Polling только для отладки | ||||
|         cls._instances[instance.id].stop_polling() | ||||
|         cls._instances.pop(instance.id) | ||||
| 
 | ||||
|     @classmethod | ||||
|     async def on_create(cls, instance: Bot): | ||||
|         # Polling только для отладки | ||||
|         cls._instances[instance.id] = BotInstanceDatabase(instance) | ||||
|         asyncio.get_event_loop().create_task(cls._instances[instance.id].start_polling()) | ||||
| 
 | ||||
|     @aiocache.cached(ttl=5) | ||||
|     async def _properties(self) -> BotProperties: | ||||
|         await self._bot.refresh_from_db() | ||||
|         return BotProperties(self._bot.token, self._bot.start_text, int(self._bot.token.split(":")[0]), | ||||
|                              await self._bot.super_chat_id()) | ||||
| 
 | ||||
|     async def _setup(self): | ||||
|         await super()._setup() | ||||
|         # Callback-и на добавление бота в чат и удаление бота из чата | ||||
|         self._dp.register_message_handler(self._receive_invite, content_types=[types.ContentType.NEW_CHAT_MEMBERS]) | ||||
|         self._dp.register_message_handler(self._receive_left, content_types=[types.ContentType.LEFT_CHAT_MEMBER]) | ||||
| 
 | ||||
|     async def _receive_invite(self, message: types.Message): | ||||
|         for member in message.new_chat_members: | ||||
|             if member.id == message.bot.id: | ||||
|                 chat, _ = await GroupChat.get_or_create(chat_id=message.chat.id, | ||||
|                                                         defaults={"name": message.chat.full_name}) | ||||
|                 if chat not in await self._bot.group_chats.all(): | ||||
|                     await self._bot.group_chats.add(chat) | ||||
|                     await self._bot.save() | ||||
|                 break | ||||
| 
 | ||||
|     async def _receive_left(self, message: types.Message): | ||||
|         if message.left_chat_member.id == message.bot.id: | ||||
|             chat = await self._bot.group_chats.filter(chat_id=message.chat.id).first() | ||||
|             if chat: | ||||
|                 await self._bot.group_chats.remove(chat) | ||||
|                 if self._bot.group_chat == chat: | ||||
|                     self._bot.group_chat = None | ||||
|                     await self._bot.save(update_fields=["group_chat"]) | ||||
|  | @ -1,36 +1,43 @@ | |||
| import asyncio | ||||
| import typing as ty | ||||
| import aiogram | ||||
| import aioredis | ||||
| from abc import ABC, abstractmethod | ||||
| from dataclasses import dataclass | ||||
| from aiogram import Dispatcher, types, exceptions | ||||
| from aiogram.contrib.fsm_storage.memory import MemoryStorage | ||||
| 
 | ||||
| from settings import InstanceSettings | ||||
| try: | ||||
|     from settings import InstanceSettings | ||||
| except ModuleNotFoundError: | ||||
|     from .settings import InstanceSettings | ||||
| 
 | ||||
| 
 | ||||
| class BotInstance: | ||||
|     def __init__(self, token: str, super_chat_id: int, start_text: str, | ||||
|                  invite_callback: ty.Optional[ty.Callable] = None, | ||||
|                  left_callback: ty.Optional[ty.Callable] = None, | ||||
|                  identify: ty.Optional[int] = None): | ||||
|         self._token = token | ||||
|         self._bot_id = self._token.split(":")[0] | ||||
|         self._super_chat_id = super_chat_id | ||||
|         self._start_text = start_text | ||||
| @dataclass() | ||||
| class BotProperties: | ||||
|     token: str | ||||
|     start_text: str | ||||
|     bot_id: int | ||||
|     super_chat_id: int | ||||
| 
 | ||||
| 
 | ||||
| class BotInstance(ABC): | ||||
|     def __init__(self): | ||||
|         self._redis: aioredis.Redis = None | ||||
|         self._dp: aiogram.Dispatcher = None | ||||
|         self._identify = identify | ||||
| 
 | ||||
|         self._invite_callback = invite_callback | ||||
|         self._left_callback = left_callback | ||||
|     @abstractmethod | ||||
|     async def _properties(self) -> BotProperties: | ||||
|         raise NotImplemented() | ||||
| 
 | ||||
|     def stop_polling(self): | ||||
|         self._dp.stop_polling() | ||||
| 
 | ||||
|     async def start_polling(self): | ||||
|     async def _setup(self): | ||||
|         self._redis = await aioredis.create_redis_pool(InstanceSettings.redis_path()) | ||||
| 
 | ||||
|         bot = aiogram.Bot(self._token) | ||||
|         props = await self._properties() | ||||
| 
 | ||||
|         bot = aiogram.Bot(props.token) | ||||
|         self._dp = Dispatcher(bot, storage=MemoryStorage()) | ||||
| 
 | ||||
|         # Здесь перечислены все типы сообщений, которые бот должен пересылать | ||||
|  | @ -43,29 +50,14 @@ class BotInstance: | |||
|                                                                                 types.ContentType.STICKER, | ||||
|                                                                                 types.ContentType.VIDEO, | ||||
|                                                                                 types.ContentType.VOICE]) | ||||
|         # Callback-и на добавление бота в чат и удаление бота из чата | ||||
|         self._dp.register_message_handler(self._receive_invite, content_types=[types.ContentType.NEW_CHAT_MEMBERS]) | ||||
|         self._dp.register_message_handler(self._receive_left, content_types=[types.ContentType.LEFT_CHAT_MEMBER]) | ||||
| 
 | ||||
|     async def start_polling(self): | ||||
|         await self._setup() | ||||
|         await self._dp.start_polling() | ||||
| 
 | ||||
|     def _message_unique_id(self, message_id) -> str: | ||||
|         return self._bot_id + "-" + str(message_id) | ||||
| 
 | ||||
|     async def _receive_invite(self, message: types.Message): | ||||
|         if not self._invite_callback: | ||||
|             return | ||||
| 
 | ||||
|         for member in message.new_chat_members: | ||||
|             if member.id == message.bot.id: | ||||
|                 await self._invite_callback(self._identify, message) | ||||
| 
 | ||||
|     async def _receive_left(self, message: types.Message): | ||||
|         if not self._left_callback: | ||||
|             return | ||||
| 
 | ||||
|         if message.left_chat_member.id == message.bot.id: | ||||
|             await self._left_callback(self._identify, message) | ||||
|     @classmethod | ||||
|     def _message_unique_id(cls, bot_id: int, message_id: int) -> str: | ||||
|         return f"{bot_id}_{message_id}" | ||||
| 
 | ||||
|     async def _receive_message(self, message: types.Message): | ||||
|         """ | ||||
|  | @ -73,20 +65,23 @@ class BotInstance: | |||
|         :param message: | ||||
|         :return: | ||||
|         """ | ||||
|         props = await self._properties() | ||||
|         if message.text and message.text.startswith("/start"): | ||||
|             # На команду start нужно ответить, не пересылая сообщение никуда | ||||
|             await message.answer(self._start_text) | ||||
|             await message.answer(props.start_text) | ||||
|             return | ||||
| 
 | ||||
|         if message.chat.id != self._super_chat_id: | ||||
|         if message.chat.id != props.super_chat_id: | ||||
|             # Это обычный чат: сообщение нужно переслать в супер-чат | ||||
|             new_message = await message.forward(self._super_chat_id) | ||||
|             await self._redis.set(self._message_unique_id(new_message.message_id), message.chat.id) | ||||
|             new_message = await message.forward(props.super_chat_id) | ||||
|             await self._redis.set(self._message_unique_id(props.bot_id, new_message.message_id), | ||||
|                                   message.chat.id) | ||||
|         else: | ||||
|             # Это супер-чат | ||||
|             if message.reply_to_message: | ||||
|                 # Ответ из супер-чата переслать тому пользователю, | ||||
|                 chat_id = await self._redis.get(self._message_unique_id(message.reply_to_message.message_id)) | ||||
|                 chat_id = await self._redis.get( | ||||
|                     self._message_unique_id(props.bot_id, message.reply_to_message.message_id)) | ||||
|                 if not chat_id: | ||||
|                     chat_id = message.reply_to_message.forward_from_chat | ||||
|                     if not chat_id: | ||||
|  | @ -99,7 +94,17 @@ class BotInstance: | |||
|                     await message.reply("Невозможно переслать сообщение: возможно, автор заблокировал бота") | ||||
|                     return | ||||
|             else: | ||||
|                 await message.forward(self._super_chat_id) | ||||
|                 await message.forward(props.super_chat_id) | ||||
| 
 | ||||
| 
 | ||||
| class FreezeBotInstance(BotInstance): | ||||
|     def __init__(self, token: str, start_text: str, super_chat_id: int): | ||||
|         super().__init__() | ||||
| 
 | ||||
|         self._props = BotProperties(token, start_text, int(token.split(":")[0]), super_chat_id) | ||||
| 
 | ||||
|     async def _properties(self) -> BotProperties: | ||||
|         return self._props | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|  | @ -108,9 +113,9 @@ if __name__ == '__main__': | |||
|     бот для пересылки сообщений. Все настройки этого бота задаются в переменных окружения на сервере. Бот работает  | ||||
|     в режиме polling | ||||
|     """ | ||||
|     bot = BotInstance( | ||||
|     bot = FreezeBotInstance( | ||||
|         InstanceSettings.token(), | ||||
|         InstanceSettings.super_chat_id(), | ||||
|         InstanceSettings.start_text() | ||||
|         InstanceSettings.start_text(), | ||||
|         InstanceSettings.super_chat_id() | ||||
|     ) | ||||
|     asyncio.get_event_loop().run_until_complete(bot.start_polling()) | ||||
|  |  | |||
							
								
								
									
										60
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,60 @@ | |||
| import asyncio | ||||
| 
 | ||||
| from aiogram import Bot as AioBot, Dispatcher | ||||
| from aiogram.contrib.fsm_storage.memory import MemoryStorage | ||||
| from tortoise.signals import post_delete, post_save | ||||
| from tortoise import Tortoise | ||||
| 
 | ||||
| from olgram.settings import BotSettings, TORTOISE_ORM | ||||
| 
 | ||||
| from olgram.commands.bots import router as bots_router | ||||
| from olgram.commands.start import router as start_router | ||||
| from olgram.commands.bot import router as bot_router | ||||
| 
 | ||||
| from olgram.models.models import Bot | ||||
| from extendedinstance.bot import BotInstanceDatabase | ||||
| 
 | ||||
| 
 | ||||
| @post_save(Bot) | ||||
| async def signal_post_save( | ||||
|     sender, | ||||
|     instance: Bot, | ||||
|     created: bool, | ||||
|     using_db, | ||||
|     update_fields, | ||||
| ) -> None: | ||||
|     if created: | ||||
|         await BotInstanceDatabase.on_create(instance) | ||||
| 
 | ||||
| 
 | ||||
| @post_delete(Bot) | ||||
| async def signal_post_delete(sender, instance: Bot, using_db) -> None: | ||||
|     await BotInstanceDatabase.on_delete(instance) | ||||
| 
 | ||||
| 
 | ||||
| async def init_database(): | ||||
|     await Tortoise.init(config=TORTOISE_ORM) | ||||
| 
 | ||||
| 
 | ||||
| def main(): | ||||
|     """ | ||||
|     Classic polling | ||||
|     """ | ||||
|     loop = asyncio.get_event_loop() | ||||
|     loop.run_until_complete(init_database()) | ||||
| 
 | ||||
|     bot = AioBot(BotSettings.token()) | ||||
|     dp = Dispatcher(bot, storage=MemoryStorage()) | ||||
| 
 | ||||
|     start_router.setup(dp) | ||||
|     bots_router.setup(dp) | ||||
|     bot_router.setup(dp) | ||||
| 
 | ||||
|     loop.run_until_complete(BotInstanceDatabase.run_all()) | ||||
|     loop.create_task(dp.start_polling()) | ||||
| 
 | ||||
|     loop.run_forever() | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     main() | ||||
|  | @ -1,90 +0,0 @@ | |||
| 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', 'bot_id', 'operation') | ||||
| # Пользователь выбрал чат | ||||
| select_bot_chat = CallbackData('chat_select', 'bot_id', 'chat_id') | ||||
| 
 | ||||
| 
 | ||||
| @router.callback_query_handler(select_bot.filter(), state="*") | ||||
| 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 | ||||
| 
 | ||||
|     await try_delete_message(call.message) | ||||
| 
 | ||||
|     keyboard = types.InlineKeyboardMarkup(row_width=2) | ||||
|     keyboard.insert(types.InlineKeyboardButton(text="Текст", | ||||
|                                                callback_data=bot_operation.new(bot_id=bot_id, operation="text"))) | ||||
|     keyboard.insert(types.InlineKeyboardButton(text="Чат", | ||||
|                                                callback_data=bot_operation.new(bot_id=bot_id, operation="chat"))) | ||||
|     keyboard.insert(types.InlineKeyboardButton(text="Удалить бот", | ||||
|                                                callback_data=bot_operation.new(bot_id=bot_id, operation="delete"))) | ||||
|     keyboard.insert(types.InlineKeyboardButton(text="<<Вернуться к списку ботов", | ||||
|                                                callback_data=bot_operation.new(bot_id=bot_id, operation="back"))) | ||||
| 
 | ||||
|     await AioBot.get_current().send_message(call.message.chat.id, dedent(f""" | ||||
|     Управление ботом @{bot.name}. | ||||
| 
 | ||||
|     Если у вас возникли вопросы по настройке бота, то посмотрите нашу справку /help. | ||||
|     """), reply_markup=keyboard) | ||||
| 
 | ||||
| 
 | ||||
| @router.callback_query_handler(bot_operation.filter(operation="delete"), state="*") | ||||
| async def delete_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 | ||||
| 
 | ||||
|     await bot.delete() | ||||
|     await call.answer("Бот удалён") | ||||
|     await try_delete_message(call.message) | ||||
| 
 | ||||
| 
 | ||||
| @router.callback_query_handler(bot_operation.filter(operation="chat"), state="*") | ||||
| async def chats_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 | ||||
| 
 | ||||
|     await try_delete_message(call.message) | ||||
| 
 | ||||
|     keyboard = types.InlineKeyboardMarkup(row_width=2) | ||||
| 
 | ||||
|     chats = await bot.group_chats.all() | ||||
| 
 | ||||
|     if not chats: | ||||
|         return await AioBot.get_current().send_message(call.message.chat.id, dedent(f""" | ||||
|         Этот бот не добавлен в чаты, поэтому все сообщения будут приходить вам в бот. | ||||
|         Чтобы подключить чат — просто добавьте бот @{bot.name} в чат. | ||||
|         """), reply_markup=keyboard) | ||||
| 
 | ||||
|     for chat in chats: | ||||
|         keyboard.insert(types.InlineKeyboardButton(text=chat.name, | ||||
|                                                    callback_data=select_bot_chat.new(bot_id=bot_id, chat_id=chat.id))) | ||||
| 
 | ||||
|     await AioBot.get_current().send_message(call.message.chat.id, dedent(f""" | ||||
|     В этом разделе вы можете привязать бота @{bot.name} к чату.  | ||||
|     Выберите чат, куда бот будет пересылать сообщения.  | ||||
|     """), reply_markup=keyboard) | ||||
							
								
								
									
										142
									
								
								olgram/commands/bot.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								olgram/commands/bot.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,142 @@ | |||
| """ | ||||
| Здесь работа с конкретным ботом | ||||
| """ | ||||
| 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', 'bot_id', 'operation') | ||||
| # Пользователь выбрал чат | ||||
| select_bot_chat = CallbackData('chat_select', 'bot_id', 'chat_id') | ||||
| # Пользователь выбрал чат - личные сообщения | ||||
| select_bot_chat_personal = CallbackData('chat_select_personal', 'bot_id') | ||||
| 
 | ||||
| 
 | ||||
| def check_bot_owner(handler): | ||||
|     """ | ||||
|     Этот декоратор запрещает пользователям вызывать callback's (inline кнопки) для ботов, которыми они не владеют | ||||
|     """ | ||||
|     async def wrapped(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 | ||||
| 
 | ||||
|         return await handler(bot, call, callback_data, state) | ||||
|     return wrapped | ||||
| 
 | ||||
| 
 | ||||
| @router.callback_query_handler(select_bot.filter(), state="*") | ||||
| @check_bot_owner | ||||
| async def select_bot_callback(bot: Bot, call: types.CallbackQuery, callback_data: dict, state: FSMContext): | ||||
|     """ | ||||
|     Пользователь выбрал бота для редактирования | ||||
|     """ | ||||
|     await try_delete_message(call.message) | ||||
| 
 | ||||
|     keyboard = types.InlineKeyboardMarkup(row_width=2) | ||||
|     keyboard.insert(types.InlineKeyboardButton(text="Текст", | ||||
|                                                callback_data=bot_operation.new(bot_id=bot.id, operation="text"))) | ||||
|     keyboard.insert(types.InlineKeyboardButton(text="Чат", | ||||
|                                                callback_data=bot_operation.new(bot_id=bot.id, operation="chat"))) | ||||
|     keyboard.insert(types.InlineKeyboardButton(text="Удалить бот", | ||||
|                                                callback_data=bot_operation.new(bot_id=bot.id, operation="delete"))) | ||||
|     keyboard.insert(types.InlineKeyboardButton(text="<<Вернуться к списку ботов", | ||||
|                                                callback_data=bot_operation.new(bot_id=bot.id, operation="back"))) | ||||
| 
 | ||||
|     await AioBot.get_current().send_message(call.message.chat.id, dedent(f""" | ||||
|     Управление ботом @{bot.name}. | ||||
| 
 | ||||
|     Если у вас возникли вопросы по настройке бота, то посмотрите нашу справку /help. | ||||
|     """), reply_markup=keyboard) | ||||
| 
 | ||||
| 
 | ||||
| @router.callback_query_handler(bot_operation.filter(operation="delete"), state="*") | ||||
| @check_bot_owner | ||||
| async def delete_bot_callback(bot: Bot, call: types.CallbackQuery, callback_data: dict, state: FSMContext): | ||||
|     """ | ||||
|     Кнопка "удалить" для бота | ||||
|     """ | ||||
|     await bot.delete() | ||||
|     await call.answer("Бот удалён") | ||||
|     await try_delete_message(call.message) | ||||
| 
 | ||||
| 
 | ||||
| @router.callback_query_handler(bot_operation.filter(operation="chat"), state="*") | ||||
| @check_bot_owner | ||||
| async def chats_bot_callback(bot: Bot, call: types.CallbackQuery, callback_data: dict, state: FSMContext): | ||||
|     """ | ||||
|     Кнопка "чаты" для бота | ||||
|     """ | ||||
|     await try_delete_message(call.message) | ||||
| 
 | ||||
|     keyboard = types.InlineKeyboardMarkup(row_width=2) | ||||
| 
 | ||||
|     chats = await bot.group_chats.all() | ||||
| 
 | ||||
|     if not chats: | ||||
|         return await AioBot.get_current().send_message(call.message.chat.id, dedent(f""" | ||||
|         Этот бот не добавлен в чаты, поэтому все сообщения будут приходить вам в бот. | ||||
|         Чтобы подключить чат — просто добавьте бот @{bot.name} в чат. | ||||
|         """), reply_markup=keyboard) | ||||
| 
 | ||||
|     for chat in chats: | ||||
|         keyboard.insert(types.InlineKeyboardButton(text=chat.name, | ||||
|                                                    callback_data=select_bot_chat.new(bot_id=bot.id, chat_id=chat.id))) | ||||
|     keyboard.insert(types.InlineKeyboardButton(text="Личные сообщения", | ||||
|                                                callback_data=select_bot_chat_personal.new(bot_id=bot.id))) | ||||
|     await AioBot.get_current().send_message(call.message.chat.id, dedent(f""" | ||||
|     В этом разделе вы можете привязать бота @{bot.name} к чату.  | ||||
|     Выберите чат, куда бот будет пересылать сообщения.  | ||||
|     """), reply_markup=keyboard) | ||||
| 
 | ||||
| 
 | ||||
| @router.callback_query_handler(select_bot_chat.filter(), state="*") | ||||
| @check_bot_owner | ||||
| async def chat_selected_callback(bot: Bot, call: types.CallbackQuery, callback_data: dict, state: FSMContext): | ||||
|     """ | ||||
|     Пользователь выбрал групповой чат для бота | ||||
|     """ | ||||
|     chat_id = callback_data["chat_id"] | ||||
|     chat = await bot.group_chats.filter(id=chat_id).first() | ||||
|     if not chat: | ||||
|         await call.answer("Нельзя привязать бота к этому чату") | ||||
|         return | ||||
|     bot.group_chat = chat | ||||
|     await bot.save() | ||||
|     await call.answer(f"Выбран чат {chat.name}") | ||||
| 
 | ||||
| 
 | ||||
| @router.callback_query_handler(select_bot_chat_personal.filter(), state="*") | ||||
| @check_bot_owner | ||||
| async def chat_selected_personal_callback(bot: Bot, call: types.CallbackQuery, callback_data: dict, state: FSMContext): | ||||
|     """ | ||||
|     Пользователь выбрал личный чат для бота | ||||
|     """ | ||||
|     bot.group_chat = None | ||||
|     await bot.save() | ||||
|     await call.answer(f"Выбран личный чат") | ||||
| 
 | ||||
| 
 | ||||
| @router.callback_query_handler(bot_operation.filter(operation="text"), state="*") | ||||
| @check_bot_owner | ||||
| async def text_bot_callback(bot: Bot, call: types.CallbackQuery, callback_data: dict, state: FSMContext): | ||||
|     """ | ||||
|     Кнопка "текст" для бота | ||||
|     """ | ||||
|     await AioBot.get_current().send_message(call.message.chat.id, dedent(f""" | ||||
|     Текущий текст бота по кнопке start: | ||||
|      | ||||
|     {bot.start_text} | ||||
|     """)) | ||||
|  | @ -1,3 +1,6 @@ | |||
| """ | ||||
| Здесь работа с ботами на первом уровне вложенности: список ботов, добавление ботов | ||||
| """ | ||||
| from aiogram import types, Bot as AioBot | ||||
| from aiogram.dispatcher import FSMContext | ||||
| from aiogram.utils.exceptions import Unauthorized, TelegramAPIError | ||||
|  | @ -1,3 +1,7 @@ | |||
| """ | ||||
| Здесь простые команды на первом уровне вложенности: /start /help | ||||
| """ | ||||
| 
 | ||||
| from aiogram import types | ||||
| from aiogram.dispatcher import FSMContext | ||||
| from textwrap import dedent | ||||
							
								
								
									
										30
									
								
								olgram/migrations/models/0_20210711121349_init.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								olgram/migrations/models/0_20210711121349_init.sql
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| -- upgrade -- | ||||
| CREATE TABLE IF NOT EXISTS "group_chat" ( | ||||
|     "id" SERIAL NOT NULL PRIMARY KEY, | ||||
|     "chat_id" INT NOT NULL UNIQUE, | ||||
|     "name" VARCHAR(50) NOT NULL | ||||
| ); | ||||
| CREATE INDEX IF NOT EXISTS "idx_group_chat_chat_id_5da32d" ON "group_chat" ("chat_id"); | ||||
| CREATE TABLE IF NOT EXISTS "user" ( | ||||
|     "id" SERIAL NOT NULL PRIMARY KEY, | ||||
|     "telegram_id" INT NOT NULL UNIQUE | ||||
| ); | ||||
| CREATE INDEX IF NOT EXISTS "idx_user_telegra_66ffbd" ON "user" ("telegram_id"); | ||||
| CREATE TABLE IF NOT EXISTS "bot" ( | ||||
|     "id" SERIAL NOT NULL PRIMARY KEY, | ||||
|     "token" VARCHAR(50) NOT NULL UNIQUE, | ||||
|     "name" VARCHAR(33) NOT NULL, | ||||
|     "start_text" TEXT NOT NULL, | ||||
|     "group_chat_id" INT REFERENCES "group_chat" ("id") ON DELETE SET NULL, | ||||
|     "owner_id" INT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE | ||||
| ); | ||||
| CREATE TABLE IF NOT EXISTS "aerich" ( | ||||
|     "id" SERIAL NOT NULL PRIMARY KEY, | ||||
|     "version" VARCHAR(255) NOT NULL, | ||||
|     "app" VARCHAR(20) NOT NULL, | ||||
|     "content" JSONB NOT NULL | ||||
| ); | ||||
| CREATE TABLE IF NOT EXISTS "bot_group_chat" ( | ||||
|     "bot_id" INT NOT NULL REFERENCES "bot" ("id") ON DELETE SET NULL, | ||||
|     "groupchat_id" INT NOT NULL REFERENCES "group_chat" ("id") ON DELETE SET NULL | ||||
| ); | ||||
|  | @ -11,11 +11,19 @@ class Bot(Model): | |||
|     name = fields.CharField(max_length=33) | ||||
|     start_text = fields.TextField(default=dedent(""" | ||||
|     Здравствуйте! | ||||
|     Напишите ваш вопрос и мы ответим Вам в ближайшее время. | ||||
|     Напишите ваш вопрос и мы ответим вам в ближайшее время. | ||||
|     """)) | ||||
| 
 | ||||
|     super_chat_id = fields.IntField() | ||||
|     group_chats = fields.ManyToManyField("models.GroupChat", related_name="bots", on_delete=fields.relational.SET_NULL) | ||||
|     group_chat = fields.ForeignKeyField("models.GroupChat", related_name="active_bots", | ||||
|                                         on_delete=fields.relational.SET_NULL, | ||||
|                                         null=True) | ||||
| 
 | ||||
|     async def super_chat_id(self): | ||||
|         group_chat = await self.group_chat | ||||
|         if group_chat: | ||||
|             return group_chat.chat_id | ||||
|         return (await self.owner).telegram_id | ||||
| 
 | ||||
|     class Meta: | ||||
|         table = 'bot' | ||||
|  |  | |||
|  | @ -47,3 +47,19 @@ class DatabaseSettings(AbstractSettings): | |||
|     @classmethod | ||||
|     def database_name(cls) -> str: | ||||
|         return cls._get_env("POSTGRES_DB") | ||||
| 
 | ||||
|     @classmethod | ||||
|     def host(cls) -> str: | ||||
|         return cls._get_env("POSTGRES_HOST") | ||||
| 
 | ||||
| 
 | ||||
| TORTOISE_ORM = { | ||||
|     "connections": {"default": f'postgres://{DatabaseSettings.user()}:{DatabaseSettings.password()}' | ||||
|                                f'@{DatabaseSettings.host()}/{DatabaseSettings.database_name()}'}, | ||||
|     "apps": { | ||||
|         "models": { | ||||
|             "models": ["olgram.models.models", "aerich.models"], | ||||
|             "default_connection": "default", | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
|  | @ -1,15 +0,0 @@ | |||
| from tortoise import Tortoise | ||||
| from olgram.settings import DatabaseSettings | ||||
| 
 | ||||
| 
 | ||||
| async def init_database(): | ||||
|     # Here we create a SQLite DB using file "db.sqlite3" | ||||
|     #  also specify the app name of "models" | ||||
|     #  which contain models from "app.models" | ||||
|     await Tortoise.init( | ||||
|         db_url=f'postgres://{DatabaseSettings.user()}:{DatabaseSettings.password()}' | ||||
|                f'@localhost:5430/{DatabaseSettings.database_name()}', | ||||
|         modules={'models': ['olgram.models.models']} | ||||
|     ) | ||||
|     # Generate the schema | ||||
|     await Tortoise.generate_schemas() | ||||
|  | @ -1,5 +1,6 @@ | |||
| aiogram | ||||
| tortoise-orm[asyncpg] | ||||
| aerich | ||||
| aerich==0.5.4 | ||||
| python-dotenv | ||||
| aioredis | ||||
| aioredis | ||||
| aiocache | ||||
|  | @ -1 +0,0 @@ | |||
| 
 | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user