diff --git a/anymusicbot/__init__.py b/bot/__init__.py similarity index 82% rename from anymusicbot/__init__.py rename to bot/__init__.py index ab42a3e..d3bb4fd 100644 --- a/anymusicbot/__init__.py +++ b/bot/__init__.py @@ -17,5 +17,8 @@ async def runner(): def main(): import asyncio + from rich.traceback import install + install(show_locals=True) + print('Starting...') asyncio.run(runner()) diff --git a/anymusicbot/__main__.py b/bot/__main__.py similarity index 100% rename from anymusicbot/__main__.py rename to bot/__main__.py diff --git a/anymusicbot/callbacks/__init__.py b/bot/callbacks/__init__.py similarity index 100% rename from anymusicbot/callbacks/__init__.py rename to bot/callbacks/__init__.py diff --git a/anymusicbot/callbacks/factories/__init__.py b/bot/callbacks/factories/__init__.py similarity index 100% rename from anymusicbot/callbacks/factories/__init__.py rename to bot/callbacks/factories/__init__.py diff --git a/anymusicbot/common.py b/bot/common.py similarity index 65% rename from anymusicbot/common.py rename to bot/common.py index 5146b5f..9dc7c3e 100644 --- a/anymusicbot/common.py +++ b/bot/common.py @@ -1,7 +1,8 @@ from aiogram import Bot, Dispatcher +from bot.modules.fsm import InDbStorage from .utils.config import config bot = Bot(token=config.telegram.bot_token) -dp = Dispatcher() +dp = Dispatcher(storage=InDbStorage()) __all__ = ['bot', 'dp', 'config'] diff --git a/anymusicbot/filters/__init__.py b/bot/filters/__init__.py similarity index 100% rename from anymusicbot/filters/__init__.py rename to bot/filters/__init__.py diff --git a/anymusicbot/handlers/__init__.py b/bot/handlers/__init__.py similarity index 100% rename from anymusicbot/handlers/__init__.py rename to bot/handlers/__init__.py diff --git a/anymusicbot/handlers/initialize/__init__.py b/bot/handlers/initialize/__init__.py similarity index 100% rename from anymusicbot/handlers/initialize/__init__.py rename to bot/handlers/initialize/__init__.py diff --git a/anymusicbot/handlers/initialize/initializer.py b/bot/handlers/initialize/initializer.py similarity index 75% rename from anymusicbot/handlers/initialize/initializer.py rename to bot/handlers/initialize/initializer.py index 629ce9e..7985fd2 100644 --- a/anymusicbot/handlers/initialize/initializer.py +++ b/bot/handlers/initialize/initializer.py @@ -8,3 +8,6 @@ router = Router() @router.startup() async def startup(bot: Bot): print(f'[green]Started as[/] @{(await bot.me()).username}') + + from bot.modules.database import pull + await pull() diff --git a/anymusicbot/keyboards/__init__.py b/bot/keyboards/__init__.py similarity index 100% rename from anymusicbot/keyboards/__init__.py rename to bot/keyboards/__init__.py diff --git a/anymusicbot/keyboards/inline/__init__.py b/bot/keyboards/inline/__init__.py similarity index 100% rename from anymusicbot/keyboards/inline/__init__.py rename to bot/keyboards/inline/__init__.py diff --git a/anymusicbot/middlewares/__init__.py b/bot/middlewares/__init__.py similarity index 100% rename from anymusicbot/middlewares/__init__.py rename to bot/middlewares/__init__.py diff --git a/anymusicbot/modules/__init__.py b/bot/modules/__init__.py similarity index 100% rename from anymusicbot/modules/__init__.py rename to bot/modules/__init__.py diff --git a/bot/modules/database/__init__.py b/bot/modules/database/__init__.py new file mode 100644 index 0000000..2b5227c --- /dev/null +++ b/bot/modules/database/__init__.py @@ -0,0 +1,7 @@ +from .db import Db +from .pull_db import pull + + +db = Db() + +__all__ = ['db', 'pull'] diff --git a/bot/modules/database/db.py b/bot/modules/database/db.py new file mode 100644 index 0000000..f5095a9 --- /dev/null +++ b/bot/modules/database/db.py @@ -0,0 +1,17 @@ +from .db_model import DBDict +import os.path +from bot.utils.config import config + +DB = os.path.join(config.local.db_path, 'db') + +if not os.path.isfile(DB): + open('sync', 'w') + + +class Db(object): + def __init__(self): + self.fsm = DBDict('fsm') + self.config = DBDict('config') + + async def write(self): + await self.config.write() diff --git a/bot/modules/database/db_model.py b/bot/modules/database/db_model.py new file mode 100644 index 0000000..ac25e28 --- /dev/null +++ b/bot/modules/database/db_model.py @@ -0,0 +1,48 @@ +from sqlitedict import SqliteDict +from bot.common import bot +from bot.utils.config import config +from aiogram.types import FSInputFile, InputMediaDocument +from aiogram import exceptions +from pydantic import ValidationError +import time +from .meta import DBMeta +import os.path + +DB = os.path.join(config.local.db_path, 'db') +DB_CHAT = config.telegram.db_chat + + +class DBDict(SqliteDict): + def __init__(self, tablename: str): + super().__init__(DB, tablename=tablename, autocommit=True) + + async def write(self): + try: + DBMeta().update_time = time.time_ns() + + await bot.edit_message_media( + media=InputMediaDocument(media=FSInputFile(DB)), + chat_id=DB_CHAT, + message_id=DBMeta().message_id + ) + await bot.edit_message_caption( + caption=str(DBMeta()), + chat_id=DB_CHAT, + message_id=DBMeta().message_id + ) + + except (ValidationError, exceptions.TelegramBadRequest): + DBMeta().update_time = time.time_ns() + + self['db_message_id'] = ( + await bot.send_document( + chat_id=DB_CHAT, document=FSInputFile(DB), + disable_notification=True + ) + ).message_id + + DBMeta().message_id = self['db_message_id'] + await bot.edit_message_caption( + caption=str(DBMeta()), + chat_id=DB_CHAT, message_id=self.get('db_message_id') + ) diff --git a/bot/modules/database/meta.py b/bot/modules/database/meta.py new file mode 100644 index 0000000..90e7b4f --- /dev/null +++ b/bot/modules/database/meta.py @@ -0,0 +1,106 @@ +import os.path +from bot.utils.config import config +from bot.common import bot +from aiogram.exceptions import TelegramBadRequest +import asyncio + +loop = asyncio.get_event_loop() + + +DBMETA = os.path.join(config.local.db_path, 'dbmeta') +APP_ID = config.local.app_id +DB_CHAT = config.telegram.db_chat + + +def meta_property(prop_name): + def getter(self): + return self[prop_name] + + def setter(self, value): + self[prop_name] = value + + return property(getter, setter) + + +class DBMeta: + app_id = meta_property('app_id') + message_id = meta_property('message_id') + update_time = meta_property('update_time') + + def __init__(self): + if not os.path.isfile(DBMETA): + open(DBMETA, 'w').write(f'{APP_ID}|None|0') + + def __getitem__(self, item): + try: + return open(DBMETA).read().split('|')[{ + "app_id": 0, + "message_id": 1, + "update_time": 2 + }.get(item)] + except TypeError: + return None + + def __setitem__(self, key, value): + meta = open(DBMETA).read().split('|') + meta[{ + "app_id": 0, + "message_id": 1, + "update_time": 2 + }[key]] = value + open(DBMETA, 'w').write('|'.join(str(x) for x in meta)) + + def __str__(self): + return open(DBMETA).read() + + +def cloud_meta_property(self, prop_name): + async def getter(): + return await self.get(prop_name) + + return getter() + + +class CloudMeta: + def __init__(self): + def prop_generator(name): + return cloud_meta_property(self, name) + + self.app_id = prop_generator('app_id') + self.message_id = prop_generator('message_id') + self.update_time = prop_generator('update_time') + + @staticmethod + async def get(item): + try: + if not DBMeta().update_time or not bot.cloudmeta_message_text: + raise AttributeError + + except AttributeError: + try: + message = await bot.forward_message( + DB_CHAT, DB_CHAT, + DBMeta().message_id + ) + + bot.cloudmeta_message_text = message.caption + + await message.delete() + + except TelegramBadRequest: + print('Cannot get CloudMeta - writing DBDict') + from .db_model import DBDict + await DBDict('config').write() + message = await bot.forward_message( + DB_CHAT, DB_CHAT, + DBMeta().message_id + ) + bot.cloudmeta_message_text = message.caption + await message.delete() + + cloudmeta = bot.cloudmeta_message_text.split('|') + return cloudmeta[{ + "app_id": 0, + "message_id": 1, + "update_time": 2 + }.get(item)] diff --git a/bot/modules/database/pull_db.py b/bot/modules/database/pull_db.py new file mode 100644 index 0000000..9044bf3 --- /dev/null +++ b/bot/modules/database/pull_db.py @@ -0,0 +1,55 @@ +import os +from .meta import DBMeta, CloudMeta +from bot.common import bot +from bot.utils.config import config +from sqlitedict import SqliteDict + +DB = os.path.join(config.local.db_path, 'db') +DB_CHAT = config.telegram.db_chat + + +async def pull(): + if DBMeta().message_id == 'None': + from . import db + print('No dbmeta file') + if msg_id := db.config.get('db_message_id'): + print('Found message id in in-db config') + DBMeta().message_id = msg_id + await db.write() + + if not os.path.isfile('sync'): + try: + if not bot.cloudmeta_message_text: + print('Cloudmeta initialized incorrectly') + raise AttributeError + else: + return + except AttributeError: + if int(DBMeta().update_time) >= int(await CloudMeta().update_time): + print('DB is up-to-date') + return + else: + print('Database file is new. Trying to download cloud data') + os.remove('sync') + + print('DB is not up-to-date') + + message = await bot.forward_message(DB_CHAT, DB_CHAT, DBMeta().message_id) + + await message.delete() + + await bot.download( + message.document, + destination=DB + 'b' + ) + + from . import db + for table in db.__dict__.keys(): + new_table = SqliteDict(DB + 'b', tablename=table) + for key in new_table.keys(): + getattr(db, table)[key] = new_table[key] + new_table.close() + + await db.write() + + print('Synced') diff --git a/bot/modules/fsm/__init__.py b/bot/modules/fsm/__init__.py new file mode 100644 index 0000000..ba1a55b --- /dev/null +++ b/bot/modules/fsm/__init__.py @@ -0,0 +1 @@ +from .in_db import InDbStorage diff --git a/bot/modules/fsm/in_db.py b/bot/modules/fsm/in_db.py new file mode 100644 index 0000000..702d4d2 --- /dev/null +++ b/bot/modules/fsm/in_db.py @@ -0,0 +1,36 @@ +from bot.modules.database import db +from dataclasses import dataclass, field +from typing import Any, DefaultDict, Dict, Optional + +from aiogram.fsm.state import State +from aiogram.fsm.storage.base import ( + BaseStorage, + StateType, + StorageKey, +) + + +@dataclass +class MemoryStorageRecord: + data: Dict[str, Any] = field(default_factory=dict) + state: Optional[str] = None + + +class InDbStorage(BaseStorage): + def __init__(self) -> None: + self.storage: DefaultDict[StorageKey, MemoryStorageRecord] = db.fsm + + async def close(self) -> None: + pass + + async def set_state(self, key: StorageKey, state: StateType = None) -> None: + self.storage[key].state = state.state if isinstance(state, State) else state + + async def get_state(self, key: StorageKey) -> Optional[str]: + return self.storage[key].state + + async def set_data(self, key: StorageKey, data: Dict[str, Any]) -> None: + self.storage[key].data = data.copy() + + async def get_data(self, key: StorageKey) -> Dict[str, Any]: + return self.storage[key].data.copy() diff --git a/anymusicbot/utils/__init__.py b/bot/utils/__init__.py similarity index 100% rename from anymusicbot/utils/__init__.py rename to bot/utils/__init__.py diff --git a/anymusicbot/utils/config/__init__.py b/bot/utils/config/__init__.py similarity index 100% rename from anymusicbot/utils/config/__init__.py rename to bot/utils/config/__init__.py diff --git a/anymusicbot/utils/config/_config.py b/bot/utils/config/_config.py similarity index 100% rename from anymusicbot/utils/config/_config.py rename to bot/utils/config/_config.py diff --git a/config.toml.example b/config.toml.example index baddf2e..5dd1d21 100644 --- a/config.toml.example +++ b/config.toml.example @@ -5,6 +5,7 @@ admin_id = 0 [local] db_path = 'db/' +app_id = 'ANY-MUSIC-BOT' [tokens.spotify] client_id = '' diff --git a/pyproject.toml b/pyproject.toml index bccbbd1..45766c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "anymusicbot" +name = "AnyMusicBot" version = "0.1.0" description = "" authors = ["BarsTiger"] @@ -12,6 +12,7 @@ rich = "^13.6.0" py-deezer = "^1.1.4.post1" soundcloud-lib = "^0.6.1" shazamio = { path = "lib/ShazamIO" } +sqlitedict = "^2.1.0" [build-system]