import { AsyncReturnType } from "type-fest" ; import { api , config , id , redis , Connection } from "./../index" ; import * as RedisModule from "../modules/redis" ; export namespace chatRoom { /**
     * Middleware definition for processing chat events.  Can be of the
     *
     * ```js
     *  var chatMiddleware = {
     *    name: 'chat middleware',
     *    priority: 1000,
     *    join: (connection, room) => {
     *      // announce all connections entering a room
     *      api.chatRoom.broadcast(null, room, 'I have joined the room: ' + connection.id, callback)
     *    },
     *    leave:(connection, room, callback) => {
     *      // announce all connections leaving a room
     *      api.chatRoom.broadcast(null, room, 'I have left the room: ' + connection.id, callback)
     *    },
     *    // Will be executed once per client connection before delivering the message.
     *    say: (connection, room, messagePayload) => {
     *      // do stuff
     *      log(messagePayload)
     *    },
     *    // Will be executed only once, when the message is sent to the server.
     *    onSayReceive: (connection, room, messagePayload) => {
     *      // do stuff
     *      log(messagePayload)
     *    }
     * }
     * api.chatRoom.addMiddleware(chatMiddleware)
     * ```
     */ export interface ChatMiddleware { /**Unique name for the middleware. */ name : string ; /**Module load order. Defaults to `api.config.general.defaultMiddlewarePriority`. */ priority ? : number ; /**Called when a connection joins a room. */ join ? : Function ; /**Called when a connection leaves a room. */ leave ? : Function ; /**Called when a connection says a message to a room. */ onSayReceive ? : Function ; /**Called when a connection is about to receive a say message. */ say ? : Function ; } export interface ChatPubSubMessage extends RedisModule . redis . PubSubMessage { messageType : string ; serverToken : string ; serverId : string | number ; message : any ; sentAt : number ; connection : { id : string ; room : string ; } ; } export function client ( ) { if ( api . redis . clients && api . redis . clients . client ) { return api . redis . clients . client ; } else { throw new Error ( "redis not connected, chatRoom cannot be used" ) ; } } /**
     * Add a middleware component to connection handling.
     */ export async function addMiddleware ( data ) { if ( ! data . name ) { throw new Error ( "middleware.name is required" ) ; } if ( ! data . priority ) { data . priority = config . general . defaultMiddlewarePriority ; } data . priority = Number ( data . priority ) ; api . chatRoom . middleware [ data . name ] = data ; api . chatRoom . globalMiddleware . push ( data . name ) ; api . chatRoom . globalMiddleware . sort ( ( a , b ) => { if ( api . chatRoom . middleware [ a ] . priority > api . chatRoom . middleware [ b ] . priority ) { return 1 ; } else { return - 1 ; } } ) ; } /**
     * List all chat rooms created
     */ export async function list ( ) { return client ( ) . smembers ( api . chatRoom . keys . rooms ) ; } /**
     * Add a new chat room.  Throws an error if the room already exists.
     */ export async function add ( room ) { const found = await chatRoom . exists ( room ) ; if ( found === false ) { return client ( ) . sadd ( api . chatRoom . keys . rooms , room ) ; } else { throw new Error ( await config . errors . connectionRoomExists ( room ) ) ; } } /**
     * Remove an existing chat room.  All connections in the room will be removed.  Throws an error if the room does not exist.
     */ export async function destroy ( room ) { const found = await chatRoom . exists ( room ) ; if ( found === true ) { await api . chatRoom . broadcast ( null , room , await config . errors . connectionRoomHasBeenDeleted ( room ) ) ; const membersHash = await client ( ) . hgetall ( api . chatRoom . keys . members + room ) ; for ( const id in membersHash ) { await chatRoom . removeMember ( id , room , false ) ; } await client ( ) . srem ( api . chatRoom . keys . rooms , room ) ; await client ( ) . del ( api . chatRoom . keys . members + room ) ; } else { throw new Error ( await config . errors . connectionRoomNotExist ( room ) ) ; } } /**
     * Check if a room exists.
     */ export async function exists ( room ) { const isMember = await client ( ) . sismember ( api . chatRoom . keys . rooms , room ) ; let found = false ; if ( isMember === 1 || isMember . toString ( ) === "true" ) found = true ; return found ; } /**
     * Configures what properties of connections in a room to return via `api.chatRoom.roomStatus`
     */ export async function sanitizeMemberDetails ( memberData ) { return { id : memberData . id , joinedAt : memberData . joinedAt , } ; } /**
     * Learn about the connections in the room.
     * Returns a hash of the form { room: room, members: cleanedMembers, membersCount: count }.  Members is an array of connections in the room sanitized via `api.chatRoom.sanitizeMemberDetails`
     */ export async function roomStatus ( room ) { if ( room ) { const found = await chatRoom . exists ( room ) ; if ( found === true ) { const key = api . chatRoom . keys . members + room ; const members = ( await api . redis . clients . client . hgetall ( key ) ) as { [ key ] : string ; } ; const cleanedMembers = { } ; let count = 0 ; for ( const id in members ) { const data = JSON . parse ( members [ id ] ) ; cleanedMembers [ id ] = await chatRoom . sanitizeMemberDetails ( data ) ; count ++ ; } return { room : room , members : cleanedMembers , membersCount : count , } ; } else { throw new Error ( await config . errors . connectionRoomNotExist ( room ) ) ; } } else { throw new Error ( await config . errors . connectionRoomRequired ( ) ) ; } } /**
     * An overwrite-able method which configures what properties of connections in a room are initially stored about a connection when added via `api.chatRoom.addMember`
     */ export async function generateMemberDetails ( connection ) { return { id : connection . id , joinedAt : new Date ( ) . getTime ( ) , host : id , } ; } /**
     * Add a connection (via id) to a room.  Throws errors if the room does not exist, or the connection is already in the room.  Middleware errors also throw.
     */ export async function addMember ( connectionId , room ) { const connection = api . connections . connections [ connectionId ] ; if ( ! connection ) { return redis . doCluster ( "api.chatRoom.addMember" , [ connectionId , room ] , connectionId , true ) ; } if ( connection . rooms . includes ( room ) ) { throw new Error ( await config . errors . connectionAlreadyInRoom ( connection , room ) ) ; } if ( ! connection . rooms . includes ( room ) ) { const found = await chatRoom . exists ( room ) ; if ( ! found ) { throw new Error ( await config . errors . connectionRoomNotExist ( room ) ) ; } await api . chatRoom . runMiddleware ( connection , room , "join" ) ; if ( ! connection . rooms . includes ( room ) ) { connection . rooms . push ( room ) ; } const memberDetails = await chatRoom . generateMemberDetails ( connection ) ; await client ( ) . hset ( api . chatRoom . keys . members + room , connection . id , JSON . stringify ( memberDetails ) ) ; } return true ; } /**
     * Remote a connection (via id) from a room.  Throws errors if the room does not exist, or the connection is not in the room.  Middleware errors also throw.
     * toWaitRemote: Should this method wait until the remote Actionhero server (the one the connection is connected too) responds?
     */ export async function removeMember ( connectionId , room , toWaitRemote = true ) { const connection = api . connections . connections [ connectionId ] ; if ( ! connection ) { return redis . doCluster ( "api.chatRoom.removeMember" , [ connectionId , room ] , connectionId , toWaitRemote ) ; } if ( ! connection . rooms . includes ( room ) ) { throw new Error ( await config . errors . connectionNotInRoom ( connection , room ) ) ; } if ( connection . rooms . includes ( room ) ) { const found = await chatRoom . exists ( room ) ; if ( ! found ) { throw new Error ( await config . errors . connectionRoomNotExist ( room ) ) ; } await api . chatRoom . runMiddleware ( connection , room , "leave" ) ; if ( connection . rooms . includes ( room ) ) { const index = connection . rooms . indexOf ( room ) ; connection . rooms . splice ( index , 1 ) ; } await client ( ) . hdel ( api . chatRoom . keys . members + room , connection . id ) ; } return true ; } /**
     * Send a message to all clients connected to this room
     * - connection:
     *        - {} send to every connections
     *        - should either be a real client you are emulating (found in api.connections)
     *        - a mock
     * - room is the string name of an already-existing room
     * - message can be anything: string, json, object, etc
     */ export async function broadcast ( connection , room , message ) { return api . chatRoom . broadcast ( connection , room , message ) ; } }