diff --git a/maubot/standalone/__main__.py b/maubot/standalone/__main__.py index 75e65c2..97ce5cd 100644 --- a/maubot/standalone/__main__.py +++ b/maubot/standalone/__main__.py @@ -1,5 +1,5 @@ -# supportportal - A maubot plugin to manage customer support on Matrix. -# Copyright (C) 2019 Tulir Asokan +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2021 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 @@ -13,12 +13,13 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Optional +from typing import Optional, Type, cast from aiohttp import ClientSession import logging.config import importlib import argparse import asyncio +import os.path import signal import copy import sys @@ -29,22 +30,26 @@ import sqlalchemy as sql from mautrix.util.config import RecursiveDict, BaseMissingError from mautrix.util.db import Base -from mautrix.types import (UserID, Filter, RoomFilter, RoomEventFilter, StrippedStateEvent, - EventType, Membership) +from mautrix.util.logging import TraceLogger +from mautrix.types import (Filter, RoomFilter, RoomEventFilter, StrippedStateEvent, + EventType, Membership, FilterID, SyncToken) -from .config import Config from ..plugin_base import Plugin from ..loader import PluginMeta from ..matrix import MaubotMatrixClient from ..lib.store_proxy import SyncStoreProxy from ..__meta__ import __version__ +from .config import Config +from .loader import FileSystemLoader +from .database import NextBatch parser = argparse.ArgumentParser( description="A plugin-based Matrix bot system -- standalone mode.", prog="python -m maubot.standalone") parser.add_argument("-c", "--config", type=str, default="config.yaml", metavar="", help="the path to your config file") -parser.add_argument("-b", "--base-config", type=str, default="example-config.yaml", +parser.add_argument("-b", "--base-config", type=str, + default="pkg://maubot.standalone/example-config.yaml", metavar="", help="the path to the example config " "(for automatic config updates)") parser.add_argument("-m", "--meta", type=str, default="maubot.yaml", @@ -75,23 +80,11 @@ else: module = meta.modules[0] main_class = meta.main_class bot_module = importlib.import_module(module) -plugin = getattr(bot_module, main_class) +plugin: Type[Plugin] = getattr(bot_module, main_class) +loader = FileSystemLoader(os.path.dirname(args.meta)) log.info(f"Initializing standalone {meta.id} v{meta.version} on maubot {__version__}") - -class NextBatch(Base): - __tablename__ = "standalone_next_batch" - - user_id: str = sql.Column(sql.String(255), primary_key=True) - next_batch: str = sql.Column(sql.String(255)) - filter_id: str = sql.Column(sql.String(255)) - - @classmethod - def get(cls, user_id: UserID) -> Optional['NextBatch']: - return cls._select_one_or_none(cls.c.user_id == user_id) - - log.debug("Opening database") db = sql.create_engine(config["database"]) Base.metadata.bind = db @@ -104,7 +97,7 @@ access_token = config["user.credentials.access_token"] nb = NextBatch.get(user_id) if not nb: - nb = NextBatch(user_id=user_id, next_batch="", filter_id="") + nb = NextBatch(user_id=user_id, next_batch=SyncToken(""), filter_id=FilterID("")) nb.insert() bot_config = None @@ -135,8 +128,8 @@ if meta.config: loop = asyncio.get_event_loop() -client: MaubotMatrixClient = None -bot: Plugin = None +client: Optional[MaubotMatrixClient] = None +bot: Optional[Plugin] = None async def main(): @@ -144,9 +137,10 @@ async def main(): global client, bot + client_log = logging.getLogger("maubot.client").getChild(user_id) client = MaubotMatrixClient(mxid=user_id, base_url=homeserver, token=access_token, - client_session=http_client, loop=loop, store=SyncStoreProxy(nb), - log=logging.getLogger("maubot.client").getChild(user_id)) + client_session=http_client, loop=loop, log=client_log, + sync_store=SyncStoreProxy(nb)) while True: try: @@ -181,9 +175,10 @@ async def main(): if displayname != "disable": await client.set_displayname(displayname) + plugin_log = cast(TraceLogger, logging.getLogger("maubot.instance.__main__")) bot = plugin(client=client, loop=loop, http=http_client, instance_id="__main__", - log=logging.getLogger("maubot.instance.__main__"), config=bot_config, - database=db if meta.database else None, webapp=None, webapp_url=None) + log=plugin_log, config=bot_config, database=db if meta.database else None, + webapp=None, webapp_url=None, loader=loader) await bot.internal_start() diff --git a/maubot/standalone/config.py b/maubot/standalone/config.py index daf8c20..8f0a2ee 100644 --- a/maubot/standalone/config.py +++ b/maubot/standalone/config.py @@ -1,5 +1,5 @@ # maubot - A plugin-based Matrix bot system. -# Copyright (C) 2019 Tulir Asokan +# Copyright (C) 2021 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 diff --git a/maubot/standalone/database.py b/maubot/standalone/database.py new file mode 100644 index 0000000..70dafb4 --- /dev/null +++ b/maubot/standalone/database.py @@ -0,0 +1,33 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2021 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 typing import Optional + +import sqlalchemy as sql + +from mautrix.util.db import Base +from mautrix.types import UserID, SyncToken, FilterID + + +class NextBatch(Base): + __tablename__ = "standalone_next_batch" + + user_id: UserID = sql.Column(sql.String(255), primary_key=True) + next_batch: SyncToken = sql.Column(sql.String(255)) + filter_id: FilterID = sql.Column(sql.String(255)) + + @classmethod + def get(cls, user_id: UserID) -> Optional['NextBatch']: + return cls._select_one_or_none(cls.c.user_id == user_id) diff --git a/maubot/standalone/example-config.yaml b/maubot/standalone/example-config.yaml new file mode 100644 index 0000000..af671e0 --- /dev/null +++ b/maubot/standalone/example-config.yaml @@ -0,0 +1,41 @@ +# Bot account details +user: + credentials: + id: "@bot:example.com" + homeserver: https://example.com + access_token: foo + # Enable /sync? This is not needed for purely unencrypted webhook-based bots, but is necessary in most other cases. + sync: true + # Automatically accept invites? + autojoin: false + # The displayname and avatar URL to set for the bot on startup. + displayname: Standalone Bot + avatar_url: mxc://maunium.net/AKwRzQkTbggfVZGEqexbYLIO + +# The database for the plugin. Also used to store the sync token. +database: sqlite:///bot.db + +# Config for the plugin. Refer to the plugin's base-config.yaml to find what (if anything) to put here. +plugin_config: {} + +# Standard Python logging configuration +logging: + version: 1 + formatters: + colored: + (): maubot.lib.color_log.ColorFormatter + format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" + handlers: + console: + class: logging.StreamHandler + formatter: colored + loggers: + maubot: + level: DEBUG + mau: + level: DEBUG + aiohttp: + level: INFO + root: + level: DEBUG + handlers: [console] diff --git a/maubot/standalone/loader.py b/maubot/standalone/loader.py new file mode 100644 index 0000000..d7a089a --- /dev/null +++ b/maubot/standalone/loader.py @@ -0,0 +1,41 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2021 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 typing import List +import os.path +import os + +from ..loader import BasePluginLoader + +class FileSystemLoader(BasePluginLoader): + def __init__(self, path: str) -> None: + self.path = path + + @property + def source(self) -> str: + return self.path + + def sync_read_file(self, path: str) -> bytes: + with open(os.path.join(self.path, path), "rb") as file: + return file.read() + + async def read_file(self, path: str) -> bytes: + return self.sync_read_file(path) + + def sync_list_files(self, directory: str) -> List[str]: + return os.listdir(os.path.join(self.path, directory)) + + async def list_files(self, directory: str) -> List[str]: + return self.sync_list_files(directory) diff --git a/setup.py b/setup.py index 0fd99ff..eca1e05 100644 --- a/setup.py +++ b/setup.py @@ -70,5 +70,6 @@ setuptools.setup( "management/frontend/build/static/media/*", ], "maubot.cli": ["res/*"], + "maubot.standalone": ["example-config.yaml"], }, )