import * as express from "express" ; import { Intent } from "./Intent" ; import { AppserviceJoinRoomStrategy , EncryptedRoomEvent , EventKind , IAppserviceCryptoStorageProvider , IAppserviceStorageProvider , IJoinRoomStrategy , IPreprocessor , LogService , MatrixClient , MemoryStorageProvider , Metrics , OTKAlgorithm , } from ".." ; import { EventEmitter } from "events" ; import * as morgan from "morgan" ; import { MatrixBridge } from "./MatrixBridge" ; import * as LRU from "lru-cache" ; import { IApplicationServiceProtocol } from "./http_responses" ; const EDU_ANNOTATION_KEY = "io.t2bot.sdk.bot.type" ; enum EduAnnotation { ToDevice = "to_device" , Ephemeral = "ephemeral" } category export interface IAppserviceRegistration { /**
     * Optional ID for the appplication service. Used by homeservers to track which application
     * service registers what.
     */ id ? : string ; /**
     * Optional URL at which the application service can be contacted.
     */ url ? : string ; /**
     * The token the application service uses to communicate with the homeserver.
     */ as_token : string ; /**
     * The token the homeserver uses to communicate with the application service.
     */ hs_token : string ; /**
     * The application service's own localpart (eg: "_irc_bot" in the user ID "@_irc_bot:domain.com")
     */ sender_localpart : string ; /**
     * The various namespaces the application service can support.
     */ namespaces : { /**
         * The user namespaces the application service is requesting.
         */ users : { /**
             * Whether or not the application service holds an exclusive lock on the namespace. This
             * means that no other user on the homeserver may register users that match this namespace.
             */ exclusive : boolean ; /**
             * The regular expression that the homeserver uses to determine if a user is in this namespace.
             */ regex : string ; /**
             * An optional group ID to enable flair for users in this namespace.
             */ groupId ? : string ; } [ ] ; /**
         * The room namespaces the application service is requesting. This is not for alises.
         */ rooms : { /**
             * Whether or not the application service holds an exclusive lock on the namespace.
             */ exclusive : boolean ; /**
             * The regular expression that the homeserver uses to determine if a user is in this namespace.
             */ regex : string ; } [ ] ; /**
         * The room alias namespaces the application service is requesting.
         */ aliases : { /**
             * Whether or not the application service holds an exclusive lock on the namespace. This means
             * that no other user on the homeserver may register aliases that match this namespace.
             */ exclusive : boolean ; /**
             * The regular expression that the homeserver uses to determine if an alias is in this namespace.
             */ regex : string ; } [ ] ; } ; /**
     * The protocols the application service supports. Optional.
     */ protocols ? : string [ ] ; /**
     * If the application service is rate limited by the homeserver. Optional.
     */ rate_limited ? : boolean ; see https "de.sorunome.msc2409.push_ephemeral" ? : boolean ; } category export interface IAppserviceOptions { /**
     * The port to listen for requests from the homeserver on.
     */ port : number ; /**
     * The bind address to listen for requests on.
     */ bindAddress : string ; /**
     * The name of the homeserver, as presented over federation (eg: "matrix.org")
     */ homeserverName : string ; /**
     * The URL to the homeserver's client server API (eg: "https://matrix.org")
     */ homeserverUrl : string ; /**
     * The storage provider to use for this application service.
     */ storage ? : IAppserviceStorageProvider ; /**
     * The storage provider to use for setting up encryption. Encryption will be
     * disabled for all intents and the appservice if not configured.
     */ cryptoStorage ? : IAppserviceCryptoStorageProvider ; /**
     * The registration for this application service.
     */ registration : IAppserviceRegistration ; /**
     * The join strategy to use for all intents, if any.
     */ joinStrategy ? : IJoinRoomStrategy ; /**
     * Options for how Intents are handled.
     */ intentOptions ? : { /**
         * The maximum number of intents to keep cached. Defaults to 10 thousand.
         */ maxCached ? : number ; /**
         * The maximum age in milliseconds to keep an Intent around for, provided
         * the maximum number of intents has been reached. Defaults to 60 minutes.
         */ maxAgeMs ? : number ; /**
         * If false (default), crypto will not be automatically set up for all intent
         * instances - it will need to be manually enabled with
         * `await intent.enableEncryption()`.
         *
         * If true, crypto will be automatically set up.
         *
         * Note that the appservice bot account is considered an intent.
         */ encryption ? : boolean ; } ; } category export class Appservice extends EventEmitter { /**
     * The metrics instance for this appservice. This will raise all metrics
     * from this appservice instance as well as any intents/MatrixClients created
     * by the appservice.
     */ public readonly metrics : Metrics = new Metrics ( ) ; private readonly userPrefix : string | null ; private readonly aliasPrefix : string | null ; private readonly registration : IAppserviceRegistration ; private readonly storage : IAppserviceStorageProvider ; private readonly cryptoStorage : IAppserviceCryptoStorageProvider ; private readonly bridgeInstance = new MatrixBridge ( this ) ; private app = express ( ) ; private appServer : any ; private intentsCache : LRU ; private eventProcessors : { [ eventType ] : IPreprocessor [ ] ; } = { } ; private pendingTransactions : { [ txnId ] : Promise < any > ; } = { } ; param { IAppserviceOptions } options constructor ( private options ) { super ( ) ; options . joinStrategy = new AppserviceJoinRoomStrategy ( options . joinStrategy , this ) ; if ( ! options . intentOptions ) options . intentOptions = { } ; if ( ! options . intentOptions . maxAgeMs ) options . intentOptions . maxAgeMs = 60 * 60 * 1000 ; if ( ! options . intentOptions . maxCached ) options . intentOptions . maxCached = 10000 ; this . intentsCache = new LRU ( { max : options . intentOptions . maxCached , maxAge : options . intentOptions . maxAgeMs , } ) ; this . registration = options . registration ; if ( ! this . registration . protocols ) { this . registration . protocols = [ ] ; } this . storage = options . storage || new MemoryStorageProvider ( ) ; options . storage = this . storage ; this . cryptoStorage = options . cryptoStorage ; this . app . use ( express . json ( { limit : Number . MAX_SAFE_INTEGER } ) ) ; this . app . use ( morgan ( "combined" ) ) ; this . app . set ( 'etag' , false ) ; this . app . get ( "/users/:userId" , this . onUser . bind ( this ) ) ; this . app . get ( "/rooms/:roomAlias" , this . onRoomAlias . 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 . get ( "/_matrix/app/v1/rooms/:roomAlias" , this . onRoomAlias . bind ( this ) ) ; this . app . put ( "/_matrix/app/v1/transactions/:txnId" , this . onTransaction . bind ( this ) ) ; this . app . get ( "/_matrix/app/v1/thirdparty/protocol/:protocol" , this . onThirdpartyProtocol . bind ( this ) ) ; this . app . get ( "/_matrix/app/v1/thirdparty/user/:protocol" , this . onThirdpartyUser . bind ( this ) ) ; this . app . get ( "/_matrix/app/v1/thirdparty/user" , this . onThirdpartyUser . bind ( this ) ) ; this . app . get ( "/_matrix/app/v1/thirdparty/location/:protocol" , this . onThirdpartyLocation . bind ( this ) ) ; this . app . get ( "/_matrix/app/v1/thirdparty/location" , this . onThirdpartyLocation . bind ( this ) ) ; if ( ! this . registration . namespaces || ! this . registration . namespaces . users || this . registration . namespaces . users . length === 0 ) { throw new Error ( "No user namespaces in registration" ) ; } if ( this . registration . namespaces . users . length !== 1 ) { throw new Error ( "Too many user namespaces registered: expecting exactly one" ) ; } let userPrefix = ( this . registration . namespaces . users [ 0 ] . regex || "" ) . split ( ":" ) [ 0 ] ; if ( ! userPrefix . endsWith ( ".*" ) && ! userPrefix . endsWith ( ".+" ) ) { this . userPrefix = null ; } else { this . userPrefix = userPrefix . substring ( 0 , userPrefix . length - 2 ) ; } if ( ! this . registration . namespaces || ! this . registration . namespaces . aliases || this . registration . namespaces . aliases . length === 0 || this . registration . namespaces . aliases . length !== 1 ) { this . aliasPrefix = null ; } else { this . aliasPrefix = ( this . registration . namespaces . aliases [ 0 ] . regex || "" ) . split ( ":" ) [ 0 ] ; if ( ! this . aliasPrefix . endsWith ( ".*" ) && ! this . aliasPrefix . endsWith ( ".+" ) ) { this . aliasPrefix = null ; } else { this . aliasPrefix = this . aliasPrefix . substring ( 0 , this . aliasPrefix . length - 2 ) ; } } } /**
     * Gets the express app instance which is serving requests. Not recommended for
     * general usage, but may be used to append routes to the web server.
     */ public get expressAppInstance ( ) { return this . app ; } /**
     * Gets the bridge-specific APIs for this application service.
     */ public get bridge ( ) : MatrixBridge { return this . bridgeInstance ; } /**
     * Get the application service's "bot" user ID (the sender_localpart).
     */ public get botUserId ( ) : string { return this . getUserId ( this . registration . sender_localpart ) ; } returns { Intent } public get botIntent ( ) : Intent { return this . getIntentForUserId ( this . botUserId ) ; } returns { MatrixClient } public get botClient ( ) : MatrixClient { return this . botIntent . underlyingClient ; } returns { Promise < void > } public begin ( ) { return new Promise < void > ( ( resolve , reject ) => { this . appServer = this . app . listen ( this . options . port , this . options . bindAddress , ( ) => resolve ( ) ) ; } ) . then ( async ( ) => { if ( this . options . intentOptions ?. encryption ) { await this . botIntent . enableEncryption ( ) ; } else { await this . botIntent . ensureRegistered ( ) ; } } ) ; } /**
     * Stops the application service, freeing the web server.
     */ public stop ( ) { if ( ! this . appServer ) return ; this . appServer . close ( ) ; } param localpart returns { Intent } public getIntent ( localpart ) { return this . getIntentForUserId ( this . getUserId ( localpart ) ) ; } param localpart returns { string } public getUserId ( localpart ) { return `@${ localpart }:${ this . options . homeserverName }` ; } param suffix returns { Intent } public getIntentForSuffix ( suffix ) { return this . getIntentForUserId ( this . getUserIdForSuffix ( suffix ) ) ; } param suffix returns { string } public getUserIdForSuffix ( suffix ) { if ( ! this . userPrefix ) { throw new Error ( `Cannot use getUserIdForSuffix, provided namespace did not include a valid suffix` ) ; } return `${ this . userPrefix }${ suffix }:${ this . options . homeserverName }` ; } param { string } userId returns { Intent } public getIntentForUserId ( userId ) { let intent = this . intentsCache . get ( userId ) ; if ( ! intent ) { intent = new Intent ( this . options , userId , this ) ; this . intentsCache . set ( userId , intent ) ; if ( this . options . intentOptions . encryption ) { intent . enableEncryption ( ) . catch ( e => { LogService . error ( "Appservice" , `Failed to set up crypto on intent ${ userId }` , e ) ; throw e ; } ) ; } } return intent ; } param { string } userId returns { string } public getSuffixForUserId ( userId ) { if ( ! this . userPrefix ) { throw new Error ( `Cannot use getUserIdForSuffix, provided namespace did not include a valid suffix` ) ; } if ( ! userId || ! userId . startsWith ( this . userPrefix ) || ! userId . endsWith ( `:${ this . options . homeserverName }` ) ) { return null ; } return userId . split ( '' ) . slice ( this . userPrefix . length ) . reverse ( ) . slice ( this . options . homeserverName . length + 1 ) . reverse ( ) . join ( '' ) ; } param { string } userId returns { boolean } public isNamespacedUser ( userId ) { return userId === this . botUserId || ! ! this . registration . namespaces ?. users . find ( ( { regex } ) => new RegExp ( regex ) . test ( userId ) ) ; } param localpart returns { string } public getAlias ( localpart ) { return `#${ localpart }:${ this . options . homeserverName }` ; } param suffix returns { string } public getAliasForSuffix ( suffix ) { if ( ! this . aliasPrefix ) { throw new Error ( "Invalid configured alias prefix" ) ; } return `${ this . aliasPrefix }${ suffix }:${ this . options . homeserverName }` ; } param suffix returns { string } public getAliasLocalpartForSuffix ( suffix ) { if ( ! this . aliasPrefix ) { throw new Error ( "Invalid configured alias prefix" ) ; } return `${ this . aliasPrefix . substr ( 1 ) }${ suffix }` ; } param { string } alias returns { string } public getSuffixForAlias ( alias ) { if ( ! this . aliasPrefix ) { throw new Error ( "Invalid configured alias prefix" ) ; } if ( ! alias || ! this . isNamespacedAlias ( alias ) ) { return null ; } return alias . split ( '' ) . slice ( this . aliasPrefix . length ) . reverse ( ) . slice ( this . options . homeserverName . length + 1 ) . reverse ( ) . join ( '' ) ; } param { string } alias returns { boolean } public isNamespacedAlias ( alias ) { if ( ! this . aliasPrefix ) { throw new Error ( "Invalid configured alias prefix" ) ; } return alias . startsWith ( this . aliasPrefix ) && alias . endsWith ( ":" + this . options . homeserverName ) ; } param { IPreprocessor } preprocessor public addPreprocessor ( preprocessor ) { if ( ! preprocessor ) throw new Error ( "Preprocessor cannot be null" ) ; const eventTypes = preprocessor . getSupportedEventTypes ( ) ; if ( ! eventTypes ) return ; for ( const eventType of eventTypes ) { if ( ! this . eventProcessors [ eventType ] ) this . eventProcessors [ eventType ] = [ ] ; this . eventProcessors [ eventType ] . push ( preprocessor ) ; } } param { string } networkId param { string } roomId param { "public" | "private" } visibility return { Promise < any > } public setRoomDirectoryVisibility ( networkId , roomId , visibility ) { roomId = encodeURIComponent ( roomId ) ; networkId = encodeURIComponent ( networkId ) ; return this . botClient . doRequest ( "PUT" , `/_matrix/client/r0/directory/list/appservice/${ networkId }/${ roomId }` , null , { visibility , } ) ; } private async processEphemeralEvent ( event ) { 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 , EventKind . EphemeralEvent ) ; } return event ; } private async processEvent ( event ) { 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 , EventKind . RoomEvent ) ; } return event ; } private processMembershipEvent ( event ) { 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 ) { 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 async onTransaction ( req , res ) { if ( ! this . isAuthed ( req ) ) { res . status ( 401 ) . json ( { errcode : "AUTH_FAILED" , error : "Authentication failed" } ) ; return ; } if ( typeof ( req . body ) !== "object" ) { res . status ( 400 ) . json ( { errcode : "BAD_REQUEST" , error : "Expected JSON" } ) ; return ; } if ( ! req . body [ "events" ] || ! Array . isArray ( req . body [ "events" ] ) ) { res . status ( 400 ) . json ( { errcode : "BAD_REQUEST" , error : "Invalid JSON: expected events" } ) ; return ; } const txnId = req . params [ "txnId" ] ; if ( await Promise . resolve ( this . storage . isTransactionCompleted ( txnId ) ) ) { res . status ( 200 ) . json ( { } ) ; return ; } if ( this . pendingTransactions [ txnId ] ) { try { await this . pendingTransactions [ txnId ] ; res . status ( 200 ) . json ( { } ) ; } catch ( e ) { LogService . error ( "Appservice" , e ) ; res . status ( 500 ) . json ( { } ) ; } return ; } LogService . info ( "Appservice" , "Processing transaction " + txnId ) ; this . pendingTransactions [ txnId ] = new Promise < void > ( async ( resolve ) => { const byUserId = { } ; const orderedEdus = [ ] ; if ( Array . isArray ( req . body [ "de.sorunome.msc2409.to_device" ] ) ) { orderedEdus . push ( ... req . body [ "de.sorunome.msc2409.to_device" ] . map ( e => ( { ... e , unsigned : { ... e [ 'unsigned' ] , [ EDU_ANNOTATION_KEY ] : EduAnnotation . ToDevice , } , } ) ) ) ; } if ( Array . isArray ( req . body [ "de.sorunome.msc2409.ephemeral" ] ) ) { orderedEdus . push ( ... req . body [ "de.sorunome.msc2409.ephemeral" ] . map ( e => ( { ... e , unsigned : { ... e [ 'unsigned' ] , [ EDU_ANNOTATION_KEY ] : EduAnnotation . Ephemeral , } , } ) ) ) ; } for ( let event of orderedEdus ) { if ( event [ 'edu_type' ] ) event [ 'type' ] = event [ 'edu_type' ] ; LogService . info ( "Appservice" , `Processing ${ event [ 'unsigned' ] [ EDU_ANNOTATION_KEY ] } event of type ${ event [ "type" ] }` ) ; event = await this . processEphemeralEvent ( event ) ; this . emit ( "ephemeral.event" , event ) ; if ( event [ "type" ] === "m.room.encrypted" || event [ EDU_ANNOTATION_KEY ] === EduAnnotation . ToDevice ) { const toUser = event [ "to_user_id" ] ; const intent = this . getIntentForUserId ( toUser ) ; await intent . enableEncryption ( ) ; if ( ! byUserId [ toUser ] ) byUserId [ toUser ] = { counts : null , toDevice : null , unusedFallbacks : null } ; if ( ! byUserId [ toUser ] . toDevice ) byUserId [ toUser ] . toDevice = [ ] ; byUserId [ toUser ] . toDevice . push ( event ) ; } } if ( this . cryptoStorage ) { const deviceLists = req . body [ "org.matrix.msc3202.device_lists" ] ?? { changed : [ ] , removed : [ ] } ; const otks = req . body [ "org.matrix.msc3202.device_one_time_key_counts" ] ; if ( otks ) { for ( const userId of Object . keys ( otks ) ) { const intent = this . getIntentForUserId ( userId ) ; await intent . enableEncryption ( ) ; const otksForUser = otks [ userId ] [ intent . underlyingClient . crypto . clientDeviceId ] ; if ( otksForUser ) { if ( ! byUserId [ userId ] ) byUserId [ userId ] = { counts : null , toDevice : null , unusedFallbacks : null } ; byUserId [ userId ] . counts = otksForUser ; } } } const fallbacks = req . body [ "org.matrix.msc3202.device_unused_fallback_key_types" ] ; if ( fallbacks ) { for ( const userId of Object . keys ( fallbacks ) ) { const intent = this . getIntentForUserId ( userId ) ; await intent . enableEncryption ( ) ; const fallbacksForUser = fallbacks [ userId ] [ intent . underlyingClient . crypto . clientDeviceId ] ; if ( Array . isArray ( fallbacksForUser ) && ! fallbacksForUser . includes ( OTKAlgorithm . Signed ) ) { if ( ! byUserId [ userId ] ) byUserId [ userId ] = { counts : null , toDevice : null , unusedFallbacks : null } ; byUserId [ userId ] . unusedFallbacks = fallbacksForUser ; } } } for ( const userId of Object . keys ( byUserId ) ) { const intent = this . getIntentForUserId ( userId ) ; await intent . enableEncryption ( ) ; const info = byUserId [ userId ] ; const userStorage = this . storage . storageForUser ( userId ) ; if ( ! info . toDevice ) info . toDevice = [ ] ; if ( ! info . unusedFallbacks ) info . unusedFallbacks = JSON . parse ( await userStorage . readValue ( "last_unused_fallbacks" ) || "[]" ) ; if ( ! info . counts ) info . counts = JSON . parse ( await userStorage . readValue ( "last_counts" ) || "{}" ) ; LogService . info ( "Appservice" , `Updating crypto state for ${ userId }` ) ; await intent . underlyingClient . crypto . updateSyncData ( info . toDevice , info . counts , info . unusedFallbacks , deviceLists . changed , deviceLists . removed ) ; } } for ( let event of req . body [ "events" ] ) { LogService . info ( "Appservice" , `Processing event of type ${ event [ "type" ] }` ) ; event = await this . processEvent ( event ) ; if ( event [ 'type' ] === 'm.room.encrypted' && this . cryptoStorage ) { this . emit ( "room.encrypted_event" , event [ "room_id" ] , event ) ; try { const encrypted = new EncryptedRoomEvent ( event ) ; const roomId = event [ 'room_id' ] ; try { event = ( await this . botClient . crypto . decryptRoomEvent ( encrypted , roomId ) ) . raw ; event = await this . processEvent ( event ) ; this . emit ( "room.decrypted_event" , roomId , event ) ; LogService . info ( "Appservice" , `Processing decrypted event of type ${ event [ "type" ] }` ) ; } catch ( e1 ) { LogService . warn ( "Appservice" , `Bot client was not able to decrypt ${ roomId } ${ event [ 'event_id' ] } - trying other intents` ) ; let tryUserId ; try { const userIdsInRoom = await this . botClient . getJoinedRoomMembers ( roomId ) ; tryUserId = userIdsInRoom . find ( u => this . isNamespacedUser ( u ) ) ; } catch ( e ) { LogService . error ( "Appservice" , "Failed to get members of room - cannot decrypt message" ) ; } if ( tryUserId ) { const intent = this . getIntentForUserId ( tryUserId ) ; event = ( await intent . underlyingClient . crypto . decryptRoomEvent ( encrypted , roomId ) ) . raw ; event = await this . processEvent ( event ) ; this . emit ( "room.decrypted_event" , roomId , event ) ; LogService . info ( "Appservice" , `Processing decrypted event of type ${ event [ "type" ] }` ) ; } else { throw e1 ; } } } catch ( e ) { LogService . error ( "Appservice" , `Decryption error on ${ event [ 'room_id' ] } ${ event [ 'event_id' ] }` , e ) ; this . emit ( "room.failed_decryption" , event [ 'room_id' ] , event , e ) ; } } this . emit ( "room.event" , event [ "room_id" ] , event ) ; if ( event [ 'type' ] === 'm.room.message' ) { this . emit ( "room.message" , event [ "room_id" ] , event ) ; } if ( event [ 'type' ] === 'm.room.member' && this . isNamespacedUser ( event [ 'state_key' ] ) ) { this . processMembershipEvent ( event ) ; } if ( event [ 'type' ] === 'm.room.tombstone' && event [ 'state_key' ] === '' ) { this . emit ( "room.archived" , event [ 'room_id' ] , event ) ; } if ( event [ 'type' ] === 'm.room.create' && event [ 'state_key' ] === '' && event [ 'content' ] && event [ 'content' ] [ 'predecessor' ] ) { this . emit ( "room.upgraded" , event [ 'room_id' ] , event ) ; } } resolve ( ) ; } ) ; try { await this . pendingTransactions [ txnId ] ; await Promise . resolve ( this . storage . setTransactionCompleted ( txnId ) ) ; res . status ( 200 ) . json ( { } ) ; } catch ( e ) { LogService . error ( "Appservice" , e ) ; res . status ( 500 ) . json ( { } ) ; } } private async onUser ( req , res ) { if ( ! this . isAuthed ( req ) ) { res . status ( 401 ) . json ( { errcode : "AUTH_FAILED" , error : "Authentication failed" } ) ; return ; } const userId = req . params [ "userId" ] ; this . emit ( "query.user" , userId , async ( result ) => { if ( result . then ) result = await result ; if ( result === false ) { res . status ( 404 ) . json ( { 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 ) . json ( result ) ; } } ) ; } private async onRoomAlias ( req , res ) { if ( ! this . isAuthed ( req ) ) { res . status ( 401 ) . json ( { errcode : "AUTH_FAILED" , error : "Authentication failed" } ) ; return ; } const roomAlias = req . params [ "roomAlias" ] ; this . emit ( "query.room" , roomAlias , async ( result ) => { if ( result . then ) result = await result ; if ( result === false ) { res . status ( 404 ) . json ( { errcode : "ROOM_DOES_NOT_EXIST" , error : "Room not created" } ) ; } else { const intent = this . botIntent ; await intent . ensureRegistered ( ) ; result [ "room_alias_name" ] = roomAlias . substring ( 1 ) . split ( ':' ) [ 0 ] ; result [ "__roomId" ] = await intent . underlyingClient . createRoom ( result ) ; res . status ( 200 ) . json ( result ) ; } } ) ; } private onThirdpartyProtocol ( req , res ) { if ( ! this . isAuthed ( req ) ) { res . status ( 401 ) . json ( { errcode : "AUTH_FAILED" , error : "Authentication failed" } ) ; return ; } const protocol = req . params [ "protocol" ] ; if ( ! this . registration . protocols . includes ( protocol ) ) { res . status ( 404 ) . json ( { errcode : "PROTOCOL_NOT_HANDLED" , error : "Protocol is not handled by this appservice" } ) ; return ; } this . emit ( "thirdparty.protocol" , protocol , ( protocolResponse ) => { res . status ( 200 ) . json ( protocolResponse ) ; } ) ; } private handleThirdpartyObject ( req , res , objType , matrixId ? ) { if ( ! this . isAuthed ( req ) ) { res . status ( 401 ) . json ( { errcode : "AUTH_FAILED" , error : "Authentication failed" } ) ; return ; } const protocol = req . params [ "protocol" ] ; const responseFunc = ( items ) => { if ( items && items . length > 0 ) { res . status ( 200 ) . json ( items ) ; return ; } res . status ( 404 ) . json ( { errcode : "NO_MAPPING_FOUND" , error : "No mappings found" } ) ; } ; if ( protocol ) { if ( ! this . registration . protocols . includes ( protocol ) ) { res . status ( 404 ) . json ( { errcode : "PROTOCOL_NOT_HANDLED" , error : "Protocol is not handled by this appservice" } ) ; return ; } delete req . query . access_token ; this . emit ( `thirdparty.${ objType }.remote` , protocol , req . query , responseFunc ) ; return ; } else if ( matrixId ) { this . emit ( `thirdparty.${ objType }.matrix` , matrixId , responseFunc ) ; return ; } res . status ( 400 ) . json ( { errcode : "INVALID_PARAMETERS" , error : "Invalid parameters given" } ) ; } private onThirdpartyUser ( req , res ) { return this . handleThirdpartyObject ( req , res , "user" , req . query [ "userid" ] as string ) ; } private onThirdpartyLocation ( req , res ) { return this . handleThirdpartyObject ( req , res , "location" , req . query [ "alias" ] as string ) ; } }