"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppService = void 0;
const express_1 = __importDefault(require("express"));
const body_parser_1 = __importDefault(require("body-parser"));
const morgan_1 = __importDefault(require("morgan"));
const util_1 = __importDefault(require("util"));
const events_1 = require("events");
const fs_1 = __importDefault(require("fs"));
const https_1 = __importDefault(require("https"));
const http_1 = __importDefault(require("http"));
const MAX_SIZE_BYTES = 5000000; // 5MB
class AppService extends events_1.EventEmitter {
    /**
     * Construct a new application service.
     * @constructor
     * @param {Object} config Configuration for this service.
     * @param {String} config.homeserverToken The incoming HS token to expect. Must
     * be set prior to calling listen(port).
     * @param {Number} config.httpMaxSizeBytes The max number of bytes allowed on an
     * incoming HTTP request. Default: 5000000.
     * @throws If a homeserver token is not supplied.
     */
    constructor(config) {
        super();
        this.config = config;
        this.lastProcessedTxnId = "";
        const app = (0, express_1.default)();
        app.use((0, morgan_1.default)("combined", {
            stream: {
                write: this.onMorganLog.bind(this),
            }
        }));
        app.use(body_parser_1.default.json({
            limit: this.config.httpMaxSizeBytes || MAX_SIZE_BYTES,
        }));
        app.get("/_matrix/app/v1/users/:userId", this.onGetUsers.bind(this));
        app.get("/_matrix/app/v1/rooms/:alias", this.onGetRoomAlias.bind(this));
        app.put("/_matrix/app/v1/transactions/:txnId", this.onTransaction.bind(this));
        app.get("/users/:userId", this.onGetUsers.bind(this));
        app.get("/rooms/:alias", this.onGetRoomAlias.bind(this));
        app.put("/transactions/:txnId", this.onTransaction.bind(this));
        app.get("/health", this.onHealthCheck.bind(this));
        this.app = app;
    }
    /***
     * Begin listening on the specified port.
     * @param {Number} port The port to listen on.
     * @param {String} hostname Optional hostname to listen on
     * @param {Number} backlog Maximum length of the queue of pending connections
     * @param {Function} callback The callback for the "listening" event. Optional.
     * @returns {Promise} When the server is listening, if a callback is not provided.
     */
    listen(port, hostname, backlog, callback) {
        const tlsKey = process.env.MATRIX_AS_TLS_KEY;
        const tlsCert = process.env.MATRIX_AS_TLS_CERT;
        let serverApp;
        if (tlsKey || tlsCert) {
            if (!(tlsKey && tlsCert)) {
                throw new Error("MATRIX_AS_TLS_KEY and MATRIX_AS_TLS_CERT should be defined together!");
            }
            if (!fs_1.default.existsSync(tlsKey)) {
                throw new Error("Could not open MATRIX_AS_TLS_KEY: " + tlsKey);
            }
            if (!fs_1.default.existsSync(tlsCert)) {
                throw new Error("Could not open MATRIX_AS_TLS_CERT: " + tlsCert);
            }
            const options = {
                key: fs_1.default.readFileSync(tlsKey),
                cert: fs_1.default.readFileSync(tlsCert)
            };
            serverApp = https_1.default.createServer(options, this.app);
        }
        else {
            serverApp = http_1.default.createServer({}, this.app);
        }
        if (callback) {
            this.server = serverApp.listen(port, hostname, backlog, callback);
            return;
        }
        return new Promise((resolve, reject) => {
            serverApp.once("error", reject);
            serverApp.once("listening", resolve);
            this.server = serverApp.listen(port, hostname, backlog);
        });
    }
    /**
     * Closes the HTTP server.
     * @returns {Promise} When the operation has completed
     * @throws If the server has not been started.
     */
    async close() {
        if (!this.server) {
            throw Error("Server has not started");
        }
        return util_1.default.promisify(this.server.close).apply(this.server);
    }
    /**
     * Override this method to handle alias queries.
     * @param {string} alias The queried room alias
     * @param {Function} callback The callback to invoke when complete.
     * @return {Promise} A promise to resolve when complete (if callback isn't supplied)
     */
    onAliasQuery(alias, callback) {
        callback(); // stub impl
        return null;
    }
    /**
     * Override this method to handle user queries.
     * @param {string} userId The queried user ID.
     * @param {Function} callback The callback to invoke when complete.
     * @return {Promise} A promise to resolve when complete (if callback isn't supplied)
     */
    onUserQuery(userId, callback) {
        callback(); // stub impl
        return null;
    }
    /**
     * Set the token that should be used to verify incoming events.
     * @param {string} hsToken The home server token
     */
    setHomeserverToken(hsToken) {
        this.config.homeserverToken = hsToken;
    }
    /**
     * The Express App instance for the appservice, which
     * can be extended with paths.
     */
    get expressApp() {
        return this.app;
    }
    onMorganLog(str) {
        const redactedStr = str.replace(/access_token=.*?(&|\s|$)/, "access_token=<REDACTED>$1");
        this.emit("http-log", redactedStr);
    }
    isInvalidToken(req, res) {
        const providedToken = req.query.access_token;
        if (providedToken !== this.config.homeserverToken) {
            res.status(403);
            res.send({
                errcode: "M_FORBIDDEN",
                error: "Bad token supplied,"
            });
            return true;
        }
        return false;
    }
    async onGetUsers(req, res) {
        if (this.isInvalidToken(req, res)) {
            return;
        }
        const possiblePromise = this.onUserQuery(req.params.userId, () => {
            res.send({});
        });
        if (!possiblePromise) {
            return;
        }
        try {
            await possiblePromise;
            res.send({});
        }
        catch (e) {
            res.send({
                errcode: "M_UNKNOWN",
                error: e instanceof Error ? e.message : ""
            });
        }
    }
    async onGetRoomAlias(req, res) {
        if (this.isInvalidToken(req, res)) {
            return;
        }
        const possiblePromise = this.onAliasQuery(req.params.alias, function () {
            res.send({});
        });
        if (!possiblePromise) {
            return;
        }
        try {
            await possiblePromise;
            res.send({});
        }
        catch (e) {
            res.send({
                errcode: "M_UNKNOWN",
                error: e instanceof Error ? e.message : ""
            });
        }
    }
    onTransaction(req, res) {
        if (this.isInvalidToken(req, res)) {
            return;
        }
        const txnId = req.params.txnId;
        if (!txnId) {
            res.send("Missing transaction ID.");
            return;
        }
        if (!req.body) {
            res.send("Missing body.");
            return;
        }
        const events = req.body.events || [];
        const ephemeral = req.body["de.sorunome.msc2409.ephemeral"] || [];
        if (this.lastProcessedTxnId === txnId) {
            res.send({}); // duplicate
            return;
        }
        for (const event of events) {
            this.emit("event", event);
            if (event.type) {
                this.emit("type:" + event.type, event);
            }
        }
        for (const event of ephemeral) {
            this.emit("ephemeral", event);
            if (event.type) {
                this.emit("ephemeral_type:" + event.type, event);
            }
        }
        this.lastProcessedTxnId = txnId;
        res.send({});
    }
    onHealthCheck(req, res) {
        res.send('OK');
    }
}
exports.AppService = AppService;
