git » sdk » commit e52cfd0

Basic support for group DM

author Stephen Paul Weber
2023-10-11 18:40:23 UTC
committer Stephen Paul Weber
2023-10-11 18:40:23 UTC
parent bac32e87592ebf08a0aebf2854600ecba17e5cf0

Basic support for group DM

xmpp/Chat.hx +13 -3
xmpp/ChatMessage.hx +74 -2
xmpp/JID.hx +1 -0
xmpp/persistence/browser.js +22 -9

diff --git a/xmpp/Chat.hx b/xmpp/Chat.hx
index 8458fb5..0eda2d2 100644
--- a/xmpp/Chat.hx
+++ b/xmpp/Chat.hx
@@ -42,6 +42,8 @@ abstract class Chat {
 
 	abstract public function getDisplayName():String;
 
+	abstract public function getParticipants():Array<String>;
+
 	public function isDirectChat():Bool { return type.match(ChatTypeDirect); };
 	public function isGroupChat():Bool  { return type.match(ChatTypeGroup);  };
 	public function isPublicChat():Bool { return type.match(ChatTypePublic); };
@@ -156,6 +158,10 @@ class DirectChat extends Chat {
 		return this.displayName;
 	}
 
+	public function getParticipants() {
+		return chatId.split("\n");
+	}
+
 	public function getMessages(beforeId:Null<String>, beforeTime:Null<String>, handler:(Array<ChatMessage>)->Void):Void {
 		persistence.getMessages(client.jid, chatId, beforeId, beforeTime, (messages) -> {
 			if (messages.length > 0) {
@@ -166,9 +172,9 @@ class DirectChat extends Chat {
 				var sync = new MessageSync(this.client, this.stream, filter);
 				sync.onMessages((messages) -> {
 					for (message in messages.messages) {
-						persistence.storeMessage(chatId, message);
+						persistence.storeMessage(client.jid, message);
 					}
-					handler(messages.messages);
+					handler(messages.messages.filter((m) -> m.chatId() == chatId));
 				});
 				sync.fetchNext();
 			}
@@ -177,7 +183,11 @@ class DirectChat extends Chat {
 
 	public function sendMessage(message:ChatMessage):Void {
 		client.chatActivity(this);
-		client.sendStanza(message.asStanza());
+		message.recipients = getParticipants().map((p) -> JID.parse(p));
+		for (recipient in message.recipients) {
+			message.to = recipient;
+			client.sendStanza(message.asStanza());
+		}
 	}
 
 	public function bookmark() {
diff --git a/xmpp/ChatMessage.hx b/xmpp/ChatMessage.hx
index 3746f99..4ddbc9f 100644
--- a/xmpp/ChatMessage.hx
+++ b/xmpp/ChatMessage.hx
@@ -1,6 +1,7 @@
 package xmpp;
 
 import haxe.Exception;
+using Lambda;
 
 import xmpp.JID;
 
@@ -22,8 +23,11 @@ class ChatMessage {
 
 	public var timestamp (default, set) : Null<String> = null;
 
-	private var to: Null<JID> = null;
+	public var to: Null<JID> = null;
 	private var from: Null<JID> = null;
+	private var sender: Null<JID> = null;
+	public var recipients: Array<JID> = [];
+	private var replyTo: Array<JID> = [];
 
 	var threadId (default, null): Null<String> = null;
 
@@ -47,6 +51,7 @@ class ChatMessage {
 		msg.to = to == null ? null : JID.parse(to);
 		final from = stanza.attr.get("from");
 		msg.from = from == null ? null : JID.parse(from);
+		msg.sender = msg.from;
 		final localJid = JID.parse(localJidStr);
 		final localJidBare = localJid.asBare();
 		final domain = localJid.domain;
@@ -60,6 +65,8 @@ class ChatMessage {
 			}
 		}
 
+		final localId = stanza.attr.get("id");
+		if (localId != null) msg.localId = localId;
 		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) {
@@ -69,6 +76,56 @@ class ChatMessage {
 		}
 		msg.direction = (msg.to == null || msg.to.asBare().equals(localJidBare)) ? MessageReceived : MessageSent;
 
+		final recipients: Map<String, Bool> = [];
+		final replyTo: Map<String, Bool> = [];
+		if (msg.to != null) {
+			recipients[msg.to.asBare().asString()] = true;
+		}
+		if (msg.direction == MessageReceived && msg.from != null) {
+			replyTo[msg.from.asString()] = true;
+		} else if(msg.to != null) {
+			replyTo[msg.to.asString()] = true;
+		}
+
+		final addresses = stanza.getChild("addresses", "http://jabber.org/protocol/address");
+		var anyExtendedReplyTo = false;
+		if (addresses != null) {
+			for (address in addresses.allTags("address")) {
+				final jid = address.attr.get("jid");
+				if (address.attr.get("type") == "noreply") {
+					replyTo.clear();
+				} else if (jid == null) {
+					trace("No support for addressing to non-jid", address);
+					return null;
+				} else if (address.attr.get("type") == "to" || address.attr.get("type") == "cc") {
+					recipients[JID.parse(jid).asBare().asString()] = true;
+					if (!anyExtendedReplyTo) replyTo[JID.parse(jid).asString()] = true; // reply all
+				} else if (address.attr.get("type") == "replyto" || address.attr.get("type") == "replyroom") {
+					if (!anyExtendedReplyTo) {
+						replyTo.clear();
+						anyExtendedReplyTo = true;
+					}
+					replyTo[JID.parse(jid).asString()] = true;
+				} else if (address.attr.get("type") == "ofrom") {
+					if (JID.parse(jid).domain == msg.sender?.domain) {
+						// TODO: check that domain supports extended addressing
+						msg.sender = JID.parse(jid);
+					}
+				}
+			}
+		}
+
+		msg.recipients = ({ iterator: () -> recipients.keys() }).map((s) -> JID.parse(s));
+		msg.recipients.sort((x, y) -> Reflect.compare(x.asString(), y.asString()));
+		msg.replyTo = ({ iterator: () -> replyTo.keys() }).map((s) -> JID.parse(s));
+		msg.replyTo.sort((x, y) -> Reflect.compare(x.asString(), y.asString()));
+
+		final msgFrom = msg.from;
+		if (msg.direction == MessageReceived && msgFrom != null && msg.replyTo.find((r) -> r.asBare().equals(msgFrom.asBare())) == null) {
+			trace("Don't know what chat message without from in replyTo belongs in", stanza);
+			return null;
+		}
+
 		if (msg.text == null) return null;
 
 		return msg;
@@ -93,7 +150,15 @@ class ChatMessage {
 	}
 
 	public function chatId():String {
-		return (isIncoming() ? from?.asBare()?.asString() : to?.asBare()?.asString()) ?? throw "from or to is null";
+		if (isIncoming()) {
+			return replyTo.map((r) -> r.asBare().asString()).join("\n");
+		} else {
+			return recipients.map((r) -> r.asString()).join("\n");
+		}
+	}
+
+	public function senderId():String {
+		return sender?.asBare().asString() ?? throw "sender is null";
 	}
 
 	public function account():String {
@@ -110,6 +175,13 @@ class ChatMessage {
 		if (to != null) attrs.set("to", to.asString());
 		if (localId != null) attrs.set("id", localId);
 		var stanza = new Stanza("message", attrs);
+		if (recipients.length > 1) {
+			final addresses = stanza.tag("addresses", { xmlns: "http://jabber.org/protocol/address" });
+			for (recipient in recipients) {
+				addresses.tag("address", { type: "to", jid: recipient.asString(), delivered: "true" }).up();
+			}
+			addresses.up();
+		}
 		if (text != null) stanza.textTag("body", text);
 		return stanza;
 	}
diff --git a/xmpp/JID.hx b/xmpp/JID.hx
index cedb018..511ed46 100644
--- a/xmpp/JID.hx
+++ b/xmpp/JID.hx
@@ -1,5 +1,6 @@
 package xmpp;
 
+@:expose
 class JID {
 	public final node : Null<String>;
 	public final domain : String;
diff --git a/xmpp/persistence/browser.js b/xmpp/persistence/browser.js
index a58c4c6..5fa6b7b 100644
--- a/xmpp/persistence/browser.js
+++ b/xmpp/persistence/browser.js
@@ -13,6 +13,7 @@ exports.xmpp.persistence = {
 					const store = upgradeDb.createObjectStore("messages", { keyPath: "serverId" });
 					store.createIndex("account", ["account", "timestamp"]);
 					store.createIndex("chats", ["account", "chatId", "timestamp"]);
+					store.createIndex("localId", ["account", "chatId", "localId"]);
 				}
 				if (!db.objectStoreNames.contains("keyvaluepairs")) {
 					upgradeDb.createObjectStore("keyvaluepairs");
@@ -71,12 +72,21 @@ exports.xmpp.persistence = {
 			storeMessage: function(account, message) {
 				const tx = db.transaction(["messages"], "readwrite");
 				const store = tx.objectStore("messages");
-				store.put({
-					...message,
-					account: account,
-					chatId: message.chatId(),
-					timestamp: new Date(message.timestamp),
-					direction: message.direction.toString()
+				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
+
+					store.put({
+						...message,
+						account: account,
+						chatId: message.chatId(),
+						to: message.to?.asString(),
+						from: message.from?.asString(),
+						sender: message.sender?.asString(),
+						recipients: message.recipients.map((r) => r.asString()),
+						replyTo: message.replyTo.map((r) => r.asString()),
+						timestamp: new Date(message.timestamp),
+						direction: message.direction.toString()
+					});
 				});
 			},
 
@@ -101,12 +111,15 @@ exports.xmpp.persistence = {
 						message.localId = value.localId;
 						message.serverId = value.serverId;
 						message.timestamp = value.timestamp && value.timestamp.toISOString();
-						message.to = value.to;
-						message.from = value.from;
+						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.replyTo = value.replyTo;
 						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);
 						event.target.result.continue();