From b124f682272df8a9e6589f6a15e8d74a80abc146 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 19 Dec 2018 21:17:36 -0700 Subject: [PATCH] Appservice transaction handling, user queries, bot-backed invites, etc Includes added functions for setting profiles and inviting users. Changes to the join strategy are also included to better support appservice bot-backed invites. Not included is room handling and 3rd party stuff. --- examples/appservice.ts | 18 ++- package-lock.json | 25 +++ package.json | 2 +- src/MatrixClient.ts | 47 +++++- src/appservice/Appservice.ts | 153 +++++++++++++++++-- src/appservice/Intent.ts | 41 +++-- src/index.ts | 1 + src/storage/IAppserviceStorageProvider.ts | 13 ++ src/storage/MemoryStorageProvider.ts | 15 +- src/storage/SimpleFsStorageProvider.ts | 18 ++- src/strategies/AppserviceJoinRoomStrategy.ts | 20 +++ src/strategies/JoinRoomStrategy.ts | 4 +- tsconfig-examples.json | 2 +- 13 files changed, 321 insertions(+), 38 deletions(-) create mode 100644 src/strategies/AppserviceJoinRoomStrategy.ts diff --git a/examples/appservice.ts b/examples/appservice.ts index 12ef6824..2ebf7da9 100644 --- a/examples/appservice.ts +++ b/examples/appservice.ts @@ -41,7 +41,7 @@ const appservice = new Appservice(options); AutojoinRoomsMixin.setupOnAppservice(appservice); appservice.on("room.event", (roomId, event) => { - console.log(`Received event ${event["id"]} (${event["type"]}) from ${event["sender"]} in ${roomId}`); + console.log(`Received event ${event["event_id"]} (${event["type"]}) from ${event["sender"]} in ${roomId}`); }); appservice.on("room.message", (roomId, event) => { @@ -49,15 +49,27 @@ appservice.on("room.message", (roomId, event) => { if (event["content"]["msgtype"] !== "m.text") return; const body = event["content"]["body"]; - console.log(`Received message ${event["id"]} from ${event["sender"]} in ${roomId}: ${body}`); + console.log(`Received message ${event["event_id"]} from ${event["sender"]} in ${roomId}: ${body}`); // We'll create fake ghosts based on the event ID. Typically these users would be mapped // by some other means and not arbitrarily. The ghost here also echos whatever the original // user said. - const intent = appservice.getIntentForSuffix(event["id"].replace(/^[a-z0-9]/g, '_')); + const intent = appservice.getIntentForSuffix(event["event_id"].toLowerCase().replace(/[^a-z0-9]/g, '_')); intent.sendText(roomId, body, "m.notice"); }); +appservice.on("query.user", (userId, createUser) => { + // This is called when the homeserver queries a user's existence. At this point, a + // user should be created. To do that, give an object or Promise of an object in the + // form below to the createUser function (as shown). To prevent the creation of a user, + // pass false to createUser, like so: createUser(false); + console.log(`Received query for user ${userId}`); + createUser({ + display_name: "Test User", + avatar_mxc: "mxc://localhost/somewhere", + }); +}); + // Note: The following 3 handlers only fire for appservice users! These will NOT be fired // for everyone. diff --git a/package-lock.json b/package-lock.json index 33e0b367..a2865770 100644 --- a/package-lock.json +++ b/package-lock.json @@ -109,6 +109,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -679,6 +687,18 @@ "brace-expansion": "^1.1.7" } }, + "morgan": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", + "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", + "requires": { + "basic-auth": "~2.0.0", + "debug": "2.6.9", + "depd": "~1.1.2", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -702,6 +722,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/package.json b/package.json index 0b3ed19a..ac7f8444 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "prepublishOnly": "npm run build", "build": "tsc", "lint": "tslint --project ./tsconfig.json -t stylish", - "build:examples": "tsc -p tsconfig-examples.json", "example:appservice": "npm run build:examples && node lib/examples/appservice.js" }, @@ -33,6 +32,7 @@ "express": "^4.16.4", "hash.js": "^1.1.7", "lowdb": "^1.0.0", + "morgan": "^1.9.1", "request": "^2.88.0", "tslint": "^5.11.0", "typescript": "^3.1.1" diff --git a/src/MatrixClient.ts b/src/MatrixClient.ts index 10725ff4..d8868395 100644 --- a/src/MatrixClient.ts +++ b/src/MatrixClient.ts @@ -183,6 +183,18 @@ export class MatrixClient extends EventEmitter { }); } + /** + * Invites a user to a room. + * @param {string} userId the user ID to invite + * @param {string} roomId the room ID to invite the user to + * @returns {Promise<*>} resolves when completed + */ + public inviteUser(userId, roomId) { + return this.doRequest("POST", "/_matrix/client/r0/rooms/" + encodeURIComponent(roomId) + "/invite", null, { + user_id: userId, + }); + } + /** * Kicks a user from a room. * @param {string} userId the user ID to kick @@ -333,7 +345,7 @@ export class MatrixClient extends EventEmitter { let leaveEvent = null; for (let event of room['timeline']['events']) { if (event['type'] !== 'm.room.member') continue; - if (event['state_key'] !== this.userId) continue; + if (event['state_key'] !== await this.getUserId()) continue; const oldAge = leaveEvent && leaveEvent['unsigned'] && leaveEvent['unsigned']['age'] ? leaveEvent['unsigned']['age'] : 0; const newAge = event['unsigned'] && event['unsigned']['age'] ? event['unsigned']['age'] : 0; @@ -359,7 +371,7 @@ export class MatrixClient extends EventEmitter { let inviteEvent = null; for (let event of room['invite_state']['events']) { if (event['type'] !== 'm.room.member') continue; - if (event['state_key'] !== this.userId) continue; + if (event['state_key'] !== await this.getUserId()) continue; if (event['membership'] !== "invite") continue; const oldAge = inviteEvent && inviteEvent['unsigned'] && inviteEvent['unsigned']['age'] ? inviteEvent['unsigned']['age'] : 0; @@ -438,12 +450,36 @@ export class MatrixClient extends EventEmitter { return this.doRequest("GET", "/_matrix/client/r0/profile/" + userId); } + /** + * Sets a new display name for the user. + * @param {string} displayName the new display name for the user, or null to clear + * @returns {Promise<*>} resolves when complete + */ + public async setDisplayName(displayName: string): Promise { + const userId = encodeURIComponent(await this.getUserId()); + return this.doRequest("PUT", "/_matrix/client/r0/profile/" + userId + "/displayname", null, { + displayname: displayName, + }); + } + + /** + * Sets a new avatar url for the user. + * @param {string} avatarUrl the new avatar URL for the user, in the form of a Matrix Content URI + * @returns {Promise<*>} resolves when complete + */ + public async setAvatarUrl(avatarUrl: string): Promise { + const userId = encodeURIComponent(await this.getUserId()); + return this.doRequest("PUT", "/_matrix/client/r0/profile/" + userId + "/avatar_url", null, { + avatar_url: avatarUrl, + }); + } + /** * Joins the given room * @param {string} roomIdOrAlias the room ID or alias to join * @returns {Promise} resolves to the joined room ID */ - public joinRoom(roomIdOrAlias: string): Promise { + public async joinRoom(roomIdOrAlias: string): Promise { const apiCall = (targetIdOrAlias: string) => { targetIdOrAlias = encodeURIComponent(targetIdOrAlias); return this.doRequest("POST", "/_matrix/client/r0/join/" + targetIdOrAlias).then(response => { @@ -451,7 +487,8 @@ export class MatrixClient extends EventEmitter { }); }; - if (this.joinStrategy) return this.joinStrategy.joinRoom(roomIdOrAlias, apiCall); + const userId = await this.getUserId(); + if (this.joinStrategy) return this.joinStrategy.joinRoom(roomIdOrAlias, userId, apiCall); else return apiCall(roomIdOrAlias); } @@ -604,7 +641,7 @@ export class MatrixClient extends EventEmitter { if (body && !Buffer.isBuffer(body)) console.debug("MatrixLiteClient (REQ-" + requestId + ")", "body = " + JSON.stringify(body)); if (body && Buffer.isBuffer(body)) console.debug("MatrixLiteClient (REQ-" + requestId + ")", "body = "); - const params: {[k: string]: any} = { + const params: { [k: string]: any } = { url: url, method: method, qs: qs, diff --git a/src/appservice/Appservice.ts b/src/appservice/Appservice.ts index 82a5d4a6..b31ab01d 100644 --- a/src/appservice/Appservice.ts +++ b/src/appservice/Appservice.ts @@ -2,7 +2,8 @@ import * as express from "express"; import { Intent } from "./Intent"; import { IAppserviceStorageProvider } from "../storage/IAppserviceStorageProvider"; import { EventEmitter } from "events"; -import { IJoinRoomStrategy, MemoryStorageProvider } from ".."; +import { AppserviceJoinRoomStrategy, IJoinRoomStrategy, IPreprocessor, MemoryStorageProvider } from ".."; +import * as morgan from "morgan"; /** * Represents an application service's registration file. This is expected to be @@ -136,6 +137,8 @@ export class Appservice extends EventEmitter { private app = express(); private intents: { [userId: string]: Intent } = {}; + private eventProcessors: { [eventType: string]: IPreprocessor[] } = {}; + private pendingTransactions: { [txnId: string]: Promise } = {}; /** * Creates a new application service. @@ -144,11 +147,18 @@ export class Appservice extends EventEmitter { constructor(private options: IAppserviceOptions) { super(); + options.joinStrategy = new AppserviceJoinRoomStrategy(options.joinStrategy, this); + this.registration = options.registration; this.storage = options.storage || new MemoryStorageProvider(); - this.app.put("/transactions/:txnId", this.onTransaction); - this.app.put("/_matrix/app/v1/transactions/:txnId", this.onTransaction); + this.app.use(express.json()); + this.app.use(morgan("combined")); + + this.app.get("/users/:userId", this.onUser.bind(this)); + this.app.put("/transactions/:txnId", this.onTransaction.bind(this)); + this.app.get("/_matrix/app/v1/users/:userId", this.onUser.bind(this)); + this.app.put("/_matrix/app/v1/transactions/:txnId", this.onTransaction.bind(this)); // Everything else can 404 // TODO: Should we permit other user namespaces and instead error when trying to use doSomethingBySuffix()? @@ -164,6 +174,7 @@ export class Appservice extends EventEmitter { if (!this.userPrefix.endsWith(".*")) { throw new Error("Expected user namespace to be a prefix"); } + this.userPrefix = this.userPrefix.substring(0, this.userPrefix.length - 2); // trim off the .* part } /** @@ -188,7 +199,7 @@ export class Appservice extends EventEmitter { public begin(): Promise { return new Promise((resolve, reject) => { this.app.listen(this.options.port, this.options.bindAddress, () => resolve()); - }); + }).then(() => this.botIntent.ensureRegistered()); } /** @@ -238,7 +249,7 @@ export class Appservice extends EventEmitter { */ public getIntentForUserId(userId: string): Intent { if (!this.intents[userId]) { - this.intents[userId] = new Intent(this.options, userId); + this.intents[userId] = new Intent(this.options, userId, this); } return this.intents[userId]; } @@ -249,12 +260,134 @@ export class Appservice extends EventEmitter { * @returns {boolean} true if the user is namespaced, false otherwise */ public isNamespacedUser(userId: string): boolean { - return userId.startsWith("@" + this.userPrefix) && userId.endsWith(":" + this.options.homeserverName); + return userId === this.botUserId || (userId.startsWith(this.userPrefix) && userId.endsWith(":" + this.options.homeserverName)); + } + + /** + * Adds a preprocessor to the event pipeline. When this client encounters an event, it + * will try to run it through the preprocessors it can in the order they were added. + * @param {IPreprocessor} preprocessor the preprocessor to add + */ + public addPreprocessor(preprocessor: IPreprocessor): void { + if (!preprocessor) throw new Error("Preprocessor cannot be null"); + + const eventTypes = preprocessor.getSupportedEventTypes(); + if (!eventTypes) return; // Nothing to do + + for (const eventType of eventTypes) { + if (!this.eventProcessors[eventType]) this.eventProcessors[eventType] = []; + this.eventProcessors[eventType].push(preprocessor); + } + } + + private async processEvent(event: any): Promise { + if (!event) return event; + if (!this.eventProcessors[event["type"]]) return event; + + for (const processor of this.eventProcessors[event["type"]]) { + await processor.processEvent(event, this.botIntent.underlyingClient); + } + + return event; + } + + private processMembershipEvent(event: any): void { + if (!event["content"]) return; + + const targetMembership = event["content"]["membership"]; + if (targetMembership === "join") { + this.emit("room.join", event["room_id"], event); + } else if (targetMembership === "ban" || targetMembership === "leave") { + this.emit("room.leave", event["room_id"], event); + } else if (targetMembership === "invite") { + this.emit("room.invite", event["room_id"], event); + } + } + + private isAuthed(req: any): boolean { + let providedToken = req.query ? req.query["access_token"] : null; + if (req.headers && req.headers["Authorization"]) { + const authHeader = req.headers["Authorization"]; + if (!authHeader.startsWith("Bearer ")) providedToken = null; + else providedToken = authHeader.substring("Bearer ".length); + } + + return providedToken === this.registration.hs_token; } - private onTransaction(req, res): void { - console.log(req.body); - console.log(JSON.stringify(req.body)); - res.status(200).send({}); + private async onTransaction(req, res): Promise { + if (!this.isAuthed(req)) { + res.status(401).send({errcode: "AUTH_FAILED", error: "Authentication failed"}); + } + + if (typeof(req.body) !== "object") { + res.status(400).send({errcode: "BAD_REQUEST", error: "Expected JSON"}); + return; + } + + if (!req.body["events"] || !Array.isArray(req.body["events"])) { + res.status(400).send({errcode: "BAD_REQUEST", error: "Invalid JSON: expected events"}); + return; + } + + const txnId = req.params["txnId"]; + + if (this.storage.isTransactionCompleted(txnId)) { + res.status(200).send({}); + } + + if (this.pendingTransactions[txnId]) { + try { + await this.pendingTransactions[txnId]; + } catch (e) { + console.error(e); + res.status(500).send({}); + } + } + + console.log("Processing transaction " + txnId); + this.pendingTransactions[txnId] = new Promise(async (resolve, reject) => { + for (let event of req.body["events"]) { + console.log(`Processing event of type ${event["type"]}`); + event = await this.processEvent(event); + if (event["type"] === "m.room.message") { + if (event['type'] === 'm.room.message') this.emit("room.message", event["room_id"], event); + this.emit("room.event", event["room_id"], event); + } + if (event['type'] === 'm.room.member' && this.isNamespacedUser(event['state_key'])) { + this.processMembershipEvent(event); + } + } + + resolve(); + }); + + try { + await this.pendingTransactions[txnId]; + res.status(200).send({}); + } catch (e) { + console.error(e); + res.status(500).send({}); + } + } + + private async onUser(req, res): Promise { + if (!this.isAuthed(req)) { + res.status(401).send({errcode: "AUTH_FAILED", error: "Authentication failed"}); + } + + const userId = req.params["userId"]; + this.emit("query.user", userId, async (result) => { + if (result.then) result = await result; + if (result === false) { + res.status(404).send({errcode: "USER_DOES_NOT_EXIST", error: "User not created"}); + } else { + const intent = this.getIntentForUserId(userId); + await intent.ensureRegistered(); + if (result.display_name) await intent.underlyingClient.setDisplayName(result.display_name); + if (result.avatar_mxc) await intent.underlyingClient.setAvatarUrl(result.avatar_mxc); + res.status(200).send({}); + } + }); } } diff --git a/src/appservice/Intent.ts b/src/appservice/Intent.ts index f3818f06..bd540bb8 100644 --- a/src/appservice/Intent.ts +++ b/src/appservice/Intent.ts @@ -1,5 +1,5 @@ -import { MatrixClient } from ".."; -import { IAppserviceRegistration, IAppserviceOptions } from "./appservice"; +import { Appservice, MatrixClient } from ".."; +import { IAppserviceOptions } from "./appservice"; import { IAppserviceStorageProvider } from "../storage/IAppserviceStorageProvider"; /** @@ -19,11 +19,12 @@ export class Intent { * Creates a new intent. Intended to be created by application services. * @param {IAppserviceOptions} options The options for the application service. * @param {string} impersonateUserId The user ID to impersonate. + * @param {Appservice} appservice The application service itself. */ - constructor(options: IAppserviceOptions, private impersonateUserId: string) { + constructor(options: IAppserviceOptions, private impersonateUserId: string, private appservice: Appservice) { this.storage = options.storage; this.client = new MatrixClient(options.homeserverUrl, options.registration.as_token); - this.client.impersonateUserId(impersonateUserId); + if (impersonateUserId !== appservice.botUserId) this.client.impersonateUserId(impersonateUserId); if (options.joinStrategy) this.client.setJoinStrategy(options.joinStrategy); } @@ -59,7 +60,7 @@ export class Intent { * @returns {Promise} Resolves to the event ID of the sent message. */ public async sendText(roomId: string, body: string, msgtype: "m.text" | "m.emote" | "m.notice" = "m.text"): Promise { - return this.sendEvent(roomId, { body: body, msgtype: msgtype }); + return this.sendEvent(roomId, {body: body, msgtype: msgtype}); } /** @@ -73,12 +74,22 @@ export class Intent { return this.client.sendMessage(roomId, content); } - private async ensureRegisteredAndJoined(roomId: string) { + /** + * Ensures the user is registered and joined to the given room. + * @param {string} roomId The room ID to join + * @returns {Promise<*>} Resolves when complete + */ + public async ensureRegisteredAndJoined(roomId: string) { await this.ensureRegistered(); await this.ensureJoined(roomId); } - private async ensureJoined(roomId: string) { + /** + * Ensures the user is joined to the given room + * @param {string} roomId The room ID to join + * @returns {Promise<*>} Resolves when complete + */ + public async ensureJoined(roomId: string) { if (this.knownJoinedRooms.indexOf(roomId) !== -1) { return; } @@ -92,18 +103,26 @@ export class Intent { return; } - // TODO: Set the join strategy to retry and then rely on the appservice bot user to invite, if possible. return this.client.joinRoom(roomId); } - private async ensureRegistered() { + /** + * Ensures the user is registered + * @returns {Promise<*>} Resolves when complete + */ + public async ensureRegistered() { if (!this.storage.isUserRegistered(this.userId)) { await this.client.doRequest("POST", "/_matrix/client/r0/register", null, { type: "m.login.application_service", username: this.userId.substring(1).split(":")[0], }).catch(err => { - console.error("Encountered error registering user: "); - console.error(err); + if (err.body && err.body["errcode"] === "M_USER_IN_USE") { + if (this.userId === this.appservice.botUserId) return null; + else console.error("Error registering user: User ID is in use"); + } else { + console.error("Encountered error registering user: "); + console.error(err); + } return null; // swallow error }); diff --git a/src/index.ts b/src/index.ts index 945f04fb..428dd227 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,3 +11,4 @@ export * from "./helpers/RichReply"; export * from "./storage/SimpleFsStorageProvider"; export * from "./appservice/Appservice"; export * from "./appservice/Intent"; +export * from "./strategies/AppserviceJoinRoomStrategy"; \ No newline at end of file diff --git a/src/storage/IAppserviceStorageProvider.ts b/src/storage/IAppserviceStorageProvider.ts index 1d9a608c..0ece562a 100644 --- a/src/storage/IAppserviceStorageProvider.ts +++ b/src/storage/IAppserviceStorageProvider.ts @@ -9,4 +9,17 @@ export interface IAppserviceStorageProvider { * @returns {boolean} True if registered. */ isUserRegistered(userId: string): boolean; + + /** + * Flags a transaction as completed. + * @param {string} transactionId The transaction ID. + */ + setTransactionCompleted(transactionId: string); + + /** + * Determines if a transaction has been flagged as completed. + * @param {string} transactionId The transaction ID to check. + * @returns {boolean} True if the transaction has been completed. + */ + isTransactionCompleted(transactionId: string): boolean; } \ No newline at end of file diff --git a/src/storage/MemoryStorageProvider.ts b/src/storage/MemoryStorageProvider.ts index 9ccd1ab0..9361814e 100644 --- a/src/storage/MemoryStorageProvider.ts +++ b/src/storage/MemoryStorageProvider.ts @@ -5,13 +5,14 @@ import { IAppserviceStorageProvider } from "./IAppserviceStorageProvider"; export class MemoryStorageProvider implements IStorageProvider, IAppserviceStorageProvider { private syncToken: string; - private appserviceUsers: {[userId:string]:{registered: boolean}} = {}; + private appserviceUsers: { [userId: string]: { registered: boolean } } = {}; + private appserviceTransactions: { [txnId: string]: boolean } = {}; - setSyncToken(token: string|null): void { + setSyncToken(token: string | null): void { this.syncToken = token; } - getSyncToken(): string|null { + getSyncToken(): string | null { return this.syncToken; } @@ -32,4 +33,12 @@ export class MemoryStorageProvider implements IStorageProvider, IAppserviceStora isUserRegistered(userId: string): boolean { return this.appserviceUsers[userId] && this.appserviceUsers[userId].registered; } + + isTransactionCompleted(transactionId: string): boolean { + return !!this.appserviceTransactions[transactionId]; + } + + setTransactionCompleted(transactionId: string) { + this.appserviceTransactions[transactionId] = true; + } } \ No newline at end of file diff --git a/src/storage/SimpleFsStorageProvider.ts b/src/storage/SimpleFsStorageProvider.ts index bb86ed6f..73e66d55 100644 --- a/src/storage/SimpleFsStorageProvider.ts +++ b/src/storage/SimpleFsStorageProvider.ts @@ -17,14 +17,15 @@ export class SimpleFsStorageProvider implements IStorageProvider, IAppserviceSto syncToken: null, filter: null, appserviceUsers: {}, // userIdHash => { data } + appserviceTransactions: {}, // txnIdHash => { data } }).write(); } - setSyncToken(token: string|null): void { + setSyncToken(token: string | null): void { this.db.set('syncToken', token).write(); } - getSyncToken(): string|null { + getSyncToken(): string | null { return this.db.get('syncToken').value(); } @@ -48,4 +49,17 @@ export class SimpleFsStorageProvider implements IStorageProvider, IAppserviceSto const key = sha512().update(userId).digest('hex'); return this.db.get(`appserviceUsers.${key}.registered`).value(); } + + isTransactionCompleted(transactionId: string): boolean { + const key = sha512().update(transactionId).digest('hex'); + return this.db.get(`appserviceTransactions.${key}.completed`).value(); + } + + setTransactionCompleted(transactionId: string) { + const key = sha512().update(transactionId).digest('hex'); + this.db + .update(`appserviceTransactions.${key}.txnId`, transactionId) + .update(`appserviceTransactions.${key}.completed`, true) + .write(); + } } diff --git a/src/strategies/AppserviceJoinRoomStrategy.ts b/src/strategies/AppserviceJoinRoomStrategy.ts new file mode 100644 index 00000000..05eddc62 --- /dev/null +++ b/src/strategies/AppserviceJoinRoomStrategy.ts @@ -0,0 +1,20 @@ +import { IJoinRoomStrategy } from "./JoinRoomStrategy"; +import { Appservice } from ".."; + +export class AppserviceJoinRoomStrategy implements IJoinRoomStrategy { + + constructor(private underlyingStrategy: IJoinRoomStrategy, private appservice: Appservice) { + } + + public joinRoom(roomIdOrAlias: string, userId: string, apiCall: (roomIdOrAlias: string) => Promise): Promise { + return apiCall(roomIdOrAlias).catch(async (err) => { + console.error(err); + if (userId !== this.appservice.botUserId) { + const client = this.appservice.botIntent.underlyingClient; + const roomId = await client.resolveRoom(roomIdOrAlias); + return client.inviteUser(userId, roomId); + } + return err; + }).then(() => this.underlyingStrategy ? this.underlyingStrategy.joinRoom(roomIdOrAlias, userId, apiCall) : apiCall(roomIdOrAlias)); + } +} \ No newline at end of file diff --git a/src/strategies/JoinRoomStrategy.ts b/src/strategies/JoinRoomStrategy.ts index 9911087e..9f565db2 100644 --- a/src/strategies/JoinRoomStrategy.ts +++ b/src/strategies/JoinRoomStrategy.ts @@ -1,5 +1,5 @@ export interface IJoinRoomStrategy { - joinRoom(roomIdOrAlias: string, apiCall: (roomIdOrAlias: string) => Promise): Promise; + joinRoom(roomIdOrAlias: string, userId: string, apiCall: (roomIdOrAlias: string) => Promise): Promise; } export class SimpleRetryJoinStrategy implements IJoinRoomStrategy { @@ -13,7 +13,7 @@ export class SimpleRetryJoinStrategy implements IJoinRoomStrategy { 15 * 60 * 1000, // 15 minutes ]; - public joinRoom(roomIdOrAlias: string, apiCall: (roomIdOrAlias: string) => Promise): Promise { + public joinRoom(roomIdOrAlias: string, userId: string, apiCall: (roomIdOrAlias: string) => Promise): Promise { let currentSchedule = this.schedule[0]; const doJoin = () => waitPromise(currentSchedule).then(() => apiCall(roomIdOrAlias)); diff --git a/tsconfig-examples.json b/tsconfig-examples.json index c3aabcf0..f8f7df60 100644 --- a/tsconfig-examples.json +++ b/tsconfig-examples.json @@ -7,7 +7,7 @@ "target": "es2015", "noImplicitAny": false, "sourceMap": false, - "outDir": "./lib/examples", + "outDir": "./lib", "declaration": true, "types": [ "node"