"use strict";
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: Apache-2.0
//
// SPDX-FileAttributionText: <text>
// This modified file incorporates work from Draupnir
// https://github.com/the-draupnir-project/Draupnir
// </text>
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SqliteHashReversalStore = void 0;
const typescript_result_1 = require("@gnuxie/typescript-result");
const matrix_basic_types_1 = require("@the-draupnir-project/matrix-basic-types");
const matrix_protection_suite_1 = require("matrix-protection-suite");
const BetterSqliteStore_1 = require("./BetterSqliteStore");
const path_1 = __importDefault(require("path"));
const SqliteSchema_1 = require("./SqliteSchema");
const events_1 = __importDefault(require("events"));
const crypto_1 = require("crypto");
const log = new matrix_protection_suite_1.Logger("SqliteHashReversalStore");
// The reason why room is split into room_identification is because the creator
// is usually not readily available when discovering rooms from the anti-spam apis.
const SchemaText = [
    `
  CREATE TABLE room_sha256 (
    room_id TEXT PRIMARY KEY NOT NULL,
    sha256 TEXT NOT NULL
  ) STRICT, WITHOUT ROWID;
  CREATE INDEX idx_room_sha256 ON room_sha256 (sha256, room_id);
  CREATE TABLE room_identification (
    room_id TEXT PRIMARY KEY NOT NULL,
    creator TEXT NOT NULL,
    server TEXT NOT NULL,
    FOREIGN KEY (room_id) REFERENCES room_sha256(room_id)
  ) STRICT;
  CREATE TABLE user_sha256 (
    user_id TEXT PRIMARY KEY NOT NULL,
    server_name TEXT NOT NULL,
    sha256 TEXT NOT NULL
  ) STRICT, WITHOUT ROWID;
  CREATE INDEX idx_user_sha256 ON user_sha256 (sha256, user_id);
  CREATE TABLE server_sha256 (
    server_name TEXT PRIMARY KEY NOT NULL,
    sha256 TEXT NOT NULL
  ) STRICT, WITHOUT ROWID;
  CREATE INDEX idx_server_sha256 ON server_sha256 (sha256, server_name);
  `,
];
const SchemaOptions = {
    upgradeSteps: SchemaText.map((text) => function (db) {
        db.exec(text);
    }),
    consistencyCheck(db) {
        return (0, SqliteSchema_1.checkKnownTables)(db, [
            "room_sha256",
            "room_identification",
            "user_sha256",
            "server_sha256",
        ]);
    },
};
function wrapInTryCatch(cb, message) {
    try {
        return cb();
    }
    catch (e) {
        if (e instanceof Error) {
            return matrix_protection_suite_1.ActionException.Result(message, {
                exception: e,
                exceptionKind: matrix_protection_suite_1.ActionExceptionKind.Unknown,
            });
        }
        else {
            throw e;
        }
    }
}
class SqliteHashReversalStore extends events_1.default {
    db;
    baseStore;
    constructor(db) {
        super();
        this.db = db;
        this.baseStore = new BetterSqliteStore_1.BetterSqliteStore(SchemaOptions, db, log);
    }
    async findUserHash(hash) {
        return wrapInTryCatch(() => (0, typescript_result_1.Ok)(this.db
            .prepare(`SELECT user_id FROM user_sha256 WHERE sha256 = ?`)
            .pluck()
            .get(hash)), "error while querying hash for a user");
    }
    async findServerHash(hash) {
        return wrapInTryCatch(() => (0, typescript_result_1.Ok)(this.db
            .prepare(`SELECT server_name FROM server_sha256 WHERE sha256 = ?`)
            .pluck()
            .get(hash)), "error while querying hash for a server name");
    }
    static StoreName = "hash-store.db";
    static createToplevel(storagePath) {
        const options = {
            path: path_1.default.join(storagePath, SqliteHashReversalStore.StoreName),
            WALMode: true,
            foreignKeys: true,
            fileMustExist: false,
        };
        return new SqliteHashReversalStore((0, BetterSqliteStore_1.makeBetterSqliteDB)(options, log));
    }
    async findRoomHash(hash) {
        try {
            return (0, typescript_result_1.Ok)(this.db
                .prepare(`SELECT room_id FROM room_sha256 WHERE sha256 = ?`)
                .pluck()
                .get(hash));
        }
        catch (e) {
            if (e instanceof Error) {
                return matrix_protection_suite_1.ActionException.Result(`Unexpected error while querying for a room hash`, {
                    exception: e,
                    exceptionKind: matrix_protection_suite_1.ActionExceptionKind.Unknown,
                });
            }
            else {
                throw e;
            }
        }
    }
    reversePolicies(policies, selectEntityFromSha256) {
        const reversedPolicies = [];
        for (const policy of policies) {
            const sha256 = policy.hashes["sha256"];
            if (sha256 === undefined) {
                continue;
            }
            const entity = selectEntityFromSha256.get(sha256);
            if (entity === undefined) {
                continue;
            }
            reversedPolicies.push((0, matrix_protection_suite_1.makeReversedHashedPolicy)(entity, policy));
        }
        return reversedPolicies;
    }
    reverseRoomPolicies(policies) {
        return this.reversePolicies(policies, this.db
            .prepare(`
          SELECT room_id
          FROM room_sha256
          WHERE sha256 = ?`)
            .pluck());
    }
    reverseUserPolicies(policies) {
        return this.reversePolicies(policies, this.db
            .prepare(`
          SELECT user_id
          FROM user_sha256
          WHERE sha256 = ?`)
            .pluck());
    }
    reverseServerPolicies(policies) {
        return this.reversePolicies(policies, this.db
            .prepare(`
          SELECT server_name
          FROM server_sha256
          WHERE sha256 = ?`)
            .pluck());
    }
    async reverseHashedPolicies(policies) {
        const hashedRoomPolicies = policies.filter((policy) => policy.kind === matrix_protection_suite_1.PolicyRuleType.Room);
        const hashedUserPolicies = policies.filter((policy) => policy.kind === matrix_protection_suite_1.PolicyRuleType.User);
        const hashedServerPolicies = policies.filter((policy) => policy.kind === matrix_protection_suite_1.PolicyRuleType.Server);
        return wrapInTryCatch(() => {
            return (0, typescript_result_1.Ok)([
                ...this.reverseRoomPolicies(hashedRoomPolicies),
                ...this.reverseUserPolicies(hashedUserPolicies),
                ...this.reverseServerPolicies(hashedServerPolicies),
            ]);
        }, "Error trying to reverse hashed policies");
    }
    storeUndiscoveredEntities(entities, filterStatement, insertOrIgnore, recordFactoryFn, eventEmitFn, serverExtractFn) {
        return wrapInTryCatch(() => {
            const undiscoveredEntities = entities.filter((entity) => filterStatement.get(entity));
            const rowsToInsert = undiscoveredEntities.map((entity) => recordFactoryFn(entity, (0, crypto_1.createHash)("sha256").update(entity, "utf8").digest("base64")));
            insertOrIgnore.run(JSON.stringify(rowsToInsert));
            // try to discover servers too while we're here
            if (serverExtractFn) {
                const storeResult = this.storeUndiscoveredEntities(rowsToInsert.map(serverExtractFn), this.db
                    .prepare(`SELECT NOT EXISTS (SELECT 1 FROM server_sha256 where server_name = ?)`)
                    .pluck(), this.db.prepare(`
            INSERT OR IGNORE INTO server_sha256 (server_name, sha256)
            SELECT value ->> 'server_name', value ->> 'sha256'
            FROM json_each(?)`), (server_name, sha256) => ({ server_name, sha256 }), (serverRecords) => {
                    this.emit("ReversedHashes", [], [], serverRecords);
                });
                if ((0, typescript_result_1.isError)(storeResult)) {
                    return storeResult.elaborate("Failed to update discovered servers");
                }
            }
            eventEmitFn(rowsToInsert);
            return (0, typescript_result_1.Ok)(rowsToInsert);
        }, "Failed to insert undiscovered entities into hash store");
    }
    async storeUndiscoveredRooms(roomIDs) {
        return this.storeUndiscoveredEntities(roomIDs, this.db
            .prepare(`SELECT NOT EXISTS (SELECT 1 FROM room_sha256 where room_id = ?)`)
            .pluck(), this.db.prepare(`
        INSERT OR IGNORE INTO room_sha256 (room_id, sha256)
        SELECT value ->> 'room_id', value ->> 'sha256'
        FROM json_each(?)`), (room_id, sha256) => ({ room_id, sha256 }), (roomRecords) => {
            this.emit("ReversedHashes", roomRecords, [], []);
        });
    }
    async storeUndiscoveredUsers(userIDs) {
        return this.storeUndiscoveredEntities(userIDs, this.db
            .prepare(`SELECT NOT EXISTS (SELECT 1 FROM user_sha256 WHERE user_id = ?)`)
            .pluck(), this.db.prepare(`
        INSERT OR IGNORE INTO user_sha256 (user_id, server_name, sha256)
        SELECT value ->> 'user_id', value ->> 'server_name', value ->> 'sha256'
        FROM json_each(?)`), (user_id, sha256) => ({
            user_id,
            server_name: (0, matrix_basic_types_1.userServerName)(user_id),
            sha256,
        }), (userRecords) => {
            this.emit("ReversedHashes", [], userRecords, []);
        }, (userRecord) => userRecord.server_name);
    }
    async storeRoomIdentification(roomDetails) {
        try {
            const statement = this.db.prepare(`REPLACE INTO room_identification (room_id, creator, server)
        VALUES (?, ?, ?)`);
            statement.run([
                roomDetails.roomID,
                roomDetails.creator,
                roomDetails.server,
            ]);
            return (0, typescript_result_1.Ok)(undefined);
        }
        catch (exception) {
            if (exception instanceof Error) {
                return matrix_protection_suite_1.ActionException.Result(`Error while trying to store details about a hashed room`, {
                    exception,
                    exceptionKind: matrix_protection_suite_1.ActionExceptionKind.Unknown,
                });
            }
            else {
                throw exception;
            }
        }
    }
    async findRoomsByServer(server) {
        return wrapInTryCatch(() => (0, typescript_result_1.Ok)(this.db
            .prepare(`
              SELECT room_id FROM room_identification WHERE server = ?`)
            .pluck()
            .all(server)), "Error while trying to find rooms created by server");
    }
    async findRoomsByCreator(creator) {
        return wrapInTryCatch(() => (0, typescript_result_1.Ok)(this.db
            .prepare(`
              SELECT room_id FROM room_identification WHERE creator = ?`)
            .pluck()
            .all(creator)), "Error while trying to find created rooms");
    }
    destroy() {
        this.baseStore.destroy();
    }
}
exports.SqliteHashReversalStore = SqliteHashReversalStore;
//# sourceMappingURL=HashStore.js.map