Миграции

This commit is contained in:
mihalin 2021-07-11 12:53:33 +03:00
parent c5e0192d24
commit 415ec12b2f
22 changed files with 435 additions and 233 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
.idea
venv

10
Dockerfile Normal file
View 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"]

View File

@ -5,7 +5,4 @@ Open-source self-hosted Livegram alternative
##### #####
instance поведение (TODO: readme)
Кто-то написал сообщение в любом чате - переслать в супер-чат
Кто-то ответил на сообщение в супер-чате - переслать автору сообщения
Кто-то написал /start - отправить стартовое сообщение

4
aerich.ini Normal file
View File

@ -0,0 +1,4 @@
[aerich]
tortoise_orm = olgram.settings.TORTOISE_ORM
location = ./olgram/migrations
src_folder = .

74
bot.py
View File

@ -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()

View 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
View File

@ -0,0 +1,5 @@
#!/bin/sh
set -e
aerich upgrade
python main.py

65
extendedinstance/bot.py Normal file
View 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"])

View File

@ -1,36 +1,43 @@
import asyncio import asyncio
import typing as ty
import aiogram import aiogram
import aioredis import aioredis
from abc import ABC, abstractmethod
from dataclasses import dataclass
from aiogram import Dispatcher, types, exceptions from aiogram import Dispatcher, types, exceptions
from aiogram.contrib.fsm_storage.memory import MemoryStorage from aiogram.contrib.fsm_storage.memory import MemoryStorage
try:
from settings import InstanceSettings from settings import InstanceSettings
except ModuleNotFoundError:
from .settings import InstanceSettings
class BotInstance: @dataclass()
def __init__(self, token: str, super_chat_id: int, start_text: str, class BotProperties:
invite_callback: ty.Optional[ty.Callable] = None, token: str
left_callback: ty.Optional[ty.Callable] = None, start_text: str
identify: ty.Optional[int] = None): bot_id: int
self._token = token super_chat_id: int
self._bot_id = self._token.split(":")[0]
self._super_chat_id = super_chat_id
self._start_text = start_text class BotInstance(ABC):
def __init__(self):
self._redis: aioredis.Redis = None self._redis: aioredis.Redis = None
self._dp: aiogram.Dispatcher = None self._dp: aiogram.Dispatcher = None
self._identify = identify
self._invite_callback = invite_callback @abstractmethod
self._left_callback = left_callback async def _properties(self) -> BotProperties:
raise NotImplemented()
def stop_polling(self): def stop_polling(self):
self._dp.stop_polling() self._dp.stop_polling()
async def start_polling(self): async def _setup(self):
self._redis = await aioredis.create_redis_pool(InstanceSettings.redis_path()) 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()) self._dp = Dispatcher(bot, storage=MemoryStorage())
# Здесь перечислены все типы сообщений, которые бот должен пересылать # Здесь перечислены все типы сообщений, которые бот должен пересылать
@ -43,29 +50,14 @@ class BotInstance:
types.ContentType.STICKER, types.ContentType.STICKER,
types.ContentType.VIDEO, types.ContentType.VIDEO,
types.ContentType.VOICE]) 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() await self._dp.start_polling()
def _message_unique_id(self, message_id) -> str: @classmethod
return self._bot_id + "-" + str(message_id) def _message_unique_id(cls, bot_id: int, message_id: int) -> str:
return f"{bot_id}_{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)
async def _receive_message(self, message: types.Message): async def _receive_message(self, message: types.Message):
""" """
@ -73,20 +65,23 @@ class BotInstance:
:param message: :param message:
:return: :return:
""" """
props = await self._properties()
if message.text and message.text.startswith("/start"): if message.text and message.text.startswith("/start"):
# На команду start нужно ответить, не пересылая сообщение никуда # На команду start нужно ответить, не пересылая сообщение никуда
await message.answer(self._start_text) await message.answer(props.start_text)
return 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) new_message = await message.forward(props.super_chat_id)
await self._redis.set(self._message_unique_id(new_message.message_id), message.chat.id) await self._redis.set(self._message_unique_id(props.bot_id, new_message.message_id),
message.chat.id)
else: else:
# Это супер-чат # Это супер-чат
if message.reply_to_message: 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: if not chat_id:
chat_id = message.reply_to_message.forward_from_chat chat_id = message.reply_to_message.forward_from_chat
if not chat_id: if not chat_id:
@ -99,7 +94,17 @@ class BotInstance:
await message.reply("Невозможно переслать сообщение: возможно, автор заблокировал бота") await message.reply("Невозможно переслать сообщение: возможно, автор заблокировал бота")
return return
else: 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__': if __name__ == '__main__':
@ -108,9 +113,9 @@ if __name__ == '__main__':
бот для пересылки сообщений. Все настройки этого бота задаются в переменных окружения на сервере. Бот работает бот для пересылки сообщений. Все настройки этого бота задаются в переменных окружения на сервере. Бот работает
в режиме polling в режиме polling
""" """
bot = BotInstance( bot = FreezeBotInstance(
InstanceSettings.token(), 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()) asyncio.get_event_loop().run_until_complete(bot.start_polling())

60
main.py Normal file
View 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()

View File

@ -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
View 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}
"""))

View File

@ -1,3 +1,6 @@
"""
Здесь работа с ботами на первом уровне вложенности: список ботов, добавление ботов
"""
from aiogram import types, Bot as AioBot from aiogram import types, Bot as AioBot
from aiogram.dispatcher import FSMContext from aiogram.dispatcher import FSMContext
from aiogram.utils.exceptions import Unauthorized, TelegramAPIError from aiogram.utils.exceptions import Unauthorized, TelegramAPIError

View File

@ -1,3 +1,7 @@
"""
Здесь простые команды на первом уровне вложенности: /start /help
"""
from aiogram import types from aiogram import types
from aiogram.dispatcher import FSMContext from aiogram.dispatcher import FSMContext
from textwrap import dedent from textwrap import dedent

View 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
);

View File

@ -11,11 +11,19 @@ class Bot(Model):
name = fields.CharField(max_length=33) name = fields.CharField(max_length=33)
start_text = fields.TextField(default=dedent(""" 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_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: class Meta:
table = 'bot' table = 'bot'

View File

@ -47,3 +47,19 @@ class DatabaseSettings(AbstractSettings):
@classmethod @classmethod
def database_name(cls) -> str: def database_name(cls) -> str:
return cls._get_env("POSTGRES_DB") 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",
},
},
}

View File

@ -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()

View File

@ -1,5 +1,6 @@
aiogram aiogram
tortoise-orm[asyncpg] tortoise-orm[asyncpg]
aerich aerich==0.5.4
python-dotenv python-dotenv
aioredis aioredis
aiocache

View File

@ -1 +0,0 @@