| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-11-12 15:57:54 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-11-12 15:57:54 UTC |
| parent | 1ee69268b46ac116a40519e767c2b5fc231dddb1 |
| borogove/ChatMessage.hx | +9 | -2 |
| borogove/ChatMessageBuilder.hx | +6 | -0 |
| borogove/Client.hx | +11 | -2 |
| borogove/Message.hx | +3 | -3 |
| borogove/Persistence.hx | +1 | -1 |
| borogove/Register.hx | +2 | -12 |
| borogove/Stanza.hx | +9 | -0 |
| borogove/persistence/Dummy.hx | +1 | -1 |
| borogove/persistence/IDB.js | +6 | -5 |
| borogove/persistence/Sqlite.hx | +16 | -8 |
diff --git a/borogove/ChatMessage.hx b/borogove/ChatMessage.hx index 84881dc..7219581 100644 --- a/borogove/ChatMessage.hx +++ b/borogove/ChatMessage.hx @@ -190,7 +190,12 @@ class ChatMessage { /** Status of this message **/ - public var status: MessageStatus; + public final status: MessageStatus; + + /** + Message to go along with the message status + **/ + public final statusText: Null<String>; /** Array of past versions of this message, if it has been edited @@ -205,7 +210,7 @@ class ChatMessage { Information about the encryption used by the sender of this message. **/ - public var encryption: Null<EncryptionInfo>; + public final encryption: Null<EncryptionInfo>; @:allow(borogove) private final stanza: Null<Stanza>; @@ -232,6 +237,7 @@ class ChatMessage { ?lang: Null<String>, ?direction: MessageDirection, ?status: MessageStatus, + ?statusText: String, ?versions: Array<ChatMessage>, ?payloads: Array<Stanza>, ?encryption: Null<EncryptionInfo>, @@ -257,6 +263,7 @@ class ChatMessage { this.lang = params.lang; this.direction = params.direction ?? MessageSent; this.status = params.status ?? MessagePending; + this.statusText = params.statusText; this.versions = params.versions ?? []; this.payloads = params.payloads ?? []; this.encryption = params.encryption; diff --git a/borogove/ChatMessageBuilder.hx b/borogove/ChatMessageBuilder.hx index 4f5eba4..79bc7af 100644 --- a/borogove/ChatMessageBuilder.hx +++ b/borogove/ChatMessageBuilder.hx @@ -118,6 +118,11 @@ class ChatMessageBuilder { **/ public var status: MessageStatus = MessagePending; + /** + Human readable text to go with the status + **/ + public var statusText: Null<String> = null; + /** Array of past versions of this message, if it has been edited **/ @@ -346,6 +351,7 @@ class ChatMessageBuilder { lang: lang, direction: direction, status: status, + statusText: statusText, versions: versions, payloads: payloads, encryption: encryption, diff --git a/borogove/Client.hx b/borogove/Client.hx index 1f20cda..51ce95c 100644 --- a/borogove/Client.hx +++ b/borogove/Client.hx @@ -155,7 +155,8 @@ class Client extends EventEmitter { persistence.updateMessageStatus( this.accountId(), data.id, - MessageDeliveredToServer + MessageDeliveredToServer, + null ).then((m) -> notifyMessageHandlers(m, StatusEvent), _ -> null); return EventHandled; }); @@ -164,7 +165,8 @@ class Client extends EventEmitter { persistence.updateMessageStatus( this.accountId(), data.id, - MessageFailedToSend + MessageFailedToSend, + null ).then((m) -> notifyMessageHandlers(m, StatusEvent), _ -> null); return EventHandled; }); @@ -460,6 +462,13 @@ class Client extends EventEmitter { persistence.storeReaction(accountId(), update).then((stored) -> if (stored != null) notifyMessageHandlers(stored, ReactionEvent)); case ModerateMessageStanza(action): moderateMessage(action).then((stored) -> if (stored != null) notifyMessageHandlers(stored, CorrectionEvent)); + case ErrorMessageStanza(localId, stanza): + persistence.updateMessageStatus( + this.accountId(), + localId, + MessageFailedToSend, + stanza.getErrorText(), + ).then((m) -> notifyMessageHandlers(m, StatusEvent), _ -> null); default: // ignore trace("Ignoring non-chat message: " + stanza.toString()); diff --git a/borogove/Message.hx b/borogove/Message.hx index ed2a31c..498b488 100644 --- a/borogove/Message.hx +++ b/borogove/Message.hx @@ -24,7 +24,7 @@ enum abstract MessageType(Int) { } enum MessageStanza { - ErrorMessageStanza(stanza: Stanza); + ErrorMessageStanza(localId: Null<String>, stanza: Stanza); ChatMessageStanza(message: ChatMessage); ModerateMessageStanza(action: ModerationAction); ReactionUpdateStanza(update: ReactionUpdate); @@ -50,12 +50,13 @@ class Message { public static function fromStanza(stanza:Stanza, localJid:JID, ?addContext: (ChatMessageBuilder, Stanza)->ChatMessageBuilder, ?encryptionInfo:EncryptionInfo):Message { final fromAttr = stanza.attr.get("from"); final from = fromAttr == null ? localJid.domain : fromAttr; + final localId = stanza.attr.get("id"); if(encryptionInfo==null) { encryptionInfo = EncryptionInfo.fromStanza(stanza); } if (stanza.attr.get("type") == "error") { - return new Message(from, from, null, ErrorMessageStanza(stanza), encryptionInfo); + return new Message(from, from, null, ErrorMessageStanza(localId, stanza), encryptionInfo); } var msg = new ChatMessageBuilder(); @@ -89,7 +90,6 @@ class Message { } } - final localId = stanza.attr.get("id"); if (localId != null) msg.localId = localId; var altServerId = null; for (stanzaId in stanza.allTags("stanza-id", "urn:xmpp:sid:0")) { diff --git a/borogove/Persistence.hx b/borogove/Persistence.hx index 205da7c..e089d4f 100644 --- a/borogove/Persistence.hx +++ b/borogove/Persistence.hx @@ -23,7 +23,7 @@ interface Persistence { public function storeReaction(accountId: String, update: ReactionUpdate): Promise<Null<ChatMessage>>; public function storeMessages(accountId: String, message: Array<ChatMessage>): Promise<Array<ChatMessage>>; public function updateMessage(accountId: String, message: ChatMessage):Void; - public function updateMessageStatus(accountId: String, localId: String, status:MessageStatus): Promise<ChatMessage>; + public function updateMessageStatus(accountId: String, localId: String, status:MessageStatus, statusText: Null<String>): Promise<ChatMessage>; public function getMessage(accountId: String, chatId: String, serverId: Null<String>, localId: Null<String>): Promise<Null<ChatMessage>>; public function getMessagesBefore(accountId: String, chatId: String, beforeId: Null<String>, beforeTime: Null<String>): Promise<Array<ChatMessage>>; public function getMessagesAfter(accountId: String, chatId: String, afterId: Null<String>, afterTime: Null<String>): Promise<Array<ChatMessage>>; diff --git a/borogove/Register.hx b/borogove/Register.hx index c635d4d..68ce33c 100644 --- a/borogove/Register.hx +++ b/borogove/Register.hx @@ -71,7 +71,7 @@ class Register { **/ public function getForm() { return stream.register(domain, preAuth).then(reply -> { - final error = getError(reply); + final error = reply.getErrorText(); if (error != null) return Promise.reject(error); final query = reply.getChild("query", "jabber:iq:register"); @@ -118,7 +118,7 @@ class Register { .tag("query", { xmlns: "jabber:iq:register" }) .addChild(toSubmit), (reply) -> { - final error = getError(reply); + final error = reply.getErrorText(); if (error != null) return reject(error); // It is conventional for username@domain to be the registered JID @@ -136,14 +136,4 @@ class Register { public function disconnect() { stream.disconnect(); } - - private function getError(iq: Stanza) { - if (iq.attr.get("type") == "error") { - final error = iq.getChild("error"); - final text = error.getChildText("text", "urn:ietf:params:xml:ns:xmpp-stanzas"); - return text ?? error.getFirstChild()?.name ?? "error"; - } - - return null; - } } diff --git a/borogove/Stanza.hx b/borogove/Stanza.hx index aa9c9c4..d7d043d 100644 --- a/borogove/Stanza.hx +++ b/borogove/Stanza.hx @@ -267,6 +267,15 @@ class Stanza { return allTags()[0]; } + public function getErrorText() { + if (attr.get("type") == "error") { + final error = getError(); + return error.text ?? error.condition ?? error.type; + } + + return null; + } + public function getChild(?name:Null<String>, ?xmlns:Null<String>):Null<Stanza> { var ourXmlns = this.attr.get("xmlns"); /* diff --git a/borogove/persistence/Dummy.hx b/borogove/persistence/Dummy.hx index 48d6f8f..7451752 100644 --- a/borogove/persistence/Dummy.hx +++ b/borogove/persistence/Dummy.hx @@ -82,7 +82,7 @@ class Dummy implements Persistence { } @HaxeCBridge.noemit - public function updateMessageStatus(accountId: String, localId: String, status:MessageStatus): Promise<ChatMessage> { + public function updateMessageStatus(accountId: String, localId: String, status:MessageStatus, statusText: Null<String>): Promise<ChatMessage> { return Promise.reject("Dummy cannot updateMessageStatus"); } diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js index 99706f1..0354467 100644 --- a/borogove/persistence/IDB.js +++ b/borogove/persistence/IDB.js @@ -139,6 +139,7 @@ export default async (dbname, media, tokenize, stemmer) => { message.syncPoint = !!value.syncPoint; message.direction = value.direction; message.status = value.status; + message.statusText = value.statusText; message.timestamp = value.timestamp && value.timestamp.toISOString(); message.from = value.from && borogove.JID.parse(value.from); message.sender = value.sender && borogove.JID.parse(value.sender); @@ -480,12 +481,12 @@ export default async (dbname, media, tokenize, stemmer) => { store.put(serializeMessage(account, message)); }, - updateMessageStatus: async function(account, localId, status) { + updateMessageStatus: async function(account, localId, status, statusText) { const tx = db.transaction(["messages"], "readwrite"); const store = tx.objectStore("messages"); const result = await promisifyRequest(store.index("localId").openCursor(IDBKeyRange.bound([account, localId], [account, localId, []]))); - if (result?.value && result.value.direction === enums.MessageDirection.MessageSent && result.value.status !== enums.MessageStatus.MessageDeliveredToDevice) { - const newStatus = { ...result.value, status: status }; + if (result?.value && result.value.direction === enums.MessageDirection.MessageSent && ![enums.MessageStatus.MessageDeliveredToDevice, enums.MessageStatus.MessageFailedToSend].includes(result.value.status)) { + const newStatus = { ...result.value, status, statusText }; result.update(newStatus); return await hydrateMessage(newStatus); } @@ -501,8 +502,8 @@ export default async (dbname, media, tokenize, stemmer) => { if (!cresult) break; const value = cresult.value; - if (value?.versions?.[0]?.localId === localId && value?.direction === enums.MessageDirection.MessageSent && value?.status !== enums.MessageStatus.MessageDeliveredToDevice) { - const newStatus = { ...value, versions: [{ ...value.versions[0], status: status }, ...value.versions.slice(1)], status: status }; + if (value?.versions?.[0]?.localId === localId && value?.direction === enums.MessageDirection.MessageSent && ![enums.MessageStatus.MessageDeliveredToDevice, enums.MessageStatus.MessageFailedToSend].includes(result.value.status)) { + const newStatus = { ...value, versions: [{ ...value.versions[0], status, statusText }, ...value.versions.slice(1)], status, statusText }; cresult.update(newStatus); return await hydrateMessage(newStatus); } diff --git a/borogove/persistence/Sqlite.hx b/borogove/persistence/Sqlite.hx index 4d2005d..a9d6309 100644 --- a/borogove/persistence/Sqlite.hx +++ b/borogove/persistence/Sqlite.hx @@ -130,6 +130,12 @@ class Sqlite implements Persistence implements KeyValueStore { "PRAGMA user_version = 2;"]); } return Promise.resolve(null); + }).then(_ -> { + if (version < 3) { + return exec(["ALTER TABLE messages ADD COLUMN status_text TEXT;", + "PRAGMA user_version = 3;"]); + } + return Promise.resolve(null); }); }); }); @@ -355,7 +361,7 @@ class Sqlite implements Persistence implements KeyValueStore { @returns Promise resolving to the message or null **/ public function getMessage(accountId: String, chatId: String, serverId: Null<String>, localId: Null<String>): Promise<Null<ChatMessage>> { - var q = "SELECT stanza, direction, type, status, strftime('%FT%H:%M:%fZ', created_at / 1000.0, 'unixepoch') AS timestamp, sender_id, mam_id, mam_by, sync_point FROM messages WHERE account_id=? AND chat_id=?"; + var q = "SELECT stanza, direction, type, status, status_text, strftime('%FT%H:%M:%fZ', created_at / 1000.0, 'unixepoch') AS timestamp, sender_id, mam_id, mam_by, sync_point FROM messages WHERE account_id=? AND chat_id=?"; final params = [accountId, chatId]; if (serverId != null) { q += " AND mam_id=?"; @@ -385,6 +391,7 @@ class Sqlite implements Persistence implements KeyValueStore { messages.direction, messages.type, messages.status, + messages.status_text, strftime('%FT%H:%M:%fZ', messages.created_at / 1000.0, 'unixepoch') AS timestamp, messages.sender_id, messages.mam_id, @@ -479,7 +486,7 @@ class Sqlite implements Persistence implements KeyValueStore { params.push(MessageSent); final q = new StringBuf(); - q.add("SELECT chat_id AS chatId, stanza, direction, type, status, sender_id, mam_id, mam_by, sync_point, CASE WHEN subq.created_at IS NULL THEN COUNT(*) ELSE COUNT(*) - 1 END AS unreadCount, strftime('%FT%H:%M:%fZ', MAX(messages.created_at) / 1000.0, 'unixepoch') AS timestamp FROM messages LEFT JOIN ("); + q.add("SELECT chat_id AS chatId, stanza, direction, type, status, status_text, sender_id, mam_id, mam_by, sync_point, CASE WHEN subq.created_at IS NULL THEN COUNT(*) ELSE COUNT(*) - 1 END AS unreadCount, strftime('%FT%H:%M:%fZ', MAX(messages.created_at) / 1000.0, 'unixepoch') AS timestamp FROM messages LEFT JOIN ("); q.add(subq.toString()); q.add(") subq USING (chat_id) WHERE account_id=? AND (stanza_id IS NULL OR stanza_id='' OR stanza_id=correction_id) AND chat_id IN ("); params.push(accountId); @@ -522,13 +529,13 @@ class Sqlite implements Persistence implements KeyValueStore { } @HaxeCBridge.noemit - public function updateMessageStatus(accountId: String, localId: String, status:MessageStatus): Promise<ChatMessage> { + public function updateMessageStatus(accountId: String, localId: String, status: MessageStatus, statusText: Null<String>): Promise<ChatMessage> { return db.exec( - "UPDATE messages SET status=? WHERE account_id=? AND stanza_id=? AND direction=? AND status <> ?", - [status, accountId, localId, MessageSent, MessageDeliveredToDevice] + "UPDATE messages SET status=?, status_text=? WHERE account_id=? AND stanza_id=? AND direction=? AND status <> ? AND status <> ?", + [status, statusText, accountId, localId, MessageSent, MessageDeliveredToDevice, MessageFailedToSend] ).then(_ -> db.exec( - "SELECT stanza, direction, type, status, strftime('%FT%H:%M:%fZ', created_at / 1000.0, 'unixepoch') AS timestamp, sender_id, correction_id AS stanza_id, mam_id, mam_by, sync_point FROM messages WHERE account_id=? AND stanza_id=? AND direction=? LIMIT 1", + "SELECT stanza, direction, type, status, status_text, strftime('%FT%H:%M:%fZ', created_at / 1000.0, 'unixepoch') AS timestamp, sender_id, correction_id AS stanza_id, mam_id, mam_by, sync_point FROM messages WHERE account_id=? AND stanza_id=? AND direction=? LIMIT 1", [accountId, localId, MessageSent] ) ).then(result -> @@ -803,7 +810,7 @@ class Sqlite implements Persistence implements KeyValueStore { } else { final params = [accountId]; final q = new StringBuf(); - q.add("SELECT chat_id, stanza_id, stanza, direction, type, status, strftime('%FT%H:%M:%fZ', created_at / 1000.0, 'unixepoch') AS timestamp, sender_id, mam_id, mam_by, sync_point FROM messages WHERE account_id=? AND ("); + q.add("SELECT chat_id, stanza_id, stanza, direction, type, status, status_text, strftime('%FT%H:%M:%fZ', created_at / 1000.0, 'unixepoch') AS timestamp, sender_id, mam_id, mam_by, sync_point FROM messages WHERE account_id=? AND ("); q.add(replyTos.map(parent -> if (parent.serverId != null) { params.push(parent.chatId); @@ -831,7 +838,7 @@ class Sqlite implements Persistence implements KeyValueStore { }); } - private function hydrateMessages(accountId: String, rows: Iterator<{ stanza: String, timestamp: String, direction: MessageDirection, type: MessageType, status: MessageStatus, mam_id: String, mam_by: String, sync_point: Int, sender_id: String, ?stanza_id: String, ?versions: String, ?version_times: String }>): Array<ChatMessage> { + private function hydrateMessages(accountId: String, rows: Iterator<{ stanza: String, timestamp: String, direction: MessageDirection, type: MessageType, status: MessageStatus, status_text: Null<String>, mam_id: String, mam_by: String, sync_point: Int, sender_id: String, ?stanza_id: String, ?versions: String, ?version_times: String }>): Array<ChatMessage> { // TODO: Calls can "edit" from multiple senders, but the original direction and sender holds final accountJid = JID.parse(accountId); return { iterator: () -> rows }.map(row -> ChatMessage.fromStanza(Stanza.parse(row.stanza), accountJid, (builder, _) -> { @@ -839,6 +846,7 @@ class Sqlite implements Persistence implements KeyValueStore { builder.timestamp = row.timestamp; builder.type = row.type; builder.status = row.status; + builder.statusText = row.status_text; builder.senderId = row.sender_id; builder.serverId = row.mam_id == "" ? null : row.mam_id; builder.serverIdBy = row.mam_by == "" ? null : row.mam_by;