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"],
},
)