| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-10-11 18:40:23 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-10-11 18:40:23 UTC |
| parent | bac32e87592ebf08a0aebf2854600ecba17e5cf0 |
| 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();