import { ISmallBotConfig, MatrixJoinedRoomsResponse, MatrixRoomEvent, MatrixRoomStateResponse, MatrixSyncResponse, MatrixUserProfileResponse, MatrixWhoAmIResponse, } from "./ISmallBot.ts"; /** * Wrapper to interact with the matrix.org client API */ export class SmallBot { private requestId = 0; /** * Create an instance of `SmallBot` * ```ts * const client = new SmallBot({ * accessToken: "mysecretaccesstoken", * homeserverUrl: "https://matrix.org/", * eventHandler: async (client, roomId, event) => { * if (event.sender !== client.ownUserId) { * await client.sendRoomNotice(roomId, "You said: " + event.content.body + ""); * } * } *}); * ``` * @param config */ constructor(private config: ISmallBotConfig) { if (!config.syncTimeout) config.syncTimeout = 10000; if (!config.homeserverUrl) config.homeserverUrl = "https://matrix.org"; if (!config.logger) { config.logger = { error: (log) => console.error(log), info: (log) => console.info(log), }; } if (!config.store) { config.store = { read: () => { try { return Deno.readTextFileSync("small.store"); } catch (err) { return undefined; } }, write: (since) => { Deno.writeTextFileSync("small.store", since); }, }; } if (!config.formatHTMLtoPlain) { config.formatHTMLtoPlain = (html) => html.replace(/<[^>]+>/g, ""); } } private async syncLoop(since?: string) { const syncResponse = await this.getSync(since); for (const entry of syncResponse.rooms.join.entries()) { for (const event of entry[1].timeline.events) { await this.config.eventHandler(this, entry[0], event); } } this.config.store?.write(syncResponse.next_batch); this.syncLoop(syncResponse.next_batch); } private async doRequest(uri: string, query: string[], method?: string, body?: string) { const url = this.config.homeserverUrl + (this.config.homeserverUrl?.endsWith("/") ? "" : "/") + "_matrix/client/r0/" + uri; const q = "?access_token=" + this.config.accessToken + (query.length > 0 ? "&" : "") + query.join("&"); const response = await fetch(url + q, { method: method ? method : "GET", body: body }); this.config.logger?.info("Fetched from '" + url + "'"); return await response.json(); } private async sendEvent(roomId: string, eventType: string, content: string) { const txnId = (new Date().getTime()) + "__REQ" + (++this.requestId); await this.doRequest("rooms/" + escape(roomId) + "/send/" + eventType + "/" + txnId, [], "PUT", content); } public get ownUserId(): string | undefined { return this.config.userId; } /** * Returns `MatrixUserProfileResponse` containing the current display name of the userId * ```ts * const profile = await client.getUserProfile(event.sender); * ``` * @param userId ID of the user to retrieve the profile */ async getUserProfile(userId: string) { return await this.doRequest( "profile/" + userId, [], ); } /** * Returns `MatrixRoomStateResponse` of the given room id containing the current display name * @param roomId ID of Room to get the display name */ async getRoomStateName(roomId: string) { return await this.doRequest("rooms/" + escape(roomId) + "/state/m.room.name/", []); } /** * Listens for new events on `/sync` with a timeout based on `syncTimeout` * This method is looped automatically when `start()` is called * @param since token used to sync events from a specific point in time */ async getSync(since?: string) { const response = await this.doRequest( "sync", [ "full_state=false", "timeout=" + this.config.syncTimeout, (since ? ("since=" + since) : ""), ] ); if (response.rooms && response.rooms.join) { response.rooms.join = new Map(Object.entries(response.rooms.join)); } else { response.rooms = {join: new Map()}; } return response; } /** * Returns the `MatrixWhoAmIResponse` containing the userId of the bot */ async whoAmI() { return await this.doRequest("account/whoami", []); } /** * Returns the `MatrixJoinedRoomsResponse` containing a Map of all joined roomId's */ async joinedRooms() { return await this.doRequest("joined_rooms", []); } /** * Alias of `sendMessage` with msgType `m.notice` * @param roomId ID of the Room * @param msg the HTML body of the message `formatHTMLtoPlain` will be used to create the plain-text version */ async sendRoomNotice(roomId: string, msg: string) { await this.sendMessage(roomId, "m.notice", msg); } /** * Alias of `sendMessage` with msgType `m.text` * @param roomId ID of the Room * @param msg the HTML body of the message `formatHTMLtoPlain` will be used to create the plain-text version */ async sendRoomText(roomId: string, msg: string) { await this.sendMessage(roomId, "m.text", msg); } /** * Send a custom message to a room * ```ts * await client.sendMessage(roomId, "m.text", "hello world"); * ``` * @param roomId ID of the Room * @param msgType type like `m.text` or `m.notice` * @param formattedBody the HTML body of the message `formatHTMLtoPlain` will be used to create the plain-text version */ async sendMessage( roomId: string, msgType: string, formattedBody: string, ) { const plain = this.config.formatHTMLtoPlain ? this.config.formatHTMLtoPlain(formattedBody) : ""; const payload = { msgtype: msgType, format: "org.matrix.custom.html", body: plain, formatted_body: formattedBody }; await this.sendEvent(roomId, "m.room.message", JSON.stringify(payload)); } /** * Call this method to start the /sync loop and send all events into your custom handler * ```ts * await client.start(); * ``` */ async start() { try { if (!this.config.userId) { this.config.userId = (await this.whoAmI()).user_id; } this.syncLoop(this.config.store?.read()); } catch (err) { this.config.logger?.error(err); } } }