RocketChat/Rocket.Chat

View on GitHub
apps/meteor/server/services/voip/connector/asterisk/ami/PJSIPEndpoint.ts

Summary

Maintainability
F
3 days
Test Coverage
/**
 * This class is responsible for handling PJSIP endpoints.
 * @remarks
 * Some design notes :
 * 1. CommandFactory creates the child classes of the |Command| class and
 *       returns a reference to |Command|
 * 2. |CommandHandler| class call executeCommand method of |Command| class, which
 *    gets overriden here.
 * 3. Consumers of this class create the instance based on |Commands| object but
 *    specific command object 'knows' about asterisk-ami command. Reason for doing this is
 *    that the consumer does not and should not know about the pbx specific commands
 *       used for fetching the information. e.g for endpoint list and endpoint info,
 *       it uses pjsipshowendpoints and pjsipshowendpoint.
 * 4. Asterisk responds asynchronously and it responds using events. Command specific
 *    event handling is implemented by this class.
 * 5. Every execution of command takes action-callback, which tells whether the command can be
 *    executed or there is an error. In case of success, it ends set of events
 *    the completion of which is indicated by <action>completed event. e.g while fetching the endpoint,
 *    i.e for executing |pjsipshowendpoint|  asterisk sends the different parts of i
 *    nformation in different events. event |endpointdetail| indicates
 *    endpoint name, devicestate etc. |authdetail| indicates type of authentication, password and other
 *    auth related information. At the end of the series of these events, Asterisk sends |endpointdetailcomplete|
 *       event. At this point of time, promise, on which the consumer is waiting can be resolved.
 * 6. This class could use asynchronous callbacks. But because the connector will be used extensively by REST layer
 *    we have taken promise based approach. Promise is returned as a part of executeCommand. Caller would wait for
 *    the completion/rejection. This class will reject the promise based on error or resolve it in <command>complete
 *    event.
 * 7. Important to note that the intermediate events containing a result part for an execution of a particular command
 *    have same actionid, which is received by this class as a successful execution of a command in actionResultCallback.
 */
import { EndpointState } from '@rocket.chat/core-typings';
import type { IVoipConnectorResult, IExtensionDetails } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import type { Db } from 'mongodb';
import _ from 'underscore';

import { Command, CommandType } from '../Command';
import { Commands } from '../Commands';
import { CallbackContext } from './CallbackContext';

export class PJSIPEndpoint extends Command {
    private logger: Logger;

    constructor(command: string, parametersNeeded: boolean, db: Db) {
        super(command, parametersNeeded, db);
        this.logger = new Logger('PJSIPEndpoint');
        this._type = CommandType.AMI;
    }

    private getState(endpointState: string): EndpointState {
        /**
         * When registered endpoint can be in following state
         * Not in use : When endpoint has registered but not serving any call.
         * Ringing : Registered and ringing
         * Busy : endpoing is handling call.
         *
         * If any other state is seen, this function returns EndpointState.UNKNOWN;
         */

        let state: EndpointState = EndpointState.UNKNOWN;
        switch (endpointState.toLowerCase()) {
            case 'unavailable':
                state = EndpointState.UNREGISTERED;
                break;
            case 'not in use':
                state = EndpointState.REGISTERED;
                break;
            case 'ringing':
                state = EndpointState.RINGING;
                break;
            case 'busy':
                state = EndpointState.BUSY;
                break;
        }
        return state;
    }

    /**
     * Event handler for endpointlist event containing the information of the endpoints.
     * @remark
     * This event is generated as a result of the execution of |pjsipshowendpoints|
     */
    onEndpointList(event: any): void {
        if (event.actionid !== this.actionid) {
            this.logger.error({
                msg: 'onEndpointList() Unusual behavior. ActionId does not belong to this object',
                eventActionId: event.actionid,
                actionId: this.actionid,
            });
            return;
        }

        // A SIP address-of-record is a canonical address by which a user is known
        // If the event doesn't have an AOR, we will ignore it (as it's probably system-only)
        if (!event?.aor.trim()) {
            return;
        }

        const endPoint: IExtensionDetails = {
            extension: event.objectname,
            state: this.getState(event.devicestate),
            password: '',
            authtype: '',
        };
        const { result } = this;
        if (result.endpoints) {
            result.endpoints.push(endPoint);
        } else {
            // create an array of endpoints in the result for
            // the first time.
            result.endpoints = [];
            result.endpoints.push(endPoint);
        }
    }

    /**
     * Event handler for endpointlistcomplete event indicating that all the data
     * is received.
     */
    onEndpointListComplete(event: any): void {
        if (event.actionid !== this.actionid) {
            this.logger.error({
                msg: 'onEndpointListComplete() Unusual behavior. ActionId does not belong to this object',
                eventActionId: event.actionid,
                actionId: this.actionid,
            });
            return;
        }
        this.resetEventHandlers();
        const extensions = _.sortBy(this.result.endpoints, (o: any) => {
            return o.extension;
        });
        this.returnResolve({ result: extensions } as IVoipConnectorResult);
    }

    /**
     * Event handler for endpointdetail and authdetail event containing the endpoint specific details
     * and authentication information.
     * @remark
     * This event is generated as a result of the execution of |pjsipshowendpoint|.
     * We consolidate this endpointdetail and authdetail events because they are generated
     * as a result of same command. Nevertheless, in future, if such implementation
     * becomes difficult, it is recommended that there should be a separate handling
     * for each event.
     */
    onEndpointInfo(event: any): void {
        if (event.actionid !== this.actionid) {
            this.logger.error({
                msg: 'onEndpointInfo() Unusual behavior. ActionId does not belong to this object',
                eventActionId: event.actionid,
                actionId: this.actionid,
            });
            return;
        }
        const { result } = this;

        if (!result.endpoint) {
            const endpointDetails: IExtensionDetails = {
                extension: '',
                state: '',
                password: '',
                authtype: '',
            };
            result.endpoint = endpointDetails;
        }
        if (event.event.toLowerCase() === 'endpointdetail') {
            (result.endpoint as IExtensionDetails).extension = event.objectname;
            (result.endpoint as IExtensionDetails).state = this.getState(event.devicestate);
        } else if (event.event.toLowerCase() === 'authdetail') {
            (result.endpoint as IExtensionDetails).password = event.password;
            (result.endpoint as IExtensionDetails).authtype = event.authtype;
        }
    }

    /**
     * Event handler for endpointdetailcomplete event indicating that all the data
     * is received.
     */
    onEndpointDetailComplete(event: any): void {
        if (event.actionid !== this.actionid) {
            this.logger.error({
                msg: 'onEndpointDetailComplete() Unusual behavior. ActionId does not belong to this object',
                eventActionId: event.actionid,
                actionId: this.actionid,
            });
            return;
        }
        this.resetEventHandlers();
        const { result } = this;

        this.returnResolve({ result: result.endpoint } as IVoipConnectorResult);
    }

    /**
     * Callback for indicatiing command execution status.
     * Received actionid for the first time.
     */
    onActionResult(error: any, result: any): void {
        if (error) {
            this.logger.error({ msg: 'onActionResult()', error: JSON.stringify(error) });
            this.returnReject(`error${error} while executing command`);
        } else {
            // Set up actionid for future reference in case of success.
            this.actionid = result.actionid;
        }
    }

    setupEventHandlers(): void {
        // Setup necessary command event handlers based on the command
        switch (this.commandText) {
            case Commands.extension_list.toString(): {
                this.connection.on('endpointlist', new CallbackContext(this.onEndpointList.bind(this), this));
                this.connection.on('endpointlistcomplete', new CallbackContext(this.onEndpointListComplete.bind(this), this));
                break;
            }
            case Commands.extension_info.toString(): {
                this.connection.on('endpointdetail', new CallbackContext(this.onEndpointInfo.bind(this), this));
                this.connection.on('authdetail', new CallbackContext(this.onEndpointInfo.bind(this), this));
                this.connection.on('endpointdetailcomplete', new CallbackContext(this.onEndpointDetailComplete.bind(this), this));
                break;
            }
            default: {
                this.logger.error({ msg: `setupEventHandlers() : Unimplemented ${this.commandText}` });
                break;
            }
        }
    }

    resetEventHandlers(): void {
        switch (this.commandText) {
            case Commands.extension_list.toString(): {
                this.connection.off('endpointlist', this);
                this.connection.off('endpointlistcomplete', this);
                break;
            }
            case Commands.extension_info.toString(): {
                this.connection.off('endpointdetail', this);
                this.connection.off('authdetail', this);
                this.connection.off('endpointdetailcomplete', this);
                break;
            }
            default: {
                this.logger.error({ msg: `resetEventHandlers() : Unimplemented ${this.commandText}` });
                break;
            }
        }
    }

    async executeCommand(data: any): Promise<IVoipConnectorResult> {
        let amiCommand = {};
        // set up the specific action based on the value of |Commands|
        if (this.commandText === Commands.extension_list.toString()) {
            amiCommand = {
                action: 'pjsipshowendpoints',
            };
        } else if (this.commandText === Commands.extension_info.toString()) {
            // |pjsipshowendpoint| needs input parameter |endpoint| indicating
            // which endpoint information is to be queried.
            amiCommand = {
                action: 'pjsipshowendpoint',
                endpoint: data.extension,
            };
        }
        this.logger.debug({ msg: `executeCommand() executing AMI command ${JSON.stringify(amiCommand)}`, data });
        const actionResultCallback = this.onActionResult.bind(this);
        const eventHandlerSetupCallback = this.setupEventHandlers.bind(this);
        return super.prepareCommandAndExecution(amiCommand, actionResultCallback, eventHandlerSetupCallback);
    }
}