| author | Eric Roberts
<eric@devl.me> 2026-08-26 18:32:35 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-08-26 18:36:11 UTC |
| parent | 0794a04c6a45e73147738a515079aa34ee0962a5 |
| borogove/persistence/IDB.js | +951 | -321 |
| borogove/persistence/MediaStoreCache.js | +33 | -17 |
| npm/calls.ts | +3 | -9 |
| npm/index.ts | +36 | -36 |
| npm/persistence-browser.ts | +1 | -3 |
| test/browser-test.ts | +6 | -1 |
| test/fast-auth.spec.ts | +1 | -4 |
| test/idb.spec.ts | +201 | -181 |
| test/persistence-tests.ts | +1023 | -873 |
| test/sqlite.spec.ts | +0 | -1 |
diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js index c34c9fc..c0334b9 100644 --- a/borogove/persistence/IDB.js +++ b/borogove/persistence/IDB.js @@ -22,7 +22,7 @@ import { borogove_Stanza, borogove_Status, FractionalIndexing_between, - FractionalIndexing_BASE_95_DIGITS + FractionalIndexing_BASE_95_DIGITS, } from "./borogove.js"; import * as enums from "./borogove-enums.js"; @@ -77,20 +77,32 @@ export default async (dbname, media, tokenize, stemmer) => { "will", "with", "www", - "you" + "you", ]; - if (!tokenize) tokenize = function(s) { - return s.toLowerCase().split(/\s*\b/).filter(w => w.length > 1 && w.match(/\w/) && !stopwords.includes(w)); - } - if (!stemmer) stemmer = function(s) { return s; } + if (!tokenize) + tokenize = function (s) { + return s + .toLowerCase() + .split(/\s*\b/) + .filter((w) => w.length > 1 && w.match(/\w/) && !stopwords.includes(w)); + }; + if (!stemmer) + stemmer = function (s) { + return s; + }; // Helper functions to convert binary data to storage-safe strings // Uint8Array.to/fromBase64() is not yet widely available - function arrayBufferToBase64 (ab) { - return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), '')); + function arrayBufferToBase64(ab) { + return btoa( + new Uint8Array(ab).reduce( + (data, byte) => data + String.fromCharCode(byte), + "", + ), + ); } - function base64ToArrayBuffer (b64) { + function base64ToArrayBuffer(b64) { const binary_string = atob(b64); const len = binary_string.length; const bytes = new Uint8Array(len); @@ -107,10 +119,14 @@ export default async (dbname, media, tokenize, stemmer) => { const tx = db.transaction(["messages"], "readwrite"); const store = tx.objectStore("messages"); const count = await promisifyRequest(store.count()); - var index = FractionalIndexing_between("a ", null, FractionalIndexing_BASE_95_DIGITS); - const cursor = store.index("accounts").openCursor( - IDBKeyRange.bound([account], [account, []]) + var index = FractionalIndexing_between( + "a ", + null, + FractionalIndexing_BASE_95_DIGITS, ); + const cursor = store + .index("accounts") + .openCursor(IDBKeyRange.bound([account], [account, []])); let i = 0; let updates = []; const flushUpdates = () => { @@ -125,8 +141,18 @@ export default async (dbname, media, tokenize, stemmer) => { flushUpdates(); break; } - const sortId = index = FractionalIndexing_between(index, null, FractionalIndexing_BASE_95_DIGITS); - const terms = [...new Set(tokenize((cresult.value.text || "").replace(/^>.*/mg, "")).map(stemmer))].sort(); + const sortId = (index = FractionalIndexing_between( + index, + null, + FractionalIndexing_BASE_95_DIGITS, + )); + const terms = [ + ...new Set( + tokenize((cresult.value.text || "").replace(/^>.*/gm, "")).map( + stemmer, + ), + ), + ].sort(); updates.push({ ...cresult.value, sortId, terms }); if (i++ % 1000 === 0) { console.log("Migrating... " + i + " / " + count); @@ -147,7 +173,9 @@ export default async (dbname, media, tokenize, stemmer) => { const db = event.target.result; const tx = event.target.transaction; if (!db.objectStoreNames.contains("messages")) { - const messages = db.createObjectStore("messages", { keyPath: ["account", "serverId", "serverIdBy", "localId"] }); + const messages = db.createObjectStore("messages", { + keyPath: ["account", "serverId", "serverIdBy", "localId"], + }); messages.createIndex("localId", ["account", "localId", "chatId"]); } if (!db.objectStoreNames.contains("keyvaluepairs")) { @@ -157,49 +185,89 @@ export default async (dbname, media, tokenize, stemmer) => { db.createObjectStore("chats", { keyPath: ["account", "chatId"] }); } if (!db.objectStoreNames.contains("members")) { - const members = db.createObjectStore("members", { keyPath: ["account", "id"] }); + const members = db.createObjectStore("members", { + keyPath: ["account", "id"], + }); } if (tx.objectStore("members").indexNames.contains("chats")) { tx.objectStore("members").deleteIndex("chats"); } - if (!tx.objectStore("members").indexNames.contains("chatsWithTrueJid")) { - tx.objectStore("members").createIndex("chatsWithTrueJid", ["account", "chatId", "isSelf", "chat"]); + if ( + !tx.objectStore("members").indexNames.contains("chatsWithTrueJid") + ) { + tx.objectStore("members").createIndex("chatsWithTrueJid", [ + "account", + "chatId", + "isSelf", + "chat", + ]); } if (!db.objectStoreNames.contains("services")) { - db.createObjectStore("services", { keyPath: ["account", "serviceId"] }); + db.createObjectStore("services", { + keyPath: ["account", "serviceId"], + }); } if (!db.objectStoreNames.contains("reactions")) { - const reactions = db.createObjectStore("reactions", { keyPath: ["account", "chatId", "senderId", "updateId"] }); - reactions.createIndex("senders", ["account", "chatId", "messageId", "senderId", "timestamp"]); + const reactions = db.createObjectStore("reactions", { + keyPath: ["account", "chatId", "senderId", "updateId"], + }); + reactions.createIndex("senders", [ + "account", + "chatId", + "messageId", + "senderId", + "timestamp", + ]); } if (!db.objectStoreNames.contains("omemo_identities")) { - db.createObjectStore("omemo_identities", { keyPath: ["account", "address"] }); + db.createObjectStore("omemo_identities", { + keyPath: ["account", "address"], + }); } if (!db.objectStoreNames.contains("omemo_prekeys")) { - db.createObjectStore("omemo_prekeys", { keyPath: ["account", "keyId"] }); + db.createObjectStore("omemo_prekeys", { + keyPath: ["account", "keyId"], + }); } if (!db.objectStoreNames.contains("omemo_sessions")) { - db.createObjectStore("omemo_sessions", { keyPath: ["account", "address"] }); + db.createObjectStore("omemo_sessions", { + keyPath: ["account", "address"], + }); } if (!db.objectStoreNames.contains("omemo_sessions_meta")) { - db.createObjectStore("omemo_sessions_meta", { keyPath: ["account", "address"] }); + db.createObjectStore("omemo_sessions_meta", { + keyPath: ["account", "address"], + }); } const messagesIndexNames = tx.objectStore("messages").indexNames; if (!messagesIndexNames.contains("chatsBySortId")) { - tx.objectStore("messages").createIndex("chatsBySortId", ["account", "chatId", "sortId"]); + tx.objectStore("messages").createIndex("chatsBySortId", [ + "account", + "chatId", + "sortId", + ]); } if (!messagesIndexNames.contains("accountsBySortId")) { - tx.objectStore("messages").createIndex("accountsBySortId", ["account", "sortId"]); + tx.objectStore("messages").createIndex("accountsBySortId", [ + "account", + "sortId", + ]); } if (!messagesIndexNames.contains("terms")) { - tx.objectStore("messages").createIndex("terms", "terms", { multiEntry: true }); + tx.objectStore("messages").createIndex("terms", "terms", { + multiEntry: true, + }); } if (messagesIndexNames.contains("accounts")) { tx.objectStore("messages").deleteIndex("accounts"); } if (!messagesIndexNames.contains("chats")) { - tx.objectStore("messages").createIndex("chats", ["account", "chatId", "timestamp"]); + tx.objectStore("messages").createIndex("chats", [ + "account", + "chatId", + "timestamp", + ]); } }; dbOpenReq.onsuccess = (event) => { @@ -215,25 +283,35 @@ export default async (dbname, media, tokenize, stemmer) => { "reactions", "services", ]; - for(const storeName of storeNames) { - if(!db.objectStoreNames.contains(storeName)) { + for (const storeName of storeNames) { + if (!db.objectStoreNames.contains(storeName)) { db.close(); openDb(db.version + 1).then(resolve, reject); return; } } - const tx = db.transaction(["messages", "members", "keyvaluepairs"], "readonly"); + const tx = db.transaction( + ["messages", "members", "keyvaluepairs"], + "readonly", + ); const messagesIndexNames = tx.objectStore("messages").indexNames; - const wantIndexNames = ["chatsBySortId", "accountsBySortId", "terms", "chats"]; - for(const indexName of wantIndexNames) { - if(!messagesIndexNames.contains(indexName)) { + const wantIndexNames = [ + "chatsBySortId", + "accountsBySortId", + "terms", + "chats", + ]; + for (const indexName of wantIndexNames) { + if (!messagesIndexNames.contains(indexName)) { db.close(); openDb(db.version + 1).then(resolve, reject); return; } } - if (!tx.objectStore("members").indexNames.contains("chatsWithTrueJid")) { + if ( + !tx.objectStore("members").indexNames.contains("chatsWithTrueJid") + ) { db.close(); openDb(db.version + 1).then(resolve, reject); return; @@ -241,14 +319,27 @@ export default async (dbname, media, tokenize, stemmer) => { (async () => { const kv = tx.objectStore("keyvaluepairs"); - const ranMigrationAddSortIdAndTerms = await promisifyRequest(kv.get("__migrationAddSortIdAndTerms")); - if (!ranMigrationAddSortIdAndTerms && messagesIndexNames.contains("accounts")) { - const keys = await promisifyRequest(kv.getAllKeys(IDBKeyRange.bound("login:clientId:", "login:clientId:\uffff"))); - const accountIds = keys.map(k => k.substring(15)); + const ranMigrationAddSortIdAndTerms = await promisifyRequest( + kv.get("__migrationAddSortIdAndTerms"), + ); + if ( + !ranMigrationAddSortIdAndTerms && + messagesIndexNames.contains("accounts") + ) { + const keys = await promisifyRequest( + kv.getAllKeys( + IDBKeyRange.bound("login:clientId:", "login:clientId:\uffff"), + ), + ); + const accountIds = keys.map((k) => k.substring(15)); await migrationAddSortIdAndTerms(db, accountIds); const writeKV = db.transaction(["keyvaluepairs"], "readwrite"); - await promisifyRequest(writeKV.objectStore("keyvaluepairs").put(new Date(), "__migrationAddSortIdAndTerms")); + await promisifyRequest( + writeKV + .objectStore("keyvaluepairs") + .put(new Date(), "__migrationAddSortIdAndTerms"), + ); } if (messagesIndexNames.contains("accounts")) { @@ -279,15 +370,27 @@ export default async (dbname, media, tokenize, stemmer) => { raw.displayName, raw.photoUri, raw.isSelf ? true : false, - raw.roles.map(role => new borogove_Role(role.id, role.title)), + raw.roles.map((role) => new borogove_Role(role.id, role.title)), raw.jid instanceof borogove_JID ? raw.jid : borogove_JID.parse(raw.jid), - new Map((raw.presence?.entries() ?? []).map(([k, p]) => [k, p instanceof borogove_Stanza ? p : borogove_Stanza.parse(p)])), - raw.chat ? new borogove_AvailableChat(raw.chat, raw.displayName, raw.chat + (chat ? " (via " + chat.getDisplayName() + ")" : ""), new borogove_Caps("", [], [], [])) : null + new Map( + (raw.presence?.entries() ?? []).map(([k, p]) => [ + k, + p instanceof borogove_Stanza ? p : borogove_Stanza.parse(p), + ]), + ), + raw.chat + ? new borogove_AvailableChat( + raw.chat, + raw.displayName, + raw.chat + (chat ? " (via " + chat.getDisplayName() + ")" : ""), + new borogove_Caps("", [], [], []), + ) + : null, ); } function hydrateStringReaction(r, senderId, timestamp) { - if (r.startsWith("ni://")){ + if (r.startsWith("ni://")) { return new borogove_CustomEmojiReaction(senderId, timestamp, "", r); } else { return new borogove_Reaction(senderId, timestamp, r); @@ -296,22 +399,45 @@ export default async (dbname, media, tokenize, stemmer) => { function hydrateObjectReaction(r) { if (r.uri) { - return new borogove_CustomEmojiReaction(r.senderId, r.timestamp, r.text, r.uri, r.envelopeId); + return new borogove_CustomEmojiReaction( + r.senderId, + r.timestamp, + r.text, + r.uri, + r.envelopeId, + ); } else { - return new borogove_Reaction(r.senderId, r.timestamp, r.text, r.envelopeId, r.key); + return new borogove_Reaction( + r.senderId, + r.timestamp, + r.text, + r.envelopeId, + r.key, + ); } } function hydrateReactionsArray(reacts, senderId, timestamp) { if (!reacts) return reacts; - return reacts.map(r => typeof r === "string" ? hydrateStringReaction(r, senderId, timestamp) : hydrateObjectReaction(r)); + return reacts.map((r) => + typeof r === "string" + ? hydrateStringReaction(r, senderId, timestamp) + : hydrateObjectReaction(r), + ); } function hydrateReactions(map, timestamp) { if (!map) return new Map(); const newMap = new Map(); for (const [k, reacts] of map) { - newMap.set(k, reacts.map(reactOrSender => typeof reactOrSender === "string" ? hydrateStringReaction(k, reactOrSender, timestamp) : hydrateObjectReaction(reactOrSender))); + newMap.set( + k, + reacts.map((reactOrSender) => + typeof reactOrSender === "string" + ? hydrateStringReaction(k, reactOrSender, timestamp) + : hydrateObjectReaction(reactOrSender), + ), + ); } return newMap; } @@ -334,15 +460,30 @@ export default async (dbname, media, tokenize, stemmer) => { message.sender = value.sender && borogove_JID.parse(value.sender); message.senderId = value.senderId; message.recipients = value.recipients.map((r) => borogove_JID.parse(r)); - message.to = value.to ? borogove_JID.parse(value.to) : message.recipients[0]; + message.to = value.to + ? borogove_JID.parse(value.to) + : message.recipients[0]; message.replyTo = value.replyTo.map((r) => borogove_JID.parse(r)); message.threadId = value.threadId; - message.attachments = (value.attachments ?? []).map(a => new borogove_ChatAttachment(a.name, a.mime, a.size, a.uris, (a.hashes ?? []).map(h => new borogove_Hash(h.algorithm, h.hash)))); + message.attachments = (value.attachments ?? []).map( + (a) => + new borogove_ChatAttachment( + a.name, + a.mime, + a.size, + a.uris, + (a.hashes ?? []).map((h) => new borogove_Hash(h.algorithm, h.hash)), + ), + ); message.linkMetadata = value.linkMetadata ?? []; message.reactions = hydrateReactions(value.reactions, message.timestamp); message.text = value.text; message.lang = value.lang; - message.type = value.type || (value.isGroupchat || value.groupchat ? enums.borogove_MessageType.Channel : enums.borogove_MessageType.Chat); + message.type = + value.type || + (value.isGroupchat || value.groupchat + ? enums.borogove_MessageType.Channel + : enums.borogove_MessageType.Chat); message.stanza = value.stanza && borogove_Stanza.parse(value.stanza); if (message.stanza) { // Just use children from stanza instead of parsing again @@ -363,19 +504,34 @@ export default async (dbname, media, tokenize, stemmer) => { const tx = db.transaction(["messages"], "readonly"); store = tx.objectStore("messages"); } - if (value.replyToMessage && !value.replyToMessage[2]) value.replyToMessage[2] = value.serverIdBy ?? value.chatId; - const range = value.replyToMessage && value.replyToMessage[1] !== message.serverId && value.replyToMessage[3] !== message.localId && (!value.replyToMessage[3] ? - IDBKeyRange.bound(value.replyToMessage.slice(0, 3), [...value.replyToMessage.slice(0, 3), []]) - : IDBKeyRange.only(value.replyToMessage)); - const replyToMessage = range && await hydrateMessage((await promisifyRequest(store.openCursor(range)))?.value, store); + if (value.replyToMessage && !value.replyToMessage[2]) + value.replyToMessage[2] = value.serverIdBy ?? value.chatId; + const range = + value.replyToMessage && + value.replyToMessage[1] !== message.serverId && + value.replyToMessage[3] !== message.localId && + (!value.replyToMessage[3] + ? IDBKeyRange.bound(value.replyToMessage.slice(0, 3), [ + ...value.replyToMessage.slice(0, 3), + [], + ]) + : IDBKeyRange.only(value.replyToMessage)); + const replyToMessage = + range && + (await hydrateMessage( + (await promisifyRequest(store.openCursor(range)))?.value, + store, + )); message.replyToMessage = replyToMessage; - message.versions = await Promise.all((value.versions || []).map(v => { - v.versions = []; // No need for nested versions... - return hydrateMessage(v, store); - })); + message.versions = await Promise.all( + (value.versions || []).map((v) => { + v.versions = []; // No need for nested versions... + return hydrateMessage(v, store); + }), + ); - await Promise.all(message.attachments.map(a => a.lookup(obj))); + await Promise.all(message.attachments.map((a) => a.lookup(obj))); return message; } @@ -395,21 +551,43 @@ export default async (dbname, media, tokenize, stemmer) => { recipients: message.recipients.map((r) => r.asString()), replyTo: message.replyTo.map((r) => r.asString()), timestamp: new Date(message.timestamp), - replyToMessage: message.replyToMessage && [account, message.replyToMessage.serverId || "", message.replyToMessage.serverIdBy || "", message.replyToMessage.localId || ""], + replyToMessage: message.replyToMessage && [ + account, + message.replyToMessage.serverId || "", + message.replyToMessage.serverIdBy || "", + message.replyToMessage.localId || "", + ], versions: message.versions.map((m) => serializeMessage(account, m)), - payloads: message.stanza ? undefined : message.payloads.map((p) => p.toString()), + payloads: message.stanza + ? undefined + : message.payloads.map((p) => p.toString()), stanza: message.stanza?.toString(), - terms: [...new Set(tokenize((message.text || "").replace(/^>.*/mg, "")).map(stemmer))].sort() - } + terms: [ + ...new Set( + tokenize((message.text || "").replace(/^>.*/gm, "")).map(stemmer), + ), + ].sort(), + }; } function correctMessage(account, message, result) { // Newest (by timestamp) version wins for head - const newVersions = message.versions.length < 1 ? [message] : message.versions; + const newVersions = + message.versions.length < 1 ? [message] : message.versions; const storedVersions = result.value.versions || []; // TODO: dedupe? There shouldn't be dupes... - const versions = (storedVersions.length < 1 ? [result.value] : storedVersions).concat(newVersions.filter(nv => !storedVersions.find(sv => nv.serverId === sv.serverId)).map((nv) => serializeMessage(account, nv))).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); - const head = {...versions[0]}; + const versions = ( + storedVersions.length < 1 ? [result.value] : storedVersions + ) + .concat( + newVersions + .filter( + (nv) => !storedVersions.find((sv) => nv.serverId === sv.serverId), + ) + .map((nv) => serializeMessage(account, nv)), + ) + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + const head = { ...versions[0] }; // Can't change primary key head.serverIdBy = result.value.serverIdBy; head.serverId = result.value.serverId; @@ -448,17 +626,32 @@ export default async (dbname, media, tokenize, stemmer) => { } } for (const reaction of reactions) { - reactionsMap.set(reaction.key, [...reactionsMap.get(reaction.key) || [], reaction]); + reactionsMap.set(reaction.key, [ + ...(reactionsMap.get(reaction.key) || []), + reaction, + ]); } return reactionsMap; } async function chatPresenceAndMembersForName(account, store, rawChat) { if (rawChat.class == "DirectChat") { - return [new Map(((await promisifyRequest(store.get([account, rawChat.chatId])))?.presence?.entries() ?? []).map(([k, p]) => [k, borogove_Stanza.parse(p)])), null]; + return [ + new Map( + ( + ( + await promisifyRequest(store.get([account, rawChat.chatId])) + )?.presence?.entries() ?? [] + ).map(([k, p]) => [k, borogove_Stanza.parse(p)]), + ), + null, + ]; } - const range = IDBKeyRange.bound([account, rawChat.chatId], [account, rawChat.chatId, []]); + const range = IDBKeyRange.bound( + [account, rawChat.chatId], + [account, rawChat.chatId, []], + ); const cursor = store.index("chatsWithTrueJid").openCursor(range, "prev"); let membersForName = []; let presence = new Map(); @@ -467,9 +660,20 @@ export default async (dbname, media, tokenize, stemmer) => { if (!cresult?.value) break; if (cresult.value.isSelf) { - presence = new Map((cresult.value.presence?.entries() ?? []).map(([k, p]) => [k, borogove_Stanza.parse(p)])); - } else if (!cresult.value.roles.find(r => ["none", "outcast"].includes(r.id)) && cresult.value.id !== rawChat.chatId) { - membersForName.push({ id: cresult.value.id, displayName: cresult.value.displayName }); + presence = new Map( + (cresult.value.presence?.entries() ?? []).map(([k, p]) => [ + k, + borogove_Stanza.parse(p), + ]), + ); + } else if ( + !cresult.value.roles.find((r) => ["none", "outcast"].includes(r.id)) && + cresult.value.id !== rawChat.chatId + ) { + membersForName.push({ + id: cresult.value.id, + displayName: cresult.value.displayName, + }); } // In big rooms we don't make a name from the members @@ -483,27 +687,34 @@ export default async (dbname, media, tokenize, stemmer) => { membersForName?.sort((a, b) => a.displayName.localeCompare(b.displayName)); return [presence, membersForName]; - }; + } const obj = { - syncPoint: async function(account, chatId) { + syncPoint: async function (account, chatId) { const tx = db.transaction(["messages"], "readonly"); const store = tx.objectStore("messages"); var cursor = null; if (chatId === null) { - cursor = store.index("accountsBySortId").openCursor( - IDBKeyRange.bound([account], [account, []]), - "prev" - ); + cursor = store + .index("accountsBySortId") + .openCursor(IDBKeyRange.bound([account], [account, []]), "prev"); } else { - cursor = store.index("chatsBySortId").openCursor( - IDBKeyRange.bound([account, chatId], [account, chatId, []]), - "prev" - ); + cursor = store + .index("chatsBySortId") + .openCursor( + IDBKeyRange.bound([account, chatId], [account, chatId, []]), + "prev", + ); } while (true) { const result = await promisifyRequest(cursor); - if (!result || (result.value.syncPoint && result.value.serverId && ((chatId && result.value.serverIdBy == chatId) || result.value.serverIdBy === account))) { + if ( + !result || + (result.value.syncPoint && + result.value.serverId && + ((chatId && result.value.serverIdBy == chatId) || + result.value.serverIdBy === account)) + ) { if (!result?.value) return null; return await hydrateMessage(result.value); @@ -513,7 +724,7 @@ export default async (dbname, media, tokenize, stemmer) => { } }, - storeChats: function(account, chats) { + storeChats: function (account, chats) { const tx = db.transaction(["chats"], "readwrite"); const store = tx.objectStore("chats"); @@ -528,70 +739,114 @@ export default async (dbname, media, tokenize, stemmer) => { displayName: chat.displayName, uiState: chat.uiState, isBlocked: chat.isBlocked, - extensions: chat.extensions && chat.extensions.children.length < 1 ? null : chat.extensions?.toString(), + extensions: + chat.extensions && chat.extensions.children.length < 1 + ? null + : chat.extensions?.toString(), readUpToId: chat.readUpToId, readUpToBy: chat.readUpToBy, - notificationSettings: chat.notificationsFiltered() ? { mention: chat.notifyMention(), reply: chat.notifyReply() } : null, + notificationSettings: chat.notificationsFiltered() + ? { mention: chat.notifyMention(), reply: chat.notifyReply() } + : null, threads: chat.threads, mavUntil: chat.mavUntil, - disco: { ...chat.disco, data: chat.disco?.data?.map(d => d.toString()) }, + disco: { + ...chat.disco, + data: chat.disco?.data?.map((d) => d.toString()), + }, omemoDevices: chat.omemoContactDeviceIDs, - class: chat instanceof borogove_DirectChat ? "DirectChat" : (chat instanceof borogove_Channel ? "Channel" : "Chat") + class: + chat instanceof borogove_DirectChat + ? "DirectChat" + : chat instanceof borogove_Channel + ? "Channel" + : "Chat", }); } }, - getChats: async function(account) { + getChats: async function (account) { const tx = db.transaction(["chats", "members"], "readonly"); const store = tx.objectStore("chats"); const membersStore = tx.objectStore("members"); const range = IDBKeyRange.bound([account], [account, []]); const result = await promisifyRequest(store.getAll(range)); - return await Promise.all(result.map(async (r) => new borogove_SerializedChat( - r.chatId, - r.trusted, - r.isBookmarked, - r.avatarSha1, - ...await chatPresenceAndMembersForName(account, membersStore, r), - r.displayName, - r.uiState, - r.isBlocked, - new borogove_Status(r.status?.emoji ?? "", r.status?.text ?? ""), - r.extensions ? borogove_Stanza.parse(r.extensions) : null, - r.readUpToId, - r.readUpToBy, - r.notificationSettings === undefined ? null : r.notificationSettings != null, - r.notificationSettings?.mention, - r.notificationSettings?.reply, - r.threads || new Map(), - r.disco ? new borogove_Caps( - r.disco.node, - (r.disco.identities || []).map((identity) => new borogove_Identity(identity.category, identity.type, identity.name)), - r.disco.features || [], - (r.disco.data || []).map(s => borogove_Stanza.parse(s)) - ) : null, - r.mavUntil, - r.omemoDevices || [], - r.class - ))); + return await Promise.all( + result.map( + async (r) => + new borogove_SerializedChat( + r.chatId, + r.trusted, + r.isBookmarked, + r.avatarSha1, + ...(await chatPresenceAndMembersForName( + account, + membersStore, + r, + )), + r.displayName, + r.uiState, + r.isBlocked, + new borogove_Status(r.status?.emoji ?? "", r.status?.text ?? ""), + r.extensions ? borogove_Stanza.parse(r.extensions) : null, + r.readUpToId, + r.readUpToBy, + r.notificationSettings === undefined + ? null + : r.notificationSettings != null, + r.notificationSettings?.mention, + r.notificationSettings?.reply, + r.threads || new Map(), + r.disco + ? new borogove_Caps( + r.disco.node, + (r.disco.identities || []).map( + (identity) => + new borogove_Identity( + identity.category, + identity.type, + identity.name, + ), + ), + r.disco.features || [], + (r.disco.data || []).map((s) => borogove_Stanza.parse(s)), + ) + : null, + r.mavUntil, + r.omemoDevices || [], + r.class, + ), + ), + ); }, async storeMembers(account, chatId, members) { const tx = db.transaction(["members"], "readwrite"); const store = tx.objectStore("members"); - await Promise.all(members.map(member => promisifyRequest(store.put({ - account, - chatId, - id: member.id, - displayName: member.displayName, - photoUri: member.photoUri, - isSelf: member.isSelf ? 1 : 0, // Can't index on boolean - chat: member.chat?.chatId ?? "", - roles: member.roles, - presence: new Map([...member.presence.entries()].map(([k, p]) => [k, p.toString()])), - jid: member.jid?.asString(), - })))); + await Promise.all( + members.map((member) => + promisifyRequest( + store.put({ + account, + chatId, + id: member.id, + displayName: member.displayName, + photoUri: member.photoUri, + isSelf: member.isSelf ? 1 : 0, // Can't index on boolean + chat: member.chat?.chatId ?? "", + roles: member.roles, + presence: new Map( + [...member.presence.entries()].map(([k, p]) => [ + k, + p.toString(), + ]), + ), + jid: member.jid?.asString(), + }), + ), + ), + ); return true; }, @@ -601,24 +856,42 @@ export default async (dbname, media, tokenize, stemmer) => { const store = tx.objectStore("members"); const updatesFor = new Set(); - const pseudoMembers = await Promise.all(updates.map(async (update) => { - let member = null; - if (update.id) { - member = await promisifyRequest(store.get([account, update.id])); - } - if (update.jid && !member) { - member = await promisifyRequest(store.index("chatsWithTrueJid").get([account, chat.chatId, update.isSelf ? 1 : 0, update.jid.asString()])); - } - if (member?.id || update.id) updatesFor.add(update.id ?? member?.id); - return update.applyTo(member ? hydrateMember(chat, member) : null); - })); + const pseudoMembers = await Promise.all( + updates.map(async (update) => { + let member = null; + if (update.id) { + member = await promisifyRequest(store.get([account, update.id])); + } + if (update.jid && !member) { + member = await promisifyRequest( + store + .index("chatsWithTrueJid") + .get([ + account, + chat.chatId, + update.isSelf ? 1 : 0, + update.jid.asString(), + ]), + ); + } + if (member?.id || update.id) updatesFor.add(update.id ?? member?.id); + return update.applyTo(member ? hydrateMember(chat, member) : null); + }), + ); - await this.storeMembers(account, chat.chatId, pseudoMembers.filter(m => m?.id)); + await this.storeMembers( + account, + chat.chatId, + pseudoMembers.filter((m) => m?.id), + ); if (isFullList) { const txW = db.transaction(["members"], "readwrite"); const storeW = txW.objectStore("members"); - const range = IDBKeyRange.bound([account, chat.chatId], [account, chat.chatId, []]); + const range = IDBKeyRange.bound( + [account, chat.chatId], + [account, chat.chatId, []], + ); const cursor = storeW.index("chatsWithTrueJid").openCursor(range); while (true) { const cresult = await promisifyRequest(cursor); @@ -634,13 +907,18 @@ export default async (dbname, media, tokenize, stemmer) => { } } - return pseudoMembers.filter(m => m?.id && m?.displayName && m?.jid).map(m => hydrateMember(chat, {...m, chat: m.chat?.chatId })); + return pseudoMembers + .filter((m) => m?.id && m?.displayName && m?.jid) + .map((m) => hydrateMember(chat, { ...m, chat: m.chat?.chatId })); }, async clearMemberPresence(account, chatId) { const tx = db.transaction(["members"], "readwrite"); const store = tx.objectStore("members"); - const range = IDBKeyRange.bound(chatId ? [account, chatId] : [account], chatId ? [account, chatId, []] : [account, []]); + const range = IDBKeyRange.bound( + chatId ? [account, chatId] : [account], + chatId ? [account, chatId, []] : [account, []], + ); const cursor = store.index("chatsWithTrueJid").openCursor(range); await new Promise((resolve, reject) => { cursor.onerror = () => reject(cursor.error); @@ -668,24 +946,32 @@ export default async (dbname, media, tokenize, stemmer) => { const roleSort = { owner: 4, admin: 3, none: 1, outcast: 0 }; const tx = db.transaction(["members"], "readonly"); const store = tx.objectStore("members"); - const range = IDBKeyRange.bound([account, chat.chatId], [account, chat.chatId, []]); + const range = IDBKeyRange.bound( + [account, chat.chatId], + [account, chat.chatId, []], + ); // getAll is much faster than openCursor, // but if the room has more than 20k members this will miss people // at which point we need paging - const allMembers = await promisifyRequest(store.index("chatsWithTrueJid").getAll(range, 20000)); + const allMembers = await promisifyRequest( + store.index("chatsWithTrueJid").getAll(range, 20000), + ); let clearedOffline = false; let result = []; let member = null; while ((member = allMembers.pop())) { if (!member.id || !member.displayName || !member.jid) continue; - if (!forModerator && member.roles.find(r => r.id == "outcast")) continue; + if (!forModerator && member.roles.find((r) => r.id == "outcast")) + continue; const presenceKey = member.presence.keys().next()?.value; - const isOnline = presenceKey && !member.presence.get(presenceKey).includes('type="unavailable"'); - if (member.roles.find(r => r.id == "none") && !isOnline) continue; + const isOnline = + presenceKey && + !member.presence.get(presenceKey).includes('type="unavailable"'); + if (member.roles.find((r) => r.id == "none") && !isOnline) continue; if (!forModerator && !clearedOffline && result.length >= 1000) { - result = result.filter(m => m.__isOnline); + result = result.filter((m) => m.__isOnline); clearedOffline = true; } @@ -693,8 +979,17 @@ export default async (dbname, media, tokenize, stemmer) => { const hydrated = hydrateMember(chat, member); hydrated.__isOnline = isOnline; - hydrated.__roleSort = hydrated.roles.length < 1 ? 2 : Math.max(...hydrated.roles.map(r => roleSort[r.id] ?? 2)); - hydrated.__sortKey = hydrated.roles.map(r => r.title).sort().join(" ") + " " + hydrated.displayName; + hydrated.__roleSort = + hydrated.roles.length < 1 + ? 2 + : Math.max(...hydrated.roles.map((r) => roleSort[r.id] ?? 2)); + hydrated.__sortKey = + hydrated.roles + .map((r) => r.title) + .sort() + .join(" ") + + " " + + hydrated.displayName; result.push(hydrated); if (!forModerator && result.length >= 2000) break; @@ -714,29 +1009,43 @@ export default async (dbname, media, tokenize, stemmer) => { async getMemberDetails(account, chat, ids) { const tx = db.transaction(["members"], "readonly"); const store = tx.objectStore("members"); - return await Promise.all(ids.map(async (id) => { - const raw = await promisifyRequest(store.get([account, id])); - if (!raw?.id || !raw?.displayName || !raw?.jid) return null; + return await Promise.all( + ids.map(async (id) => { + const raw = await promisifyRequest(store.get([account, id])); + if (!raw?.id || !raw?.displayName || !raw?.jid) return null; - return hydrateMember(chat, raw); - })); + return hydrateMember(chat, raw); + }), + ); }, async storeVoiceRequest(account, chat, jid, requesting) { - await this.set(`voiceRequest:${account}\n${chat.chatId}\n${jid}`, requesting ? true : undefined); + await this.set( + `voiceRequest:${account}\n${chat.chatId}\n${jid}`, + requesting ? true : undefined, + ); return true; }, async listVoiceRequests(account, chat) { const tx = db.transaction(["keyvaluepairs", "members"], "readonly"); const kvStore = tx.objectStore("keyvaluepairs"); - const keys = await promisifyRequest(kvStore.getAllKeys(IDBKeyRange.bound(`voiceRequest:${account}\n${chat.chatId}\n`, `voiceRequest:${account}\n${chat.chatId}\n\uffff`))); - const jids = keys.map(k => k.split(/\n/)[2]); + const keys = await promisifyRequest( + kvStore.getAllKeys( + IDBKeyRange.bound( + `voiceRequest:${account}\n${chat.chatId}\n`, + `voiceRequest:${account}\n${chat.chatId}\n\uffff`, + ), + ), + ); + const jids = keys.map((k) => k.split(/\n/)[2]); const result = []; const store = tx.objectStore("members"); for (const jid of jids) { - const raw = await promisifyRequest(store.index("chatsWithTrueJid").get([account, chat.chatId, 0, jid])); + const raw = await promisifyRequest( + store.index("chatsWithTrueJid").get([account, chat.chatId, 0, jid]), + ); if (!raw?.id || !raw?.displayName || !raw?.jid) continue; result.push(hydrateMember(chat, raw)); @@ -745,14 +1054,16 @@ export default async (dbname, media, tokenize, stemmer) => { return result; }, - getChatUnreadDetails: async function(account, chat) { + getChatUnreadDetails: async function (account, chat) { const tx = db.transaction(["messages"], "readonly"); const store = tx.objectStore("messages"); - const cursor = store.index("chatsBySortId").openCursor( - IDBKeyRange.bound([account, chat.chatId], [account, chat.chatId, []]), - "prev" - ); + const cursor = store + .index("chatsBySortId") + .openCursor( + IDBKeyRange.bound([account, chat.chatId], [account, chat.chatId, []]), + "prev", + ); let rowCount = 0; let unreadCount = 0; let lastMessage = null; @@ -763,7 +1074,10 @@ export default async (dbname, media, tokenize, stemmer) => { rowCount++; const value = cresult.value; if (!lastMessage) lastMessage = hydrateMessage(value); - if (chat.readUpToId === value.serverId || value.direction == enums.borogove_MessageDirection.MessageSent) { + if ( + chat.readUpToId === value.serverId || + value.direction == enums.borogove_MessageDirection.MessageSent + ) { break; } else { unreadCount++; @@ -775,14 +1089,22 @@ export default async (dbname, media, tokenize, stemmer) => { return { message, unreadCount }; }, - getChatsUnreadDetails: function(account, chatsArray) { - return Promise.all(chatsArray.map(async chat => { - const details = await this.getChatUnreadDetails(account, chat); - return { chatId: chat.chatId, ...details }; - })); + getChatsUnreadDetails: function (account, chatsArray) { + return Promise.all( + chatsArray.map(async (chat) => { + const details = await this.getChatUnreadDetails(account, chat); + return { chatId: chat.chatId, ...details }; + }), + ); }, - getMessage: async function(account, chatId, serverId, localId, store = null) { + getMessage: async function ( + account, + chatId, + serverId, + localId, + store = null, + ) { if (!serverId && !localId) throw "Can't getMessage by no id"; if (!store) { @@ -791,7 +1113,9 @@ export default async (dbname, media, tokenize, stemmer) => { } let result = null; if (serverId) { - const cursor = store.openCursor(IDBKeyRange.bound([account, serverId], [account, serverId, []])); + const cursor = store.openCursor( + IDBKeyRange.bound([account, serverId], [account, serverId, []]), + ); while (true) { const cresult = await promisifyRequest(cursor); if (!cresult) break; @@ -802,7 +1126,11 @@ export default async (dbname, media, tokenize, stemmer) => { cresult.continue(); } } else { - result = await promisifyRequest(store.index("localId").openCursor(IDBKeyRange.only([account, localId, chatId]))); + result = await promisifyRequest( + store + .index("localId") + .openCursor(IDBKeyRange.only([account, localId, chatId])), + ); } if (!result || !result.value) return null; const message = result.value; @@ -817,18 +1145,67 @@ export default async (dbname, media, tokenize, stemmer) => { const reactionStore = tx.objectStore("reactions"); let result; if (update.serverId) { - result = await promisifyRequest(store.openCursor(IDBKeyRange.bound([account, update.serverId, update.serverIdBy], [account, update.serverId, update.serverIdBy, []]))); + result = await promisifyRequest( + store.openCursor( + IDBKeyRange.bound( + [account, update.serverId, update.serverIdBy], + [account, update.serverId, update.serverIdBy, []], + ), + ), + ); } else { - result = await promisifyRequest(store.index("localId").openCursor(IDBKeyRange.only([account, update.localId, update.chatId]))); + result = await promisifyRequest( + store + .index("localId") + .openCursor( + IDBKeyRange.only([account, update.localId, update.chatId]), + ), + ); } - const lastFromSender = await promisifyRequest(reactionStore.index("senders").openCursor(IDBKeyRange.bound( - [account, update.chatId, update.serverId || update.localId, update.senderId], - [account, update.chatId, update.serverId || update.localId, update.senderId, []] - ), "prev")); - const reactions = update.getReactions(hydrateReactionsArray(lastFromSender?.value?.reactions)); - await promisifyRequest(reactionStore.put({...update, reactions: reactions, append: (update.kind === enums.borogove_ReactionUpdateKind.AppendReactions ? update.reactions : null), messageId: update.serverId || update.localId, timestamp: new Date(update.timestamp), account: account})); + const lastFromSender = await promisifyRequest( + reactionStore + .index("senders") + .openCursor( + IDBKeyRange.bound( + [ + account, + update.chatId, + update.serverId || update.localId, + update.senderId, + ], + [ + account, + update.chatId, + update.serverId || update.localId, + update.senderId, + [], + ], + ), + "prev", + ), + ); + const reactions = update.getReactions( + hydrateReactionsArray(lastFromSender?.value?.reactions), + ); + await promisifyRequest( + reactionStore.put({ + ...update, + reactions: reactions, + append: + update.kind === enums.borogove_ReactionUpdateKind.AppendReactions + ? update.reactions + : null, + messageId: update.serverId || update.localId, + timestamp: new Date(update.timestamp), + account: account, + }), + ); if (!result || !result.value) return null; - if (lastFromSender?.value && lastFromSender.value.timestamp > new Date(update.timestamp)) return; + if ( + lastFromSender?.value && + lastFromSender.value.timestamp > new Date(update.timestamp) + ) + return; const message = result.value; setReactions(message.reactions, update.senderId, reactions); store.put(message); @@ -839,7 +1216,7 @@ export default async (dbname, media, tokenize, stemmer) => { const tx = db.transaction(["messages", "reactions"], "readwrite"); const store = tx.objectStore("messages"); const promises = []; -tx.onerror = console.error; + tx.onerror = console.error; for (const [index, m] of messages.entries()) { const isLast = index + 1 >= messages.length; promises.push(this.storeMessage(tx, store, account, m, isLast)); @@ -851,37 +1228,120 @@ tx.onerror = console.error; async storeMessage(tx, store, account, message, wait) { if (!message.chatId()) throw "Cannot store a message with no chatId"; if (!message.sortId) throw "Cannot store a message with no sortId"; - if (!message.serverId && !message.localId) throw "Cannot store a message with no id"; - if (!message.serverId && message.isIncoming()) throw "Cannot store an incoming message with no server id"; - if (message.serverId && !message.serverIdBy) throw "Cannot store a message with a server id and no by"; + if (!message.serverId && !message.localId) + throw "Cannot store a message with no id"; + if (!message.serverId && message.isIncoming()) + throw "Cannot store an incoming message with no server id"; + if (message.serverId && !message.serverIdBy) + throw "Cannot store a message with a server id and no by"; // Hydrate reply stubs - const replyToMessage = message.replyToMessage && !message.replyToMessage.stanza ? - await this.getMessage(account, message.chatId(), message.replyToMessage.serverId, message.replyToMessage.localId, store) : - message.replyToMessage; + const replyToMessage = + message.replyToMessage && !message.replyToMessage.stanza + ? await this.getMessage( + account, + message.chatId(), + message.replyToMessage.serverId, + message.replyToMessage.localId, + store, + ) + : message.replyToMessage; message.replyToMessage = replyToMessage ?? message.replyToMessage; - const result = await promisifyRequest(store.index("localId").openCursor(IDBKeyRange.only([account, message.localId || [], message.chatId()]))); - const reactionResult = await promisifyRequest(tx.objectStore("reactions").openCursor(IDBKeyRange.only([account, message.chatId(), message.senderId, message.localId || ""]))); + const result = await promisifyRequest( + store + .index("localId") + .openCursor( + IDBKeyRange.only([ + account, + message.localId || [], + message.chatId(), + ]), + ), + ); + const reactionResult = await promisifyRequest( + tx + .objectStore("reactions") + .openCursor( + IDBKeyRange.only([ + account, + message.chatId(), + message.senderId, + message.localId || "", + ]), + ), + ); if (reactionResult?.value?.append && message.html().trim() == "") { - const reactToMesssage = await this.getMessage(account, message.chatId(), reactionResult.value.serverId, reactionResult.value.localId, store); - const previouslyAppended = hydrateReactionsArray(reactionResult.value.append, reactionResult.value.senderId, reactionResult.value.timestamp).map(r => r.key); + const reactToMesssage = await this.getMessage( + account, + message.chatId(), + reactionResult.value.serverId, + reactionResult.value.localId, + store, + ); + const previouslyAppended = hydrateReactionsArray( + reactionResult.value.append, + reactionResult.value.senderId, + reactionResult.value.timestamp, + ).map((r) => r.key); const reactions = []; for (const [k, reacts] of reactToMessage?.reactions || []) { for (const react of reacts) { - if (react.senderId === message.senderId && !previouslyAppended.includes(k)) reactions.push(react); + if ( + react.senderId === message.senderId && + !previouslyAppended.includes(k) + ) + reactions.push(react); } } - return await this.storeReaction(account, new borogove_ReactionUpdate(message.localId, reactionResult.value.serverId, reactionResult.value.serverIdBy, reactionResult.value.localId, message.chatId(), message.senderId, message.timestamp, reactions, enums.borogove_ReactionUpdateKind.CompleteReactions), tx); - } else if (result?.value && !message.isIncoming() && result?.value.direction === enums.borogove_MessageDirection.MessageSent && message.versions.length < 1) { + return await this.storeReaction( + account, + new borogove_ReactionUpdate( + message.localId, + reactionResult.value.serverId, + reactionResult.value.serverIdBy, + reactionResult.value.localId, + message.chatId(), + message.senderId, + message.timestamp, + reactions, + enums.borogove_ReactionUpdateKind.CompleteReactions, + ), + tx, + ); + } else if ( + result?.value && + !message.isIncoming() && + result?.value.direction === + enums.borogove_MessageDirection.MessageSent && + message.versions.length < 1 + ) { // Duplicate, we trust our own sent ids await promisifyRequest(result.delete()); - } else if (result?.value && (result.value.senderId == message.senderId || result.value.type == enums.borogove_MessageType.MessageCall) && (message.versions.length > 0 || (result.value.versions || []).length > 0)) { - return await hydrateMessage(correctMessage(account, message, result), store); + } else if ( + result?.value && + (result.value.senderId == message.senderId || + result.value.type == enums.borogove_MessageType.MessageCall) && + (message.versions.length > 0 || + (result.value.versions || []).length > 0) + ) { + return await hydrateMessage( + correctMessage(account, message, result), + store, + ); } // There may be reactions already if we are paging backwards - const cursor = tx.objectStore("reactions").index("senders").openCursor(IDBKeyRange.bound([account, message.chatId(), message.getReplyId() || ""], [account, message.chatId(), message.getReplyId() || "", []]), "prev"); + const cursor = tx + .objectStore("reactions") + .index("senders") + .openCursor( + IDBKeyRange.bound( + [account, message.chatId(), message.getReplyId() || ""], + [account, message.chatId(), message.getReplyId() || "", []], + ), + "prev", + ); const reactions = new Map(); const reactionTimes = new Map(); while (true) { @@ -894,13 +1354,21 @@ tx.onerror = console.error; } else { store.put(toPut).onerror = console.error; } - await Promise.all(message.attachments.map(a => a.lookup(this))); + await Promise.all(message.attachments.map((a) => a.lookup(this))); return message; } const time = reactionTimes.get(cresult.value.senderId); if (!time || time < cresult.value.timestamp) { - setReactions(reactions, cresult.value.senderId, hydrateReactionsArray(cresult.value.reactions, cresult.senderId, cresult.timestamp)); + setReactions( + reactions, + cresult.value.senderId, + hydrateReactionsArray( + cresult.value.reactions, + cresult.senderId, + cresult.timestamp, + ), + ); reactionTimes.set(cresult.value.senderId, cresult.value.timestamp); } @@ -908,28 +1376,53 @@ tx.onerror = console.error; } }, - updateMessage: function(account, message) { + updateMessage: function (account, message) { if (!message.chatId()) throw "Cannot store a message with no chatId"; if (!message.sortId) throw "Cannot store a message with no sortId"; - if (!message.serverId && !message.localId) throw "Cannot store a message with no id"; - if (!message.serverId && message.isIncoming()) throw "Cannot store an incoming message with no server id"; - if (message.serverId && !message.serverIdBy) throw "Cannot store a message with a server id and no by"; + if (!message.serverId && !message.localId) + throw "Cannot store a message with no id"; + if (!message.serverId && message.isIncoming()) + throw "Cannot store an incoming message with no server id"; + if (message.serverId && !message.serverIdBy) + throw "Cannot store a message with a server id and no by"; const tx = db.transaction(["messages"], "readwrite"); const store = tx.objectStore("messages"); store.put(serializeMessage(account, message)); }, - updateMessageStatus: async function(account, localId, status, statusText) { + updateMessageStatus: async function (account, localId, status, statusText) { const idToLookup = recentCorrections[localId] ?? localId; const tx = db.transaction(["messages"], "readwrite"); const store = tx.objectStore("messages"); - const result = await promisifyRequest(store.index("localId").openCursor(IDBKeyRange.bound([account, idToLookup], [account, idToLookup, []]))); + const result = await promisifyRequest( + store + .index("localId") + .openCursor( + IDBKeyRange.bound([account, idToLookup], [account, idToLookup, []]), + ), + ); const value = result?.value; - if (value && value.direction === enums.borogove_MessageDirection.MessageSent && ![enums.borogove_MessageStatus.MessageDeliveredToDevice, enums.borogove_MessageStatus.MessageFailedToSend].includes(value.status)) { - const newStatus = (value.versions?.length || 0) > 0 ? - { ...result.value, versions: [{ ...value.versions[0], status, statusText }, ...value.versions.slice(1)], status, statusText } : - { ...value, status, statusText }; + if ( + value && + value.direction === enums.borogove_MessageDirection.MessageSent && + ![ + enums.borogove_MessageStatus.MessageDeliveredToDevice, + enums.borogove_MessageStatus.MessageFailedToSend, + ].includes(value.status) + ) { + const newStatus = + (value.versions?.length || 0) > 0 + ? { + ...result.value, + versions: [ + { ...value.versions[0], status, statusText }, + ...value.versions.slice(1), + ], + status, + statusText, + } + : { ...value, status, statusText }; result.update(newStatus); return await hydrateMessage(newStatus); } @@ -937,33 +1430,58 @@ tx.onerror = console.error; throw "Message not found: " + localId; }, - getMessagesBefore: async function(account, chatId, before) { + getMessagesBefore: async function (account, chatId, before) { const tx = db.transaction(["messages"], "readonly"); const store = tx.objectStore("messages"); - const cursor = before?.type === enums.borogove_MessageType.MessageChannelPrivate ? - store.index("chats").openCursor( - IDBKeyRange.bound([account, chatId], [account, chatId, new Date(before.timestamp)]), - "prev" - ) : store.index("chatsBySortId").openCursor( - IDBKeyRange.bound([account, chatId], [account, chatId, before?.sortId || []]), - "prev" - ); - const messages = await this.getMessagesFromCursor(cursor, before, m => m.type === enums.borogove_MessageType.MessageChannelPrivate); + const cursor = + before?.type === enums.borogove_MessageType.MessageChannelPrivate + ? store + .index("chats") + .openCursor( + IDBKeyRange.bound( + [account, chatId], + [account, chatId, new Date(before.timestamp)], + ), + "prev", + ) + : store + .index("chatsBySortId") + .openCursor( + IDBKeyRange.bound( + [account, chatId], + [account, chatId, before?.sortId || []], + ), + "prev", + ); + const messages = await this.getMessagesFromCursor( + cursor, + before, + (m) => m.type === enums.borogove_MessageType.MessageChannelPrivate, + ); if (messages.length > 0 && messages[0].serverIdBy === chatId) { const earliest = new Date(messages[messages.length - 1].timestamp); const tx = db.transaction(["messages"], "readonly"); const store = tx.objectStore("messages"); - const pmCursor = store.index("chats").openCursor( - IDBKeyRange.bound([account, chatId], [account, chatId, before ? new Date(before.timestamp) : []]), - "prev" - ); + const pmCursor = store + .index("chats") + .openCursor( + IDBKeyRange.bound( + [account, chatId], + [account, chatId, before ? new Date(before.timestamp) : []], + ), + "prev", + ); const promisePMs = []; while (true) { const cresult = await promisifyRequest(pmCursor); if (!cresult?.value || cresult.value.timestamp < earliest) break; - if (cresult.value.type === enums.borogove_MessageType.MessageChannelPrivate && (!before || before.serverId !== cresult.value.serverId)) { + if ( + cresult.value.type === + enums.borogove_MessageType.MessageChannelPrivate && + (!before || before.serverId !== cresult.value.serverId) + ) { promisePMs.push(hydrateMessage(cresult.value)); } @@ -972,7 +1490,7 @@ tx.onerror = console.error; const pms = await Promise.all(promisePMs); for (const pm of pms) { - const idx = messages.findIndex(m => m.timestamp <= pm.timestamp); + const idx = messages.findIndex((m) => m.timestamp <= pm.timestamp); if (idx >= 0) messages.splice(idx, 0, pm); } } @@ -980,31 +1498,55 @@ tx.onerror = console.error; return messages.reverse(); }, - getMessagesAfter: async function(account, chatId, after) { - const index = after?.type === enums.borogove_MessageType.MessageChannelPrivate ? "chats" : "chatsBySortId"; - const bound = after ? [after?.type === enums.borogove_MessageType.MessageChannelPrivate ? new Date(after.timestamp) : after.sortId] : []; + getMessagesAfter: async function (account, chatId, after) { + const index = + after?.type === enums.borogove_MessageType.MessageChannelPrivate + ? "chats" + : "chatsBySortId"; + const bound = after + ? [ + after?.type === enums.borogove_MessageType.MessageChannelPrivate + ? new Date(after.timestamp) + : after.sortId, + ] + : []; const tx = db.transaction(["messages"], "readonly"); const store = tx.objectStore("messages"); - const cursor = store.index(index).openCursor( - IDBKeyRange.bound([account, chatId, ...bound], [account, chatId, []]), - "next" + const cursor = store + .index(index) + .openCursor( + IDBKeyRange.bound([account, chatId, ...bound], [account, chatId, []]), + "next", + ); + const messages = await this.getMessagesFromCursor( + cursor, + after, + (m) => m.type === enums.borogove_MessageType.MessageChannelPrivate, ); - const messages = await this.getMessagesFromCursor(cursor, after, m => m.type === enums.borogove_MessageType.MessageChannelPrivate); if (messages.length > 0 && messages[0].serverIdBy === chatId) { const latest = new Date(messages[messages.length - 1].timestamp); const tx = db.transaction(["messages"], "readonly"); const store = tx.objectStore("messages"); - const pmCursor = store.index("chats").openCursor( - IDBKeyRange.bound([account, chatId, ...(after ? [new Date(after.timestamp)] : [])], [account, chatId, []]), - "next" - ); + const pmCursor = store + .index("chats") + .openCursor( + IDBKeyRange.bound( + [account, chatId, ...(after ? [new Date(after.timestamp)] : [])], + [account, chatId, []], + ), + "next", + ); const promisePMs = []; while (true) { const cresult = await promisifyRequest(pmCursor); if (!cresult?.value) break; - if (cresult.value.type === enums.borogove_MessageType.MessageChannelPrivate && (!after || after.serverId !== cresult.value.serverId)) { + if ( + cresult.value.type === + enums.borogove_MessageType.MessageChannelPrivate && + (!after || after.serverId !== cresult.value.serverId) + ) { promisePMs.push(hydrateMessage(cresult.value)); } @@ -1015,39 +1557,54 @@ tx.onerror = console.error; const pms = await Promise.all(promisePMs); for (const pm of pms) { - const idx = messages.findLastIndex(m => m.timestamp < pm.timestamp); - if (idx >= 0) messages.splice(idx+1, 0, pm); + const idx = messages.findLastIndex((m) => m.timestamp < pm.timestamp); + if (idx >= 0) messages.splice(idx + 1, 0, pm); } } return messages; }, - getMessagesAround: async function(account, around) { + getMessagesAround: async function (account, around) { if (!around) throw "Cannot look around nothing"; if (!around.sortId) throw "Cannot look around no sortId"; - if (around.type == enums.borogove_MessageType.MessageChannelPrivate) throw "Cannot look around PM"; + if (around.type == enums.borogove_MessageType.MessageChannelPrivate) + throw "Cannot look around PM"; const chatId = around.chatId(); const before = this.getMessagesBefore(account, chatId, around); const tx = db.transaction(["messages"], "readonly"); const store = tx.objectStore("messages"); - const cursor = store.index("chatsBySortId").openCursor( - IDBKeyRange.bound([account, chatId, around.sortId], [account, chatId, []]), - "next" - ); + const cursor = store + .index("chatsBySortId") + .openCursor( + IDBKeyRange.bound( + [account, chatId, around.sortId], + [account, chatId, []], + ), + "next", + ); const aroundAndAfter = this.getMessagesFromCursor(cursor, null); - return Promise.all([before, aroundAndAfter]).then(result => result.flat()); + return Promise.all([before, aroundAndAfter]).then((result) => + result.flat(), + ); }, - getMessagesFromCursor: async function(cursor, notIncluding, filter) { + getMessagesFromCursor: async function (cursor, notIncluding, filter) { const result = []; while (true) { const cresult = await promisifyRequest(cursor); if (cresult && result.length < 50) { const value = cresult.value; - if ((notIncluding?.serverId && notIncluding?.serverId === value?.serverId) || (notIncluding?.localId && !value?.serverId && notIncluding?.localId === value?.localId) || (filter && filter(value))) { + if ( + (notIncluding?.serverId && + notIncluding?.serverId === value?.serverId) || + (notIncluding?.localId && + !value?.serverId && + notIncluding?.localId === value?.localId) || + (filter && filter(value)) + ) { cresult.continue(); continue; } @@ -1060,7 +1617,7 @@ tx.onerror = console.error; } }, - searchMessages: async function(account, chatId, q) { + searchMessages: async function (account, chatId, q) { const qTerms = new Set(tokenize(q).map(stemmer)); const tx = db.transaction(["messages"], "readonly"); const store = tx.objectStore("messages"); @@ -1070,7 +1627,9 @@ tx.onerror = console.error; let probeTerm = null; let probeScore = null; for (const term of qTerms) { - const score = await promisifyRequest(index.count(IDBKeyRange.only(term))); + const score = await promisifyRequest( + index.count(IDBKeyRange.only(term)), + ); if (!probeTerm || score < probeScore) { probeTerm = term; probeScore = score; @@ -1085,43 +1644,59 @@ tx.onerror = console.error; const cresult = await promisifyRequest(cursor); if (!cresult?.value) break; - if (cresult.value.account === account && (!chatId || cresult.value.chatId === chatId) && new Set(cresult.value.terms || []).isSupersetOf(qTerms)) { + if ( + cresult.value.account === account && + (!chatId || cresult.value.chatId === chatId) && + new Set(cresult.value.terms || []).isSupersetOf(qTerms) + ) { result.push(hydrateMessageSync(cresult.value)); } cresult.continue(); } - return result.sort((a, b) => a.timestamp < b.timestamp ? -1 : (a.timestamp > b.timestamp ? 1 : 0)); + return result.sort((a, b) => + a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0, + ); }, - hasMedia: function(hash) { + hasMedia: function (hash) { return media.hasMedia(hash); }, - removeMedia: function(hashAlgorithm, hash) { + removeMedia: function (hashAlgorithm, hash) { return media.removeMedia(hashAlgorithm, hash); }, - storeMedia: function(mime, buffer) { + storeMedia: function (mime, buffer) { return media.storeMedia(mime, buffer); }, - storeCaps: function(caps) { + storeCaps: function (caps) { const tx = db.transaction(["keyvaluepairs"], "readwrite"); const store = tx.objectStore("keyvaluepairs"); - store.put({ ...caps, data: caps.data.map(d => d.toString()) }, "caps:" + caps.ver()).onerror = console.error; + store.put( + { ...caps, data: caps.data.map((d) => d.toString()) }, + "caps:" + caps.ver(), + ).onerror = console.error; }, - getCaps: async function(ver) { + getCaps: async function (ver) { const tx = db.transaction(["keyvaluepairs"], "readonly"); const store = tx.objectStore("keyvaluepairs"); const raw = await promisifyRequest(store.get("caps:" + ver)); if (raw) { return new borogove_Caps( raw.node, - raw.identities.map((identity) => new borogove_Identity(identity.category, identity.type, identity.name)), + raw.identities.map( + (identity) => + new borogove_Identity( + identity.category, + identity.type, + identity.name, + ), + ), raw.features, - (raw.data || []).map(s => borogove_Stanza.parse(s)) + (raw.data || []).map((s) => borogove_Stanza.parse(s)), ); } @@ -1160,8 +1735,8 @@ tx.onerror = console.error; async storeOmemoDeviceList(chatId, deviceIds) { const tx = db.transaction(["keyvaluepairs"], "readwrite"); const store = tx.objectStore("keyvaluepairs"); - const key = "omemo:devices:"+chatId; - if(deviceIds.length>0) { + const key = "omemo:devices:" + chatId; + if (deviceIds.length > 0) { await promisifyRequest(store.put(deviceIds, key)); } else { await promisifyRequest(store.delete(key)); @@ -1173,7 +1748,9 @@ tx.onerror = console.error; async getOmemoDeviceList(chatId) { const tx = db.transaction(["keyvaluepairs"], "readonly"); const store = tx.objectStore("keyvaluepairs"); - const result = await promisifyRequest(store.get("omemo:devices:"+chatId)); + const result = await promisifyRequest( + store.get("omemo:devices:" + chatId), + ); return result || []; }, @@ -1181,10 +1758,15 @@ tx.onerror = console.error; const tx = db.transaction(["keyvaluepairs"], "readwrite"); const store = tx.objectStore("keyvaluepairs"); const storedKeyPair = { - "privKey": arrayBufferToBase64(keyPair.privKey), - "pubKey": arrayBufferToBase64(keyPair.pubKey), + privKey: arrayBufferToBase64(keyPair.privKey), + pubKey: arrayBufferToBase64(keyPair.pubKey), }; - await promisifyRequest(store.put(storedKeyPair, "omemo:prekeys:"+account+":"+keyId.toString())); + await promisifyRequest( + store.put( + storedKeyPair, + "omemo:prekeys:" + account + ":" + keyId.toString(), + ), + ); return keyPair; }, @@ -1192,7 +1774,7 @@ tx.onerror = console.error; async removeOmemoPreKey(account, keyId) { const tx = db.transaction(["keyvaluepairs"], "readwrite"); const store = tx.objectStore("keyvaluepairs"); - const keyName = "omemo:prekeys:"+account+":"+keyId.toString(); + const keyName = "omemo:prekeys:" + account + ":" + keyId.toString(); await promisifyRequest(store.delete(keyName)); return true; @@ -1201,13 +1783,15 @@ tx.onerror = console.error; async getOmemoPreKey(account, keyId) { const tx = db.transaction(["keyvaluepairs"], "readonly"); const store = tx.objectStore("keyvaluepairs"); - const result = await promisifyRequest(store.get("omemo:prekeys:"+account+":"+keyId.toString())); + const result = await promisifyRequest( + store.get("omemo:prekeys:" + account + ":" + keyId.toString()), + ); if (!result) { return null; } else { return { - "privKey": base64ToArrayBuffer(result.privKey), - "pubKey": base64ToArrayBuffer(result.pubKey), + privKey: base64ToArrayBuffer(result.privKey), + pubKey: base64ToArrayBuffer(result.pubKey), }; } }, @@ -1215,8 +1799,8 @@ tx.onerror = console.error; getOmemoPreKeys(account) { const tx = db.transaction(["keyvaluepairs"], "readonly"); const store = tx.objectStore("keyvaluepairs"); - const prefix = "omemo:prekeys:"+account+":"; - const keyRange = IDBKeyRange.bound(prefix, prefix + '\uffff'); + const prefix = "omemo:prekeys:" + account + ":"; + const keyRange = IDBKeyRange.bound(prefix, prefix + "\uffff"); const prekeys = []; const req = store.openCursor(keyRange); @@ -1224,21 +1808,21 @@ tx.onerror = console.error; return new Promise((resolve, reject) => { req.onsuccess = (event) => { const cursor = event.target.result; - if(cursor) { + if (cursor) { const splitDbKey = cursor.key.split(":"); const keyId = parseInt(splitDbKey[splitDbKey.length - 1], 10); prekeys.push({ keyId: keyId, keyPair: { - "privKey": base64ToArrayBuffer(cursor.value.privKey), - "pubKey": base64ToArrayBuffer(cursor.value.pubKey), + privKey: base64ToArrayBuffer(cursor.value.privKey), + pubKey: base64ToArrayBuffer(cursor.value.pubKey), }, }); cursor.continue(); } else { resolve(prekeys); } - } + }; req.onerror = (e) => { console.error(e); @@ -1256,7 +1840,7 @@ tx.onerror = console.error; const store = tx.objectStore("keyvaluepairs"); await Promise.all([ await promisifyRequest(store.put(sm, "sm:" + account)), - await promisifyRequest(store.put(sortId, "sortId:" + account)) + await promisifyRequest(store.put(sortId, "sortId:" + account)), ]); return true; }, @@ -1264,12 +1848,18 @@ tx.onerror = console.error; async getStreamManagement(account) { const tx = db.transaction(["keyvaluepairs"], "readonly"); const store = tx.objectStore("keyvaluepairs"); - const sm = await promisifyRequest(store.get("sm:" + account)) || null; - const sortId = await promisifyRequest(store.get("sortId:" + account)) ?? "a "; + const sm = (await promisifyRequest(store.get("sm:" + account))) || null; + const sortId = + (await promisifyRequest(store.get("sortId:" + account))) ?? "a "; if (sm instanceof ArrayBuffer || !sm) { return { sm, sortId }; } else { - return { sm: new Blob([JSON.stringify(sm)], {type: "text/plain; charset=utf-8"}).arrayBuffer(), sortId }; + return { + sm: new Blob([JSON.stringify(sm)], { + type: "text/plain; charset=utf-8", + }).arrayBuffer(), + sortId, + }; } }, @@ -1284,16 +1874,23 @@ tx.onerror = console.error; promisifyRequest(store.get("rosterVer:" + login)), ]).then((result) => { if (result[1]) { - store.put((result[2] || 0) + 1, "login:fastCount:" + login).onerror = console.error; + store.put((result[2] || 0) + 1, "login:fastCount:" + login).onerror = + console.error; } - return { clientId: result[0], token: result[1], fastCount: result[2] || 0, displayName: result[3], rosterVer: result[4] }; + return { + clientId: result[0], + token: result[1], + fastCount: result[2] || 0, + displayName: result[3], + rosterVer: result[4], + }; }); }, getOmemoId(account) { const tx = db.transaction(["keyvaluepairs"], "readonly"); const store = tx.objectStore("keyvaluepairs"); - return promisifyRequest(store.get("omemo:id:"+account)).then( + return promisifyRequest(store.get("omemo:id:" + account)).then( (result) => result ?? null, ); }, @@ -1301,7 +1898,7 @@ tx.onerror = console.error; getOmemoIdentityKey(account) { const tx = db.transaction(["keyvaluepairs"], "readonly"); const store = tx.objectStore("keyvaluepairs"); - return promisifyRequest(store.get("omemo:key:"+account)).then( + return promisifyRequest(store.get("omemo:key:" + account)).then( (result) => result ?? null, ); }, @@ -1309,9 +1906,9 @@ tx.onerror = console.error; async getOmemoSignedPreKey(account, keyId) { const tx = db.transaction(["keyvaluepairs"], "readonly"); const store = tx.objectStore("keyvaluepairs"); - const dbKey = "omemo:signed-prekey:"+account+":"+keyId.toString(); + const dbKey = "omemo:signed-prekey:" + account + ":" + keyId.toString(); const result = await promisifyRequest(store.get(dbKey)); - if(!result) { + if (!result) { return null; } else { return { @@ -1328,7 +1925,8 @@ tx.onerror = console.error; async storeOmemoSignedPreKey(account, signedKey) { const tx = db.transaction(["keyvaluepairs"], "readwrite"); const store = tx.objectStore("keyvaluepairs"); - const dbKey = "omemo:signed-prekey:"+account+":"+signedKey.keyId.toString(); + const dbKey = + "omemo:signed-prekey:" + account + ":" + signedKey.keyId.toString(); const storedKey = { privKey: arrayBufferToBase64(signedKey.keyPair.privKey), pubKey: arrayBufferToBase64(signedKey.keyPair.pubKey), @@ -1340,7 +1938,10 @@ tx.onerror = console.error; }, async removeAccount(account, completely) { - const tx = db.transaction(["keyvaluepairs", "services", "messages", "chats", "reactions"], "readwrite"); + const tx = db.transaction( + ["keyvaluepairs", "services", "messages", "chats", "reactions"], + "readwrite", + ); const store = tx.objectStore("keyvaluepairs"); store.delete("login:clientId:" + account); store.delete("login:token:" + account); @@ -1351,7 +1952,9 @@ tx.onerror = console.error; if (!completely) return true; const servicesStore = tx.objectStore("services"); - const servicesCursor = servicesStore.openCursor(IDBKeyRange.bound([account], [account, []])); + const servicesCursor = servicesStore.openCursor( + IDBKeyRange.bound([account], [account, []]), + ); servicesCursor.onsuccess = (event) => { if (event.target.result) { event.target.result.delete(); @@ -1360,7 +1963,9 @@ tx.onerror = console.error; }; const messagesStore = tx.objectStore("messages"); - const messagesCursor = messagesStore.openCursor(IDBKeyRange.bound([account], [account, []])); + const messagesCursor = messagesStore.openCursor( + IDBKeyRange.bound([account], [account, []]), + ); messagesCursor.onsuccess = (event) => { if (event.target.result) { event.target.result.delete(); @@ -1369,7 +1974,9 @@ tx.onerror = console.error; }; const chatsStore = tx.objectStore("chats"); - const chatsCursor = chatsStore.openCursor(IDBKeyRange.bound([account], [account, []])); + const chatsCursor = chatsStore.openCursor( + IDBKeyRange.bound([account], [account, []]), + ); chatsCursor.onsuccess = (event) => { if (event.target.result) { event.target.result.delete(); @@ -1378,7 +1985,9 @@ tx.onerror = console.error; }; const reactionsStore = tx.objectStore("reactions"); - const reactionsCursor = reactionsStore.openCursor(IDBKeyRange.bound([account], [account, []])); + const reactionsCursor = reactionsStore.openCursor( + IDBKeyRange.bound([account], [account, []]), + ); reactionsCursor.onsuccess = (event) => { if (event.target.result) { event.target.result.delete(); @@ -1392,8 +2001,12 @@ tx.onerror = console.error; async listAccounts() { const tx = db.transaction(["keyvaluepairs"], "readonly"); const store = tx.objectStore("keyvaluepairs"); - const keys = await promisifyRequest(store.getAllKeys(IDBKeyRange.bound("login:clientId:", "login:clientId:\uffff"))); - return keys.map(k => k.substring(15)); + const keys = await promisifyRequest( + store.getAllKeys( + IDBKeyRange.bound("login:clientId:", "login:clientId:\uffff"), + ), + ); + return keys.map((k) => k.substring(15)); }, storeService(account, serviceId, name, node, caps) { @@ -1416,16 +2029,24 @@ tx.onerror = console.error; const store = tx.objectStore("services"); // Almost full scan shouldn't be too expensive, how many services are we aware of? - const cursor = store.openCursor(IDBKeyRange.bound([account], [account, []])); + const cursor = store.openCursor( + IDBKeyRange.bound([account], [account, []]), + ); const result = []; while (true) { const cresult = await promisifyRequest(cursor); if (cresult) { const value = cresult.value; - result.push(this.getCaps(value.caps).then((caps) => ({ ...value, caps: caps }))); + result.push( + this.getCaps(value.caps).then((caps) => ({ ...value, caps: caps })), + ); cresult.continue(); } else { - return await Promise.all(result).then((items) => items.filter((item) => item.caps && item.caps.features.includes(feature))); + return await Promise.all(result).then((items) => + items.filter( + (item) => item.caps && item.caps.features.includes(feature), + ), + ); } } }, @@ -1446,11 +2067,13 @@ tx.onerror = console.error; async storeOmemoContactIdentityKey(account, address, identityKey) { const tx = db.transaction(["omemo_identities"], "readwrite"); const store = tx.objectStore("omemo_identities"); - await promisifyRequest(store.put({ - account: account, - address: address, - pubKey: arrayBufferToBase64(identityKey), - })); + await promisifyRequest( + store.put({ + account: account, + address: address, + pubKey: arrayBufferToBase64(identityKey), + }), + ); return identityKey; }, @@ -1464,22 +2087,26 @@ tx.onerror = console.error; async storeOmemoSession(account, address, session) { const tx = db.transaction(["omemo_sessions"], "readwrite"); const store = tx.objectStore("omemo_sessions"); - await promisifyRequest(store.put({ - account: account, - address: address, - session: session, - })); + await promisifyRequest( + store.put({ + account: account, + address: address, + session: session, + }), + ); return session; }, async storeOmemoMetadata(account, address, metadata) { const tx = db.transaction(["omemo_sessions_meta"], "readwrite"); const store = tx.objectStore("omemo_sessions_meta"); - await promisifyRequest(store.put({ - account: account, - address: address, - metadata: metadata, - })); + await promisifyRequest( + store.put({ + account: account, + address: address, + metadata: metadata, + }), + ); return metadata; }, @@ -1492,7 +2119,10 @@ tx.onerror = console.error; async removeOmemoSession(account, address) { // Remove session and any stored metadata - const tx = db.transaction(["omemo_sessions", "omemo_sessions_meta"], "readwrite"); + const tx = db.transaction( + ["omemo_sessions", "omemo_sessions_meta"], + "readwrite", + ); const path = [account, address]; tx.objectStore("omemo_sessions").delete(path); tx.objectStore("omemo_sessions_meta").delete(path); @@ -1509,12 +2139,12 @@ tx.onerror = console.error; set(k, v) { const tx = db.transaction(["keyvaluepairs"], "readwrite"); const store = tx.objectStore("keyvaluepairs"); - if (typeof(v) === "undefined") { + if (typeof v === "undefined") { return promisifyRequest(store.delete(k)); } else { return promisifyRequest(store.put(v, k)); } - } + }, }; media.setKV(obj); diff --git a/borogove/persistence/MediaStoreCache.js b/borogove/persistence/MediaStoreCache.js index b970dc1..222119c 100644 --- a/borogove/persistence/MediaStoreCache.js +++ b/borogove/persistence/MediaStoreCache.js @@ -7,7 +7,14 @@ export default (cacheName, { routeHashPath } = { routeHashPath: null }) => { let cache = null; // Allow the definitions to be sync function mkNiUrl(hashAlgorithm, hashBytes) { - const b64url = btoa(Array.from(new Uint8Array(hashBytes), (x) => String.fromCodePoint(x)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + const b64url = btoa( + Array.from(new Uint8Array(hashBytes), (x) => + String.fromCodePoint(x), + ).join(""), + ) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); return "/.well-known/ni/" + hashAlgorithm + "/" + b64url; } @@ -22,18 +29,24 @@ export default (cacheName, { routeHashPath } = { routeHashPath: null }) => { const tmpPath = "/tmp/" + crypto.randomUUID(); await cache.put( tmpPath, - new Response(source.pipeThrough(new TransformStream({ - start(controller) {}, - flush(controller) {}, - transform(chunk, controller) { - sha256.update(chunk); - sha1.update(chunk); - controller.enqueue(chunk); - } - })), { headers: { "Content-Type": mime } }) + new Response( + source.pipeThrough( + new TransformStream({ + start(controller) {}, + flush(controller) {}, + transform(chunk, controller) { + sha256.update(chunk); + sha1.update(chunk); + controller.enqueue(chunk); + }, + }), + ), + { headers: { "Content-Type": mime } }, + ), ); const sha256NiUrl = mkNiUrl("sha-256", sha256.digest().hash); - if (this.kv) await this.kv.set(mkNiUrl("sha-1", sha1.digest().hash), sha256NiUrl); + if (this.kv) + await this.kv.set(mkNiUrl("sha-1", sha1.digest().hash), sha256NiUrl); // Copy then delete because move is not supported const written = await cache.match(tmpPath); await cache.put(sha256NiUrl, written); @@ -46,7 +59,7 @@ export default (cacheName, { routeHashPath } = { routeHashPath: null }) => { if (hashAlgorithm === "sha-256") { niUrl = mkNiUrl(hashAlgorithm, hash); } else { - niUrl = this.kv && await this.kv.get(mkNiUrl(hashAlgorithm, hash)); + niUrl = this.kv && (await this.kv.get(mkNiUrl(hashAlgorithm, hash))); if (!niUrl) return; } @@ -60,7 +73,7 @@ export default (cacheName, { routeHashPath } = { routeHashPath: null }) => { if (uri.split("/")[3] === "sha-256") { niUrl = uri; } else { - niUrl = this.kv && await this.kv.get(uri); + niUrl = this.kv && (await this.kv.get(uri)); if (!niUrl) { return null; } @@ -75,7 +88,7 @@ export default (cacheName, { routeHashPath } = { routeHashPath: null }) => { if (!response) return null; return niUrl; - } + }, }; if (routeHashPath) { @@ -84,19 +97,22 @@ export default (cacheName, { routeHashPath } = { routeHashPath: null }) => { const r = await o.getMediaResponse(uri); if (r) return r; } - await new Promise(resolve => setTimeout(resolve, 5000)); + await new Promise((resolve) => setTimeout(resolve, 5000)); return await waitForMedia(uri); }; self.addEventListener("fetch", (event) => { const url = new URL(event.request.url); - if (url.origin === self.location.origin && url.pathname.startsWith("/.well-known/ni/")) { + if ( + url.origin === self.location.origin && + url.pathname.startsWith("/.well-known/ni/") + ) { event.respondWith(waitForMedia(url.pathname)); } }); } - return caches.open(cacheName).then(c => { + return caches.open(cacheName).then((c) => { cache = c; return o; }); diff --git a/npm/calls.ts b/npm/calls.ts index 1ce1db1..f7a72d8 100644 --- a/npm/calls.ts +++ b/npm/calls.ts @@ -1,9 +1,3 @@ -export { - borogove_calls_CallStatus as CallStatus, -} from "./borogove-enums"; -export { - borogove_calls_InitiatedSession as InitiatedSession, -} from "./borogove.js"; -export type { - borogove_calls_Session as Session, -} from "./borogove.js"; +export { borogove_calls_CallStatus as CallStatus } from "./borogove-enums"; +export { borogove_calls_InitiatedSession as InitiatedSession } from "./borogove.js"; +export type { borogove_calls_Session as Session } from "./borogove.js"; diff --git a/npm/index.ts b/npm/index.ts index a23b8a1..ee1e251 100644 --- a/npm/index.ts +++ b/npm/index.ts @@ -11,44 +11,44 @@ export { borogove_UserState as UserState, } from "./borogove-enums.js"; export { - borogove_AvailableChat as AvailableChat, - borogove_AttachmentUploadError as AttachmentUploadError, - borogove_AvailableChatIterator as AvailableChatIterator, - borogove_Channel as Channel, - borogove_Chat as Chat, - borogove_ChatAttachment as ChatAttachment, - borogove_ChatMessage as ChatMessage, - borogove_ChatMessageBuilder as ChatMessageBuilder, - borogove_Client as Client, - borogove_Command as Command, - borogove_CommandSession as CommandSession, - borogove_Config as Config, - borogove_CustomEmojiReaction as CustomEmojiReaction, - borogove_DirectChat as DirectChat, - borogove_EventEmitter as EventEmitter, - borogove_Form as Form, - borogove_FormItem as FormItem, - borogove_FormField as FormField, - borogove_FormOption as FormOption, - borogove_Hash as Hash, - borogove_Html as Html, - borogove_Identicon as Identicon, - borogove_Importer as Importer, - borogove_LinkMetadata as LinkMetadata, - borogove_Notification as Notification, - borogove_Member as Member, - borogove_Profile as Profile, - borogove_ProfileBuilder as ProfileBuilder, - borogove_ProfileItem as ProfileItem, - borogove_Push as Push, - borogove_Reaction as Reaction, - borogove_Register as Register, - borogove_SerializedChat as SerializedChat, - borogove_Status as Status, + borogove_AvailableChat as AvailableChat, + borogove_AttachmentUploadError as AttachmentUploadError, + borogove_AvailableChatIterator as AvailableChatIterator, + borogove_Channel as Channel, + borogove_Chat as Chat, + borogove_ChatAttachment as ChatAttachment, + borogove_ChatMessage as ChatMessage, + borogove_ChatMessageBuilder as ChatMessageBuilder, + borogove_Client as Client, + borogove_Command as Command, + borogove_CommandSession as CommandSession, + borogove_Config as Config, + borogove_CustomEmojiReaction as CustomEmojiReaction, + borogove_DirectChat as DirectChat, + borogove_EventEmitter as EventEmitter, + borogove_Form as Form, + borogove_FormItem as FormItem, + borogove_FormField as FormField, + borogove_FormOption as FormOption, + borogove_Hash as Hash, + borogove_Html as Html, + borogove_Identicon as Identicon, + borogove_Importer as Importer, + borogove_LinkMetadata as LinkMetadata, + borogove_Notification as Notification, + borogove_Member as Member, + borogove_Profile as Profile, + borogove_ProfileBuilder as ProfileBuilder, + borogove_ProfileItem as ProfileItem, + borogove_Push as Push, + borogove_Reaction as Reaction, + borogove_Register as Register, + borogove_SerializedChat as SerializedChat, + borogove_Status as Status, } from "./borogove.js"; export type { - borogove_FormSection as FormSection, - borogove_Persistence as Persistence, + borogove_FormSection as FormSection, + borogove_Persistence as Persistence, } from "./borogove.js"; import * as persistence from "./persistence.js"; diff --git a/npm/persistence-browser.ts b/npm/persistence-browser.ts index abb67fe..1713c62 100644 --- a/npm/persistence-browser.ts +++ b/npm/persistence-browser.ts @@ -1,9 +1,7 @@ export { default as IDB } from "./IDB.js"; export { default as MediaStoreCache } from "./MediaStoreCache.js"; -export { - borogove_persistence_Dummy as Dummy, -} from "./borogove.js"; +export { borogove_persistence_Dummy as Dummy } from "./borogove.js"; export type { borogove_persistence_KeyValueStore as KeyValueStore, borogove_persistence_MediaStore as MediaStore, diff --git a/test/browser-test.ts b/test/browser-test.ts index 1e28d50..036e096 100644 --- a/test/browser-test.ts +++ b/test/browser-test.ts @@ -1,4 +1,9 @@ -import { test as base, expect, type JSHandle, type Page } from "@playwright/test"; +import { + test as base, + expect, + type JSHandle, + type Page, +} from "@playwright/test"; import type { borogove_Persistence } from "../npm/borogove-browser"; import type { borogove_persistence_Sqlite } from "../npm/sqlite-wasm"; diff --git a/test/fast-auth.spec.ts b/test/fast-auth.spec.ts index e14e6df..8886eea 100644 --- a/test/fast-auth.spec.ts +++ b/test/fast-auth.spec.ts @@ -137,10 +137,7 @@ test("a rejected unexpired FAST token uses a non-empty password before reconnect }); persistence.getChats = async () => []; persistence.getChatsUnreadDetails = async () => []; - const client = new borogove.Client( - "tester@127.0.0.1", - persistence, - ); + const client = new borogove.Client("tester@127.0.0.1", persistence); client.stream.debug = false; const passwordRequested = Promise.withResolvers<void>(); client.addPasswordNeededListener(() => { diff --git a/test/idb.spec.ts b/test/idb.spec.ts index a8e52e0..ee2e55c 100644 --- a/test/idb.spec.ts +++ b/test/idb.spec.ts @@ -1,203 +1,223 @@ import { idbTest as test, expect } from "./browser-test"; import { sharedPersistenceTests } from "./persistence-tests"; - sharedPersistenceTests(test); // TODO: Share this with SQLite once direct-chat reply hydration is fixed. -test("hydrate message with incomplete replyToMessage", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "parent", - serverIdBy: "hatter@example.com", - localId: "loc1", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - const parentStub = builder.build(); - - builder.setBody(borogove.Html.text("Hello")); - const parentMsg = builder.build(); - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "child", - serverIdBy: "hatter@example.com", - localId: "loc2", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "a1"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.recipients = [builder2.to]; - builder2.replyTo = [builder2.from]; - builder2.replyToMessage = parentStub; - const childMsg = builder2.build(); - - await persistence.storeMessages("alice@example.com", [parentMsg]); - const [childStored] = await persistence.storeMessages("alice@example.com", [ - childMsg, - ]); - - return childStored.replyToMessage.body().toPlainText(); - }, { borogove, persistence }); +test("hydrate message with incomplete replyToMessage", async ({ + page, + borogove, + persistence, +}) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "parent", + serverIdBy: "hatter@example.com", + localId: "loc1", + senderId: "hatter@example.com", + direction: 0, + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + const parentStub = builder.build(); + + builder.setBody(borogove.Html.text("Hello")); + const parentMsg = builder.build(); + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "child", + serverIdBy: "hatter@example.com", + localId: "loc2", + senderId: "hatter@example.com", + direction: 0, + }); + builder2.sortId = "a1"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("hatter@example.com"); + builder2.recipients = [builder2.to]; + builder2.replyTo = [builder2.from]; + builder2.replyToMessage = parentStub; + const childMsg = builder2.build(); + + await persistence.storeMessages("alice@example.com", [parentMsg]); + const [childStored] = await persistence.storeMessages( + "alice@example.com", + [childMsg], + ); + + return childStored.replyToMessage.body().toPlainText(); + }, + { borogove, persistence }, + ); expect(result).toBe("Hello"); }); - -test("getMembers filters hidden rows for non-moderators", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-members-5@example.com"; - chat.getDisplayName = () => "Tea Room"; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-5@example.com/owner", - displayName: "Zulu", - photoUri: null, - isSelf: false, - roles: [{ id: "owner", title: "Owner" }], - jid: borogove.JID.parse("zulu@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "zulu@example.com" }, - }, - { - id: "room-members-5@example.com/outcast", - displayName: "Banned", - photoUri: null, - isSelf: false, - roles: [{ id: "outcast", title: "Banned" }], - jid: borogove.JID.parse("banned@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "banned@example.com" }, - }, - { - id: "room-members-5@example.com/guest-offline", - displayName: "Guest", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Guest" }], - jid: borogove.JID.parse("guest@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse('<presence type="unavailable" />')], - ]), - chat: { chatId: "guest@example.com" }, - }, - { - id: "room-members-5@example.com/guest-offline2", - displayName: "Guest2", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Guest" }], - jid: borogove.JID.parse("guest2@example.com"), - presence: new Map(), - chat: { chatId: "guest2@example.com" }, - }, - { - id: "room-members-5@example.com/admin", - displayName: "Alpha", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alpha@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "alpha@example.com" }, - }, - ]); - - const normal = await persistence.getMembers( - "alice@example.com", - chat, - false, - ); - - return normal.map((m) => m.displayName); - }, { borogove, persistence }); +test("getMembers filters hidden rows for non-moderators", async ({ + page, + borogove, + persistence, +}) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-members-5@example.com"; + chat.getDisplayName = () => "Tea Room"; + + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: "room-members-5@example.com/owner", + displayName: "Zulu", + photoUri: null, + isSelf: false, + roles: [{ id: "owner", title: "Owner" }], + jid: borogove.JID.parse("zulu@example.com"), + presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "zulu@example.com" }, + }, + { + id: "room-members-5@example.com/outcast", + displayName: "Banned", + photoUri: null, + isSelf: false, + roles: [{ id: "outcast", title: "Banned" }], + jid: borogove.JID.parse("banned@example.com"), + presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "banned@example.com" }, + }, + { + id: "room-members-5@example.com/guest-offline", + displayName: "Guest", + photoUri: null, + isSelf: false, + roles: [{ id: "none", title: "Guest" }], + jid: borogove.JID.parse("guest@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse('<presence type="unavailable" />')], + ]), + chat: { chatId: "guest@example.com" }, + }, + { + id: "room-members-5@example.com/guest-offline2", + displayName: "Guest2", + photoUri: null, + isSelf: false, + roles: [{ id: "none", title: "Guest" }], + jid: borogove.JID.parse("guest2@example.com"), + presence: new Map(), + chat: { chatId: "guest2@example.com" }, + }, + { + id: "room-members-5@example.com/admin", + displayName: "Alpha", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("alpha@example.com"), + presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), + chat: { chatId: "alpha@example.com" }, + }, + ]); + + const normal = await persistence.getMembers( + "alice@example.com", + chat, + false, + ); + + return normal.map((m) => m.displayName); + }, + { borogove, persistence }, + ); expect(result).toEqual(["Zulu", "Alpha"]); }); - test("hydrate message with incomplete replyToMessage keys", async ({ page, borogove, persistence, }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "parent", - serverIdBy: "hatter@example.com", - localId: "loc1", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - const parentMsg = builder.build(); - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "child", - serverIdBy: "hatter@example.com", - localId: "loc2", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "a1"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.recipients = [builder2.to]; - builder2.replyTo = [builder2.from]; - builder2.replyToMessage = parentMsg; - const childMsg = builder2.build(); - - await persistence.storeMessages("alice@example.com", [parentMsg, childMsg]); - - const db = await new Promise<IDBDatabase>((resolve, reject) => { - const req = indexedDB.open("snikket"); - req.onsuccess = () => resolve(req.result); - req.onerror = () => reject(req.error); - }); - const tx = db.transaction(["messages"], "readwrite"); - const store = tx.objectStore("messages"); - const key = ["alice@example.com", "child", "hatter@example.com", "loc2"]; - const rawChild = await new Promise<{ replyToMessage: string[] }>((resolve) => { - const req = store.get(key); - req.onsuccess = () => resolve(req.result); - }); - - rawChild.replyToMessage = ["alice@example.com", "parent", "", ""]; - - await new Promise<void>((resolve) => { - const req = store.put(rawChild); - req.onsuccess = () => resolve(); - }); - await new Promise<void>((resolve) => { - tx.oncomplete = () => resolve(); - }); - - const retrievedChild = await persistence.getMessage( - "alice@example.com", - "hatter@example.com", - "child", - "loc2", - ); - return { - hasReply: !!retrievedChild.replyToMessage, - replyServerId: retrievedChild.replyToMessage - ? retrievedChild.replyToMessage.serverId - : null, - }; - }, { borogove, persistence }); + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "parent", + serverIdBy: "hatter@example.com", + localId: "loc1", + senderId: "hatter@example.com", + direction: 0, + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + const parentMsg = builder.build(); + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "child", + serverIdBy: "hatter@example.com", + localId: "loc2", + senderId: "hatter@example.com", + direction: 0, + }); + builder2.sortId = "a1"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("hatter@example.com"); + builder2.recipients = [builder2.to]; + builder2.replyTo = [builder2.from]; + builder2.replyToMessage = parentMsg; + const childMsg = builder2.build(); + + await persistence.storeMessages("alice@example.com", [ + parentMsg, + childMsg, + ]); + + const db = await new Promise<IDBDatabase>((resolve, reject) => { + const req = indexedDB.open("snikket"); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + const tx = db.transaction(["messages"], "readwrite"); + const store = tx.objectStore("messages"); + const key = ["alice@example.com", "child", "hatter@example.com", "loc2"]; + const rawChild = await new Promise<{ replyToMessage: string[] }>( + (resolve) => { + const req = store.get(key); + req.onsuccess = () => resolve(req.result); + }, + ); + + rawChild.replyToMessage = ["alice@example.com", "parent", "", ""]; + + await new Promise<void>((resolve) => { + const req = store.put(rawChild); + req.onsuccess = () => resolve(); + }); + await new Promise<void>((resolve) => { + tx.oncomplete = () => resolve(); + }); + + const retrievedChild = await persistence.getMessage( + "alice@example.com", + "hatter@example.com", + "child", + "loc2", + ); + return { + hasReply: !!retrievedChild.replyToMessage, + replyServerId: retrievedChild.replyToMessage + ? retrievedChild.replyToMessage.serverId + : null, + }; + }, + { borogove, persistence }, + ); expect(result.hasReply).toBe(true); expect(result.replyServerId).toBe("parent"); diff --git a/test/persistence-tests.ts b/test/persistence-tests.ts index a1c7061..eee2ea3 100644 --- a/test/persistence-tests.ts +++ b/test/persistence-tests.ts @@ -44,7 +44,11 @@ export function sharedPersistenceTests(test: PersistenceTest) { expect(result.threadSubject).toBe("Introductions"); }); - test("storeChats and getChats with status", async ({ page, borogove, persistence }) => { + test("storeChats and getChats with status", async ({ + page, + borogove, + persistence, + }) => { const result = await page.evaluate( async ({ borogove, persistence }) => { const chat = new borogove.DirectChat( @@ -75,7 +79,11 @@ export function sharedPersistenceTests(test: PersistenceTest) { expect(result.statusText).toBe("Time for tea!"); }); - test("getChats uses member presence for direct chats", async ({ page, borogove, persistence }) => { + test("getChats uses member presence for direct chats", async ({ + page, + borogove, + persistence, + }) => { const result = await page.evaluate( async ({ borogove, persistence }) => { const chat = new borogove.DirectChat( @@ -203,8 +211,11 @@ export function sharedPersistenceTests(test: PersistenceTest) { expect(result.presenceKeys).toEqual(["desk"]); }); - - test("hydrate replyToMessage for groupchats", async ({ page, borogove, persistence }) => { + test("hydrate replyToMessage for groupchats", async ({ + page, + borogove, + persistence, + }) => { const result = await page.evaluate( async ({ borogove, persistence }) => { const builder = new borogove.ChatMessageBuilder({ @@ -407,40 +418,47 @@ export function sharedPersistenceTests(test: PersistenceTest) { expect(result[0]).toBe("Hello world"); }); - test("1:1 come back ordered by sortId", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "alice@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.replyTo = [builder.from]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "alice@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.replyTo = [builder.from]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder.build(), - ]); - - return await persistence.getMessagesBefore( - "alice@example.com", - "hatter@example.com", - ); - }, { borogove, persistence }); + test("1:1 come back ordered by sortId", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "alice@example.com", + senderId: "hatter@example.com", + direction: 0, + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.replyTo = [builder.from]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "alice@example.com", + senderId: "hatter@example.com", + direction: 0, + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("hatter@example.com"); + builder2.replyTo = [builder.from]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder.build(), + ]); + + return await persistence.getMessagesBefore( + "alice@example.com", + "hatter@example.com", + ); + }, + { borogove, persistence }, + ); expect(result.length).toBe(2); expect(result[0].serverId).toBe("1"); @@ -452,57 +470,60 @@ export function sharedPersistenceTests(test: PersistenceTest) { borogove, persistence, }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "a0"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder.from.asBare()]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder3.build(), - builder.build(), - ]); - - return await persistence.getMessagesBefore( - "alice@example.com", - "teaparty@example.com", - ); - }, { borogove, persistence }); + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "a0"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesBefore( + "alice@example.com", + "teaparty@example.com", + ); + }, + { borogove, persistence }, + ); expect(result.length).toBe(3); expect(result[0].serverId).toBe("1"); @@ -515,72 +536,75 @@ export function sharedPersistenceTests(test: PersistenceTest) { borogove, persistence, }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "Z~"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder.from.asBare()]; - - const builder4 = new borogove.ChatMessageBuilder({ - serverId: "4", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:04Z", - }); - builder4.sortId = "c0"; - builder4.to = borogove.JID.parse("alice@example.com"); - builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder4.replyTo = [builder.from.asBare()]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder4.build(), - builder3.build(), - builder.build(), - ]); - - return await persistence.getMessagesBefore( - "alice@example.com", - "teaparty@example.com", - builder4.build(), - ); - }, { borogove, persistence }); + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "Z~"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + const builder4 = new borogove.ChatMessageBuilder({ + serverId: "4", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:04Z", + }); + builder4.sortId = "c0"; + builder4.to = borogove.JID.parse("alice@example.com"); + builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder4.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder4.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesBefore( + "alice@example.com", + "teaparty@example.com", + builder4.build(), + ); + }, + { borogove, persistence }, + ); expect(result.length).toBe(3); expect(result[0].serverId).toBe("1"); @@ -589,72 +613,75 @@ export function sharedPersistenceTests(test: PersistenceTest) { }); test("getMessagesBefore a PM", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "Z~"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder.from.asBare()]; - - const builder4 = new borogove.ChatMessageBuilder({ - serverId: "4", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:04Z", - }); - builder4.sortId = "c0"; - builder4.to = borogove.JID.parse("alice@example.com"); - builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder4.replyTo = [builder.from.asBare()]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder4.build(), - builder3.build(), - builder.build(), - ]); - - return await persistence.getMessagesBefore( - "alice@example.com", - "teaparty@example.com", - builder3.build(), - ); - }, { borogove, persistence }); + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "Z~"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + const builder4 = new borogove.ChatMessageBuilder({ + serverId: "4", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:04Z", + }); + builder4.sortId = "c0"; + builder4.to = borogove.JID.parse("alice@example.com"); + builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder4.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder4.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesBefore( + "alice@example.com", + "teaparty@example.com", + builder3.build(), + ); + }, + { borogove, persistence }, + ); expect(result.length).toBe(2); expect(result[0].serverId).toBe("1"); @@ -666,57 +693,60 @@ export function sharedPersistenceTests(test: PersistenceTest) { borogove, persistence, }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "a1"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder.from.asBare()]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder3.build(), - builder.build(), - ]); - - return await persistence.getMessagesAfter( - "alice@example.com", - "teaparty@example.com", - ); - }, { borogove, persistence }); + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "a1"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesAfter( + "alice@example.com", + "teaparty@example.com", + ); + }, + { borogove, persistence }, + ); expect(result.length).toBe(3); expect(result[0].serverId).toBe("1"); @@ -729,72 +759,75 @@ export function sharedPersistenceTests(test: PersistenceTest) { borogove, persistence, }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "Z~"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder.from.asBare()]; - - const builder4 = new borogove.ChatMessageBuilder({ - serverId: "4", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:04Z", - }); - builder4.sortId = "c0"; - builder4.to = borogove.JID.parse("alice@example.com"); - builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder4.replyTo = [builder.from.asBare()]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder4.build(), - builder3.build(), - builder.build(), - ]); - - return await persistence.getMessagesAfter( - "alice@example.com", - "teaparty@example.com", - builder.build(), - ); - }, { borogove, persistence }); + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "Z~"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + const builder4 = new borogove.ChatMessageBuilder({ + serverId: "4", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:04Z", + }); + builder4.sortId = "c0"; + builder4.to = borogove.JID.parse("alice@example.com"); + builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder4.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder4.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesAfter( + "alice@example.com", + "teaparty@example.com", + builder.build(), + ); + }, + { borogove, persistence }, + ); expect(result.length).toBe(3); expect(result[0].serverId).toBe("2"); @@ -803,115 +836,133 @@ export function sharedPersistenceTests(test: PersistenceTest) { }); test("getMessagesAfter a PM", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - serverId: "1", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:00Z", - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder.replyTo = [builder.from.asBare()]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "2", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:01Z", - }); - builder2.sortId = "b0"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder2.replyTo = [builder.from.asBare()]; - - const builder3 = new borogove.ChatMessageBuilder({ - serverId: "3", - serverIdBy: "alice@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannelPrivate, - timestamp: "2020-01-01T00:00:03Z", - }); - builder3.sortId = "Z~"; - builder3.to = borogove.JID.parse("alice@example.com"); - builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder3.replyTo = [builder.from.asBare()]; - - const builder4 = new borogove.ChatMessageBuilder({ - serverId: "4", - serverIdBy: "teaparty@example.com", - senderId: "teaparty@example.com/hatter", - direction: 0, - type: borogove.MessageType.MessageChannel, - timestamp: "2020-01-01T00:00:04Z", - }); - builder4.sortId = "c0"; - builder4.to = borogove.JID.parse("alice@example.com"); - builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); - builder4.replyTo = [builder.from.asBare()]; - - await persistence.storeMessages("alice@example.com", [ - builder2.build(), - builder4.build(), - builder3.build(), - builder.build(), - ]); - - return await persistence.getMessagesAfter( - "alice@example.com", - "teaparty@example.com", - builder3.build(), - ); - }, { borogove, persistence }); + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "Z~"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + const builder4 = new borogove.ChatMessageBuilder({ + serverId: "4", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:04Z", + }); + builder4.sortId = "c0"; + builder4.to = borogove.JID.parse("alice@example.com"); + builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder4.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder4.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesAfter( + "alice@example.com", + "teaparty@example.com", + builder3.build(), + ); + }, + { borogove, persistence }, + ); expect(result.length).toBe(1); expect(result[0].serverId).toBe("4"); }); test("updateMessageStatus", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const builder = new borogove.ChatMessageBuilder({ - localId: "loc1", - senderId: "alice@example.com", - direction: 1, // MessageSent - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("hatter@example.com"); - builder.from = borogove.JID.parse("alice@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - await persistence.storeMessages("alice@example.com", [builder.build()]); - - const updated = await persistence.updateMessageStatus( - "alice@example.com", - "loc1", - 1, - "Delivered", - ); // MessageDelivered - return { status: updated.status, statusText: updated.statusText }; - }, { borogove, persistence }); + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const builder = new borogove.ChatMessageBuilder({ + localId: "loc1", + senderId: "alice@example.com", + direction: 1, // MessageSent + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("hatter@example.com"); + builder.from = borogove.JID.parse("alice@example.com"); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + await persistence.storeMessages("alice@example.com", [builder.build()]); + + const updated = await persistence.updateMessageStatus( + "alice@example.com", + "loc1", + 1, + "Delivered", + ); // MessageDelivered + return { status: updated.status, statusText: updated.statusText }; + }, + { borogove, persistence }, + ); expect(result.status).toBe(1); expect(result.statusText).toBe("Delivered"); }); - test("removeAccount and listAccounts", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - await persistence.storeLogin("alice@example.com", "client1", "Alice", null); - await persistence.storeLogin("bob@example.com", "client2", "Bob", null); + test("removeAccount and listAccounts", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + await persistence.storeLogin( + "alice@example.com", + "client1", + "Alice", + null, + ); + await persistence.storeLogin("bob@example.com", "client2", "Bob", null); - const accountsBefore = await persistence.listAccounts(); - await persistence.removeAccount("alice@example.com", true); - const accountsAfter = await persistence.listAccounts(); + const accountsBefore = await persistence.listAccounts(); + await persistence.removeAccount("alice@example.com", true); + const accountsAfter = await persistence.listAccounts(); - return { accountsBefore, accountsAfter }; - }, { borogove, persistence }); + return { accountsBefore, accountsAfter }; + }, + { borogove, persistence }, + ); expect(result.accountsBefore).toContain("alice@example.com"); expect(result.accountsBefore).toContain("bob@example.com"); @@ -920,143 +971,169 @@ export function sharedPersistenceTests(test: PersistenceTest) { }); test("getChatUnreadDetails", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.DirectChat.prototype); - chat.chatId = "hatter@example.com"; - chat.readUpToId = "srv1"; - chat.notificationsFiltered = () => false; - - const builder = new borogove.ChatMessageBuilder({ - serverId: "srv1", - serverIdBy: "hatter@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder.sortId = "a0"; - builder.to = borogove.JID.parse("alice@example.com"); - builder.from = borogove.JID.parse("hatter@example.com"); - builder.recipients = [builder.to]; - builder.replyTo = [builder.from]; - - const builder2 = new borogove.ChatMessageBuilder({ - serverId: "srv2", - serverIdBy: "hatter@example.com", - senderId: "hatter@example.com", - direction: 0, - }); - builder2.sortId = "a1"; - builder2.to = borogove.JID.parse("alice@example.com"); - builder2.from = borogove.JID.parse("hatter@example.com"); - builder2.recipients = [builder2.to]; - builder2.replyTo = [builder2.from]; - - await persistence.storeMessages("alice@example.com", [ - builder.build(), - builder2.build(), - ]); - - return await persistence.getChatUnreadDetails("alice@example.com", chat); - }, { borogove, persistence }); - - expect(result.unreadCount).toBe(1); - expect(result.message.serverId).toBe("srv2"); - }); + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const chat = Object.create(borogove.DirectChat.prototype); + chat.chatId = "hatter@example.com"; + chat.readUpToId = "srv1"; + chat.notificationsFiltered = () => false; - test("media storage functions", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - function bufferToByteStream(buffer) { - let done = false; - return new ReadableStream({ - type: "bytes", - async pull(controller) { - if (done) { - controller.close(); - } else { - controller.enqueue(buffer); - done = true; - } - }, + const builder = new borogove.ChatMessageBuilder({ + serverId: "srv1", + serverIdBy: "hatter@example.com", + senderId: "hatter@example.com", + direction: 0, }); - } - - const buffer = new Uint8Array([1, 2, 3]); - const sha256 = await crypto.subtle.digest("SHA-256", buffer.buffer); - await persistence.storeMedia("image/png", bufferToByteStream(buffer)); - const hash = new borogove.Hash("sha-256", sha256); - const hasBefore = await persistence.hasMedia(hash); - await persistence.removeMedia("sha-256", sha256); - const hasAfter = await persistence.hasMedia(hash); - - return { hasBefore, hasAfter }; - }, { borogove, persistence }); - - expect(result.hasBefore).toBe( + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.recipients = [builder.to]; + builder.replyTo = [builder.from]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "srv2", + serverIdBy: "hatter@example.com", + senderId: "hatter@example.com", + direction: 0, + }); + builder2.sortId = "a1"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("hatter@example.com"); + builder2.recipients = [builder2.to]; + builder2.replyTo = [builder2.from]; + + await persistence.storeMessages("alice@example.com", [ + builder.build(), + builder2.build(), + ]); + + return await persistence.getChatUnreadDetails( + "alice@example.com", + chat, + ); + }, + { borogove, persistence }, + ); + + expect(result.unreadCount).toBe(1); + expect(result.message.serverId).toBe("srv2"); + }); + + test("media storage functions", async ({ page, borogove, persistence }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + function bufferToByteStream(buffer) { + let done = false; + return new ReadableStream({ + type: "bytes", + async pull(controller) { + if (done) { + controller.close(); + } else { + controller.enqueue(buffer); + done = true; + } + }, + }); + } + + const buffer = new Uint8Array([1, 2, 3]); + const sha256 = await crypto.subtle.digest("SHA-256", buffer.buffer); + await persistence.storeMedia("image/png", bufferToByteStream(buffer)); + const hash = new borogove.Hash("sha-256", sha256); + const hasBefore = await persistence.hasMedia(hash); + await persistence.removeMedia("sha-256", sha256); + const hasAfter = await persistence.hasMedia(hash); + + return { hasBefore, hasAfter }; + }, + { borogove, persistence }, + ); + + expect(result.hasBefore).toBe( "/.well-known/ni/sha-256/A5BYxvLAy0ksUzsKTRTvd8wPeKvMztUofYShogEc-4E", ); expect(result.hasAfter).toBe(null); }); - test("storeStreamManamagement and getStreamManagement", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - await persistence.storeLogin("alice@example.com", "", "", null); // or updating with SM may not work - await persistence.storeStreamManagement( - "alice@example.com", - new Uint8Array([1, 2, 0, 4]).buffer, - "ZZ", - ); - const result = await persistence.getStreamManagement("alice@example.com"); - return { - smIsArrayBuffer: result.sm instanceof ArrayBuffer, - smIsEq: result.sm - ? indexedDB.cmp(result.sm, new Uint8Array([1, 2, 0, 4]).buffer) - : "null", - sortId: result.sortId, - }; - }, { borogove, persistence }); + test("storeStreamManamagement and getStreamManagement", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + await persistence.storeLogin("alice@example.com", "", "", null); // or updating with SM may not work + await persistence.storeStreamManagement( + "alice@example.com", + new Uint8Array([1, 2, 0, 4]).buffer, + "ZZ", + ); + const result = + await persistence.getStreamManagement("alice@example.com"); + return { + smIsArrayBuffer: result.sm instanceof ArrayBuffer, + smIsEq: result.sm + ? indexedDB.cmp(result.sm, new Uint8Array([1, 2, 0, 4]).buffer) + : "null", + sortId: result.sortId, + }; + }, + { borogove, persistence }, + ); expect(result.smIsEq).toBe(0); expect(result.smIsArrayBuffer).toBe(true); expect(result.sortId).toBe("ZZ"); }); - test("getMembers hydrates persisted member data", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-members-1@example.com"; - chat.getDisplayName = () => "Tea Room"; - - const member = { - id: "room-members-1@example.com/occ-1", - displayName: "Alice", - photoUri: "photo:alice", - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map([ - [ - "laptop", - borogove.Stanza.parse("<presence><show>away</show></presence>"), - ], - ]), - chat: { chatId: "alice@example.com" }, - }; - - await persistence.storeMembers("alice@example.com", chat.chatId, [member]); - const [stored] = await persistence.getMembers( - "alice@example.com", - chat, - false, - ); - - return { - id: stored.id, - displayName: stored.displayName, - chatId: stored.chat?.chatId, - roleIds: stored.roles.map((r) => r.id), - presenceKeys: [...stored.presence.keys()], - showPresence: stored.showPresence, - }; - }, { borogove, persistence }); + test("getMembers hydrates persisted member data", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-members-1@example.com"; + chat.getDisplayName = () => "Tea Room"; + + const member = { + id: "room-members-1@example.com/occ-1", + displayName: "Alice", + photoUri: "photo:alice", + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("alice@example.com"), + presence: new Map([ + [ + "laptop", + borogove.Stanza.parse("<presence><show>away</show></presence>"), + ], + ]), + chat: { chatId: "alice@example.com" }, + }; + + await persistence.storeMembers("alice@example.com", chat.chatId, [ + member, + ]); + const [stored] = await persistence.getMembers( + "alice@example.com", + chat, + false, + ); + + return { + id: stored.id, + displayName: stored.displayName, + chatId: stored.chat?.chatId, + roleIds: stored.roles.map((r) => r.id), + presenceKeys: [...stored.presence.keys()], + showPresence: stored.showPresence, + }; + }, + { borogove, persistence }, + ); expect(result.id).toBe("room-members-1@example.com/occ-1"); expect(result.displayName).toBe("Alice"); @@ -1080,10 +1157,7 @@ export function sharedPersistenceTests(test: PersistenceTest) { persistence, storeIncompleteMember, }) => { - const chat = createChannel( - persistence, - "room-members-7@example.com", - ); + const chat = createChannel(persistence, "room-members-7@example.com"); chat.displayName = "A Chat"; chat.trusted = true; @@ -1124,52 +1198,61 @@ export function sharedPersistenceTests(test: PersistenceTest) { expect(result).toEqual(["Alpha", null]); }); - test("storeMemberUpdates merges existing member data", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-members-2@example.com"; - chat.getDisplayName = () => "Tea Room"; + test("storeMemberUpdates merges existing member data", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-members-2@example.com"; + chat.getDisplayName = () => "Tea Room"; + + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: "room-members-2@example.com/occ-1", + displayName: "Alice", + photoUri: null, + isSelf: false, + roles: [ + { id: "admin", title: "Admin" }, + { id: "urn:xmpp:hats:test", title: "Tea Host" }, + ], + jid: borogove.JID.parse("alice@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "alice@example.com" }, + }, + ]); - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-2@example.com/occ-1", - displayName: "Alice", - photoUri: null, - isSelf: false, - roles: [ - { id: "admin", title: "Admin" }, - { id: "urn:xmpp:hats:test", title: "Tea Host" }, - ], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "alice@example.com" }, - }, - ]); - - const updates = [ - new borogove.MemberUpdate( - "room-members-2@example.com/occ-1", - borogove.JID.parse("alice@example.com"), - "Alice Cooper", + const updates = [ + new borogove.MemberUpdate( + "room-members-2@example.com/occ-1", + borogove.JID.parse("alice@example.com"), + "Alice Cooper", + false, + null, + new Map([["mobile", borogove.Stanza.parse("<presence />")]]), + ), + ]; + + const updated = await persistence.storeMemberUpdates( + "alice@example.com", + chat, + updates, false, - null, - new Map([["mobile", borogove.Stanza.parse("<presence />")]]), - ), - ]; - - const updated = await persistence.storeMemberUpdates( - "alice@example.com", - chat, - updates, - false, - ); - - return { - updatedRoleIds: updated[0].roles.map((r) => r.id), - updatedPresenceKeys: [...updated[0].presence.keys()].sort(), - updatedDisplayName: updated[0].displayName, - }; - }, { borogove, persistence }); + ); + + return { + updatedRoleIds: updated[0].roles.map((r) => r.id), + updatedPresenceKeys: [...updated[0].presence.keys()].sort(), + updatedDisplayName: updated[0].displayName, + }; + }, + { borogove, persistence }, + ); expect(result.updatedRoleIds).toEqual(["urn:xmpp:hats:test"]); expect(result.updatedPresenceKeys).toEqual(["desk", "mobile"]); @@ -1181,57 +1264,62 @@ export function sharedPersistenceTests(test: PersistenceTest) { borogove, persistence, }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-members-2b@example.com"; - chat.getDisplayName = () => "Tea Room"; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-2b@example.com/occ-1", - displayName: "Alice", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map(), - chat: { chatId: "alice@example.com" }, - }, - { - id: "room-members-2b@example.com/occ-2", - displayName: "Bob", - photoUri: null, - isSelf: false, - roles: [{ id: "owner", title: "Owner" }], - jid: borogove.JID.parse("bob@example.com"), - presence: new Map(), - chat: { chatId: "bob@example.com" }, - }, - ]); - - await persistence.storeMemberUpdates( - "alice@example.com", - chat, - [ - new borogove.MemberUpdate( - "room-members-2b@example.com/occ-1", - borogove.JID.parse("alice@example.com"), - "Alice", - false, - null, - new Map(), - ), - ], - true, - ); - const members = await persistence.getMembers( - "alice@example.com", - chat, - true, - ); - - return members.find((m) => m.id.endsWith("occ-2")).roles.map((r) => r.id); - }, { borogove, persistence }); + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-members-2b@example.com"; + chat.getDisplayName = () => "Tea Room"; + + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: "room-members-2b@example.com/occ-1", + displayName: "Alice", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("alice@example.com"), + presence: new Map(), + chat: { chatId: "alice@example.com" }, + }, + { + id: "room-members-2b@example.com/occ-2", + displayName: "Bob", + photoUri: null, + isSelf: false, + roles: [{ id: "owner", title: "Owner" }], + jid: borogove.JID.parse("bob@example.com"), + presence: new Map(), + chat: { chatId: "bob@example.com" }, + }, + ]); + + await persistence.storeMemberUpdates( + "alice@example.com", + chat, + [ + new borogove.MemberUpdate( + "room-members-2b@example.com/occ-1", + borogove.JID.parse("alice@example.com"), + "Alice", + false, + null, + new Map(), + ), + ], + true, + ); + const members = await persistence.getMembers( + "alice@example.com", + chat, + true, + ); + + return members + .find((m) => m.id.endsWith("occ-2")) + .roles.map((r) => r.id); + }, + { borogove, persistence }, + ); expect(result).toEqual([]); }); @@ -1241,225 +1329,294 @@ export function sharedPersistenceTests(test: PersistenceTest) { borogove, persistence, }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat1 = Object.create(borogove.Channel.prototype); - chat1.chatId = "room-members-3@example.com"; - chat1.getDisplayName = () => "Tea Room"; - const chat2 = Object.create(borogove.Channel.prototype); - chat2.chatId = "room-members-4@example.com"; - chat2.getDisplayName = () => "Other Room"; - - await persistence.storeMembers("alice@example.com", chat1.chatId, [ - { - id: "room-members-3@example.com/occ-1", - displayName: "Alice", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "alice@example.com" }, - }, - { - id: "room-members-4@example.com/occ-1", - displayName: "Bob", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("bob@example.com"), - presence: new Map([["phone", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "bob@example.com" }, - }, - ]); - - await persistence.storeMemberUpdates( - "alice@example.com", - chat1, - [ - new borogove.MemberUpdate( - null, - borogove.JID.parse("alice@example.com"), - "Alice Renamed", - false, - null, - new Map(), - ), - ], - false, - ); - const [chat1Member] = await persistence.getMemberDetails( - "alice@example.com", - chat1, - ["room-members-3@example.com/occ-1"], - ); - - return chat1Member.displayName; - }, { borogove, persistence }); + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const chat1 = Object.create(borogove.Channel.prototype); + chat1.chatId = "room-members-3@example.com"; + chat1.getDisplayName = () => "Tea Room"; + const chat2 = Object.create(borogove.Channel.prototype); + chat2.chatId = "room-members-4@example.com"; + chat2.getDisplayName = () => "Other Room"; + + await persistence.storeMembers("alice@example.com", chat1.chatId, [ + { + id: "room-members-3@example.com/occ-1", + displayName: "Alice", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("alice@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "alice@example.com" }, + }, + { + id: "room-members-4@example.com/occ-1", + displayName: "Bob", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("bob@example.com"), + presence: new Map([ + ["phone", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "bob@example.com" }, + }, + ]); + + await persistence.storeMemberUpdates( + "alice@example.com", + chat1, + [ + new borogove.MemberUpdate( + null, + borogove.JID.parse("alice@example.com"), + "Alice Renamed", + false, + null, + new Map(), + ), + ], + false, + ); + const [chat1Member] = await persistence.getMemberDetails( + "alice@example.com", + chat1, + ["room-members-3@example.com/occ-1"], + ); + + return chat1Member.displayName; + }, + { borogove, persistence }, + ); expect(result).toBe("Alice Renamed"); }); - test("clearMemberPresence only clears the targeted chat", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat1 = Object.create(borogove.Channel.prototype); - chat1.chatId = "room-members-4a@example.com"; - chat1.getDisplayName = () => "Tea Room"; - const chat2 = Object.create(borogove.Channel.prototype); - chat2.chatId = "room-members-4b@example.com"; - chat2.getDisplayName = () => "Other Room"; - - await persistence.storeMembers("alice@example.com", chat1.chatId, [ - { - id: "room-members-4a@example.com/occ-1", - displayName: "Alice", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alice@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "alice@example.com" }, - }, - ]); - await persistence.storeMembers("alice@example.com", chat2.chatId, [ - { - id: "room-members-4b@example.com/occ-1", - displayName: "Bob", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("bob@example.com"), - presence: new Map([["phone", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "bob@example.com" }, - }, - ]); - - await persistence.clearMemberPresence("alice@example.com", chat1.chatId); - const [chat1Member] = await persistence.getMemberDetails( - "alice@example.com", - chat1, - ["room-members-4a@example.com/occ-1"], - ); - const [chat2Member] = await persistence.getMemberDetails( - "alice@example.com", - chat2, - ["room-members-4b@example.com/occ-1"], - ); - - return { - chat1PresenceKeys: [...chat1Member.presence.keys()], - chat2PresenceKeys: [...chat2Member.presence.keys()], - }; - }, { borogove, persistence }); + test("clearMemberPresence only clears the targeted chat", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const chat1 = Object.create(borogove.Channel.prototype); + chat1.chatId = "room-members-4a@example.com"; + chat1.getDisplayName = () => "Tea Room"; + const chat2 = Object.create(borogove.Channel.prototype); + chat2.chatId = "room-members-4b@example.com"; + chat2.getDisplayName = () => "Other Room"; + + await persistence.storeMembers("alice@example.com", chat1.chatId, [ + { + id: "room-members-4a@example.com/occ-1", + displayName: "Alice", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("alice@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "alice@example.com" }, + }, + ]); + await persistence.storeMembers("alice@example.com", chat2.chatId, [ + { + id: "room-members-4b@example.com/occ-1", + displayName: "Bob", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("bob@example.com"), + presence: new Map([ + ["phone", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "bob@example.com" }, + }, + ]); + + await persistence.clearMemberPresence( + "alice@example.com", + chat1.chatId, + ); + const [chat1Member] = await persistence.getMemberDetails( + "alice@example.com", + chat1, + ["room-members-4a@example.com/occ-1"], + ); + const [chat2Member] = await persistence.getMemberDetails( + "alice@example.com", + chat2, + ["room-members-4b@example.com/occ-1"], + ); + + return { + chat1PresenceKeys: [...chat1Member.presence.keys()], + chat2PresenceKeys: [...chat2Member.presence.keys()], + }; + }, + { borogove, persistence }, + ); expect(result.chat1PresenceKeys).toEqual([]); expect(result.chat2PresenceKeys).toEqual(["phone"]); }); - test("getMembers includes moderator-visible rows", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-members-6@example.com"; - chat.getDisplayName = () => "Tea Room"; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-members-6@example.com/owner", - displayName: "Zulu", - photoUri: null, - isSelf: false, - roles: [{ id: "owner", title: "Owner" }], - jid: borogove.JID.parse("zulu@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "zulu@example.com" }, - }, - { - id: "room-members-6@example.com/outcast", - displayName: "Banned", - photoUri: null, - isSelf: false, - roles: [{ id: "outcast", title: "Banned" }], - jid: borogove.JID.parse("banned@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "banned@example.com" }, - }, - { - id: "room-members-6@example.com/guest-offline", - displayName: "Guest", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Guest" }], - jid: borogove.JID.parse("guest@example.com"), - presence: new Map([ - ["desk", borogove.Stanza.parse('<presence type="unavailable" />')], - ]), - chat: { chatId: "guest@example.com" }, - }, - { - id: "room-members-6@example.com/admin", - displayName: "Alpha", - photoUri: null, - isSelf: false, - roles: [{ id: "admin", title: "Admin" }], - jid: borogove.JID.parse("alpha@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "alpha@example.com" }, - }, - ]); - - const moderator = await persistence.getMembers( - "alice@example.com", - chat, - true, - ); - return moderator.map((m) => m.displayName); - }, { borogove, persistence }); + test("getMembers includes moderator-visible rows", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-members-6@example.com"; + chat.getDisplayName = () => "Tea Room"; + + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: "room-members-6@example.com/owner", + displayName: "Zulu", + photoUri: null, + isSelf: false, + roles: [{ id: "owner", title: "Owner" }], + jid: borogove.JID.parse("zulu@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "zulu@example.com" }, + }, + { + id: "room-members-6@example.com/outcast", + displayName: "Banned", + photoUri: null, + isSelf: false, + roles: [{ id: "outcast", title: "Banned" }], + jid: borogove.JID.parse("banned@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "banned@example.com" }, + }, + { + id: "room-members-6@example.com/guest-offline", + displayName: "Guest", + photoUri: null, + isSelf: false, + roles: [{ id: "none", title: "Guest" }], + jid: borogove.JID.parse("guest@example.com"), + presence: new Map([ + [ + "desk", + borogove.Stanza.parse('<presence type="unavailable" />'), + ], + ]), + chat: { chatId: "guest@example.com" }, + }, + { + id: "room-members-6@example.com/admin", + displayName: "Alpha", + photoUri: null, + isSelf: false, + roles: [{ id: "admin", title: "Admin" }], + jid: borogove.JID.parse("alpha@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "alpha@example.com" }, + }, + ]); + + const moderator = await persistence.getMembers( + "alice@example.com", + chat, + true, + ); + return moderator.map((m) => m.displayName); + }, + { borogove, persistence }, + ); expect(result).toEqual(["Zulu", "Alpha", "Banned"]); }); - test("storeVoiceRequest and listVoiceRequests", async ({ page, borogove, persistence }) => { - const result = await page.evaluate(async ({ borogove, persistence }) => { - const chat = Object.create(borogove.Channel.prototype); - chat.chatId = "room-voice-requests@example.com"; - chat.getDisplayName = () => "Tea Room"; - - await persistence.storeMembers("alice@example.com", chat.chatId, [ - { - id: "room-voice-requests@example.com/occ-1", - displayName: "Bob", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Participant" }], - jid: borogove.JID.parse("bob@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "bob@example.com" }, - }, - { - id: "room-voice-requests@example.com/occ-2", - displayName: "Charlie", - photoUri: null, - isSelf: false, - roles: [{ id: "none", title: "Participant" }], - jid: borogove.JID.parse("charlie@example.com"), - presence: new Map([["desk", borogove.Stanza.parse("<presence />")]]), - chat: { chatId: "charlie@example.com" }, - } - ]); + test("storeVoiceRequest and listVoiceRequests", async ({ + page, + borogove, + persistence, + }) => { + const result = await page.evaluate( + async ({ borogove, persistence }) => { + const chat = Object.create(borogove.Channel.prototype); + chat.chatId = "room-voice-requests@example.com"; + chat.getDisplayName = () => "Tea Room"; - await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", true); - await persistence.storeVoiceRequest("alice@example.com", chat, "charlie@example.com", true); + await persistence.storeMembers("alice@example.com", chat.chatId, [ + { + id: "room-voice-requests@example.com/occ-1", + displayName: "Bob", + photoUri: null, + isSelf: false, + roles: [{ id: "none", title: "Participant" }], + jid: borogove.JID.parse("bob@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "bob@example.com" }, + }, + { + id: "room-voice-requests@example.com/occ-2", + displayName: "Charlie", + photoUri: null, + isSelf: false, + roles: [{ id: "none", title: "Participant" }], + jid: borogove.JID.parse("charlie@example.com"), + presence: new Map([ + ["desk", borogove.Stanza.parse("<presence />")], + ]), + chat: { chatId: "charlie@example.com" }, + }, + ]); - const requests1 = await persistence.listVoiceRequests("alice@example.com", chat); + await persistence.storeVoiceRequest( + "alice@example.com", + chat, + "bob@example.com", + true, + ); + await persistence.storeVoiceRequest( + "alice@example.com", + chat, + "charlie@example.com", + true, + ); - await persistence.storeVoiceRequest("alice@example.com", chat, "bob@example.com", false); + const requests1 = await persistence.listVoiceRequests( + "alice@example.com", + chat, + ); - const requests2 = await persistence.listVoiceRequests("alice@example.com", chat); + await persistence.storeVoiceRequest( + "alice@example.com", + chat, + "bob@example.com", + false, + ); - return { - requests1: requests1.map((m) => m.displayName).sort(), - requests2: requests2.map((m) => m.displayName).sort(), - }; - }, { borogove, persistence }); + const requests2 = await persistence.listVoiceRequests( + "alice@example.com", + chat, + ); + + return { + requests1: requests1.map((m) => m.displayName).sort(), + requests2: requests2.map((m) => m.displayName).sort(), + }; + }, + { borogove, persistence }, + ); expect(result.requests1).toEqual(["Bob", "Charlie"]); expect(result.requests2).toEqual(["Charlie"]); @@ -1478,10 +1635,7 @@ export function sharedPersistenceTests(test: PersistenceTest) { expect(result).toBeNull(); }); - test("storeOmemoId stores the ID", async ({ - page, - persistence, - }) => { + test("storeOmemoId stores the ID", async ({ page, persistence }) => { const account = "omemo-existing@example.com"; const omemoId = 12345; const result = await page.evaluate( @@ -1501,9 +1655,7 @@ export function sharedPersistenceTests(test: PersistenceTest) { }) => { const result = await page.evaluate( async (persistence) => - persistence.getOmemoIdentityKey( - "omemo-identity-not-found@example.com", - ), + persistence.getOmemoIdentityKey("omemo-identity-not-found@example.com"), persistence, ); @@ -1540,9 +1692,7 @@ export function sharedPersistenceTests(test: PersistenceTest) { }) => { const result = await page.evaluate( async (persistence) => - persistence.getOmemoDeviceList( - "omemo-devices-not-found@example.com", - ), + persistence.getOmemoDeviceList("omemo-devices-not-found@example.com"), persistence, ); @@ -1623,7 +1773,10 @@ export function sharedPersistenceTests(test: PersistenceTest) { await persistence.removeOmemoPreKey(identifier, keyId); const afterRemove = await persistence.getOmemoPreKey(identifier, keyId); - return { loadedKeyPair: pageHelpers.buffersToKeyPair(loadedKeyPair), afterRemove }; + return { + loadedKeyPair: pageHelpers.buffersToKeyPair(loadedKeyPair), + afterRemove, + }; }, { persistence, identifier, keyId, keyPair, pageHelpers }, ); @@ -1859,10 +2012,7 @@ const makeKeyPair = (): TestKeyPair => ({ pubKey: makeKey().reverse(), }); -function expectKeyPair( - actual: TestKeyPair, - expected: TestKeyPair, -) { +function expectKeyPair(actual: TestKeyPair, expected: TestKeyPair) { expectKey(actual.privKey, expected.privKey); expectKey(actual.pubKey, expected.pubKey); } diff --git a/test/sqlite.spec.ts b/test/sqlite.spec.ts index 22af01e..d60e4a5 100644 --- a/test/sqlite.spec.ts +++ b/test/sqlite.spec.ts @@ -91,5 +91,4 @@ test.describe("not webkit", () => { expect(result).toEqual(["Zulu", "Alpha"]); }); - });