| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-05-03 20:38:34 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-05-03 20:38:34 UTC |
| parent | 087fc331050e74888d0f5bbd7bd395183362eee1 |
| borogove/AsyncLock.hx | +9 | -0 |
| borogove/AttachmentSource.cpp.hx | +6 | -0 |
| borogove/AvailableChatIterator.hx | +10 | -0 |
| borogove/Caps.hx | +42 | -0 |
| borogove/Chat.hx | +31 | -5 |
| borogove/Client.hx | +10 | -0 |
| borogove/DataForm.hx | +3 | -0 |
| borogove/EncryptionInfo.hx | +3 | -0 |
| borogove/Form.hx | +6 | -0 |
| borogove/Identicon.hx | +3 | -0 |
| borogove/JID.hx | +32 | -0 |
| borogove/Message.hx | +9 | -0 |
| borogove/Presence.hx | +3 | -0 |
| borogove/Profile.hx | +33 | -0 |
| borogove/ReactionUpdate.hx | +12 | -0 |
| borogove/Status.hx | +6 | -0 |
diff --git a/borogove/AsyncLock.hx b/borogove/AsyncLock.hx index e9e4e36..4970b40 100644 --- a/borogove/AsyncLock.hx +++ b/borogove/AsyncLock.hx @@ -6,10 +6,19 @@ import thenshim.Promise; class AsyncLock { private var p: Promise<Any>; + /** + Create a new lock with no pending work. + **/ public function new() { p = Promise.resolve(null); } + /** + Run one async operation at a time in call order. + + @param fn operation to enqueue + @returns Promise resolving or rejecting with the result of `fn` + **/ public function run<T>(fn: () -> Promise<T>): Promise<T> { final next = p.then(_ -> fn()); p = next.then(_->{}, _->{}); // prevent chain break diff --git a/borogove/AttachmentSource.cpp.hx b/borogove/AttachmentSource.cpp.hx index 4f4e5c3..3906d76 100644 --- a/borogove/AttachmentSource.cpp.hx +++ b/borogove/AttachmentSource.cpp.hx @@ -15,6 +15,12 @@ class AttachmentSource { public final name: String; public final size: Int; + /** + Create an attachment source from a local file path and MIME type. + + @param path path to the local file + @param mime MIME type to advertise for the upload + **/ public function new(path: String, mime: String) { this.name = haxe.io.Path.withoutDirectory(path); this.path = sys.FileSystem.fullPath(path); diff --git a/borogove/AvailableChatIterator.hx b/borogove/AvailableChatIterator.hx index 2b97757..9860e93 100644 --- a/borogove/AvailableChatIterator.hx +++ b/borogove/AvailableChatIterator.hx @@ -165,12 +165,22 @@ class AvailableChatIterator { return this; } + /** + Get the next AvailableChat from this iterator for JavaScript async iteration. + + @returns Promise resolving to the next iterator result + **/ public function next(): Promise<{ done: Bool, ?value: AvailableChat }> { return internalNext().then(v -> { return { done: v == null, value: v }; }); } #else + /** + Get the next AvailableChat from this iterator. + + @returns Promise resolving to the next result, or null when exhausted + **/ public function next(): Promise<Null<AvailableChat>> { return internalNext(); } diff --git a/borogove/Caps.hx b/borogove/Caps.hx index eb055f8..8da2acd 100644 --- a/borogove/Caps.hx +++ b/borogove/Caps.hx @@ -50,6 +50,15 @@ class Caps { return result; } + /** + Create a capabilities description. + + @param node capability node identifier + @param identities disco identities advertised by the entity + @param features disco feature namespaces advertised by the entity + @param data extended disco data forms + @param ver optional precomputed capability hash bytes + **/ public function new(node: String, identities: Array<Identity>, features: Array<String>, data: Array<DataForm>, ?ver: BytesData) { if (ver == null) { // If we won't need to generate ver we don't actually need to sort @@ -67,11 +76,20 @@ class Caps { } } + /** + Check whether these capabilities describe a channel-like chat target. + + @param chatId ID to evaluate against the capability set + @returns true when the target looks like a MUC/channel + **/ public function isChannel(chatId: String) { if (chatId.indexOf("@") < 0) return false; // MUC must have a localpart return features.contains("http://jabber.org/protocol/muc") && identities.find((identity) -> identity.category == "conference") != null; } + /** + Build a disco#info query payload for this capability set. + **/ public function discoReply():Stanza { final query = new Stanza("query", { xmlns: "http://jabber.org/protocol/disco#info" }); for (identity in identities) { @@ -84,6 +102,12 @@ class Caps { return query; } + /** + Add capability advertisements to a stanza. + + @param stanza stanza to mutate + @returns the same stanza for chaining + **/ public function addC(stanza: Stanza): Stanza { stanza.tag("c", { xmlns: "http://jabber.org/protocol/caps", @@ -160,11 +184,17 @@ class Caps { return Hash.sha1(bytesOfString(s)); } + /** + Get the raw XEP-0115 capability hash object for this capability set. + **/ public function verRaw(): Hash { if (_ver == null) _ver = computeVer(); return _ver; } + /** + Get the XEP-0115 capability hash encoded in base64. + **/ public function ver(): String { return verRaw().toBase64(); } @@ -177,6 +207,9 @@ class Identity { public final name:String; public final lang:String; + /** + Create a disco identity. + **/ public function new(category:String, type: String, name: String, lang: Null<String> = null) { this.category = category; this.type = type; @@ -184,16 +217,25 @@ class Identity { this.lang = lang ?? ""; } + /** + Add this identity to a disco#info payload. + **/ public function addToDisco(stanza: Stanza) { var attrs: haxe.DynamicAccess<String> = { category: category, type: type, name: name }; if (lang != null && lang != "") attrs.set("xml:lang", lang); stanza.tag("identity", attrs).up(); } + /** + Get the identity string used when computing capability hashes. + **/ public function ver(): String { return category + "/" + type + "/" + (lang ?? "") + "/" + name; } + /** + Write the identity in canonical capability-hash form. + **/ public function writeTo(out: haxe.io.Output) { out.writeS(category); out.writeByte(0x1f); diff --git a/borogove/Chat.hx b/borogove/Chat.hx index 3bff4c3..1a6ac4c 100644 --- a/borogove/Chat.hx +++ b/borogove/Chat.hx @@ -35,6 +35,9 @@ typedef StringMapNullableKey = Map<Null<String>, String>; typedef StringMapNullableKey = haxe.ds.ObjectMap<Null<String>, String>; #end +/** + Persistent UI state for a chat in the local client. +**/ enum abstract UiState(Int) { var Pinned; var Open; // or Unspecified @@ -42,6 +45,9 @@ enum abstract UiState(Int) { var Invited; } +/** + Chat state notifications received from another participant. +**/ enum abstract UserState(Int) { var Gone; var Inactive; @@ -50,11 +56,14 @@ enum abstract UserState(Int) { var Paused; } -// Describes the current encryption mode of the conversation -// This mode is a high-level representation of the user/app *intent* -// for the current conversation - e.g. not a guarantee that incoming -// messages will always match this expectation. It is used to determine -// the logic for outgoing messages, though. +/** + Describes the current encryption mode of the conversation. + + This mode is a high-level representation of the user/app *intent* + for the current conversation - e.g. not a guarantee that incoming + messages will always match this expectation. It is used to determine + the logic for outgoing messages, though. +**/ enum abstract EncryptionMode(Int) { var Unencrypted; // No end-to-end encryption var EncryptedOMEMO; // Use OMEMO @@ -750,6 +759,11 @@ abstract class Chat { return session; } + /** + Add additional media streams to the active call in this chat. + + @param streams media streams to add to the current session + **/ @HaxeCBridge.noemit public function addMedia(streams: Array<MediaStream>) { if (callStatus() != Ongoing) throw "cannot add media when no call ongoing"; @@ -890,6 +904,9 @@ abstract class Chat { return commandJids().length > 0; } + /** + List commands exposed by this chat + **/ public function commands(): Promise<Array<Command>> { return thenshim.PromiseTools.all(commandJids().map(jid -> new Promise((resolve, reject) -> { final itemsGet = new DiscoItemsGet(jid.asString(), "http://jabber.org/protocol/commands"); @@ -1669,6 +1686,9 @@ class Channel extends Chat { return uiState != Closed && uiState != Invited; } + /** + Whether this channel is members-only/private. + **/ public function isPrivate() { return disco.features.contains("muc_membersonly"); } @@ -2237,6 +2257,9 @@ class SerializedChat { public final notifyMention: Bool; public final notifyReply: Bool; + /** + Create a serialized chat snapshot suitable for persistence. + **/ public function new(chatId: String, trusted: Bool, isBookmarked: Bool, avatarSha1: Null<BytesData>, presence: Map<String, Presence>, displayName: Null<String>, uiState: Null<UiState>, isBlocked: Null<Bool>, status: Status, extensions: Null<String>, readUpToId: Null<String>, readUpToBy: Null<String>, notificationsFiltered: Null<Bool>, notifyMention: Bool, notifyReply: Bool, threads: StringMapNullableKey, disco: Null<Caps>, omemoContactDeviceIDs: Array<Int>, klass: String) { this.chatId = chatId; this.trusted = trusted; @@ -2259,6 +2282,9 @@ class SerializedChat { this.klass = klass; } + /** + Recreate a live Chat object from this serialized representation. + **/ public function toChat(client: Client, stream: GenericStream, persistence: Persistence) { final extensionsStanza = Stanza.parse(extensions); var filterN = notificationsFiltered ?? false; diff --git a/borogove/Client.hx b/borogove/Client.hx index 18264d0..77723e1 100644 --- a/borogove/Client.hx +++ b/borogove/Client.hx @@ -44,6 +44,9 @@ using StringTools; import HaxeCBridge; #end +/** + Reason a chat-message listener was triggered. +**/ enum abstract ChatMessageEvent(Int) { var DeliveryEvent; var CorrectionEvent; @@ -859,6 +862,13 @@ class Client extends EventEmitter { ); } + /** + Publish an account status/activity item. + + @param status status payload to publish + @param expires expiration in seconds for the published item + @param publicAccess when true, make the item world-readable + **/ public function setStatus(status: Status, expires: Int = 86400, publicAccess: Bool = false) { publishWithOptions( new Stanza("iq", { type: "set" }) diff --git a/borogove/DataForm.hx b/borogove/DataForm.hx index dac063e..9f9872f 100644 --- a/borogove/DataForm.hx +++ b/borogove/DataForm.hx @@ -34,6 +34,9 @@ abstract DataForm(Stanza) from Stanza to Stanza { return this.allTags("item")?.map(row -> row.allTags("field")); } + /** + Get the field with the given name, if present. + **/ public function field(name: String): Null<Field> { final matches = fields.filter(f -> f.name == name); if (matches.length > 1) { diff --git a/borogove/EncryptionInfo.hx b/borogove/EncryptionInfo.hx index 7fd1691..358eaa7 100644 --- a/borogove/EncryptionInfo.hx +++ b/borogove/EncryptionInfo.hx @@ -4,6 +4,9 @@ package borogove; import HaxeCBridge; #end +/** + Outcome of decrypting an incoming message. +**/ enum abstract EncryptionStatus(Int) { var DecryptionSuccess; // Message was encrypted, and we decrypted it var DecryptionFailure; // Message is encrypted, and we failed to decrypt it diff --git a/borogove/Form.hx b/borogove/Form.hx index 82d02b3..2221c0a 100644 --- a/borogove/Form.hx +++ b/borogove/Form.hx @@ -272,10 +272,16 @@ class FormLayoutSection implements FormSection { this.section = section; } + /** + Get the layout section label, if any. + **/ public function title() { return section.attr.get("label"); } + /** + Get the renderable items contained in this layout section. + **/ public function items() { final items = []; for (child in section.allTags()) { diff --git a/borogove/Identicon.hx b/borogove/Identicon.hx index d9e9615..db56328 100644 --- a/borogove/Identicon.hx +++ b/borogove/Identicon.hx @@ -16,6 +16,9 @@ import HaxeCBridge; @:build(HaxeSwiftBridge.expose()) #end class Identicon { + /** + Generate a deterministic SVG identicon as a data URI. + **/ public static function svg(source: String) { final sha = Sha1.make(bytesOfString(source)); final input = new BytesInput(sha); diff --git a/borogove/JID.hx b/borogove/JID.hx index 54f32c3..c583769 100644 --- a/borogove/JID.hx +++ b/borogove/JID.hx @@ -6,6 +6,14 @@ class JID { public final domain : String; public final resource : Null<String>; + /** + Create a JID from its parts. + + @param node localpart, or null for a domain JID + @param domain domainpart + @param resource resourcepart, if any + @param raw when false, escape the node according to JID escaping rules + **/ public function new(?node:String, domain:String, ?resource:String, ?raw = false) { this.node = node == null || raw == true ? node : StringTools.replace(StringTools.replace(StringTools.replace( @@ -38,6 +46,9 @@ class JID { this.resource = resource; } + /** + Parse a JID string into its components. + **/ public static function parse(jid:String):JID { var resourceDelimiter = jid.indexOf("/"); var nodeDelimiter = jid.indexOf("@"); @@ -52,26 +63,44 @@ class JID { ); } + /** + Get the bare JID without any resource. + **/ public function asBare():JID { return new JID(this.node, this.domain, null, true); } + /** + Return a copy of this JID with a different resource. + **/ public function withResource(resource: String): JID { return new JID(this.node, this.domain, resource, true); } + /** + Check whether this JID looks valid enough to use. + **/ public function isValid():Bool { return domain.indexOf(".") >= 0; } + /** + Check whether this JID has no localpart. + **/ public function isDomain():Bool { return node == null; } + /** + Check whether this JID has no resourcepart. + **/ public function isBare():Bool { return resource == null; } + /** + Compare two JIDs for exact equality. + **/ public function equals(rhs:JID):Bool { return ( this.node == rhs.node && @@ -80,6 +109,9 @@ class JID { ); } + /** + Render this JID back to its string form. + **/ public function asString():String { return ( (this.node != null ? this.node + "@" : "") + diff --git a/borogove/Message.hx b/borogove/Message.hx index b992114..29f59ea 100644 --- a/borogove/Message.hx +++ b/borogove/Message.hx @@ -5,11 +5,17 @@ import borogove.Reaction; using Lambda; using StringTools; +/** + Direction of a chat message relative to the local account. +**/ enum abstract MessageDirection(Int) { var MessageReceived; var MessageSent; } +/** + Delivery state for an outgoing or incoming message. +**/ enum abstract MessageStatus(Int) { var MessagePending; // Message is waiting in client for sending var MessageDeliveredToServer; // Server acknowledged receipt of the message @@ -17,6 +23,9 @@ enum abstract MessageStatus(Int) { var MessageFailedToSend; // There was an error sending this message } +/** + High-level category of the message +**/ enum abstract MessageType(Int) { var MessageChat; var MessageCall; diff --git a/borogove/Presence.hx b/borogove/Presence.hx index a537e20..9ab8b52 100644 --- a/borogove/Presence.hx +++ b/borogove/Presence.hx @@ -14,6 +14,9 @@ abstract Presence(Stanza) from Stanza to Stanza { public var hats(get, never): Null<Array<Role>>; public var avatarHash(get, never): Null<Hash>; + /** + Create a presence stanza wrapper from caps, MUC metadata, and avatar hash. + **/ public function new(caps: Null<Caps>, mucUser: Null<MucUser>, avatarHash: Null<Hash>): Presence { final stanza = new Stanza("presence", { xmlns: "jabber:client" }); if (caps != null) caps.addC(stanza); diff --git a/borogove/Profile.hx b/borogove/Profile.hx index 8ab1724..4b68ed5 100644 --- a/borogove/Profile.hx +++ b/borogove/Profile.hx @@ -53,41 +53,68 @@ class ProfileItem { this.key = item.name; } + /** + Get parameter items attached to this profile item. + **/ public function parameters(): Array<ProfileItem> { final params = item.getChild("parameters")?.allTags() ?? []; return params.map(param -> new ProfileItem(param, id + "/" + ID.unique())); } + /** + Get text values for this profile item. + **/ public function text(): Array<String> { return item.allTags("text").map(s -> s.getText()); } + /** + Get URI values for this profile item. + **/ public function uri(): Array<String> { return item.allTags("uri").map(s -> s.getText()); } + /** + Get date values for this profile item. + **/ public function date(): Array<String> { return item.allTags("date").map(s -> s.getText()); } + /** + Get time values for this profile item. + **/ public function time(): Array<String> { return item.allTags("time").map(s -> s.getText()); } + /** + Get datetime values for this profile item. + **/ public function datetime(): Array<String> { return item.allTags("datetime").map(s -> s.getText()); } + /** + Get boolean values for this profile item. + **/ @HaxeCBridge.noemit public function boolean(): Array<Bool> { return item.allTags("boolean").map(s -> s.getText() == "true"); } + /** + Get integer values for this profile item. + **/ @HaxeCBridge.noemit public function integer(): Array<Int> { return item.allTags("integer").map(s -> Std.parseInt(s.getText()) ?? 0); } + /** + Get language-tag values for this profile item. + **/ public function languageTag(): Array<String> { return item.allTags("language-tag").map(s -> s.getText()); } @@ -138,6 +165,9 @@ class ProfileBuilder { private final vcard: Stanza; private var items: Array<ProfileItem> = []; + /** + Create a mutable builder from an existing profile. + **/ public function new(profile: Profile) { vcard = profile.vcard.clone(); final els = vcard.allTags().filter(el -> @@ -210,6 +240,9 @@ class ProfileBuilder { vcard.removeChild(prop.item); } + /** + Build an immutable Profile from the current builder state. + **/ public function build() { return new Profile(vcard.clone(), items.array()); } diff --git a/borogove/ReactionUpdate.hx b/borogove/ReactionUpdate.hx index 292b57b..1b460be 100644 --- a/borogove/ReactionUpdate.hx +++ b/borogove/ReactionUpdate.hx @@ -3,6 +3,9 @@ package borogove; import borogove.Reaction; using Lambda; +/** + How a reaction update should be applied to the existing reaction set. +**/ enum abstract ReactionUpdateKind(Int) { var EmojiReactions; var AppendReactions; @@ -22,6 +25,9 @@ class ReactionUpdate { public final reactions: Array<Reaction>; public final kind: ReactionUpdateKind; + /** + Create a reaction update for one message. + **/ public function new(updateId: String, serverId: Null<String>, serverIdBy: Null<String>, localId: Null<String>, chatId: String, senderId: String, timestamp: String, reactions: Array<Reaction>, kind: ReactionUpdateKind) { if (serverId == null && localId == null) throw "ReactionUpdate serverId and localId cannot both be null"; if (serverId != null && serverIdBy == null) throw "serverId requires serverIdBy"; @@ -36,6 +42,12 @@ class ReactionUpdate { this.kind = kind; } + /** + Apply this update to an existing reaction list. + + @param existingReactions reactions already known for the message + @returns the updated reaction list + **/ public function getReactions(existingReactions: Null<Array<Reaction>>): Array<Reaction> { if (kind == AppendReactions) { // TODO: make sure a new non-custom react doesn't override any customs we've added final set: Map<String, Bool> = []; diff --git a/borogove/Status.hx b/borogove/Status.hx index 15a956c..2333448 100644 --- a/borogove/Status.hx +++ b/borogove/Status.hx @@ -14,11 +14,17 @@ class Status { public final emoji: String; public final text: String; + /** + Create a status value with emoji and text. + **/ public function new(emoji: String, text: String) { this.emoji = emoji; this.text = text; } + /** + Render this status as plain text. + **/ public function toString() { return emoji + (emoji == "" || text == "" ? "" : " ") + text; }