| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-11-29 03:41:23 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-11-29 03:41:23 UTC |
| parent | ac18ccd2448c2e683b8f644f1ca368a739b93a43 |
| borogove/Chat.hx | +38 | -6 |
| borogove/Client.hx | +54 | -6 |
| borogove/Message.hx | +19 | -0 |
diff --git a/borogove/Chat.hx b/borogove/Chat.hx index 9d55226..94eb96f 100644 --- a/borogove/Chat.hx +++ b/borogove/Chat.hx @@ -32,6 +32,7 @@ enum abstract UiState(Int) { var Pinned; var Open; // or Unspecified var Closed; // Archived + var Invited; } enum abstract UserState(Int) { @@ -335,7 +336,7 @@ abstract class Chat { Pin or unpin this chat **/ public function togglePinned(): Void { - uiState = uiState == Pinned ? Open : Pinned; + uiState = uiState != Pinned ? Pinned : Open; persistence.storeChats(client.accountId(), [this]); client.sortChats(); client.trigger("chats/update", [this]); @@ -441,6 +442,7 @@ abstract class Chat { private function updateFromRoster(item: { fn: Null<String>, subscription: String }) { setTrusted(item.subscription == "both" || item.subscription == "from"); if (item.fn != null && item.fn != "") displayName = item.fn; + if (uiState == Invited) uiState = Open; } /** @@ -520,9 +522,11 @@ abstract class Chat { p == null || p.isSelf ? null : p.displayName; }).filter(fn -> fn != null).join(", "); } + } else if (uiState == Invited) { + return '${displayName} (${chatId})'; } - return this.displayName; + return displayName; } @:allow(borogove) @@ -575,6 +579,10 @@ abstract class Chat { **/ public function setTrusted(trusted:Bool) { this.trusted = trusted; + if (trusted && uiState == Invited) { + uiState = Open; + client.trigger("chats/update", [this]); + } } /** @@ -628,6 +636,7 @@ abstract class Chat { @param video do we want video in this call **/ public function startCall(audio: Bool, video: Bool) { + if (uiState == Invited) uiState = Open; final session = new OutgoingProposedSession(client, JID.parse(chatId)); jingleSessions.set(session.sid, session); session.propose(audio, video); @@ -644,6 +653,7 @@ abstract class Chat { Accept any incoming calls in this Chat **/ public function acceptCall() { + if (uiState == Invited) uiState = Open; for (session in jingleSessions) { session.accept(); } @@ -968,6 +978,7 @@ class DirectChat extends Chat { @HaxeCBridge.noemit // on superclass as abstract public function sendMessage(message: ChatMessageBuilder):Void { + if (uiState == Invited) uiState = Open; if (typingTimer != null) typingTimer.stop(); client.chatActivity(this); message = prepareOutgoingMessage(message); @@ -1095,6 +1106,10 @@ class DirectChat extends Chat { @HaxeCBridge.noemit // on superclass as abstract public function bookmark() { + if (uiState == Invited) { + uiState = Open; + client.trigger("chats/update", [this]); + } final attr: DynamicAccess<String> = { jid: chatId }; if (displayName != null && displayName != "" && displayName != chatId) { attr["name"] = displayName; @@ -1134,7 +1149,10 @@ class DirectChat extends Chat { @HaxeCBridge.noemit // on superclass as abstract public function close() { if (typingTimer != null) typingTimer.stop(); - // Should this remove from roster? + if (uiState == Invited) { + client.sendStanza(new Stanza("presence", { to: chatId, type: "unsubscribed", id: ID.short() })); + } + // Should this remove from roster? Or set untrusted? uiState = Closed; persistence.storeChats(client.accountId(), [this]); if (!isBlocked) sendChatState("gone", null); @@ -1170,7 +1188,9 @@ class Channel extends Chat { @:allow(borogove) private function selfPing(refresh: Bool) { - if (uiState == Closed){ + if (uiState == Invited) return; + + if (uiState == Closed) { client.sendPresence( getFullJid().asString(), (stanza) -> { @@ -1209,6 +1229,11 @@ class Channel extends Chat { @:allow(borogove) private function join() { + if (uiState == Invited || uiState == Closed) { + // Do not join + return; + } + presence = []; // About to ask for a fresh set _nickInUse = null; outbox.pause(); @@ -1400,8 +1425,13 @@ class Channel extends Chat { sync.fetchNext(); } + override public function setTrusted(trusted: Bool) { + super.setTrusted(trusted); + if (trusted) selfPing(true); + } + override public function isTrusted() { - return uiState != Closed; + return uiState != Closed && uiState != Invited; } public function isPrivate() { @@ -1571,6 +1601,7 @@ class Channel extends Chat { @HaxeCBridge.noemit // on superclass as abstract public function sendMessage(message:ChatMessageBuilder):Void { + if (uiState == Invited) uiState = Open; if (typingTimer != null) typingTimer.stop(); client.chatActivity(this); message = prepareOutgoingMessage(message); @@ -1668,12 +1699,13 @@ class Channel extends Chat { @HaxeCBridge.noemit // on superclass as abstract public function bookmark() { + if (uiState == Invited) uiState = Open; stream.sendIq( new Stanza("iq", { type: "set" }) .tag("pubsub", { xmlns: "http://jabber.org/protocol/pubsub" }) .tag("publish", { node: "urn:xmpp:bookmarks:1" }) .tag("item", { id: chatId }) - .tag("conference", { xmlns: "urn:xmpp:bookmarks:1", name: getDisplayName(), autojoin: uiState == Closed ? "false" : "true" }) + .tag("conference", { xmlns: "urn:xmpp:bookmarks:1", name: getDisplayName(), autojoin: uiState == Closed || uiState == Invited ? "false" : "true" }) .textTag("nick", client.displayName()) // Redundant but some other clients want it .addChild(extensions) .up().up() diff --git a/borogove/Client.hx b/borogove/Client.hx index 866c629..86f6fa1 100644 --- a/borogove/Client.hx +++ b/borogove/Client.hx @@ -413,6 +413,20 @@ class Client extends EventEmitter { this.trigger("chats/update", [chat]); } + if (stanza.attr.get("from") != null && stanza.attr.get("type") == "subscribe") { + final from = JID.parse(stanza.attr.get("from")); + final chat = getChat(from.asBare().asString()); + final nick = stanza.getChildText("nick", "http://jabber.org/protocol/nick"); + if (chat == null) { + startChatWith(from.asBare().asString(), _-> Invited, (chat) -> { + if (chat.displayName == chat.chatId && nick != null) chat.displayName = nick; + }); + } else if (chat.uiState == Closed) { + chat.uiState = Invited; + if (chat.displayName == chat.chatId && nick != null) chat.displayName = nick; + } + } + return EventUnhandled; }); } @@ -469,6 +483,35 @@ class Client extends EventEmitter { MessageFailedToSend, stanza.getErrorText(), ).then((m) -> notifyMessageHandlers(m, StatusEvent), _ -> null); + case MucInviteStanza(serverId, serverIdBy, reason, password): + final chat = getChat(message.chatId); + if (chat == null) { + startChatWith(message.chatId, _ -> Invited, (chat) -> { + final inviteExt = chat.extensions.tag("invite", { xmlns: "http://jabber.org/protocol/muc#user", from: message.senderId }); + if (reason != null) inviteExt.textTag("reason", reason); + if (password != null) inviteExt.textTag("password", password); + if (message.threadId != null) inviteExt.tag("continue", { thread: message.threadId }).up(); + if (serverId != null && serverIdBy != null) { + inviteExt.tag("stanza-id", { xmlns: "urn:xmpp:sid:0", by: serverIdBy, id: serverId }).up(); + } + inviteExt.up(); + this.trigger("chats/update", [chat]); + persistence.storeChats(accountId(), [chat]); + }); + } else if (chat.uiState == Closed) { + chat.extensions.removeChildren("invite", "http://jabber.org/protocol/muc#user"); + final inviteExt = chat.extensions.tag("invite", { xmlns: "http://jabber.org/protocol/muc#user", from: message.senderId }); + if (reason != null) inviteExt.textTag("reason", reason); + if (password != null) inviteExt.textTag("password", password); + if (message.threadId != null) inviteExt.tag("continue", { thread: message.threadId }).up(); + if (serverId != null && serverIdBy != null) { + inviteExt.tag("stanza-id", { xmlns: "urn:xmpp:sid:0", by: serverIdBy, id: serverId }).up(); + } + inviteExt.up(); + chat.uiState = Invited; + this.trigger("chats/update", [chat]); + persistence.storeChats(accountId(), [chat]); + } default: // ignore trace("Ignoring non-chat message: " + stanza.toString()); @@ -602,7 +645,7 @@ class Client extends EventEmitter { final upTo = item.getChild("displayed", "urn:xmpp:mds:displayed:0")?.getChild("stanza-id", "urn:xmpp:sid:0"); final chat = getChat(item.attr.get("id")); if (chat == null) { - startChatWith(item.attr.get("id"), (caps) -> Closed, (chat) -> chat.markReadUpToId(upTo.attr.get("id"), upTo.attr.get("by"))); + startChatWith(item.attr.get("id"), _ -> Closed, (chat) -> chat.markReadUpToId(upTo.attr.get("id"), upTo.attr.get("by"))); } else { chat.markReadUpToId(upTo.attr.get("id"), upTo.attr.get("by")).then(_ -> { persistence.storeChats(accountId(), [chat]); @@ -1556,30 +1599,33 @@ class Client extends EventEmitter { sendQuery(rosterGet); } - private function startChatWith(jid: String, handleCaps: (Caps)->UiState, handleChat: (Chat)->Void) { + private function startChatWith(jid: String, handleCaps: (Null<Caps>)->UiState, handleChat: (Chat)->Void) { final discoGet = new DiscoInfoGet(jid); discoGet.onFinished(() -> { final resultCaps = discoGet.getResult(); + final uiState = handleCaps(resultCaps); if (resultCaps == null) { final err = discoGet.responseStanza?.getChild("error")?.getChild(null, "urn:ietf:params:xml:ns:xmpp-stanzas"); if (err == null || err?.name == "service-unavailable" || err?.name == "feature-not-implemented") { final chat = getDirectChat(jid, false); + chat.uiState = uiState; handleChat(chat); persistence.storeChats(accountId(), [chat]); + this.trigger("chats/update", [chat]); } } else { persistence.storeCaps(resultCaps); - final uiState = handleCaps(resultCaps); if (resultCaps.isChannel(jid)) { final chat = new Channel(this, this.stream, this.persistence, jid, uiState, false, null, resultCaps); chat.setupNotifications(); - handleChat(chat); chats.unshift(chat); + if (inSync && sendAvailable) chat.selfPing(false); + handleChat(chat); persistence.storeChats(accountId(), [chat]); this.trigger("chats/update", [chat]); - if (inSync && sendAvailable) chat.selfPing(false); } else { final chat = getDirectChat(jid, false); + chat.uiState = uiState; handleChat(chat); persistence.storeChats(accountId(), [chat]); this.trigger("chats/update", [chat]); @@ -1612,7 +1658,7 @@ class Client extends EventEmitter { final upTo = item.getChild("displayed", "urn:xmpp:mds:displayed:0")?.getChild("stanza-id", "urn:xmpp:sid:0"); final chat = getChat(item.attr.get("id")); if (chat == null) { - startChatWith(item.attr.get("id"), (caps) -> Closed, (chat) -> chat.markReadUpToId(upTo.attr.get("id"), upTo.attr.get("by"))); + startChatWith(item.attr.get("id"), _ -> Closed, (chat) -> chat.markReadUpToId(upTo.attr.get("id"), upTo.attr.get("by"))); } else { chat.markReadUpToId(upTo.attr.get("id"), upTo.attr.get("by")).then(_ -> null, e -> e != null ? Promise.reject(e) : null); chatsToUpdate.push(chat); @@ -1633,6 +1679,8 @@ class Client extends EventEmitter { startChatWith( item.attr.get("id"), (caps) -> { + if (caps == null) return Open; + final identity = caps.identities[0]; final conf = item.getChild("conference", "urn:xmpp:bookmarks:1"); if (conf.attr.get("name") == null) { diff --git a/borogove/Message.hx b/borogove/Message.hx index 498b488..e36c186 100644 --- a/borogove/Message.hx +++ b/borogove/Message.hx @@ -28,6 +28,7 @@ enum MessageStanza { ChatMessageStanza(message: ChatMessage); ModerateMessageStanza(action: ModerationAction); ReactionUpdateStanza(update: ReactionUpdate); + MucInviteStanza(serverId: Null<String>, serverIdBy: Null<String>, reason: Null<String>, password: Null<String>); UnknownMessageStanza(stanza: Stanza); } @@ -117,6 +118,24 @@ class Message { if (msg.from != null && msg.from.asBare().equals(localJidBare)) msg.direction = MessageSent; msg.status = msg.direction == MessageReceived ? MessageDeliveredToDevice : MessageDeliveredToServer; // Delivered to us, a device + final mucDirectInvite = stanza.getChild("x", "jabber:x:conference"); + if (mucDirectInvite != null) { + final mucJid = mucDirectInvite.attr.get("jid"); + if (mucJid != null) { + return new Message(mucJid, from, mucDirectInvite.attr.get("thread"), MucInviteStanza(msg.serverId, msg.serverIdBy, mucDirectInvite.attr.get("reason"), mucDirectInvite.attr.get("password")), encryptionInfo); + } + } + + final mucUser = stanza.getChild("x", "http://jabber.org/protocol/muc#user"); + final mucInvite = mucUser?.getChild("invite"); + if (mucInvite != null) { + final threadId = mucInvite.getChild("continue")?.attr?.get("thread"); + final reason = mucInvite.getChildText("reason"); + final password = mucInvite.getChildText("password"); + // NOTE: this senderId is unverified and unverifiable + return new Message(from, mucInvite.attr.get("from") ?? from, threadId, MucInviteStanza(msg.serverId, msg.serverIdBy, reason, password), encryptionInfo); + } + final recipients: Map<String, Bool> = []; final replyTo: Map<String, Bool> = []; if (msg.to != null) {