mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-04-30 12:55:25 +00:00
d7adaef0bf
While this isn't a true presentation style interface, the idea is that there is an argument stream to commands that we pattern match commands against, and there are different mediums that the commands can be invoked from. There are translators between presentation types and also between commands and mediums to do things like render the result of the command for Matrix etc. This is all inspired by the Common Lisp Interface Manager (CLIM). But there are significant differences since, hello, this is essentially being made for Matrix bots and appservices. This commit is NOT contributed under the Apache-2.0 License. Copyright (C) 2022 Gnuxie <Gnuxie@protonmail.com> All rights reserved.
190 lines
9.0 KiB
TypeScript
190 lines
9.0 KiB
TypeScript
/**
|
|
* Copyright (C) 2022 Gnuxie <Gnuxie@protonmail.com>
|
|
* All rights reserved.
|
|
*
|
|
* This file is modified and is NOT licensed under the Apache License.
|
|
* This modified file incorperates work from mjolnir
|
|
* https://github.com/matrix-org/mjolnir
|
|
* which included the following license notice:
|
|
|
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*
|
|
* However, this file is modified and the modifications in this file
|
|
* are NOT distributed, contributed, committed, or licensed under the Apache License.
|
|
*/
|
|
|
|
import { AppServiceRegistration, Bridge, Request, WeakEvent, BridgeContext, MatrixUser, Logger } from "matrix-appservice-bridge";
|
|
import { MjolnirManager } from ".//MjolnirManager";
|
|
import { DataStore, PgDataStore } from ".//datastore";
|
|
import { Api } from "./Api";
|
|
import { IConfig } from "./config/config";
|
|
import { AccessControl } from "./AccessControl";
|
|
import { AppserviceCommandHandler } from "./bot/AppserviceCommandHandler";
|
|
|
|
const log = new Logger("AppService");
|
|
/**
|
|
* Responsible for setting up listeners and delegating functionality to a matrix-appservice-bridge `Bridge` for
|
|
* the entrypoint of the application.
|
|
*/
|
|
export class MjolnirAppService {
|
|
|
|
private readonly api: Api;
|
|
private readonly commands: AppserviceCommandHandler;
|
|
|
|
/**
|
|
* The constructor is private because we want to ensure intialization steps are followed,
|
|
* use `makeMjolnirAppService`.
|
|
*/
|
|
private constructor(
|
|
public readonly config: IConfig,
|
|
public readonly bridge: Bridge,
|
|
public readonly mjolnirManager: MjolnirManager,
|
|
private readonly accessControl: AccessControl,
|
|
private readonly dataStore: DataStore,
|
|
) {
|
|
this.api = new Api(config.homeserver.url, mjolnirManager);
|
|
this.commands = new AppserviceCommandHandler(this);
|
|
}
|
|
|
|
/**
|
|
* Make and initialize the app service from the config, ready to be started.
|
|
* @param config The appservice's config, not mjolnir's, see `src/appservice/config`.
|
|
* @param dataStore A datastore to persist infomration about the mjolniren to.
|
|
* @param registrationFilePath A file path to the registration file to read the namespace and tokens from.
|
|
* @returns A new `MjolnirAppService`.
|
|
*/
|
|
public static async makeMjolnirAppService(config: IConfig, dataStore: DataStore, registrationFilePath: string) {
|
|
const bridge = new Bridge({
|
|
homeserverUrl: config.homeserver.url,
|
|
domain: config.homeserver.domain,
|
|
registration: registrationFilePath,
|
|
// We lazily initialize the controller to avoid null checks
|
|
// It also allows us to combine constructor/initialize logic
|
|
// to make the code base much simpler. A small hack to pay for an overall less hacky code base.
|
|
controller: {
|
|
onUserQuery: () => {throw new Error("Mjolnir uninitialized")},
|
|
onEvent: () => {throw new Error("Mjolnir uninitialized")},
|
|
},
|
|
suppressEcho: false,
|
|
});
|
|
await bridge.initialise();
|
|
const accessControlListId = await bridge.getBot().getClient().resolveRoom(config.accessControlList);
|
|
const accessControl = await AccessControl.setupAccessControl(accessControlListId, bridge);
|
|
const mjolnirManager = await MjolnirManager.makeMjolnirManager(dataStore, bridge, accessControl);
|
|
const appService = new MjolnirAppService(
|
|
config,
|
|
bridge,
|
|
mjolnirManager,
|
|
accessControl,
|
|
dataStore
|
|
);
|
|
bridge.opts.controller = {
|
|
onUserQuery: appService.onUserQuery.bind(appService),
|
|
onEvent: appService.onEvent.bind(appService),
|
|
};
|
|
return appService;
|
|
}
|
|
|
|
/**
|
|
* Start the appservice for the end user with the appropriate settings from their config and registration file.
|
|
* @param port The port to make the appservice listen for transactions from the homeserver on (usually sourced from the cli).
|
|
* @param config The parsed configuration file.
|
|
* @param registrationFilePath A path to their homeserver registration file.
|
|
*/
|
|
public static async run(port: number, config: IConfig, registrationFilePath: string): Promise<MjolnirAppService> {
|
|
Logger.configure(config.logging ?? { console: "debug" });
|
|
const dataStore = new PgDataStore(config.db.connectionString);
|
|
await dataStore.init();
|
|
const service = await MjolnirAppService.makeMjolnirAppService(config, dataStore, registrationFilePath);
|
|
// The call to `start` MUST happen last. As it needs the datastore, and the mjolnir manager to be initialized before it can process events from the homeserver.
|
|
await service.start(port);
|
|
return service;
|
|
}
|
|
|
|
public onUserQuery (queriedUser: MatrixUser) {
|
|
return {}; // auto-provision users with no additonal data
|
|
}
|
|
|
|
/**
|
|
* Handle an individual event pushed by the homeserver to us.
|
|
* This function is async (and anything downstream would be anyway), which does mean that events can be processed out of order.
|
|
* Not a huge problem for us, but is something to be aware of.
|
|
* @param request A matrix-appservice-bridge request encapsulating a Matrix event.
|
|
* @param context Additional context for the Matrix event.
|
|
*/
|
|
public async onEvent(request: Request<WeakEvent>, context: BridgeContext) {
|
|
const mxEvent = request.getData();
|
|
// Provision a new mjolnir for the invitee when the appservice bot (designated by this.bridge.botUserId) is invited to a room.
|
|
// Acts as an alternative to the web api provided for the widget.
|
|
if ('m.room.member' === mxEvent.type) {
|
|
if ('invite' === mxEvent.content['membership'] && mxEvent.state_key === this.bridge.botUserId) {
|
|
log.info(`${mxEvent.sender} has sent an invitation to the appservice bot ${this.bridge.botUserId}, attempting to provision them a mjolnir`);
|
|
try {
|
|
await this.mjolnirManager.provisionNewMjolnir(mxEvent.sender)
|
|
} catch (e: any) {
|
|
log.error(`Failed to provision a mjolnir for ${mxEvent.sender} after they invited ${this.bridge.botUserId}:`, e);
|
|
// continue, we still want to reject this invitation.
|
|
}
|
|
try {
|
|
// reject the invite to keep the room clean and make sure the invetee doesn't get confused and think this is their mjolnir.
|
|
await this.bridge.getBot().getClient().leaveRoom(mxEvent.room_id);
|
|
} catch (e: any) {
|
|
log.warn("Unable to reject an invite to a room", e);
|
|
}
|
|
}
|
|
}
|
|
this.accessControl.handleEvent(mxEvent['room_id'], mxEvent);
|
|
this.mjolnirManager.onEvent(request, context);
|
|
this.commands.handleEvent(mxEvent);
|
|
}
|
|
|
|
/**
|
|
* Start the appservice. See `run`.
|
|
* @param port The port that the appservice should listen on to receive transactions from the homeserver.
|
|
*/
|
|
private async start(port: number) {
|
|
await this.bridge.getBot().getClient().joinRoom(this.config.adminRoom);
|
|
log.info("Starting MjolnirAppService, Matrix-side to listen on port", port);
|
|
this.api.start(this.config.webAPI.port);
|
|
await this.bridge.listen(port);
|
|
log.info("MjolnirAppService started successfully");
|
|
}
|
|
|
|
/**
|
|
* Stop listening to requests from both the homeserver and web api and disconnect from the datastore.
|
|
*/
|
|
public async close(): Promise<void> {
|
|
await this.bridge.close();
|
|
await this.dataStore.close();
|
|
await this.api.close();
|
|
}
|
|
|
|
/**
|
|
* Generate a registration file for a fresh deployment of the appservice.
|
|
* Included to satisfy `matrix-appservice-bridge`'s `Cli` utility which allows a registration file to be registered when setting up a deployment of an appservice.
|
|
* @param reg Any existing paramaters to be included in the registration, to be mutated by this method.
|
|
* @param callback To call when the registration has been generated with the final registration.
|
|
*/
|
|
public static generateRegistration(reg: AppServiceRegistration, callback: (finalRegistration: AppServiceRegistration) => void) {
|
|
reg.setId(AppServiceRegistration.generateToken());
|
|
reg.setHomeserverToken(AppServiceRegistration.generateToken());
|
|
reg.setAppServiceToken(AppServiceRegistration.generateToken());
|
|
reg.setSenderLocalpart("mjolnir-bot");
|
|
reg.addRegexPattern("users", "@mjolnir_.*", true);
|
|
reg.setRateLimited(false);
|
|
callback(reg);
|
|
}
|
|
}
|