# maubot - A plugin-based Matrix bot system. # Copyright (C) 2022 Tulir Asokan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from __future__ import annotations import asyncio import sys from mautrix.api import HTTPAPI from mautrix.util.async_db import Database, DatabaseException, PostgresDatabase, Scheme from mautrix.util.program import Program from .__meta__ import __version__ from .client import Client from .config import Config from .db import init as init_db, upgrade_table from .instance import PluginInstance from .lib.future_awaitable import FutureAwaitable from .lib.state_store import PgStateStore from .loader.zip import init as init_zip_loader from .management.api import init as init_mgmt_api from .server import MaubotServer try: from mautrix.crypto.store import PgCryptoStore except ImportError: PgCryptoStore = None class Maubot(Program): config: Config server: MaubotServer db: Database crypto_db: Database | None plugin_postgres_db: PostgresDatabase | None state_store: PgStateStore config_class = Config module = "maubot" name = "maubot" version = __version__ command = "python -m maubot" description = "A plugin-based Matrix bot system." def prepare_log_websocket(self) -> None: from .management.api.log import init, stop_all init(self.loop) self.add_shutdown_actions(FutureAwaitable(stop_all)) def prepare_arg_parser(self) -> None: super().prepare_arg_parser() self.parser.add_argument( "--ignore-unsupported-database", action="store_true", help="Run even if the database schema is too new", ) self.parser.add_argument( "--ignore-foreign-tables", action="store_true", help="Run even if the database contains tables from other programs (like Synapse)", ) def prepare_db(self) -> None: self.db = Database.create( self.config["database"], upgrade_table=upgrade_table, db_args=self.config["database_opts"], owner_name=self.name, ignore_foreign_tables=self.args.ignore_foreign_tables, ) init_db(self.db) if PgCryptoStore: if self.config["crypto_database"] == "default": self.crypto_db = self.db else: self.crypto_db = Database.create( self.config["crypto_database"], upgrade_table=PgCryptoStore.upgrade_table, ignore_foreign_tables=self.args.ignore_foreign_tables, ) else: self.crypto_db = None if self.config["plugin_databases.postgres"] == "default": if self.db.scheme != Scheme.POSTGRES: self.log.critical( 'Using "default" as the postgres plugin database URL is only allowed if ' "the default database is postgres." ) sys.exit(24) assert isinstance(self.db, PostgresDatabase) self.plugin_postgres_db = self.db elif self.config["plugin_databases.postgres"]: plugin_db = Database.create( self.config["plugin_databases.postgres"], db_args={ **self.config["database_opts"], **self.config["plugin_databases.postgres_opts"], }, ) if plugin_db.scheme != Scheme.POSTGRES: self.log.critical("The plugin postgres database URL must be a postgres database") sys.exit(24) assert isinstance(plugin_db, PostgresDatabase) self.plugin_postgres_db = plugin_db else: self.plugin_postgres_db = None def prepare(self) -> None: super().prepare() if self.config["api_features.log"]: self.prepare_log_websocket() HTTPAPI.default_ua = f"maubot/{self.version} {HTTPAPI.default_ua}" init_zip_loader(self.config) self.prepare_db() Client.init_cls(self) PluginInstance.init_cls(self) management_api = init_mgmt_api(self.config, self.loop) self.server = MaubotServer(management_api, self.config, self.loop) self.state_store = PgStateStore(self.db) async def start_db(self) -> None: self.log.debug("Starting database...") ignore_unsupported = self.args.ignore_unsupported_database self.db.upgrade_table.allow_unsupported = ignore_unsupported self.state_store.upgrade_table.allow_unsupported = ignore_unsupported try: await self.db.start() await self.state_store.upgrade_table.upgrade(self.db) if self.plugin_postgres_db and self.plugin_postgres_db is not self.db: await self.plugin_postgres_db.start() if self.crypto_db: PgCryptoStore.upgrade_table.allow_unsupported = ignore_unsupported if self.crypto_db is not self.db: await self.crypto_db.start() else: await PgCryptoStore.upgrade_table.upgrade(self.db) except DatabaseException as e: self.log.critical("Failed to initialize database", exc_info=e) if e.explanation: self.log.info(e.explanation) sys.exit(25) async def system_exit(self) -> None: if hasattr(self, "db"): self.log.trace("Stopping database due to SystemExit") await self.db.stop() async def start(self) -> None: await self.start_db() await asyncio.gather(*[plugin.load() async for plugin in PluginInstance.all()]) await asyncio.gather(*[client.start() async for client in Client.all()]) await super().start() async for plugin in PluginInstance.all(): await plugin.load() await self.server.start() async def stop(self) -> None: self.add_shutdown_actions(*(client.stop() for client in Client.cache.values())) await super().stop() self.log.debug("Stopping server") try: await asyncio.wait_for(self.server.stop(), 5) except asyncio.TimeoutError: self.log.warning("Stopping server timed out") await self.db.stop() Maubot().run()