grigori-gru/jira-to-matrix

View on GitHub
src/messengers/slack-api.ts

Summary

Maintainability
D
2 days
Test Coverage
/* eslint-disable no-undefined */
import http from 'http';
import Ramda from 'ramda';
import { WebClient } from '@slack/web-api';
import { fromString } from 'html-to-text';
import express from 'express';
import bodyParser, { OptionsJson } from 'body-parser';
import { BaseChatApi } from './base-api';
import { ChatConfig } from '../types';
import { LoggerInstance } from 'winston';
import { Commands } from '../bot/commands';
// import { MessengerApi } from '../types';

export class SlackApi extends BaseChatApi {
    // export class SlackApi extends BaseChatApi {
    commandServer;
    count = 0;
    client: any;

    constructor(commands: Commands, config: ChatConfig, logger: LoggerInstance, sdk: WebClient) {
        super(commands, config, logger, sdk);
        this.sdk = sdk || new WebClient(config.password);
    }

    /**
     * Transform ldap user name to Slack user id
     *
     * @param {string} shortName shortName of user from ldap
     * @returns {string} user email like ii_ivanov@ example.com
     */
    getChatUserId(shortName) {
        return shortName && `${shortName.toLocaleLowerCase()}@${this.config.messenger.domain}`;
    }

    /**
     * Leave room by id
     *
     * @param {string} roomId matrix room id
     */
    leaveRoom() {
        // TODO override
        undefined;
    }

    /**
     * Get id
     *
     * @returns {string} user id
     */
    getBotId() {
        return this.config.user;
    }

    /**
     * Slack event handler
     *
     * @param {object} eventBody slack event
     */
    async _eventHandler(eventBody) {
        try {
            const info = await this.getRoomInfo(eventBody.channel_id);
            const commandName = eventBody.command.slice(2);

            const options = {
                chatApi: this,
                sender: eventBody.user_name.replace(/[0-9]/g, ''),
                roomName: info.name.toUpperCase(),
                roomId: eventBody.channel_id,
                bodyText: eventBody.text,
            };
            //! TODO options are incorrect!!!
            await this.commands.run(commandName, options as any);
        } catch (error) {
            this.logger.error('Error while handling slash command from Slack');
        }
    }

    /**
     * Slack slash command listener
     */
    _slashCommandsListener() {
        const app = express();

        const options: OptionsJson = {
            strict: false,
        };
        app.use(bodyParser.urlencoded(options)).post('/commands', async (req, res) => {
            await this._eventHandler(req.body);
            res.send(200);
        });

        this.commandServer = http.createServer(app);
        this.commandServer.listen(this.config.messenger.eventPort, () => {
            this.logger.info(`Slack commands are listening on port ${this.config.messenger.eventPort}`);
        });
    }

    /**
     * @private
     * @returns {Promise} connected slackClient
     */
    async _startClient() {
        try {
            await this.sdk.auth.test();
            this.logger.info('Slack client started!');
            this.client = this.sdk;
        } catch (err) {
            throw ['Error in slack connection', err].join('\n');
        }
    }

    /**
     * Get bot which joined to room in chat
     *
     * @param {string} userId chat user id
     * @returns {Promise<User>} void
     */
    getUser() {
        return true;
    }

    /**
     * @returns {object} connected slackClient with api for Jira
     */
    async connect() {
        if (this.isConnected()) {
            return;
        }
        try {
            await this._startClient();
            await this._slashCommandsListener();
            return this;
        } catch (err) {
            throw ['Error in slack connection', err].join('\n');
        }
    }

    /**
     * @returns {boolean} connect status
     */
    isConnected() {
        return this.commandServer && this.commandServer.listening;
    }

    /**
     * disconnected slackClient
     */
    disconnect() {
        if (this.commandServer) {
            this.commandServer.close();
            this.logger.info('Disconnected from Slack');
        }
    }

    /**
     * Send message to Slack room
     *
     * @param  {string} channel slack room id
     * @param  {string} infoMessage info message body
     * @param  {string} textBody markdown message body
     */
    async sendHtmlMessage(channel, infoMessage, textBody) {
        try {
            const text = fromString(textBody);
            const attachments = [
                {
                    text,
                    mrkdwn_in: ['text'],
                },
            ];
            await this.client.chat.postMessage({ channel, attachments });
        } catch (err) {
            throw ['Error in sendHtmlMessage', err].join('\n');
        }
    }

    /**
     * @param  {string} email user mail
     * @returns {string|undefined} slack user id or undefined
     */
    async _getUserIdByEmail(email) {
        try {
            const userInfo = await this.client.users.lookupByEmail({ email });

            return Ramda.path(['user', 'id'], userInfo);
        } catch (error) {
            this.logger.error(`Error getting user from slack by email "${email}"`, error);
        }
    }

    /**
     * Set topic to channel
     *
     * @param  {string} channel channel id
     * @param  {string} topic new topic
     * @returns {boolean} request result
     */
    async setRoomTopic(channel, topic) {
        try {
            const res = await this.client.conversations.setTopic({ channel, topic });

            return res.ok;
        } catch (err) {
            this.logger.error(err);
            throw ['Error while setting channel topic', err].join('\n');
        }
    }

    /**
     * Set purpose to channel
     *
     * @param  {string} channel channel id
     * @param  {string} purpose new topic
     * @returns {boolean} request result
     */
    async setPurpose(channel, purpose) {
        try {
            const res = await this.client.conversations.setPurpose({ channel, purpose });

            return res.ok;
        } catch (err) {
            this.logger.error(err);
            throw ['Error while setting channel purpose', err].join('\n');
        }
    }

    /**
     * Create Slack channel
     *
     * @param  {object} options create channel options
     * @param  {string} options.name name for channel, less than 21 sign, lowerCase, no space
     * @param  {string} options.topic slack channel topic
     * @param  {Array} options.invite user emails to invite
     * @param  {string[]} options.purpose issue summary
     * @returns {string} Slack channel id
     */
    async createRoom({ name, topic, invite, purpose }) {
        try {
            const options = {
                is_private: true,
                name: name.toLowerCase(),
            };
            const { channel } = await this.client.conversations.create(options);
            const roomId = channel.id;
            await Promise.all(
                invite.map(async name => {
                    try {
                        await this.invite(roomId, name);
                    } catch (error) {
                        this.logger.warn(
                            // eslint-disable-next-line
                            `User ${name} is not in slack, try to add him. Room for jira issue ${name} will be without him.`,
                        );
                    }
                }),
            );
            await this.setRoomTopic(roomId, topic);
            await this.setPurpose(roomId, purpose);

            return roomId;
        } catch (err) {
            this.logger.error(err);
            throw ['Error while creating room', err].join('\n');
        }
    }

    // ! TODO OVERRIDE
    /**
     * @param {string} name displayName
     * @returns {string} slack id
     */
    getUserIdByDisplayName(name) {
        return name;
    }

    /**
     * Get slack channel id by name
     *
     * @param  {string} name slack channel name
     * @returns {string|undefined} channel id if exists
     */
    async getRoomId(name) {
        const searchingName = name.toLowerCase();
        try {
            // ? Limit of channels is only 1000 now
            const { channels } = await this.client.users.conversations({ limit: 1000, types: 'private_channel' });
            const channel = channels.find(item => item.name === searchingName);

            const roomId = Ramda.path(['id'], channel);
            if (!roomId) {
                throw `No channel for ${searchingName}`;
            }

            return roomId;
        } catch (err) {
            throw [`Error getting channel id by name "${searchingName}" from Slack`, err].join('\n');
        }
    }

    /**
     * Check if user is in matrix room
     *
     * @param {string} roomId matrix room id
     * @param {string} name slack user name like ii_ivanov
     * @returns {Promise<boolean>} return true if user in room
     */
    async isRoomMember(roomId, name) {
        const email = this._getEmail(name);
        const slackId = await this._getUserIdByEmail(email);
        const roomMembers = await this.getRoomMembers({ roomId });

        return roomMembers.includes(slackId);
    }

    /**
     * Invite user to slack channel
     *
     * @param  {string} channel slack channel id
     * @param  {string} name slack user name or email
     */
    async invite(channel, name) {
        const email = this._getEmail(name);
        try {
            const userId = await this._getUserIdByEmail(email);
            const response = await this.client.conversations.invite({ channel, users: userId });

            return response.ok;
        } catch (err) {
            this.logger.error(err);
            throw [`Error while inviting user ${email} to a channel ${channel}`, err].join('\n');
        }
    }

    /**
     * get channel members
     *
     * @param {string} name channel name
     * @param {string} slack channel id
     * @returns {string[]} channel members like [nfakjgba, fabfaif]
     */
    async getRoomMembers({ name, roomId }: { roomId?: string; name: string } | { roomId: string; name?: string }) {
        try {
            const channel = roomId || (await this.getRoomId(name));
            const { members } = await this.client.conversations.members({ channel });

            return members;
        } catch (err) {
            throw [`Error while getting slack members from channel ${name}`, err].join('\n');
        }
    }

    /**
     * Set new name to the channel
     *
     * @param  {string} channel channel id
     * @param  {string} name new topic
     * @returns {boolean} request result
     */
    async setRoomName(channel, name) {
        try {
            const res = await this.client.conversations.rename({ channel, name });

            return res.ok;
        } catch (err) {
            this.logger.error(err);
            throw ['Error while setting channel topic', err].join('\n');
        }
    }

    /**
     * Get Conversation Info by id
     *
     * @param {string} channel conversation Id
     */
    async getRoomInfo(channel) {
        try {
            const roomInfo = await this.client.conversations.info({ channel });

            return roomInfo.channel;
        } catch (err) {
            this.logger.error(err);
            throw [`Error while getting info by channel ${channel}`, err].join('\n');
        }
    }

    /**
     * Transform name to email or just return email
     *
     * @param {string} name name or email
     * @returns {string} email
     */
    _getEmail(name) {
        return name.includes('@') ? name : `${name}@${this.config.messenger.domain}`;
    }

    /**
     * Get room id by name
     *
     * @param {string} name roomname
     * @returns {Promise<string|false>} return roomId or false if not exisits
     */
    async getRoomIdByName(name) {
        try {
            const roomId = await this.getRoomId(name);
            return roomId;
        } catch (error) {
            return false;
        }
    }

    setPower() {
        this.logger.warn('Set power command is not available now');
    }

    /**
     * compose room name for slack chat
     *
     * @param {string} key Jira issue key
     * @returns {string} room name for jira issue in slack
     */
    composeRoomName(key) {
        return `${key.toLowerCase()}`;
    }

    /**
     * Update room name
     */
    async updateRoomName(roomId: string, newRoomName: string) {
        await this.setRoomName(roomId, newRoomName);
        // await this.setPurpose(roomId, roomData.summary);
    }

    /**
     * Update room info data
     *
     * @param  {string} roomId room id
     * @param  {string} topic new room topic
     * @returns {Promise<void>} void
     */
    async updateRoomData(roomId, topic) {
        await this.setRoomTopic(roomId, topic);
    }

    /**
     * Check if user is in room
     *
     * @param {string} channel channel id
     * @returns {boolean} return true if user in this room
     */
    async isInRoom(channel) {
        try {
            await this.getRoomInfo(channel);

            return true;
        } catch (error) {
            return false;
        }
    }

    /**
     * Get matrix room by alias
     */
    getRoomAdmins() {
        return [];
    }

    getAllMessagesFromRoom() {
        return [];
    }

    kickUserByRoom() {
        return {};
    }

    /**
     * Delete matrix room alias
     *
     * @param {string} aliasPart aliasPart
     * @returns {string|undefined} return allias if command is succedded
     */
    deleteRoomAlias(aliasPart) {
        return aliasPart;
    }

    /**
     * Get room data by room id
     *
     * @param {string} roomId matrix room id
     */
    getRoomDataById(roomId) {
        return roomId;
    }

    upload() {
        return undefined;
    }

    uploadContent() {
        return undefined as any;
    }

    createAlias() {
        return undefined;
    }

    getDownloadLink() {
        return undefined;
    }

    setRoomJoinedByUrl() {
        return undefined;
    }

    joinRoom() {
        return undefined;
    }
    getRooms() {
        return undefined;
    }
    setRoomAvatar() {
        return undefined;
    }
    getAllEventsFromRoom() {
        return undefined;
    }
    getRoomLink(idOrAlias: string) {
        return idOrAlias;
    }
}