Equicord/src/equicordplugins/messageLoggerEnhanced/db.ts

270 lines
8.1 KiB
TypeScript
Raw Normal View History

/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
2026-04-08 23:54:25 -04:00
import { ChannelStore, Toasts } from "@webpack/common";
2024-11-19 17:08:13 -05:00
import { DBSchema, IDBPDatabase, openDB } from "idb";
import { LoggedMessageJSON } from "./types";
import { getMessageStatus } from "./utils";
import { stripTransientRenderState } from "./utils/cleanUp";
import { DB_NAME, DB_VERSION } from "./utils/constants";
import { getAttachmentBlobUrl } from "./utils/saveImage";
export enum DBMessageStatus {
DELETED = "DELETED",
EDITED = "EDITED",
GHOST_PINGED = "GHOST_PINGED",
}
export interface DBMessageRecord {
message_id: string;
channel_id: string;
status: DBMessageStatus;
message: LoggedMessageJSON;
}
export interface MLIDB extends DBSchema {
messages: {
key: string;
value: DBMessageRecord;
indexes: {
by_channel_id: string;
by_status: DBMessageStatus;
by_timestamp: string;
by_timestamp_and_message_id: [string, string];
};
};
}
2026-04-12 20:05:45 -04:00
export let db: IDBPDatabase<MLIDB>;
export const cachedMessages = new Map<string, LoggedMessageJSON>();
2026-04-12 20:05:45 -04:00
// this is probably not the best way to do this
async function cacheRecords(records: DBMessageRecord[]) {
for (const r of records) {
cacheRecord(r);
for (const att of r.message.attachments) {
const blobUrl = await getAttachmentBlobUrl(att);
if (blobUrl) {
att.url = blobUrl + "#";
att.proxy_url = blobUrl + "#";
}
}
}
return records;
}
async function cacheRecord(record?: DBMessageRecord | null) {
if (!record) return record;
stripTransientRenderState(record.message);
cachedMessages.set(record.message_id, record.message);
return record;
}
export async function initIDB() {
2026-04-12 20:25:03 -04:00
if (db) return;
2026-04-12 20:05:45 -04:00
db = await openDB<MLIDB>(DB_NAME, DB_VERSION, {
upgrade(db) {
const messageStore = db.createObjectStore("messages", { keyPath: "message_id" });
messageStore.createIndex("by_channel_id", "channel_id");
messageStore.createIndex("by_status", "status");
messageStore.createIndex("by_timestamp", "message.timestamp");
messageStore.createIndex("by_timestamp_and_message_id", ["channel_id", "message.timestamp"]);
}
});
}
2026-04-12 20:05:45 -04:00
initIDB();
export async function hasMessageIDB(message_id: string) {
2026-04-12 20:05:45 -04:00
return cachedMessages.has(message_id) || (await db.count("messages", message_id)) > 0;
}
export async function countMessagesIDB() {
return db.count("messages");
}
export async function countMessagesByStatusIDB(status: DBMessageStatus) {
2026-04-12 20:05:45 -04:00
return db.countFromIndex("messages", "by_status", status);
}
export async function getAllMessagesIDB() {
2026-04-12 20:05:45 -04:00
return cacheRecords(await db.getAll("messages"));
}
export async function getMessagesForChannelIDB(channel_id: string) {
2026-04-12 20:05:45 -04:00
return cacheRecords(await db.getAllFromIndex("messages", "by_channel_id", channel_id));
}
export async function getMessageIDB(message_id: string) {
2026-04-12 20:05:45 -04:00
return cacheRecord(await db.get("messages", message_id));
}
export async function getMessagesByStatusIDB(status: DBMessageStatus) {
2026-04-12 20:05:45 -04:00
return cacheRecords(await db.getAllFromIndex("messages", "by_status", status));
}
export async function getOldestMessagesIDB(limit: number) {
2026-04-12 20:05:45 -04:00
return cacheRecords(await db.getAllFromIndex("messages", "by_timestamp", undefined, limit));
}
2026-02-11 00:54:49 -05:00
export async function* iterateAllMessagesIDB(batchSize = 100) {
let lastId: string | undefined;
while (true) {
const batch: DBMessageRecord[] = [];
2026-04-12 20:05:45 -04:00
// new transaction for each batch to avoid timeouts during yield
const tx = db.transaction("messages");
2026-02-11 00:54:49 -05:00
const range = lastId ? IDBKeyRange.lowerBound(lastId, true) : undefined;
let cursor = await tx.store.openCursor(range);
while (cursor && batch.length < batchSize) {
batch.push(cursor.value);
cursor = await cursor.continue();
}
if (batch.length === 0) break;
lastId = batch[batch.length - 1].message_id;
2026-04-12 20:05:45 -04:00
2026-02-11 00:54:49 -05:00
yield await cacheRecords(batch);
if (batch.length < batchSize) break;
}
}
export async function getOlderThanTimestampIDB(timestamp: string) {
2026-04-12 20:05:45 -04:00
const tx = db.transaction("messages", "readonly");
const { store } = tx;
const index = store.index("by_timestamp");
2026-04-12 20:05:45 -04:00
const cursor = await index.openCursor(IDBKeyRange.upperBound(timestamp));
2026-04-12 20:05:45 -04:00
if (!cursor) {
return [];
}
2026-04-12 20:05:45 -04:00
const messages: DBMessageRecord[] = [];
for await (const c of cursor) {
messages.push(c.value);
}
return cacheRecords(messages);
}
export async function getOlderThanTimestampForGuildsIDB(timestamp: string, currentChannelId?: string, preserveCurrentChannel?: boolean) {
const allOldMessages = await getOlderThanTimestampIDB(timestamp);
return allOldMessages.filter(record => {
const { message } = record;
const channel = ChannelStore.getChannel(message.channel_id);
const isGuildMessage = channel?.guild_id != null;
const isCurrentChannel = preserveCurrentChannel && currentChannelId && message.channel_id === currentChannelId;
return isGuildMessage && !isCurrentChannel;
});
}
export async function getDateStortedMessagesByStatusIDB(newest: boolean, limit: number, status: DBMessageStatus) {
2026-04-12 20:05:45 -04:00
const tx = db.transaction("messages", "readonly");
const { store } = tx;
const index = store.index("by_status");
2026-04-12 20:05:45 -04:00
const direction = newest ? "prev" : "next";
const cursor = await index.openCursor(IDBKeyRange.only(status), direction);
2026-04-12 20:05:45 -04:00
if (!cursor) {
console.log("No messages found");
return [];
}
const messages: DBMessageRecord[] = [];
for await (const c of cursor) {
messages.push(c.value);
if (messages.length >= limit) break;
}
return cacheRecords(messages);
}
export async function getMessagesByChannelAndAfterTimestampIDB(channel_id: string, start: string) {
2026-04-12 20:05:45 -04:00
const tx = db.transaction("messages", "readonly");
const { store } = tx;
const index = store.index("by_timestamp_and_message_id");
2026-04-12 20:05:45 -04:00
const cursor = await index.openCursor(IDBKeyRange.bound([channel_id, start], [channel_id, "\uffff"]));
2026-04-12 20:05:45 -04:00
if (!cursor) {
console.log("No messages found in range");
return [];
}
2026-04-12 20:05:45 -04:00
const messages: DBMessageRecord[] = [];
for await (const c of cursor) {
messages.push(c.value);
}
return cacheRecords(messages);
}
export async function addMessageIDB(message: LoggedMessageJSON, status: DBMessageStatus) {
stripTransientRenderState(message);
2026-04-12 20:25:03 -04:00
if (!db) await initIDB();
2026-04-12 20:05:45 -04:00
await db.put("messages", {
channel_id: message.channel_id,
message_id: message.id,
status,
message,
});
2026-04-12 20:05:45 -04:00
cachedMessages.set(message.id, message);
}
export async function addMessagesBulkIDB(messages: LoggedMessageJSON[], status?: DBMessageStatus) {
messages.forEach(stripTransientRenderState);
2026-04-12 20:05:45 -04:00
const tx = db.transaction("messages", "readwrite");
const { store } = tx;
await Promise.all([
...messages.map(message => store.add({
channel_id: message.channel_id,
message_id: message.id,
status: status ?? getMessageStatus(message),
message,
})),
tx.done
]);
messages.forEach(message => cachedMessages.set(message.id, message));
}
export async function deleteMessageIDB(message_id: string) {
2026-04-12 20:05:45 -04:00
await db.delete("messages", message_id);
2026-04-12 20:05:45 -04:00
cachedMessages.delete(message_id);
}
export async function deleteMessagesBulkIDB(message_ids: string[]) {
2026-04-12 20:05:45 -04:00
const tx = db.transaction("messages", "readwrite");
const { store } = tx;
2026-04-12 20:05:45 -04:00
await Promise.all([...message_ids.map(id => store.delete(id)), tx.done]);
message_ids.forEach(id => cachedMessages.delete(id));
}
export async function clearMessagesIDB(showToast = true) {
cachedMessages.clear();
await db.clear("messages");
if (!showToast) return;
Toasts.show({
type: Toasts.Type.MESSAGE,
message: "Cleared message log database and cache.",
id: Toasts.genId()
2026-02-14 01:13:59 -05:00
});
}