Add appservice option to standalone mode

This commit is contained in:
Tulir Asokan 2023-09-06 21:35:14 +03:00
parent 8f40a0b292
commit 92736baefd
4 changed files with 71 additions and 4 deletions

View file

@ -15,6 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations from __future__ import annotations
from typing import Callable
import asyncio import asyncio
import json import json
import logging import logging

View file

@ -30,7 +30,11 @@ from ruamel.yaml import YAML
from ruamel.yaml.comments import CommentedMap from ruamel.yaml.comments import CommentedMap
from yarl import URL from yarl import URL
from mautrix.appservice import AppServiceServerMixin
from mautrix.client import SyncStream
from mautrix.types import ( from mautrix.types import (
BaseMessageEventContentFuncs,
Event,
EventType, EventType,
Filter, Filter,
Membership, Membership,
@ -113,6 +117,9 @@ if "/" in meta.main_class:
else: else:
module = meta.modules[0] module = meta.modules[0]
main_class = meta.main_class main_class = meta.main_class
if args.meta != "maubot.yaml" and os.path.dirname(args.meta) != "":
sys.path.append(os.path.dirname(args.meta))
bot_module = importlib.import_module(module) bot_module = importlib.import_module(module)
plugin: type[Plugin] = getattr(bot_module, main_class) plugin: type[Plugin] = getattr(bot_module, main_class)
loader = FileSystemLoader(os.path.dirname(args.meta), meta) loader = FileSystemLoader(os.path.dirname(args.meta), meta)
@ -131,6 +138,7 @@ user_id = config["user.credentials.id"]
device_id = config["user.credentials.device_id"] device_id = config["user.credentials.device_id"]
homeserver = config["user.credentials.homeserver"] homeserver = config["user.credentials.homeserver"]
access_token = config["user.credentials.access_token"] access_token = config["user.credentials.access_token"]
appservice_listener = config["user.appservice"]
crypto_store = state_store = None crypto_store = state_store = None
if device_id and not OlmMachine: if device_id and not OlmMachine:
@ -188,6 +196,10 @@ if meta.webapp:
resource = PrefixResource(web_base_path) resource = PrefixResource(web_base_path)
resource.add_route(hdrs.METH_ANY, _handle_plugin_request) resource.add_route(hdrs.METH_ANY, _handle_plugin_request)
web_app.router.register_resource(resource) web_app.router.register_resource(resource)
elif appservice_listener:
web_app = web.Application()
web_runner = web.AppRunner(web_app, access_log_class=AccessLogger)
public_url = plugin_webapp = None
else: else:
web_app = web_runner = public_url = plugin_webapp = None web_app = web_runner = public_url = plugin_webapp = None
@ -195,6 +207,31 @@ loop = asyncio.get_event_loop()
client: MaubotMatrixClient | None = None client: MaubotMatrixClient | None = None
bot: Plugin | None = None bot: Plugin | None = None
appservice: AppServiceServerMixin | None = None
if appservice_listener:
assert web_app is not None, "web_app is always set when appservice_listener is set"
appservice = AppServiceServerMixin(
ephemeral_events=True,
encryption_events=True,
log=logging.getLogger("maubot.appservice"),
hs_token=config["user.hs_token"],
)
appservice.register_routes(web_app)
@appservice.matrix_event_handler
async def handle_appservice_event(evt: Event) -> None:
if isinstance(evt.content, BaseMessageEventContentFuncs):
evt.content.trim_reply_fallback()
fake_sync_stream = SyncStream.JOINED_ROOM
if evt.type.is_ephemeral:
fake_sync_stream |= SyncStream.EPHEMERAL
else:
fake_sync_stream |= SyncStream.TIMELINE
setattr(evt, "source", fake_sync_stream)
tasks = client.dispatch_manual_event(evt.type, evt, include_global_handlers=True)
await asyncio.gather(*tasks)
async def main(): async def main():
@ -217,6 +254,8 @@ async def main():
state_store=state_store, state_store=state_store,
device_id=device_id, device_id=device_id,
) )
if appservice:
client.api.as_user_id = user_id
client.ignore_first_sync = config["user.ignore_first_sync"] client.ignore_first_sync = config["user.ignore_first_sync"]
client.ignore_initial_sync = config["user.ignore_initial_sync"] client.ignore_initial_sync = config["user.ignore_initial_sync"]
if crypto_store: if crypto_store:
@ -225,6 +264,11 @@ async def main():
await crypto_store.open() await crypto_store.open()
client.crypto = OlmMachine(client, crypto_store, state_store) client.crypto = OlmMachine(client, crypto_store, state_store)
if appservice:
appservice.otk_handler = client.crypto.handle_as_otk_counts
appservice.device_list_handler = client.crypto.handle_as_device_lists
appservice.to_device_handler = client.crypto.handle_as_to_device_event
client.api.as_device_id = device_id
crypto_device_id = await crypto_store.get_device_id() crypto_device_id = await crypto_store.get_device_id()
if crypto_device_id and crypto_device_id != device_id: if crypto_device_id and crypto_device_id != device_id:
log.fatal( log.fatal(
@ -272,6 +316,8 @@ async def main():
) )
await nb.put_filter_id(filter_id) await nb.put_filter_id(filter_id)
_ = client.start(nb.filter_id) _ = client.start(nb.filter_id)
elif appservice_listener and crypto_store and not client.crypto.account.shared:
await client.crypto.share_keys()
if config["user.autojoin"]: if config["user.autojoin"]:
log.debug("Autojoin is enabled") log.debug("Autojoin is enabled")
@ -334,9 +380,14 @@ async def stop(suppress_stop_error: bool = False) -> None:
except Exception: except Exception:
if not suppress_stop_error: if not suppress_stop_error:
log.exception("Error stopping bot") log.exception("Error stopping bot")
if web_runner: if web_runner and web_runner.server:
try:
await web_runner.shutdown() await web_runner.shutdown()
await web_runner.cleanup() await web_runner.cleanup()
except RuntimeError:
if not suppress_stop_error:
await db.stop()
raise
await db.stop() await db.stop()
@ -347,6 +398,10 @@ signal.signal(signal.SIGTERM, signal.default_int_handler)
try: try:
log.info("Starting plugin") log.info("Starting plugin")
loop.run_until_complete(main()) loop.run_until_complete(main())
except SystemExit:
loop.run_until_complete(stop(suppress_stop_error=True))
loop.close()
raise
except (Exception, KeyboardInterrupt) as e: except (Exception, KeyboardInterrupt) as e:
if isinstance(e, KeyboardInterrupt): if isinstance(e, KeyboardInterrupt):
log.info("Startup interrupted, stopping") log.info("Startup interrupted, stopping")

View file

@ -33,9 +33,13 @@ class Config(BaseFileConfig):
copy("user.credentials.access_token") copy("user.credentials.access_token")
copy("user.credentials.device_id") copy("user.credentials.device_id")
copy("user.sync") copy("user.sync")
copy("user.appservice")
copy("user.hs_token")
copy("user.autojoin") copy("user.autojoin")
copy("user.displayname") copy("user.displayname")
copy("user.avatar_url") copy("user.avatar_url")
copy("user.ignore_initial_sync")
copy("user.ignore_first_sync")
if "server" in base: if "server" in base:
copy("server.hostname") copy("server.hostname")
copy("server.port") copy("server.port")

View file

@ -5,9 +5,15 @@ user:
homeserver: https://example.com homeserver: https://example.com
access_token: foo access_token: foo
# If you want to enable encryption, set the device ID corresponding to the access token here. # If you want to enable encryption, set the device ID corresponding to the access token here.
# When using an appservice, you should use appservice login manually to generate a device ID and access token.
device_id: null device_id: null
# Enable /sync? This is not needed for purely unencrypted webhook-based bots, but is necessary in most other cases. # Enable /sync? This is not needed for purely unencrypted webhook-based bots, but is necessary in most other cases.
sync: true sync: true
# Receive appservice transactions? This will add a /_matrix/app/v1/transactions endpoint on
# the HTTP server configured below. The base_path will not be applied for the /transactions path.
appservice: false
# When appservice mode is enabled, the hs_token for the appservice.
hs_token: null
# Automatically accept invites? # Automatically accept invites?
autojoin: false autojoin: false
# The displayname and avatar URL to set for the bot on startup. # The displayname and avatar URL to set for the bot on startup.
@ -21,7 +27,8 @@ user:
# if you want the bot to handle messages that were sent while the bot was down. # if you want the bot to handle messages that were sent while the bot was down.
ignore_first_sync: true ignore_first_sync: true
# Web server settings. These will only take effect if the plugin requests it using `webapp: true` in the meta file. # Web server settings. These will only take effect if the plugin requests it using `webapp: true` in the meta file,
# or if user -> appservice is set to true.
server: server:
# The IP and port to listen to. # The IP and port to listen to.
hostname: 0.0.0.0 hostname: 0.0.0.0