<commit_before>/*
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { BaseSlackHandler, ISlackEvent, ISlackMessageEvent, ISlackMessage } from "./BaseSlackHandler";
import { BridgedRoom } from "./BridgedRoom";
import { Main } from "./Main";
import { Logging } from "matrix-appservice-bridge";
const log = Logging.get("SlackEventHandler");

interface ISlackEventChannelRenamed extends ISlackEvent {
    // https://api.slack.com/events/channel_rename
    id: string;
    name: string;
    created: number;
}

interface ISlackEventTeamDomainChanged extends ISlackEvent {
    url: string;
    domain: string;
}

interface ISlackEventReaction extends ISlackEvent {
    // https://api.slack.com/events/reaction_added
    reaction: string;
    item: ISlackMessage;
}

const HTTP_OK = 200;

export type EventHandlerCallback = (status: number, body?: string, headers?: {[header: string]: string}) => void;

export class SlackEventHandler extends BaseSlackHandler {
    /**
     * SUPPORTED_EVENTS corresponds to the types of events
     * handled in `handle`. This is useful if you need to subscribe
     * to events in order to handle them.
     */
    protected static SUPPORTED_EVENTS: string[] = ["message", "reaction_added", "reaction_removed",
    "team_domain_change", "channel_rename", "user_typing"];
    constructor(main: Main) {
        super(main);
    }

    public onVerifyUrl(challenge: string, response: EventHandlerCallback) {
        response(
            HTTP_OK,
            JSON.stringify({challenge}),
            {"Content-Type": "application/json"},
        );
    }

    /**
     * Handles a slack event request.
     * @param ISlackEventParams
     */
    public async handle(event: ISlackEvent, teamId: string, response: EventHandlerCallback) {
        try {
            log.debug("Received slack event:", event, teamId);

            const endTimer = this.main.startTimer("remote_request_seconds");
            // See https://api.slack.com/events-api#responding_to_events
            // We must respond within 3 seconds or it will be sent again!
            response(HTTP_OK, "OK");

            let err: string|null = null;
            try {
                switch (event.type) {
                    case "message":
                        await this.handleMessageEvent(event as ISlackMessageEvent, teamId);
                        break;
                    case "reaction_added":
                    case "reaction_removed":
                        await this.handleReaction(event as ISlackEventReaction, teamId);
                        break;
                    case "channel_rename":
                        await this.handleChannelRenameEvent(event as ISlackEventChannelRenamed);
                        break;
                    case "team_domain_change":
                        await this.handleDomainChangeEvent(event as ISlackEventTeamDomainChanged, teamId);
                        break;
                    case "user_typing":
                        await this.handleTyping(event, teamId);
                        break;
                    // XXX: Unused?
                    case "file_comment_added":
                    default:
                        err = "unknown_event";
                }
            } catch (ex) {
                log.warn("Didn't handle event");
                err = ex;
            }

            if (err === "unknown_channel") {
                const chanIdMix = `${event.channel} (${teamId})`;
                log.warn(`Ignoring message from unrecognised slack channel id: ${chanIdMix}`);
                this.main.incCounter("received_messages", {side: "remote"});
                endTimer({outcome: "dropped"});
                return;
            } else if (err === "unknown_team") {
                log.warn(`Ignoring message from unrecognised slack team id: ${teamId}`);
                this.main.incCounter("received_messages", {side: "remote"});
                endTimer({outcome: "dropped"});
                return;
            } else if (err === "unknown_event") {
                endTimer({outcome: "dropped"});
            } else if (err !== null) {
                log.warn("Error when handing event:", err);
                endTimer({outcome: "fail"});
            }

            if (err === null) {
                endTimer({outcome: "success"});
            } else {
                log.error("Failed to handle slack event:", err);
            }
        } catch (e) {
            log.error("SlackEventHandler.handle failed:", e);
        }
    }

    /**
     * Attempts to handle the `message` event.
     *
     * Sends a message to Matrix if it understands enough of the message to do so.
     * Attempts to make the message as native-matrix feeling as it can.
     * @param ISlackEventParamsMessage The slack message event to handle.
     */
    protected async handleMessageEvent(event: ISlackMessageEvent, teamId: string) {
        const room = this.main.rooms.getBySlackChannelId(event.channel) as BridgedRoom;
        const team = await this.main.datastore.getTeam(teamId);
        if (!room) { throw Error("unknown_channel"); }
        if (!team) { throw Error("unknown_team"); }

        if (event.bot_id && (event.bot_id === team.bot_id)) {
            return;
        }

        // Only count received messages that aren't self-reflections
        this.main.incCounter("received_messages", {side: "remote"});

        const msg = Object.assign({}, event, {
            channel_id: event.channel,<commit_msg>Don't log stack traces for unknown teams/rooms/event<commit_after>/*
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { BaseSlackHandler, ISlackEvent, ISlackMessageEvent, ISlackMessage } from "./BaseSlackHandler";
import { BridgedRoom } from "./BridgedRoom";
import { Main } from "./Main";
import { Logging } from "matrix-appservice-bridge";
const log = Logging.get("SlackEventHandler");

interface ISlackEventChannelRenamed extends ISlackEvent {
    // https://api.slack.com/events/channel_rename
    id: string;
    name: string;
    created: number;
}

interface ISlackEventTeamDomainChanged extends ISlackEvent {
    url: string;
    domain: string;
}

interface ISlackEventReaction extends ISlackEvent {
    // https://api.slack.com/events/reaction_added
    reaction: string;
    item: ISlackMessage;
}

const HTTP_OK = 200;

export type EventHandlerCallback = (status: number, body?: string, headers?: {[header: string]: string}) => void;

export class SlackEventHandler extends BaseSlackHandler {
    /**
     * SUPPORTED_EVENTS corresponds to the types of events
     * handled in `handle`. This is useful if you need to subscribe
     * to events in order to handle them.
     */
    protected static SUPPORTED_EVENTS: string[] = ["message", "reaction_added", "reaction_removed",
    "team_domain_change", "channel_rename", "user_typing"];
    constructor(main: Main) {
        super(main);
    }

    public onVerifyUrl(challenge: string, response: EventHandlerCallback) {
        response(
            HTTP_OK,
            JSON.stringify({challenge}),
            {"Content-Type": "application/json"},
        );
    }

    /**
     * Handles a slack event request.
     * @param ISlackEventParams
     */
    public async handle(event: ISlackEvent, teamId: string, response: EventHandlerCallback) {
        try {
            log.debug("Received slack event:", event, teamId);

            const endTimer = this.main.startTimer("remote_request_seconds");
            // See https://api.slack.com/events-api#responding_to_events
            // We must respond within 3 seconds or it will be sent again!
            response(HTTP_OK, "OK");

            let err: Error|null = null;
            try {
                switch (event.type) {
                    case "message":
                        await this.handleMessageEvent(event as ISlackMessageEvent, teamId);
                        break;
                    case "reaction_added":
                    case "reaction_removed":
                        await this.handleReaction(event as ISlackEventReaction, teamId);
                        break;
                    case "channel_rename":
                        await this.handleChannelRenameEvent(event as ISlackEventChannelRenamed);
                        break;
                    case "team_domain_change":
                        await this.handleDomainChangeEvent(event as ISlackEventTeamDomainChanged, teamId);
                        break;
                    case "user_typing":
                        await this.handleTyping(event, teamId);
                        break;
                    // XXX: Unused?
                    case "file_comment_added":
                    default:
                        err = Error("unknown_event");
                }
            } catch (ex) {
                log.warn("Didn't handle event");
                err = ex;
            }

            if (err === null) {
                endTimer({outcome: "success"});
            } else if (!(err instanceof Error)) {
                log.warn("Error when handing event:", err);
                endTimer({outcome: "fail"});
            } else if (err.message === "unknown_channel") {
                const chanIdMix = `${event.channel} (${teamId})`;
                log.warn(`Ignoring message from unrecognised slack channel id: ${chanIdMix}`);
                this.main.incCounter("received_messages", {side: "remote"});
                endTimer({outcome: "dropped"});
                return;
            } else if (err.message === "unknown_team") {
                log.warn(`Ignoring message from unrecognised slack team id: ${teamId}`);
                this.main.incCounter("received_messages", {side: "remote"});
                endTimer({outcome: "dropped"});
                return;
            } else if (err.message === "unknown_event") {
                endTimer({outcome: "dropped"});
            } else {
                log.warn("Error when handing event:", err);
                endTimer({outcome: "fail"});
            }
        } catch (e) {
            log.error("SlackEventHandler.handle failed:", e);
        }
    }

    /**
     * Attempts to handle the `message` event.
     *
     * Sends a message to Matrix if it understands enough of the message to do so.
     * Attempts to make the message as native-matrix feeling as it can.
     * @param ISlackEventParamsMessage The slack message event to handle.
     */
    protected async handleMessageEvent(event: ISlackMessageEvent, teamId: string) {
        const room = this.main.rooms.getBySlackChannelId(event.channel) as BridgedRoom;
        const team = await this.main.datastore.getTeam(teamId);
        if (!room) { throw Error("unknown_channel"); }
        if (!team) { throw Error("unknown_team"); }

        if (event.bot_id && (event.bot_id === team.bot_id)) {
            return;
        }

        // Only count received messages that aren't self-reflections
        this.main.incCounter("received_messages", {side: "remote"});

        const msg = Object.assign({}, event, {
            channel_id: event.channel,
{'fraction_non_alphanumeric': 0.10274085015591662, 'fraction_numerical': 0.0019694731659281144, 'mean_word_length': 2.0876615150747404, 'pattern_counts': {'":': 16, '<': 3, '<?xml version=': 0, '>': 5, 'https://': 6, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1970278', 'n_tokens_mistral': 3298, 'n_tokens_neox': 3123, 'n_words': 928}