| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2024-11-06 22:02:07 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2024-11-06 22:02:07 UTC |
| parent | b98ab7c83cc0406df5a76c730d96802e24ce4c1f |
| snikket/Chat.hx | +50 | -8 |
| snikket/Client.hx | +61 | -2 |
| snikket/persistence/Sqlite.hx | +6 | -3 |
| snikket/persistence/browser.js | +2 | -0 |
| snikket/queries/BlocklistGet.hx | +45 | -0 |
diff --git a/snikket/Chat.hx b/snikket/Chat.hx index d02ddcf..9033345 100644 --- a/snikket/Chat.hx +++ b/snikket/Chat.hx @@ -57,6 +57,7 @@ abstract class Chat { **/ @:allow(snikket) public var uiState(default, null): UiState = Open; + public var isBlocked(default, null): Bool = false; @:allow(snikket) private var extensions: Stanza; private var _unreadCount = 0; @@ -71,12 +72,13 @@ abstract class Chat { private var activeThread: Null<String> = null; @:allow(snikket) - private function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState = Open, extensions: Null<Stanza> = null, readUpToId: Null<String> = null, readUpToBy: Null<String> = null) { + private function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState = Open, isBlocked = false, extensions: Null<Stanza> = null, readUpToId: Null<String> = null, readUpToBy: Null<String> = null) { this.client = client; this.stream = stream; this.persistence = persistence; this.chatId = chatId; this.uiState = uiState; + this.isBlocked = isBlocked; this.extensions = extensions ?? new Stanza("extensions", { xmlns: "urn:xmpp:bookmarks:1" }); this.readUpToId = readUpToId; this.readUpToBy = readUpToBy; @@ -272,6 +274,44 @@ abstract class Chat { **/ abstract public function close():Void; + /** + Block this chat so it will not re-open + **/ + public function block(reportSpam: Null<ChatMessage>, onServer: Bool): Void { + if (reportSpam != null && !onServer) throw "Can't report SPAM if not sending to server"; + isBlocked = true; + if (uiState != Closed) close(); // close persists + if (onServer) { + final iq = new Stanza("iq", { type: "set", id: ID.short() }) + .tag("block", { xmlns: "urn:xmpp:blocking" }) + .tag("item", { jid: chatId }); + if (reportSpam != null) { + iq + .tag("report", { xmlns: "urn:xmpp:reporting:1", reason: "urn:xmpp:reporting:spam" }) + .tag("stanza-id", { xmlns: "urn:xmpp:sid:0", by: reportSpam.serverIdBy, id: reportSpam.serverId }); + } + stream.sendIq(iq, (response) -> {}); + } + } + + /** + Unblock this chat so it will not open again + **/ + public function unblock(onServer: Bool): Void { + isBlocked = false; + uiState = Open; + persistence.storeChat(client.accountId(), this); + client.trigger("chats/update", [this]); + if (onServer) { + stream.sendIq( + new Stanza("iq", { type: "set", id: ID.short() }) + .tag("unblock", { xmlns: "urn:xmpp:blocking" }) + .tag("item", { jid: chatId }).up().up(), + (response) -> {} + ); + } + } + /** An ID of the most recent message in this chat **/ @@ -581,8 +621,8 @@ abstract class Chat { #end class DirectChat extends Chat { @:allow(snikket) - private function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState = Open, extensions: Null<Stanza> = null, readUpToId: Null<String> = null, readUpToBy: Null<String> = null) { - super(client, stream, persistence, chatId, uiState, extensions, readUpToId, readUpToBy); + private function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState = Open, isBlocked = false, extensions: Null<Stanza> = null, readUpToId: Null<String> = null, readUpToBy: Null<String> = null) { + super(client, stream, persistence, chatId, uiState, isBlocked, extensions, readUpToId, readUpToBy); } @HaxeCBridge.noemit // on superclass as abstract @@ -813,8 +853,8 @@ class Channel extends Chat { private var inSync = true; @:allow(snikket) - private function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState = Open, extensions = null, readUpToId = null, readUpToBy = null, ?disco: Caps) { - super(client, stream, persistence, chatId, uiState, extensions, readUpToId, readUpToBy); + private function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState = Open, isBlocked = false, extensions = null, readUpToId = null, readUpToBy = null, ?disco: Caps) { + super(client, stream, persistence, chatId, uiState, isBlocked, extensions, readUpToId, readUpToBy); if (disco != null) this.disco = disco; } @@ -1290,19 +1330,21 @@ class SerializedChat { public final presence:Map<String, Presence>; public final displayName:Null<String>; public final uiState:UiState; + public final isBlocked:Bool; public final extensions:String; public final readUpToId:Null<String>; public final readUpToBy:Null<String>; public final disco:Null<Caps>; public final klass:String; - public function new(chatId: String, trusted: Bool, avatarSha1: Null<BytesData>, presence: Map<String, Presence>, displayName: Null<String>, uiState: Null<UiState>, extensions: Null<String>, readUpToId: Null<String>, readUpToBy: Null<String>, disco: Null<Caps>, klass: String) { + public function new(chatId: String, trusted: Bool, avatarSha1: Null<BytesData>, presence: Map<String, Presence>, displayName: Null<String>, uiState: Null<UiState>, isBlocked: Null<Bool>, extensions: Null<String>, readUpToId: Null<String>, readUpToBy: Null<String>, disco: Null<Caps>, klass: String) { this.chatId = chatId; this.trusted = trusted; this.avatarSha1 = avatarSha1; this.presence = presence; this.displayName = displayName; this.uiState = uiState ?? Open; + this.isBlocked = isBlocked ?? false; this.extensions = extensions ?? "<extensions xmlns='urn:app:bookmarks:1' />"; this.readUpToId = readUpToId; this.readUpToBy = readUpToBy; @@ -1314,9 +1356,9 @@ class SerializedChat { final extensionsStanza = Stanza.fromXml(Xml.parse(extensions)); final chat = if (klass == "DirectChat") { - new DirectChat(client, stream, persistence, chatId, uiState, extensionsStanza, readUpToId, readUpToBy); + new DirectChat(client, stream, persistence, chatId, uiState, isBlocked, extensionsStanza, readUpToId, readUpToBy); } else if (klass == "Channel") { - final channel = new Channel(client, stream, persistence, chatId, uiState, extensionsStanza, readUpToId, readUpToBy); + final channel = new Channel(client, stream, persistence, chatId, uiState, isBlocked, extensionsStanza, readUpToId, readUpToBy); channel.disco = disco ?? new Caps("", [], ["http://jabber.org/protocol/muc"]); channel; } else { diff --git a/snikket/Client.hx b/snikket/Client.hx index eae4e2a..c7d0a54 100644 --- a/snikket/Client.hx +++ b/snikket/Client.hx @@ -16,6 +16,7 @@ import snikket.EventHandler; import snikket.PubsubEvent; import snikket.Stream; import snikket.jingle.Session; +import snikket.queries.BlocklistGet; import snikket.queries.BoB; import snikket.queries.DiscoInfoGet; import snikket.queries.DiscoItemsGet; @@ -389,6 +390,44 @@ class Client extends EventEmitter { return IqResult; }); + stream.onIq(Set, "block", "urn:xmpp:blocking", (stanza) -> { + if ( + stanza.attr.get("from") != null && + stanza.attr.get("from") != jid.domain + ) { + return IqNoResult; + } + + for (item in stanza.getChild("block", "urn:xmpp:blocking")?.allTags("item") ?? []) { + if (item.attr.get("jid") != null) serverBlocked(item.attr.get("jid")); + } + + return IqResult; + }); + + stream.onIq(Set, "unblock", "urn:xmpp:blocking", (stanza) -> { + if ( + stanza.attr.get("from") != null && + stanza.attr.get("from") != jid.domain + ) { + return IqNoResult; + } + + final unblocks = stanza.getChild("unblock", "urn:xmpp:blocking")?.allTags("item"); + if (unblocks == null) { + // unblock all + for (chat in chats) { + if (chat.isBlocked) chat.unblock(false); + } + } else { + for (item in unblocks) { + if (item.attr.get("jid") != null) getChat(item.attr.get("jid"))?.unblock(false); + } + } + + return IqResult; + }); + stream.on("presence", function(event) { final stanza:Stanza = event.stanza; final c = stanza.getChild("c", "http://jabber.org/protocol/caps"); @@ -805,7 +844,7 @@ class Client extends EventEmitter { } final chat = if (availableChat.isChannel()) { - final channel = new Channel(this, this.stream, this.persistence, availableChat.chatId, Open, null, availableChat.caps); + final channel = new Channel(this, this.stream, this.persistence, availableChat.chatId, Open, false, null, availableChat.caps); chats.unshift(channel); channel.selfPing(false); channel; @@ -1066,6 +1105,7 @@ class Client extends EventEmitter { @:allow(snikket) private function chatActivity(chat: Chat, trigger = true) { + if (chat.isBlocked) return; // Don't notify blocked chats if (chat.uiState == Closed) { chat.uiState = Open; persistence.storeChat(accountId(), chat); @@ -1154,6 +1194,8 @@ class Client extends EventEmitter { @:allow(snikket) private function notifyMessageHandlers(message: ChatMessage) { + final chat = getChat(message.chatId()); + if (chat != null && chat.isBlocked) return; // Don't notify blocked chats for (handler in chatMessageHandlers) { handler(message); } @@ -1188,7 +1230,7 @@ class Client extends EventEmitter { persistence.storeCaps(resultCaps); final uiState = handleCaps(resultCaps); if (resultCaps.isChannel(jid)) { - final chat = new Channel(this, this.stream, this.persistence, jid, uiState, null, resultCaps); + final chat = new Channel(this, this.stream, this.persistence, jid, uiState, false, null, resultCaps); handleChat(chat); chats.unshift(chat); persistence.storeChat(accountId(), chat); @@ -1202,8 +1244,25 @@ class Client extends EventEmitter { sendQuery(discoGet); } + private function serverBlocked(blocked: String) { + final chat = getChat(blocked); + if (chat == null) { + startChatWith(blocked, (caps) -> Closed, (chat) -> chat.block(null, false)); + } else { + chat.block(null, false); + } + } + // This is called right before we're going to trigger for all chats anyway, so don't bother with single triggers private function bookmarksGet(callback: ()->Void) { + final blockingGet = new BlocklistGet(); + blockingGet.onFinished(() -> { + for (blocked in blockingGet.getResult()) { + serverBlocked(blocked); + } + }); + sendQuery(blockingGet); + final mdsGet = new PubsubGet(null, "urn:xmpp:mds:displayed:0"); mdsGet.onFinished(() -> { for (item in mdsGet.getResult()) { diff --git a/snikket/persistence/Sqlite.hx b/snikket/persistence/Sqlite.hx index cf88f5a..0869cbe 100644 --- a/snikket/persistence/Sqlite.hx +++ b/snikket/persistence/Sqlite.hx @@ -57,7 +57,8 @@ class Sqlite implements Persistence { trusted BOOLEAN NOT NULL, avatar_sha1 BLOB, fn TEXT, - ui_state TEXT, + ui_state TEXT NOT NULL, + blocked BOOLEAN NOT NULL, extensions TEXT, read_up_to_id TEXT, read_up_to_by TEXT, @@ -133,6 +134,8 @@ class Sqlite implements Persistence { q.add(","); db.addValue(q, chat.uiState); q.add(","); + db.addValue(q, chat.isBlocked); + q.add(","); db.addValue(q, chat.extensions); q.add(","); db.addValue(q, chat.readUpTo()); @@ -149,12 +152,12 @@ class Sqlite implements Persistence { // TODO: presence // TODO: disco final q = new StringBuf(); - q.add("SELECT chat_id, trusted, avatar_sha1, fn, ui_state, extensions, read_up_to_id, read_up_to_by, class FROM chats WHERE account_id="); + q.add("SELECT chat_id, trusted, avatar_sha1, fn, ui_state, blocked, extensions, read_up_to_id, read_up_to_by, class FROM chats WHERE account_id="); db.addValue(q, accountId); final result = db.request(q.toString()); final chats = []; for (row in result) { - chats.push(new SerializedChat(row.chat_id, row.trusted, row.avatar_sha1, [], row.fn, row.ui_state, row.extensions, row.read_up_to_id, row.read_up_to_by, null, Reflect.field(row, "class"))); + chats.push(new SerializedChat(row.chat_id, row.trusted, row.avatar_sha1, [], row.fn, row.ui_state, row.blocked, row.extensions, row.read_up_to_id, row.read_up_to_by, null, Reflect.field(row, "class"))); } callback(chats); } diff --git a/snikket/persistence/browser.js b/snikket/persistence/browser.js index c0901c2..3c3251d 100644 --- a/snikket/persistence/browser.js +++ b/snikket/persistence/browser.js @@ -208,6 +208,7 @@ const browser = (dbname, tokenize, stemmer) => { presence: new Map([...chat.presence.entries()].map(([k, p]) => [k, { caps: p.caps?.ver(), mucUser: p.mucUser?.toString() }])), displayName: chat.displayName, uiState: chat.uiState, + isBlocked: chat.isBlocked, extensions: chat.extensions?.toString(), readUpToId: chat.readUpToId, readUpToBy: chat.readUpToBy, @@ -231,6 +232,7 @@ const browser = (dbname, tokenize, stemmer) => { ))), r.displayName, r.uiState, + r.isBlocked, r.extensions, r.readUpToId, r.readUpToBy, diff --git a/snikket/queries/BlocklistGet.hx b/snikket/queries/BlocklistGet.hx new file mode 100644 index 0000000..a4a54f0 --- /dev/null +++ b/snikket/queries/BlocklistGet.hx @@ -0,0 +1,45 @@ +package snikket.queries; + +import haxe.DynamicAccess; +import haxe.Exception; + +import snikket.ID; +import snikket.ResultSet; +import snikket.Stanza; +import snikket.Stream; +import snikket.queries.GenericQuery; + +class BlocklistGet extends GenericQuery { + public var xmlns(default, null) = "urn:xmpp:blocking"; + public var queryId:String = null; + public var ver:String = null; + private var responseStanza:Stanza; + private var result: Array<String>; + + public function new() { + /* Build basic query */ + queryId = ID.short(); + queryStanza = new Stanza("iq", { type: "get", id: queryId }) + .tag("blocklist", { xmlns: xmlns }).up(); + } + + public function handleResponse(stanza:Stanza) { + responseStanza = stanza; + finish(); + } + + public function getResult() { + if (responseStanza == null) { + return []; + } + if(result == null) { + final q = responseStanza.getChild("blocklist", xmlns); + if(q == null) { + return []; + } + // TODO: cannot specify namespace here due to bugs in namespace handling in allTags + result = q.allTags("item").map(el -> el.attr.get("jid")); + } + return result; + } +}