Source: components/room-link-validator.js

/**
 * The room link validator is used to determine if a room can be bridged.
 */
const ConfigValidator = require("./config-validator");
const log = require("./logging").get("room-link-validator");
const VALIDATION_CACHE_LIFETIME = 30 * 60 * 1000;
const PASSED = "RLV_PASSED";
const ERROR = "RVL_ERROR";
const ERROR_USER_CONFLICT = "RVL_USER_CONFLICT";
const ERROR_CACHED = "RVL_ERROR_CACHED";

const RULE_SCHEMA = {
    "$schema": "http://json-schema.org/draft-04/schema#",
    type: "object",
    properties: {
        userIds: {
            type: "object",
            properties: {
                exempt: {
                    type: "array",
                    items: {
                        type: "string"
                    }
                },
                conflict: {
                    type: "array",
                    items: {
                        type: "string"
                    }
                }
            }
        }
    }
};

/**
 * The RoomLinkValidator checks if a room should be linked to a remote
 * channel, given a set of rules supplied in a config. The ruleset is maintained
 * in a seperate config from the bridge config. It can be reloaded by triggering
 * an endpoint specified in the {@link Bridge} class.
 */
class RoomLinkValidator {
    /**
     * @param {string} config Config for the validator.
     * @param {string} config.ruleFile Filename for the rule file.
     * @param {string} config.rules Rules if not using a rule file, will be
     *                              overwritten if both is set.
     * @param {AppServiceBot} asBot The AS bot.
     */
    constructor (config, asBot) {
        this.conflictCache = new Map(); // roomId => number
        this.waitingRooms = new Map(); // roomId => Promise
        this.asBot = asBot;

        if (config.ruleFile) {
            this.config = new ConfigValidator(RULE_SCHEMA);
            this.ruleFile = config.ruleFile;
            this.readRuleFile();
        }
        else if (config.rules) {
            this.rules = this.evaulateRules(config.rules);
        }
        else {
            throw new Error("Either config.ruleFile or config.rules must be set");
        }
    }

    readRuleFile (filename) {
        filename = filename || this.ruleFile;
        if (!filename) {
            throw new Error("No filename given and config is not using a file");
        }
        log.info(`Detected rule config change...`);
        const rules = this.config.validate(filename);
        if (rules === undefined) {
            throw Error("Rule file contents was undefined");
        }
        log.info(`Rule file ok, checking rules...`);
        this.rules = this.evaulateRules(rules);
        log.info(`Applied new ruleset`);
        this.conflictCache.clear();
    }

    evaulateRules (rules) {
        let newRules = {userIds: {}};
        if (!rules || !rules.userIds) {
            rules = {userIds: {}};
        }
        if (!rules.userIds.conflict) {
            rules.userIds.conflict = [];
        }
        if (!rules.userIds.exempt) {
            rules.userIds.exempt = [];
        }
        newRules.userIds.conflict = rules.userIds.conflict.map(
            (regexStr) => new RegExp(regexStr)
        );
        newRules.userIds.exempt = rules.userIds.exempt.map(
            (regexStr) => new RegExp(regexStr)
        );
        return newRules;
    }

    validateRoom (roomId, cache=true) {
        const status = cache ? this._checkConflictCache(roomId) : undefined;
        if (status !== undefined) {
            return Promise.reject(status);
        }
        // Get all users in the room.
        return this.asBot.getJoinedMembers(roomId).then((joined) => {
            for (const userId of Object.keys(joined)) {
                let rule;
                let ignoreUser = false;
                for (rule of this.rules.userIds.exempt) {
                    if (rule.exec(userId) !== null) {
                        ignoreUser = true;
                        break;
                    }
                }
                if (ignoreUser) {
                    break;
                }
                for (rule of this.rules.userIds.conflict) {
                    if (rule.exec(userId) !== null) {
                        return false;
                    }
                }
            }
            return true;
        }).then((isValid) => {
            if (isValid) {
                return PASSED;
            }
            this.conflictCache.set(roomId, Date.now());
            throw ERROR_USER_CONFLICT;
        });
    }

    _checkConflictCache (roomId) {
        if (this.conflictCache.has(roomId)) {
            if (
                this.conflictCache.get(roomId) > (Date.now() - VALIDATION_CACHE_LIFETIME)
            ) {
                return ERROR_CACHED;
            }
            this.conflictCache.delete(roomId);
        }
        return undefined;
    }
}

module.exports = {
    validationStatuses: {
        PASSED,
        ERROR_USER_CONFLICT,
        ERROR_CACHED,
        ERROR,
    },
    RoomLinkValidator
};