git » sdk » commit 31282d4

Allow storing sent messages and MAM messages

author Stephen Paul Weber
2023-11-01 15:51:17 UTC
committer Stephen Paul Weber
2023-11-01 15:52:18 UTC
parent 8791e454165bb520f9bd8fcf011c22857016499d

Allow storing sent messages and MAM messages

We don't know the serverId (MAM ID) of sent messages, so we need to use
localId as the key for them instead.  The serverId of MAM messages is in
a different scope so need to mark them differently as well (using serverIdBy).

xmpp/Chat.hx +13 -0
xmpp/ChatMessage.hx +17 -3
xmpp/persistence/browser.js +42 -29

diff --git a/xmpp/Chat.hx b/xmpp/Chat.hx
index b8d539e..2267c0b 100644
--- a/xmpp/Chat.hx
+++ b/xmpp/Chat.hx
@@ -215,7 +215,13 @@ class DirectChat extends Chat {
 
 	public function sendMessage(message:ChatMessage):Void {
 		client.chatActivity(this);
+		message.timestamp = message.timestamp ?? Date.format(std.Date.now());
+		message.direction = MessageSent;
+		message.from = JID.parse(client.jid);
+		message.sender = message.from.asBare();
+		message.replyTo = [message.sender];
 		message.recipients = getParticipants().map((p) -> JID.parse(p));
+		persistence.storeMessage(client.accountId(), message);
 		for (recipient in message.recipients) {
 			message.to = recipient;
 			client.sendStanza(message.asStanza());
@@ -374,7 +380,14 @@ class Channel extends Chat {
 
 	public function sendMessage(message:ChatMessage):Void {
 		client.chatActivity(this);
+		message.timestamp = message.timestamp ?? Date.format(std.Date.now());
+		message.direction = MessageSent;
+		message.from = JID.parse(client.jid);
+		message.sender = getFullJid();
+		message.replyTo = [message.sender];
 		message.to = JID.parse(chatId);
+		message.recipients = [message.to];
+		persistence.storeMessage(client.accountId(), message);
 		client.sendStanza(message.asStanza("groupchat"));
 	}
 
diff --git a/xmpp/ChatMessage.hx b/xmpp/ChatMessage.hx
index 2670d25..5247f54 100644
--- a/xmpp/ChatMessage.hx
+++ b/xmpp/ChatMessage.hx
@@ -20,16 +20,17 @@ class ChatAttachment {
 class ChatMessage {
 	public var localId (default, set) : Null<String> = null;
 	public var serverId (default, set) : Null<String> = null;
+	public var serverIdBy : Null<String> = null;
 
 	public var timestamp (default, set) : Null<String> = null;
 
 	public var to: Null<JID> = null;
-	private var from: Null<JID> = null;
+	public var from: Null<JID> = null;
 	public var sender: Null<JID> = null;
 	public var recipients: Array<JID> = [];
 	public var replyTo: Array<JID> = [];
 
-	var threadId (default, null): Null<String> = null;
+	public var threadId (default, null): Null<String> = null;
 
 	public var attachments : Array<ChatAttachment> = [];
 
@@ -41,7 +42,10 @@ class ChatMessage {
 	public function new() { }
 
 	public static function fromStanza(stanza:Stanza, localJidStr:String):Null<ChatMessage> {
+		if (stanza.attr.get("type") == "error") return null;
+
 		var msg = new ChatMessage();
+		msg.timestamp = stanza.findText("{urn:xmpp:delay}delay@stamp") ?? Date.format(std.Date.now());
 		msg.lang = stanza.attr.get("xml:lang");
 		msg.text = stanza.getChildText("body");
 		if (msg.text != null && (msg.lang == null || msg.lang == "")) {
@@ -67,12 +71,22 @@ class ChatMessage {
 
 		final localId = stanza.attr.get("id");
 		if (localId != null) msg.localId = localId;
+		var altServerId = null;
 		for (stanzaId in stanza.allTags("stanza-id", "urn:xmpp:sid:0")) {
 			final id = stanzaId.attr.get("id");
 			if ((stanzaId.attr.get("by") == domain || stanzaId.attr.get("by") == localJidBare.asString()) && id != null) {
+				msg.serverIdBy = localJidBare.asString();
 				msg.serverId = id;
 				break;
 			}
+			altServerId = stanzaId;
+		}
+		if (msg.serverId == null && altServerId != null && stanza.attr.get("type") != "error") {
+			final id = altServerId.attr.get("id");
+			if (id != null) {
+				msg.serverId = id;
+				msg.serverIdBy = altServerId.attr.get("by");
+			}
 		}
 		msg.direction = (msg.to == null || msg.to.asBare().equals(localJidBare)) ? MessageReceived : MessageSent;
 
@@ -139,7 +153,7 @@ class ChatMessage {
 	}
 
 	public function set_serverId(serverId:String):String {
-		if(this.serverId != null) {
+		if(this.serverId != null && this.serverId != serverId) {
 			throw new Exception("Message already has a serverId set");
 		}
 		return this.serverId = serverId;
diff --git a/xmpp/persistence/browser.js b/xmpp/persistence/browser.js
index c10a2a3..7bd9498 100644
--- a/xmpp/persistence/browser.js
+++ b/xmpp/persistence/browser.js
@@ -10,8 +10,7 @@ exports.xmpp.persistence = {
 			dbOpenReq.onupgradeneeded = (event) => {
 				const upgradeDb = event.target.result;
 				if (!db.objectStoreNames.contains("messages")) {
-					const messages = upgradeDb.createObjectStore("messages", { keyPath: "serverId" });
-					messages.createIndex("account", ["account", "timestamp"]);
+					const messages = upgradeDb.createObjectStore("messages", { keyPath: ["account", "serverIdBy", "serverId", "localId"] });
 					messages.createIndex("chats", ["account", "chatId", "timestamp"]);
 					messages.createIndex("localId", ["account", "chatId", "localId"]);
 				}
@@ -47,24 +46,47 @@ exports.xmpp.persistence = {
 			});
 		}
 
+		function hydrateMessage(value) {
+			const message = new xmpp.ChatMessage();
+			message.localId = value.localId ? value.localId : null;
+			message.serverId = value.serverId ? value.serverId : null;
+			message.serverIdBy = value.serverIdBy ? value.serverIdBy : null;
+			message.timestamp = value.timestamp && value.timestamp.toISOString();
+			message.to = value.to && xmpp.JID.parse(value.to);
+			message.from = value.from && xmpp.JID.parse(value.from);
+			message.sender = value.sender && xmpp.JID.parse(value.sender);
+			message.recipients = value.recipients.map((r) => xmpp.JID.parse(r));
+			message.replyTo = value.replyTo.map((r) => xmpp.JID.parse(r));
+			message.threadId = value.threadId;
+			message.attachments = value.attachments;
+			message.text = value.text;
+			message.lang = value.lang;
+			message.direction = value.direction == "MessageReceived" ? xmpp.MessageDirection.MessageReceived : xmpp.MessageDirection.MessageSent;
+			return message;
+		}
+
 		return {
 			lastId: function(account, jid, callback) {
 				const tx = db.transaction(["messages"], "readonly");
 				const store = tx.objectStore("messages");
 				var cursor = null;
 				if (jid === null) {
-					cursor = store.index("account").openCursor(
-					IDBKeyRange.bound([account, new Date(0)], [account, new Date("9999-01-01")]),
-					"prev"
+					cursor = store.index("chats").openCursor(
+						IDBKeyRange.bound([account], [account, [], []]),
+						"prev"
 					);
 				} else {
 					cursor = store.index("chats").openCursor(
-						IDBKeyRange.bound([account, jid, new Date(0)], [account, jid, new Date("9999-01-01")]),
+						IDBKeyRange.bound([account, jid], [account, jid, []]),
 						"prev"
 					);
 				}
 				cursor.onsuccess = (event) => {
-					callback(event.target.result ? event.target.result.value.serverId : null);
+					if (!event.target.result || (event.target.result.value.serverId && (jid || event.target.result.value.serverIdBy === account))) {
+						callback(event.target.result ? event.target.result.value.serverId : null);
+					} else {
+						event.target.result.continue();
+					}
 				}
 				cursor.onerror = (event) => {
 					console.error(event);
@@ -110,11 +132,16 @@ exports.xmpp.persistence = {
 			storeMessage: function(account, message) {
 				const tx = db.transaction(["messages"], "readwrite");
 				const store = tx.objectStore("messages");
-				promisifyRequest(store.index("localId").get([account, message.chatId(), message.localId])).then((result) => {
-					if (result && message.direction === xmpp.MessageDirection.MessageSent && result.direction === "MessageSent") return; // duplicate, we trust our own stanza ids
+				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";
+				promisifyRequest(store.index("localId").get([account, message.chatId(), message.localId || []])).then((result) => {
+					if (result && !message.isIncoming() && result.direction === "MessageSent") return; // duplicate, we trust our own stanza ids
 
 					store.put({
 						...message,
+						serverId: message.serverId || "",
+						serverIdBy: message.serverIdBy || "",
+						localId: message.localId || "",
 						account: account,
 						chatId: message.chatId(),
 						to: message.to?.asString(),
@@ -128,41 +155,27 @@ exports.xmpp.persistence = {
 				});
 			},
 
-			getMessages: function(account, chatId, _beforeId, beforeTime, callback) {
-				const beforeDate = beforeTime ? new Date(beforeTime) : new Date("9999-01-01");
+			getMessages: function(account, chatId, beforeId, beforeTime, callback) {
+				const beforeDate = beforeTime ? new Date(beforeTime) : [];
 				const tx = db.transaction(["messages"], "readonly");
 				const store = tx.objectStore("messages");
 				const cursor = store.index("chats").openCursor(
-					IDBKeyRange.bound([account, chatId, new Date(0)], [account, chatId, beforeDate]),
+					IDBKeyRange.bound([account, chatId], [account, chatId, beforeDate]),
 					"prev"
 				);
 				const result = [];
 				cursor.onsuccess = (event) => {
 					if (event.target.result && result.length < 50) {
 						const value = event.target.result.value;
-						if (value.timestamp && value.timestamp.getTime() === beforeDate.getTime()) {
+						if (value.serverId === beforeId || (value.timestamp && value.timestamp.getTime() === (beforeDate instanceof Date && beforeDate.getTime()))) {
 							event.target.result.continue();
 							return;
 						}
 
-						const message = new xmpp.ChatMessage();
-						message.localId = value.localId;
-						message.serverId = value.serverId;
-						message.timestamp = value.timestamp && value.timestamp.toISOString();
-						message.to = value.to && xmpp.JID.parse(value.to);
-						message.from = value.from && xmpp.JID.parse(value.from);
-						message.sender = value.sender && xmpp.JID.parse(value.sender);
-						message.recipients = value.recipients.map((r) => xmpp.JID.parse(r));
-						message.replyTo = value.replyTo.map((r) => xmpp.JID.parse(r));
-						message.threadId = value.threadId;
-						message.attachments = value.attachments;
-						message.text = value.text;
-						message.lang = value.lang;
-						message.direction = value.direction == "MessageReceived" ? xmpp.MessageDirection.MessageReceived : xmpp.MessageDirection.MessageSent;
-						result.push(message);
+						result.unshift(hydrateMessage(value));
 						event.target.result.continue();
 					} else {
-						callback(result.reverse());
+						callback(result);
 					}
 				}
 				cursor.onerror = (event) => {