import { DeviceKeyAlgorithm , extractRequestError , IAppserviceCryptoStorageProvider , IAppserviceStorageProvider , ICryptoStorageProvider , LogService , MatrixClient , Metrics } from ".." ; import { Appservice , IAppserviceOptions } from "./Appservice" ; import { timedIntentFunctionCall } from "../metrics/decorators" ; import { UnstableAppserviceApis } from "./UnstableAppserviceApis" ; category export class Intent { /**
     * The metrics instance for this intent. Note that this will not raise metrics
     * for the underlying client - those will be available through this instance's
     * parent (the appservice).
     */ public readonly metrics : Metrics ; private readonly storage : IAppserviceStorageProvider ; private readonly cryptoStorage : IAppserviceCryptoStorageProvider ; private client : MatrixClient ; private unstableApisInstance : UnstableAppserviceApis ; private knownJoinedRooms : string [ ] = [ ] ; private cryptoSetupPromise : Promise < void > ; param { IAppserviceOptions } options param { string } impersonateUserId param { Appservice } appservice constructor ( private options , private impersonateUserId , private appservice ) { this . metrics = new Metrics ( appservice . metrics ) ; this . storage = options . storage ; this . cryptoStorage = options . cryptoStorage ; this . makeClient ( false ) ; } private makeClient ( withCrypto , accessToken ? ) { let cryptoStore ; const storage = this . storage ?. storageForUser ?. ( this . userId ) ; if ( withCrypto ) { cryptoStore = this . cryptoStorage ?. storageForUser ( this . userId ) ; if ( ! cryptoStore ) { throw new Error ( "Tried to set up client with crypto when not available" ) ; } if ( ! storage ) { throw new Error ( "Tried to set up client with crypto, but no persistent storage" ) ; } } this . client = new MatrixClient ( this . options . homeserverUrl , accessToken ?? this . options . registration . as_token , storage , cryptoStore ) ; this . client . metrics = new Metrics ( this . appservice . metrics ) ; this . unstableApisInstance = new UnstableAppserviceApis ( this . client ) ; if ( this . impersonateUserId !== this . appservice . botUserId ) { this . client . impersonateUserId ( this . impersonateUserId ) ; } if ( this . options . joinStrategy ) { this . client . setJoinStrategy ( this . options . joinStrategy ) ; } } /**
     * Gets the user ID this intent is for.
     */ public get userId ( ) : string { return this . impersonateUserId ; } /**
     * Gets the underlying MatrixClient that powers this Intent.
     */ public get underlyingClient ( ) : MatrixClient { return this . client ; } return { UnstableAppserviceApis } public get unstableApis ( ) : UnstableAppserviceApis { return this . unstableApisInstance ; } returns { Promise < void > } @ timedIntentFunctionCall ( ) public async enableEncryption ( ) { if ( ! this . cryptoSetupPromise ) { this . cryptoSetupPromise = new Promise ( async ( resolve , reject ) => { try { await this . ensureRegistered ( ) ; const storage = this . storage ?. storageForUser ?. ( this . userId ) ; this . client . impersonateUserId ( this . userId ) ; const cryptoStore = this . cryptoStorage ?. storageForUser ( this . userId ) ; if ( ! cryptoStore ) { throw new Error ( "Failed to create crypto store" ) ; } const ownDevices = await this . client . getOwnDevices ( ) ; let deviceId = await cryptoStore . getDeviceId ( ) ; if ( ! deviceId || ! ownDevices . some ( d => d . device_id === deviceId ) ) { const deviceKeys = await this . client . getUserDevices ( [ this . userId ] ) ; const userDeviceKeys = deviceKeys . device_keys [ this . userId ] ; if ( userDeviceKeys ) { const devicesWithKeys = Array . from ( Object . entries ( userDeviceKeys ) ) . filter ( d => d [ 0 ] === d [ 1 ] . device_id && ! ! d [ 1 ] . keys ?. [ `${ DeviceKeyAlgorithm . Curve25519 }:${ d [ 1 ] . device_id }` ] ) ; deviceId = devicesWithKeys [ 0 ] ?. [ 1 ] ?. device_id ; } } let prepared = false ; if ( deviceId ) { this . makeClient ( true ) ; this . client . impersonateUserId ( this . userId , deviceId ) ; const respDeviceId = ( await this . client . getWhoAmI ( ) ) . device_id ; prepared = ( respDeviceId === deviceId ) ; } if ( ! prepared ) { const accessToken = await Promise . resolve ( storage ?. readValue ( "accessToken" ) ) ; if ( ! accessToken ) { const loginBody = { type : "uk.half-shot.msc2778.login.application_service" , identifier : { type : "m.id.user" , user : this . userId , } , } ; this . client . impersonateUserId ( null ) ; const res = await this . client . doRequest ( "POST" , "/_matrix/client/r0/login" , { } , loginBody ) ; this . makeClient ( true , res [ 'access_token' ] ) ; storage . storeValue ( "accessToken" , this . client . accessToken ) ; prepared = true ; } else { this . makeClient ( true , accessToken ) ; prepared = true ; } } if ( ! prepared ) { throw new Error ( "Unable to establish a device ID" ) ; } await this . client . crypto . prepare ( await this . client . getJoinedRooms ( ) ) ; resolve ( ) ; } catch ( e ) { reject ( e ) ; } } ) ; } return this . cryptoSetupPromise ; } returns { Promise < string [ ] > } @ timedIntentFunctionCall ( ) public async getJoinedRooms ( ) { await this . ensureRegistered ( ) ; if ( this . knownJoinedRooms . length === 0 ) await this . refreshJoinedRooms ( ) ; return this . knownJoinedRooms . map ( r => r ) ; } param { string } roomId returns { Promise < any > } @ timedIntentFunctionCall ( ) public async leaveRoom ( roomId ) { await this . ensureRegistered ( ) ; return this . client . leaveRoom ( roomId ) . then ( async ( ) => { await this . refreshJoinedRooms ( ) ; } ) ; } param { string } roomIdOrAlias returns { Promise < string > } @ timedIntentFunctionCall ( ) public async joinRoom ( roomIdOrAlias ) { await this . ensureRegistered ( ) ; return this . client . joinRoom ( roomIdOrAlias ) . then ( async ( roomId ) => { await this . refreshJoinedRooms ( ) ; return roomId ; } ) ; } param { string } roomId param { string } body param { "m.text" | "m.emote" | "m.notice" } msgtype returns { Promise < string > } @ timedIntentFunctionCall ( ) public async sendText ( roomId , body , msgtype = "m.text" ) { return this . sendEvent ( roomId , { body : body , msgtype : msgtype } ) ; } param { string } roomId param { any } content returns { Promise < string > } @ timedIntentFunctionCall ( ) public async sendEvent ( roomId , content ) { await this . ensureRegisteredAndJoined ( roomId ) ; return this . client . sendMessage ( roomId , content ) ; } param { string } roomId returns { Promise < any > } @ timedIntentFunctionCall ( ) public async ensureRegisteredAndJoined ( roomId ) { await this . ensureRegistered ( ) ; await this . ensureJoined ( roomId ) ; } param { string } roomId returns { Promise < any > } @ timedIntentFunctionCall ( ) public async ensureJoined ( roomId ) { if ( this . knownJoinedRooms . indexOf ( roomId ) !== - 1 ) { return ; } await this . refreshJoinedRooms ( ) ; if ( this . knownJoinedRooms . indexOf ( roomId ) !== - 1 ) { return ; } const returnedRoomId = await this . client . joinRoom ( roomId ) ; if ( ! this . knownJoinedRooms . includes ( returnedRoomId ) ) { this . knownJoinedRooms . push ( returnedRoomId ) ; } return returnedRoomId ; } returns { Promise < string [ ] > } @ timedIntentFunctionCall ( ) public async refreshJoinedRooms ( ) { this . knownJoinedRooms = await this . client . getJoinedRooms ( ) ; return this . knownJoinedRooms . map ( r => r ) ; } returns { Promise < any > } @ timedIntentFunctionCall ( ) public async ensureRegistered ( ) { if ( ! ( await Promise . resolve ( this . storage . isUserRegistered ( this . userId ) ) ) ) { try { const result = await this . client . doRequest ( "POST" , "/_matrix/client/r0/register" , null , { type : "m.login.application_service" , username : this . userId . substring ( 1 ) . split ( ":" ) [ 0 ] , } ) ; if ( result [ 'errcode' ] ) { throw { body : result } ; } } catch ( err ) { if ( typeof ( err . body ) === "string" ) err . body = JSON . parse ( err . body ) ; if ( err . body && err . body [ "errcode" ] === "M_USER_IN_USE" ) { await Promise . resolve ( this . storage . addRegisteredUser ( this . userId ) ) ; if ( this . userId === this . appservice . botUserId ) { return null ; } else { LogService . error ( "Appservice" , "Error registering user: User ID is in use" ) ; return null ; } } else { LogService . error ( "Appservice" , "Encountered error registering user: " ) ; LogService . error ( "Appservice" , extractRequestError ( err ) ) ; } throw err ; } await Promise . resolve ( this . storage . addRegisteredUser ( this . userId ) ) ; } } }