mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Moves the repeatedly defined HEX64_REGEX constant to a shared utility module to improve maintainability and consistency. Updates all consumers to import from the new location. Signed-off-by: Jules <[email protected]> Co-authored-by: PR0M3TH3AN <[email protected]>
338 lines
7.9 KiB
JavaScript
338 lines
7.9 KiB
JavaScript
import { userLogger } from "./utils/logger.js";
|
|
import { HEX64_REGEX } from "./utils/hex.js";
|
|
|
|
const DB_NAME = "bitvidSettings";
|
|
const DB_VERSION = 1;
|
|
const STORE_NAME = "kv";
|
|
const KEY_PREFIX = "dmSnapshot:";
|
|
const LOCALSTORAGE_PREFIX = "bitvid:dmSnapshot:";
|
|
const PREVIEW_MAX_LENGTH = 160;
|
|
|
|
|
|
function isIndexedDbAvailable() {
|
|
try {
|
|
return typeof indexedDB !== "undefined";
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function openSettingsDb() {
|
|
return new Promise((resolve, reject) => {
|
|
if (!isIndexedDbAvailable()) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
|
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result;
|
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
db.createObjectStore(STORE_NAME);
|
|
}
|
|
};
|
|
|
|
request.onsuccess = () => {
|
|
resolve(request.result);
|
|
};
|
|
|
|
request.onerror = () => {
|
|
reject(request.error || new Error("Failed to open settings DB"));
|
|
};
|
|
});
|
|
}
|
|
|
|
function normalizePubkey(pubkey) {
|
|
if (typeof pubkey !== "string") {
|
|
return "";
|
|
}
|
|
|
|
const trimmed = pubkey.trim();
|
|
if (!trimmed) {
|
|
return "";
|
|
}
|
|
|
|
if (HEX64_REGEX.test(trimmed)) {
|
|
return trimmed.toLowerCase();
|
|
}
|
|
|
|
if (trimmed.startsWith("npub1") && typeof window !== "undefined") {
|
|
try {
|
|
const decoded = window?.NostrTools?.nip19?.decode?.(trimmed);
|
|
if (decoded?.type === "npub" && typeof decoded.data === "string") {
|
|
const hex = decoded.data.trim();
|
|
if (HEX64_REGEX.test(hex)) {
|
|
return hex.toLowerCase();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Ignore decode errors and fall through to return empty string.
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
function resolveStorageKeys(pubkey) {
|
|
const normalized = normalizePubkey(pubkey);
|
|
if (!normalized) {
|
|
throw new Error("Invalid pubkey for DM snapshot storage.");
|
|
}
|
|
|
|
return {
|
|
normalized,
|
|
dbKey: `${KEY_PREFIX}${normalized}`,
|
|
fallbackKey: `${LOCALSTORAGE_PREFIX}${normalized}`,
|
|
};
|
|
}
|
|
|
|
function sanitizeTimestamp(value) {
|
|
const numeric = Number(value);
|
|
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
const rounded = Math.floor(numeric);
|
|
return Number.isFinite(rounded) && rounded > 0 ? rounded : 0;
|
|
}
|
|
|
|
function sanitizePreview(value) {
|
|
if (typeof value !== "string") {
|
|
return "";
|
|
}
|
|
|
|
let preview = value.replace(/\s+/g, " ").trim();
|
|
if (!preview) {
|
|
return "";
|
|
}
|
|
|
|
if (preview.length > PREVIEW_MAX_LENGTH) {
|
|
preview = `${preview.slice(0, PREVIEW_MAX_LENGTH).trimEnd()}\u2026`;
|
|
}
|
|
|
|
return preview;
|
|
}
|
|
|
|
function normalizeSnapshotEntry(entry) {
|
|
if (!entry || typeof entry !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const remotePubkey = normalizePubkey(
|
|
entry.remotePubkey || entry.remote || entry.pubkey,
|
|
);
|
|
if (!remotePubkey) {
|
|
return null;
|
|
}
|
|
|
|
const latestTimestamp = sanitizeTimestamp(
|
|
entry.latestTimestamp ?? entry.timestamp ?? entry.lastTimestamp,
|
|
);
|
|
|
|
const preview = sanitizePreview(
|
|
entry.preview ?? entry.plaintext ?? entry.text ?? "",
|
|
);
|
|
|
|
return {
|
|
remotePubkey,
|
|
latestTimestamp,
|
|
preview,
|
|
};
|
|
}
|
|
|
|
function normalizeSnapshotList(raw) {
|
|
if (!Array.isArray(raw)) {
|
|
return [];
|
|
}
|
|
|
|
const seen = new Set();
|
|
const results = [];
|
|
|
|
for (const entry of raw) {
|
|
const normalized = normalizeSnapshotEntry(entry);
|
|
if (!normalized) {
|
|
continue;
|
|
}
|
|
|
|
if (seen.has(normalized.remotePubkey)) {
|
|
const existingIndex = results.findIndex(
|
|
(candidate) => candidate.remotePubkey === normalized.remotePubkey,
|
|
);
|
|
if (existingIndex >= 0) {
|
|
const existing = results[existingIndex];
|
|
if (normalized.latestTimestamp > existing.latestTimestamp) {
|
|
results[existingIndex] = normalized;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
seen.add(normalized.remotePubkey);
|
|
results.push(normalized);
|
|
}
|
|
|
|
results.sort(
|
|
(a, b) => (b.latestTimestamp || 0) - (a.latestTimestamp || 0),
|
|
);
|
|
|
|
return results;
|
|
}
|
|
|
|
export async function loadDirectMessageSnapshot(pubkey) {
|
|
let keys;
|
|
try {
|
|
keys = resolveStorageKeys(pubkey);
|
|
} catch (error) {
|
|
userLogger.warn("[directMessagesStore] Invalid pubkey for load", error);
|
|
return [];
|
|
}
|
|
|
|
const { normalized, dbKey, fallbackKey } = keys;
|
|
|
|
try {
|
|
const db = await openSettingsDb();
|
|
if (db) {
|
|
try {
|
|
const stored = await new Promise((resolve, reject) => {
|
|
const tx = db.transaction(STORE_NAME, "readonly");
|
|
const store = tx.objectStore(STORE_NAME);
|
|
const request = store.get(dbKey);
|
|
request.onsuccess = () => resolve(request.result || []);
|
|
request.onerror = () =>
|
|
reject(request.error || new Error("Failed to load snapshot"));
|
|
});
|
|
|
|
return normalizeSnapshotList(stored);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
userLogger.warn(
|
|
"[directMessagesStore] Failed to read IndexedDB snapshot",
|
|
error,
|
|
);
|
|
}
|
|
|
|
try {
|
|
if (typeof localStorage !== "undefined") {
|
|
const raw = localStorage.getItem(fallbackKey);
|
|
if (raw) {
|
|
return normalizeSnapshotList(JSON.parse(raw));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
userLogger.warn(
|
|
"[directMessagesStore] Failed to read fallback snapshot",
|
|
error,
|
|
);
|
|
}
|
|
|
|
// If we reached here, ensure we still return a normalized empty list for
|
|
// callers that rely on consistent return values.
|
|
return normalizeSnapshotList([]);
|
|
}
|
|
|
|
export async function saveDirectMessageSnapshot(pubkey, snapshot) {
|
|
let keys;
|
|
try {
|
|
keys = resolveStorageKeys(pubkey);
|
|
} catch (error) {
|
|
userLogger.warn("[directMessagesStore] Invalid pubkey for save", error);
|
|
return [];
|
|
}
|
|
|
|
const normalizedSnapshot = normalizeSnapshotList(snapshot);
|
|
const { dbKey, fallbackKey } = keys;
|
|
|
|
try {
|
|
const db = await openSettingsDb();
|
|
if (db) {
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
const store = tx.objectStore(STORE_NAME);
|
|
const request = store.put(normalizedSnapshot, dbKey);
|
|
request.onsuccess = () => resolve();
|
|
request.onerror = () =>
|
|
reject(request.error || new Error("Failed to write snapshot"));
|
|
});
|
|
return normalizedSnapshot;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
userLogger.warn(
|
|
"[directMessagesStore] Failed to write IndexedDB snapshot",
|
|
error,
|
|
);
|
|
}
|
|
|
|
try {
|
|
if (typeof localStorage !== "undefined") {
|
|
localStorage.setItem(fallbackKey, JSON.stringify(normalizedSnapshot));
|
|
}
|
|
} catch (error) {
|
|
userLogger.warn(
|
|
"[directMessagesStore] Failed to write fallback snapshot",
|
|
error,
|
|
);
|
|
}
|
|
|
|
return normalizedSnapshot;
|
|
}
|
|
|
|
export async function clearDirectMessageSnapshot(pubkey) {
|
|
let keys;
|
|
try {
|
|
keys = resolveStorageKeys(pubkey);
|
|
} catch (error) {
|
|
userLogger.warn("[directMessagesStore] Invalid pubkey for clear", error);
|
|
return false;
|
|
}
|
|
|
|
const { dbKey, fallbackKey } = keys;
|
|
let cleared = false;
|
|
|
|
try {
|
|
const db = await openSettingsDb();
|
|
if (db) {
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
const store = tx.objectStore(STORE_NAME);
|
|
const request = store.delete(dbKey);
|
|
request.onsuccess = () => resolve();
|
|
request.onerror = () =>
|
|
reject(request.error || new Error("Failed to clear snapshot"));
|
|
});
|
|
cleared = true;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
userLogger.warn(
|
|
"[directMessagesStore] Failed to clear IndexedDB snapshot",
|
|
error,
|
|
);
|
|
}
|
|
|
|
try {
|
|
if (typeof localStorage !== "undefined") {
|
|
localStorage.removeItem(fallbackKey);
|
|
cleared = true;
|
|
}
|
|
} catch (error) {
|
|
userLogger.warn(
|
|
"[directMessagesStore] Failed to clear fallback snapshot",
|
|
error,
|
|
);
|
|
}
|
|
|
|
return cleared;
|
|
}
|
|
|