"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.IdentityClient = void 0;
const http_1 = require("../http");
const decorators_1 = require("../metrics/decorators");
const Metrics_1 = require("../metrics/Metrics");
const crypto = require("crypto");
const UnpaddedBase64_1 = require("../helpers/UnpaddedBase64");
const MatrixProfile_1 = require("../models/MatrixProfile");
/**
 * A way to access an Identity Server using the Identity Service API from a MatrixClient.
 * @category Identity Servers
 */
class IdentityClient {
    constructor(accessToken, serverUrl, matrixClient) {
        this.accessToken = accessToken;
        this.serverUrl = serverUrl;
        this.matrixClient = matrixClient;
        this.metrics = new Metrics_1.Metrics();
    }
    /**
     * Gets account information for the logged in user.
     * @returns {Promise<IdentityServerAccount>} Resolves to the account information
     */
    getAccount() {
        return this.doRequest("GET", "/_matrix/identity/v2/account");
    }
    /**
     * Gets the terms of service for which the identity server has.
     * @returns {Promise<Policies>} Resolves to the policies of the server.
     */
    getTermsOfService() {
        return this.doRequest("GET", "/_matrix/identity/v2/terms");
    }
    /**
     * Accepts a given set of URLs from Policy objects returned by the server. This implies acceptance of
     * the terms. Note that this will not update the user's account data to consider these terms accepted
     * in the future - that is an exercise left to the caller.
     * @param {string[]} termsUrls The URLs to count as accepted.
     * @returns {Promise<void>} Resolves when complete.
     */
    acceptTerms(termsUrls) {
        return this.doRequest("POST", "/_matrix/identity/v2/terms", null, {
            user_accepts: termsUrls,
        });
    }
    /**
     * Accepts all the terms of service offered by the identity server. Note that this is only meant to be
     * used by automated bots where terms acceptance is implicit - the terms of service need to be presented
     * to the user in most cases.
     * @returns {Promise<void>} Resolves when complete.
     */
    acceptAllTerms() {
        return __awaiter(this, void 0, void 0, function* () {
            const terms = yield this.getTermsOfService();
            const urls = new Set();
            for (const policy of Object.values(terms.policies)) {
                let chosenLang = policy["en"];
                if (!chosenLang) {
                    chosenLang = policy[Object.keys(policy).find(k => k !== "version")];
                }
                if (!chosenLang)
                    continue; // skip - invalid
                urls.add(chosenLang.url);
            }
            return yield this.acceptTerms(Array.from(urls));
        });
    }
    /**
     * Looks up a series of third party identifiers (email addresses or phone numbers) to see if they have
     * associated mappings. The returned array will be ordered the same as the input, and have falsey values
     * in place of any failed/missing lookups (eg: no mapping).
     * @param {Threepid[]} identifiers The identifiers to look up.
     * @param {boolean} allowPlaintext If true, the function will accept the server's offer to use plaintext
     * lookups when no other methods are available. The function will always prefer hashed methods.
     * @returns {Promise<string[]>} Resolves to the user IDs (or falsey values) in the same order as the input.
     */
    lookup(identifiers, allowPlaintext = false) {
        return __awaiter(this, void 0, void 0, function* () {
            const hashInfo = yield this.doRequest("GET", "/_matrix/identity/v2/hash_details");
            if (!(hashInfo === null || hashInfo === void 0 ? void 0 : hashInfo["algorithms"]))
                throw new Error("Server not supported: invalid response");
            const algorithms = hashInfo === null || hashInfo === void 0 ? void 0 : hashInfo["algorithms"];
            let algorithm = algorithms.find(a => a === "sha256");
            if (!algorithm && allowPlaintext)
                algorithm = algorithms.find(a => a === "none");
            if (!algorithm)
                throw new Error("No supported hashing algorithm found");
            const body = {
                algorithm,
                pepper: hashInfo["lookup_pepper"],
                addresses: [],
            };
            for (const pid of identifiers) {
                let transformed = null;
                switch (algorithm) {
                    case "none":
                        transformed = `${pid.address.toLowerCase()} ${pid.kind}`;
                        break;
                    case "sha256":
                        transformed = UnpaddedBase64_1.UnpaddedBase64.encodeBufferUrlSafe(crypto.createHash("sha256")
                            .update(`${pid.address.toLowerCase()} ${pid.kind} ${body.pepper}`).digest());
                        break;
                    default:
                        throw new Error("Unsupported hashing algorithm (programming error)");
                }
                body.addresses.push(transformed);
            }
            const resp = yield this.doRequest("POST", "/_matrix/identity/v2/lookup", null, body);
            const mappings = (resp === null || resp === void 0 ? void 0 : resp["mappings"]) || {};
            const mxids = [];
            for (const addr of body.addresses) {
                mxids.push(mappings[addr]);
            }
            return mxids;
        });
    }
    /**
     * Creates a third party email invite. This will store the invite in the identity server, but
     * not publish the invite to the room - the caller is expected to handle the remaining part. Note
     * that this function is not required to be called when using the Client-Server API for inviting
     * third party addresses to a room. This will make several calls into the room state to populate
     * the invite details, therefore the inviter (the client backing this identity client) must be
     * present in the room.
     * @param {string} emailAddress The email address to invite.
     * @param {string} roomId The room ID to invite to.
     * @returns {Promise<IdentityServerInvite>} Resolves to the identity server's stored invite.
     */
    makeEmailInvite(emailAddress, roomId) {
        var _a, _b, _c, _d;
        return __awaiter(this, void 0, void 0, function* () {
            const req = {
                address: emailAddress,
                medium: "email",
                room_id: roomId,
                sender: yield this.matrixClient.getUserId(),
            };
            const tryFetch = (eventType, stateKey) => __awaiter(this, void 0, void 0, function* () {
                try {
                    return yield this.matrixClient.getRoomStateEvent(roomId, eventType, stateKey);
                }
                catch (e) {
                    return null;
                }
            });
            const canonicalAlias = (_a = (yield tryFetch("m.room.canonical_alias", ""))) === null || _a === void 0 ? void 0 : _a["alias"];
            const roomName = (_b = (yield tryFetch("m.room.name", ""))) === null || _b === void 0 ? void 0 : _b["name"];
            req["room_alias"] = canonicalAlias;
            req["room_avatar_url"] = (_c = (yield tryFetch("m.room.avatar", ""))) === null || _c === void 0 ? void 0 : _c["url"];
            req["room_name"] = roomName || canonicalAlias;
            req["room_join_rules"] = (_d = (yield tryFetch("m.room.join_rules", ""))) === null || _d === void 0 ? void 0 : _d["join_rule"];
            let profileInfo;
            try {
                profileInfo = yield this.matrixClient.getUserProfile(yield this.matrixClient.getUserId());
            }
            catch (e) {
                // ignore
            }
            const senderProfile = new MatrixProfile_1.MatrixProfile(yield this.matrixClient.getUserId(), profileInfo);
            req["sender_avatar_url"] = senderProfile.avatarUrl;
            req["sender_display_name"] = senderProfile.displayName;
            const inviteReq = {};
            for (const entry of Object.entries(req)) {
                if (!!entry[1])
                    inviteReq[entry[0]] = entry[1];
            }
            const qs = {};
            if (this.brand)
                qs['brand'] = this.brand;
            return yield this.doRequest("POST", "/_matrix/identity/v2/store-invite", qs, inviteReq);
        });
    }
    /**
     * Performs a web request to the server, applying appropriate authorization headers for
     * this client.
     * @param {"GET"|"POST"|"PUT"|"DELETE"} method The HTTP method to use in the request
     * @param {string} endpoint The endpoint to call. For example: "/_matrix/identity/v2/account"
     * @param {any} qs The query string to send. Optional.
     * @param {any} body The request body to send. Optional. Will be converted to JSON unless the type is a Buffer.
     * @param {number} timeout The number of milliseconds to wait before timing out.
     * @param {boolean} raw If true, the raw response will be returned instead of the response body.
     * @param {string} contentType The content type to send. Only used if the `body` is a Buffer.
     * @param {string} noEncoding Set to true to disable encoding, and return a Buffer. Defaults to false
     * @returns {Promise<any>} Resolves to the response (body), rejected if a non-2xx status code was returned.
     */
    doRequest(method, endpoint, qs = null, body = null, timeout = 60000, raw = false, contentType = "application/json", noEncoding = false) {
        const headers = {};
        if (this.accessToken) {
            headers["Authorization"] = `Bearer ${this.accessToken}`;
        }
        return http_1.doHttpRequest(this.serverUrl, method, endpoint, qs, body, headers, timeout, raw, contentType, noEncoding);
    }
    /**
     * Gets an instance of an identity client.
     * @param {OpenIDConnectToken} oidc The OpenID Connect token to register to the identity server with.
     * @param {string} serverUrl The full URL where the identity server can be reached at.
     */
    static acquire(oidc, serverUrl, mxClient) {
        return __awaiter(this, void 0, void 0, function* () {
            const account = yield http_1.doHttpRequest(serverUrl, "POST", "/_matrix/identity/v2/account/register", null, oidc);
            return new IdentityClient(account['token'], serverUrl, mxClient);
        });
    }
}
__decorate([
    decorators_1.timedIdentityClientFunctionCall(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", Promise)
], IdentityClient.prototype, "getAccount", null);
__decorate([
    decorators_1.timedIdentityClientFunctionCall(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", Promise)
], IdentityClient.prototype, "getTermsOfService", null);
__decorate([
    decorators_1.timedIdentityClientFunctionCall(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Array]),
    __metadata("design:returntype", Promise)
], IdentityClient.prototype, "acceptTerms", null);
__decorate([
    decorators_1.timedIdentityClientFunctionCall(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", Promise)
], IdentityClient.prototype, "acceptAllTerms", null);
__decorate([
    decorators_1.timedIdentityClientFunctionCall(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Array, Object]),
    __metadata("design:returntype", Promise)
], IdentityClient.prototype, "lookup", null);
__decorate([
    decorators_1.timedIdentityClientFunctionCall(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, String]),
    __metadata("design:returntype", Promise)
], IdentityClient.prototype, "makeEmailInvite", null);
__decorate([
    decorators_1.timedIdentityClientFunctionCall(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object, Object, Object, Object, Object, Object, Object]),
    __metadata("design:returntype", Promise)
], IdentityClient.prototype, "doRequest", null);
exports.IdentityClient = IdentityClient;
