diff --git a/package.json b/package.json index dd5e312..9d5ea71 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "matrix-appservice-bridge": "^5.0.0", "nedb": "^1.8.0", "parse-duration": "^1.0.2", + "pg": "^8.8.0", "shell-quote": "^1.7.3", "yaml": "^2.1.1" }, diff --git a/src/appservice/AppService.ts b/src/appservice/AppService.ts index 616a08d..70fe066 100644 --- a/src/appservice/AppService.ts +++ b/src/appservice/AppService.ts @@ -16,20 +16,23 @@ */ import { randomUUID } from "crypto"; -import { AppServiceRegistration, Bridge, Cli, Request, WeakEvent, BridgeContext, MatrixUser, UserBridgeStore, RemoteUser } from "matrix-appservice-bridge"; +import { AppServiceRegistration, Bridge, Cli, Request, WeakEvent, BridgeContext, MatrixUser } from "matrix-appservice-bridge"; +import { MatrixClient } from "matrix-bot-sdk"; // needed by appservice irc, though it looks completely dead. -import * as Datastore from "nedb"; import { MjolnirManager } from ".//MjolnirManager"; +import { DataStore, PgDataStore } from ".//datastore"; import { Api } from "./Api"; // ts-node src/appservice/AppService.ts -r -u "http://localhost:9000" # remember to add the registration to homeserver.yaml! you probably want host.docker.internal as the hostname of the appservice if using mx-tester // ts-node src/appservice/AppService -p 9000 # to start. export class MjolnirAppService { + public readonly dataStore: DataStore; public readonly bridge: Bridge; public readonly mjolnirManager: MjolnirManager = new MjolnirManager(); public constructor() { + this.dataStore = new PgDataStore("foo bar baz"); new Api("http://localhost:8081", this).start(9001); this.bridge = new Bridge({ homeserverUrl: "http://localhost:8081", @@ -39,40 +42,56 @@ export class MjolnirAppService { onUserQuery: this.onUserQuery.bind(this), onEvent: this.onEvent.bind(this), }, - userStore: new UserBridgeStore(new Datastore()), suppressEcho: false, }); } - public async provisionNewMjolnir(requestingUserId: string): Promise<[string, string]> { - // FIXME: we need to restrict who can do it (special list? ban remote users?) - const issuedMjolnirs = await this.bridge.getUserStore()!.getRemoteUsersFromMatrixId(requestingUserId); - if (issuedMjolnirs.length === 0) { + public async initStoredMjolnirs(): Promise { + for (var mjolnirRecord of await this.dataStore.list()) { + const [_mjolnirUserId, mjolnirClient] = await this.makeMatrixClient(mjolnirRecord.localPart); + await this.mjolnirManager.makeInstance( + mjolnirRecord.owner, + mjolnirRecord.managementRoom, + mjolnirClient, + ); + } + } + + public async makeMatrixClient(localPart: string): Promise<[string, MatrixClient]> { // Now we need to make one of the transparent mjolnirs and add it to the monitor. - const mjIntent = await this.bridge.getIntentFromLocalpart(`mjolnir_${randomUUID()}`); + const mjIntent = await this.bridge.getIntentFromLocalpart(localPart); await mjIntent.ensureRegistered(); // we're only doing this because it's complaining about missing profiles. // actually the user id wasn't even right, so this might not be necessary anymore. await mjIntent.ensureProfile('Mjolnir'); + return [mjIntent.userId, mjIntent.matrixClient]; + } - const managementRoomId = (await mjIntent.createRoom({ - createAsClient: true, - options: { - preset: 'private_chat', - invite: [requestingUserId], - name: `${requestingUserId}'s mjolnir` - } - })).room_id; + public async provisionNewMjolnir(requestingUserId: string): Promise<[string, string]> { + // FIXME: we need to restrict who can do it (special list? ban remote users?) + const provisionedMjolnirs = await this.dataStore.lookupByOwner(requestingUserId); + if (provisionedMjolnirs.length === 0) { + const mjolnirLocalPart = `mjolnir_${randomUUID()}`; + const [mjolnirUserId, mjolnirClient] = await this.makeMatrixClient(mjolnirLocalPart); - await this.mjolnirManager.createNew(requestingUserId, managementRoomId, mjIntent.matrixClient); - // Technically the mjolnir is a remote user, but also not because it's matrix-matrix. - //const mjAsRemote = new RemoteUser(mjIntent.userId) - //const bridgeStore = this.bridge.getUserStore()!; - //bridgeStore.setRemoteUser() - await this.bridge.getUserStore()!.linkUsers(new MatrixUser(requestingUserId), new RemoteUser(mjIntent.userId)); - return [mjIntent.userId, managementRoomId]; + const managementRoomId = await mjolnirClient.createRoom({ + preset: 'private_chat', + invite: [requestingUserId], + name: `${requestingUserId}'s mjolnir` + }); + + const mjolnir = await this.mjolnirManager.makeInstance(requestingUserId, managementRoomId, mjolnirClient); + await mjolnir.createFirstList(requestingUserId, "list"); + + await this.dataStore.store({ + localPart: mjolnirLocalPart, + owner: requestingUserId, + managementRoom: managementRoomId, + }); + + return [mjolnirUserId, managementRoomId]; } else { - throw new Error(`User: ${requestingUserId} has already provisioned ${issuedMjolnirs.length} mjolnirs.`); + throw new Error(`User: ${requestingUserId} has already provisioned ${provisionedMjolnirs.length} mjolnirs.`); } } diff --git a/src/appservice/MjolnirManager.ts b/src/appservice/MjolnirManager.ts index fcb2468..275661d 100644 --- a/src/appservice/MjolnirManager.ts +++ b/src/appservice/MjolnirManager.ts @@ -14,14 +14,10 @@ export class MjolnirManager { return config; } - public async createNew(requestingUserId: string, managementRoomId: string, client: MatrixClient) { - // FIXME: We should be creating the intent here and generating the id surely? - // rather than externally... - // FIXME: We need to verify that we haven't stored a mjolnir already if we aren't doing the above. - + public async makeInstance(requestingUserId: string, managementRoomId: string, client: MatrixClient): Promise { const managedMjolnir = new ManagedMjolnir(await Mjolnir.setupMjolnirFromConfig(client, this.getDefaultMjolnirConfig(managementRoomId))); - await managedMjolnir.createFirstList(requestingUserId, 'list') this.mjolnirs.set(await client.getUserId(), managedMjolnir); + return managedMjolnir; } public onEvent(request: Request, context: BridgeContext) { diff --git a/src/appservice/datastore.ts b/src/appservice/datastore.ts new file mode 100644 index 0000000..f69ed84 --- /dev/null +++ b/src/appservice/datastore.ts @@ -0,0 +1,66 @@ +import { Client } from "pg"; + +export interface MjolnirRecord { + localPart: string, + owner: string, + managementRoom: string, +} + +export interface DataStore { + init(): Promise; + + list(): Promise; + + store(mjolnirRecord: MjolnirRecord): Promise; + + lookupByOwner(owner: string): Promise; + + lookupByMxid(mxid: string): Promise; +} + +export class PgDataStore implements DataStore { + private pgClient: Client; + + constructor(connectionString: string) { + this.pgClient = new Client({ connectionString: connectionString }); + } + + public async init(): Promise { + await this.pgClient.connect(); + } + + public async list(): Promise { + const result = await this.pgClient.query("SELECT mxid, owner, managementRoom FROM mjolnir"); + + if (!result.rowCount) { + return []; + } + + return result.rows; + } + + public async store(mjolnirRecord: MjolnirRecord): Promise { + await this.pgClient.query( + "INSERT INTO mjolnir (mxid, owner, managementRoom) VALUES ($1, $2, $3)", + [mjolnirRecord.mxid, mjolnirRecord.owner, mjolnirRecord.managementRoom], + ); + } + + public async lookupByOwner(owner: string): Promise { + const result = await this.pgClient.query( + "SELECT mxid, owner FROM mjolnir WHERE owner = $1", + [owner], + ); + + return result.rows; + } + + public async lookupByMxid(mxid: string): Promise { + const result = await this.pgClient.query( + "SELECT mxid, owner, managementRoom FROM mjolnir WHERE mxid = $1", + [mxid], + ); + + return result.rows; + } +}