| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-07-22 19:53:31 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-07-22 19:59:15 UTC |
| parent | 04787b4066ad5833b98b6d2140d86735b8b109bb |
| borogove/Client.hx | +18 | -5 |
| borogove/Persistence.hx | +4 | -3 |
| borogove/persistence/Dummy.hx | +3 | -3 |
| borogove/persistence/IDB.js | +5 | -3 |
| borogove/persistence/Sqlite.hx | +15 | -7 |
| test/TestSqlite.hx | +3 | -3 |
diff --git a/borogove/Client.hx b/borogove/Client.hx index 8940aab..b4be2d6 100644 --- a/borogove/Client.hx +++ b/borogove/Client.hx @@ -117,6 +117,7 @@ class Client extends EventEmitter { private var token: Null<String> = null; private var fastCount: Null<Int> = null; private var sortId: String = "a "; + private var rosterVer: Null<String> = null; private final pendingCaps: Map<String, Array<(Null<Caps>)->Chat>> = []; private final brokenAvatars: Map<String, JID> = []; @@ -161,7 +162,7 @@ class Client extends EventEmitter { stream.on("fast-token", (data) -> { token = data.token; - persistence.storeLogin(this.jid.asBare().asString(), stream.clientId ?? this.jid.resource, displayName(), token); + persistence.storeLogin(this.jid.asBare().asString(), stream.clientId ?? this.jid.resource, displayName(), rosterVer, token); return EventHandled; }); @@ -295,7 +296,11 @@ class Client extends EventEmitter { var roster = new RosterGet(); roster.handleResponse(stanza); - var items = roster.getResult(); + final items = roster.getResult(); + if (roster.ver != null) { + rosterVer = roster.ver; + persistence.storeLogin(this.accountId(), stream.clientId ?? jid.resource, displayName(), rosterVer, null); + } if (items.length == 0) return IqNoResult; final chatsToUpdate = []; @@ -808,6 +813,7 @@ class Client extends EventEmitter { return persistence.getLogin(accountId()); }).then(login -> { token = login.token; + rosterVer = login.rosterVer; fastCount = login.fastCount; stream.clientId = login.clientId ?? ID.unique(); jid = jid.withResource(stream.clientId); @@ -1000,7 +1006,7 @@ class Client extends EventEmitter { private function updateDisplayName(fn: String) { if (fn == null || fn == "" || fn == displayName()) return false; _displayName = fn; - persistence.storeLogin(jid.asBare().asString(), stream.clientId ?? jid.resource, fn, null); + persistence.storeLogin(jid.asBare().asString(), stream.clientId ?? jid.resource, fn, rosterVer, null); pingAllChannels(false); return true; } @@ -1029,7 +1035,7 @@ class Client extends EventEmitter { private function onConnected(data) { // Fired on connect or reconnect if (data != null && data.jid != null) { jid = JID.parse(data.jid); - if (stream.clientId == null && !jid.isBare()) persistence.storeLogin(jid.asBare().asString(), stream.clientId ?? jid.resource, displayName(), null); + if (stream.clientId == null && !jid.isBare()) persistence.storeLogin(jid.asBare().asString(), stream.clientId ?? jid.resource, displayName(), rosterVer, null); } if (data.resumed) { @@ -2017,14 +2023,21 @@ class Client extends EventEmitter { } private function rosterGet() { - var rosterGet = new RosterGet(); + var rosterGet = new RosterGet(rosterVer); rosterGet.onFinished(() -> { final chatsToUpdate = []; + // TODO: We should update all chats not in this list as not trusted. + // Special case for when it is empty but there was no <query/> child + // to do nothing is needed also. for (item in rosterGet.getResult()) { var chat = getDirectChat(item.jid, false); chat.updateFromRoster(item); chatsToUpdate.push(cast (chat, Chat)); } + if (rosterGet.ver != null) { + rosterVer = rosterGet.ver; + persistence.storeLogin(accountId(), stream.clientId ?? jid.resource, displayName(), rosterVer, null); + } persistence.storeChats(accountId(), chatsToUpdate); this.trigger("chats/update", chatsToUpdate); }); diff --git a/borogove/Persistence.hx b/borogove/Persistence.hx index 5964b43..4b79c98 100644 --- a/borogove/Persistence.hx +++ b/borogove/Persistence.hx @@ -260,10 +260,11 @@ interface Persistence { @param accountId the account to store login state for @param clientId negotiated client ID @param displayName last known display name - @param token persisted token or null to clear it + @param rosterVer roster version string if known + @param token persisted token or null to keep the current value @returns Promise resolving to true when store succeeded **/ - public function storeLogin(accountId:String, clientId:String, displayName:String, token:Null<String>): Promise<Bool>; + public function storeLogin(accountId:String, clientId:String, displayName:String, rosterVer: Null<String>, token:Null<String>): Promise<Bool>; /** Load persisted login-related state for an account @@ -272,7 +273,7 @@ interface Persistence { @returns Promise resolving to stored login data **/ @HaxeCBridge.noemit - public function getLogin(accountId:String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String> }>; + public function getLogin(accountId:String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String>, rosterVer: Null<String> }>; /** Remove stored data for an account diff --git a/borogove/persistence/Dummy.hx b/borogove/persistence/Dummy.hx index 83950f6..2f94855 100644 --- a/borogove/persistence/Dummy.hx +++ b/borogove/persistence/Dummy.hx @@ -145,13 +145,13 @@ class Dummy implements Persistence { } @HaxeCBridge.noemit - public function storeLogin(login:String, clientId:String, displayName:String, token:Null<String>): Promise<Bool> { + public function storeLogin(login:String, clientId:String, displayName:String, rosterVer: Null<String>, token:Null<String>): Promise<Bool> { return Promise.resolve(false); } @HaxeCBridge.noemit - public function getLogin(login:String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String> }> { - return Promise.resolve({ clientId: null, token: null, fastCount: 0, displayName: null }); + public function getLogin(login:String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String>, rosterVer: Null<String> }> { + return Promise.resolve({ clientId: null, token: null, fastCount: 0, displayName: null, rosterVer: null }); } @HaxeCBridge.noemit diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js index cef1d01..664065d 100644 --- a/borogove/persistence/IDB.js +++ b/borogove/persistence/IDB.js @@ -1112,11 +1112,12 @@ tx.onerror = console.error; return null; }, - async storeLogin(login, clientId, displayName, token) { + async storeLogin(login, clientId, displayName, rosterVer, token) { const tx = db.transaction(["keyvaluepairs"], "readwrite"); const store = tx.objectStore("keyvaluepairs"); await promisifyRequest(store.put(clientId, "login:clientId:" + login)); await promisifyRequest(store.put(displayName, "fn:" + login)); + await promisifyRequest(store.put(rosterVer, "rosterVer:" + login)); if (token != null) { await promisifyRequest(store.put(token, "login:token:" + login)); await promisifyRequest(store.put(0, "login:fastCount:" + login)); @@ -1246,7 +1247,7 @@ tx.onerror = console.error; } }, - getLogin: function(login) { + getLogin(login) { const tx = db.transaction(["keyvaluepairs"], "readwrite"); const store = tx.objectStore("keyvaluepairs"); return Promise.all([ @@ -1254,11 +1255,12 @@ tx.onerror = console.error; promisifyRequest(store.get("login:token:" + login)), promisifyRequest(store.get("login:fastCount:" + login)), promisifyRequest(store.get("fn:" + login)), + promisifyRequest(store.get("rosterVer:" + login)), ]).then((result) => { if (result[1]) { store.put((result[2] || 0) + 1, "login:fastCount:" + login).onerror = console.error; } - return { clientId: result[0], token: result[1], fastCount: result[2] || 0, displayName: result[3] }; + return { clientId: result[0], token: result[1], fastCount: result[2] || 0, displayName: result[3], rosterVer: result[4] }; }); }, diff --git a/borogove/persistence/Sqlite.hx b/borogove/persistence/Sqlite.hx index 24d11a9..771daca 100644 --- a/borogove/persistence/Sqlite.hx +++ b/borogove/persistence/Sqlite.hx @@ -262,6 +262,12 @@ class Sqlite implements Persistence implements KeyValueStore { "PRAGMA user_version = 12"]); } return Promise.resolve(null); + }).then(_ -> { + if (version < 13) { + return exec(["ALTER TABLE accounts ADD COLUMN roster_ver TEXT", + "PRAGMA user_version = 13"]); + } + return Promise.resolve(null); }); }); }); @@ -1057,14 +1063,14 @@ class Sqlite implements Persistence implements KeyValueStore { } @HaxeCBridge.noemit - public function storeLogin(accountId:String, clientId:String, displayName:String, token:Null<String>): Promise<Bool> { - final params = [accountId, clientId, displayName]; + public function storeLogin(accountId:String, clientId:String, displayName:String, rosterVer:Null<String>, token:Null<String>): Promise<Bool> { + final params = [accountId, clientId, displayName, rosterVer]; final q = new StringBuf(); - q.add("INSERT INTO accounts (account_id, client_id, display_name"); + q.add("INSERT INTO accounts (account_id, client_id, display_name, roster_ver"); if (token != null) { q.add(", token, fast_count"); } - q.add(") VALUES (?,?,?"); + q.add(") VALUES (?,?,?,?"); if (token != null) { q.add(",?"); params.push(token); @@ -1074,6 +1080,8 @@ class Sqlite implements Persistence implements KeyValueStore { params.push(clientId); q.add(", display_name=?"); params.push(displayName); + q.add(", roster_ver=?"); + params.push(rosterVer); if (token != null) { q.add(", token=?"); params.push(token); @@ -1083,9 +1091,9 @@ class Sqlite implements Persistence implements KeyValueStore { } @HaxeCBridge.noemit - public function getLogin(accountId: String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String> }> { + public function getLogin(accountId: String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String>, rosterVer: Null<String> }> { return db.exec( - "SELECT client_id AS clientId, display_name AS displayName, token, COALESCE(fast_count, 0) AS fastCount FROM accounts WHERE account_id=? LIMIT 1", + "SELECT client_id AS clientId, display_name AS displayName, roster_ver AS rosterVer, token, COALESCE(fast_count, 0) AS fastCount FROM accounts WHERE account_id=? LIMIT 1", [accountId] ).then(result -> { for (row in result) { @@ -1096,7 +1104,7 @@ class Sqlite implements Persistence implements KeyValueStore { return r; } - return { clientId: null, token: null, fastCount: 0, displayName: null }; + return { clientId: null, token: null, fastCount: 0, displayName: null, rosterVer: null }; }); } diff --git a/test/TestSqlite.hx b/test/TestSqlite.hx index 646818e..35cda77 100644 --- a/test/TestSqlite.hx +++ b/test/TestSqlite.hx @@ -695,8 +695,8 @@ class TestSqlite extends utest.Test { final account1 = "alice@example.com"; final account2 = "bob@example.com"; - persistence.storeLogin(account1, "client1", "Alice", null); - persistence.storeLogin(account2, "client2", "Bob", null); + persistence.storeLogin(account1, "client1", "Alice", null, null); + persistence.storeLogin(account2, "client2", "Bob", null, null); persistence.listAccounts().then(accountsBefore -> { Assert.contains(account1, accountsBefore); @@ -969,7 +969,7 @@ class TestSqlite extends utest.Test { @:timeout(3000) public function testStoreStreamManamagementAndGetStreamManagement(async: Async) { - persistence.storeLogin("alice@example.com", "", "", null).then(_ -> + persistence.storeLogin("alice@example.com", "", "", null, null).then(_ -> persistence.storeStreamManagement("alice@example.com", Bytes.ofHex("01020004").getData(), "ZZ") ).then(_ -> persistence.getStreamManagement("alice@example.com")