| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-09-29 20:37:03 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-09-29 20:37:03 UTC |
| parent | cded225623ed3bab27e7809eaf9527b0fcdbd716 |
| HaxeCBridge.hx | +20 | -11 |
| HaxeSwiftBridge.hx | +8 | -1 |
| borogove/Chat.hx | +23 | -6 |
| borogove/ChatMessage.hx | +25 | -1 |
| borogove/ChatMessageBuilder.hx | +16 | -2 |
| borogove/Client.hx | +5 | -0 |
| borogove/Hash.hx | +36 | -0 |
| borogove/Notification.hx | +36 | -0 |
| borogove/Push.hx | +7 | -0 |
| borogove/Reaction.hx | +34 | -7 |
| borogove/persistence/MediaStoreFS.hx | +12 | -0 |
| borogove/persistence/Sqlite.hx | +20 | -0 |
diff --git a/HaxeCBridge.hx b/HaxeCBridge.hx index 5a8b2a4..2803fea 100644 --- a/HaxeCBridge.hx +++ b/HaxeCBridge.hx @@ -249,7 +249,7 @@ class HaxeCBridge { wrap = true; final atype = convertSecondaryTPtoType(path.params[0]); final aargs = atype.args; - args.push({name: "handler", type: TPath({name: "Callable", pack: ["cpp"], params: [TPType(TFunction(aargs.concat([TPath({name: "RawPointer", pack: ["cpp"], params: [TPType(TPath({ name: "Void", pack: ["cpp"] }))]})]), TPath({name: "Void", pack: []})))]})}); + args.push({name: "handler", type: TPath({name: "Callable", pack: ["cpp"], params: [TPType(TFunction(aargs.concat([TNamed("handler__context", TPath({name: "RawPointer", pack: ["cpp"], params: [TPType(TPath({ name: "Void", pack: ["cpp"] }))]}))]), TPath({name: "Void", pack: []})))]})}); promisify.push(macro v); if (atype.retainType == null) { promisifyE.push(macro false); @@ -264,13 +264,14 @@ class HaxeCBridge { promisify.push(macro handler__context); promisifyE.push(macro handler__context); wrapper.ret = TPath({name: "Void", pack: []}); + if (wrapper.doc != null) wrapper.doc = ~/@returns Promise resolving to/.replace(wrapper.doc, "@param handler which receives"); default: } if (wrap) { if (outPtr) { wrapper.kind = FFun({ret: wrapper.ret, params: fun.params, expr: macro { final out = $i{field.name}($a{passArgs}); if (outPtr != null) { cpp.Pointer.fromRaw(outPtr).set_ref(out); } return out.length; }, args: args}); } else if (promisify.length > 0) { - wrapper.kind = FFun({ret: wrapper.ret, params: fun.params, expr: macro $i{field.name}($a{passArgs}).then(v->handler($a{promisify}), e->handler($a{promisifyE})), args: args}); + wrapper.kind = FFun({ret: wrapper.ret, params: fun.params, expr: macro if (handler == null) $i{field.name}($a{passArgs}); else $i{field.name}($a{passArgs}).then(v->handler($a{promisify}), e->handler($a{promisifyE})), args: args}); } else { wrapper.kind = FFun({ret: wrapper.ret, params: fun.params, expr: macro return $i{field.name}($a{passArgs}), args: args}); } @@ -487,6 +488,7 @@ class HaxeCBridge { case FVar(_), FMethod(MethMacro): false; // skip macro methods case FMethod(_): true; } + if (!isConvertibleMethod) return; // f is public static function @@ -535,7 +537,7 @@ class HaxeCBridge { cFuncName = hx.strings.Strings.toLowerUnderscore(cFuncName); var cleanDoc = f.doc != null ? StringTools.trim(removeIndentation(f.doc)) : null; - + cConversionContext.addTypedFunctionDeclaration(cFuncName, functionDescriptor, cleanDoc, f.pos); inline function getRootCType(t: Type) { @@ -627,6 +629,9 @@ class HaxeCBridge { * Everything returned from an SDK procedure or passed to a function * pointer, both strings and opaque types, must be passed to * borogove_release when you are done with it. + * + * Thread-safety: All calls can be made from any thread. + * Callbacks may run on any thread. */ #ifndef __${hx.strings.Strings.toUpperUnderscore(namespace)}_H @@ -677,8 +682,6 @@ class HaxeCBridge { * * After executing no more calls to SDK functions can be made (as these will hang waiting for a response). * - * Thread-safety: Can be called safely called on any thread. - * * @param wait If `true`, this function will wait for all events scheduled to execute in the future on the SDK thread to complete. If `false`, immediate pending events will be finished and the SDK stopped without executing events scheduled in the future */ $prefix void ${namespace}_stop(bool wait); @@ -1140,7 +1143,7 @@ enum CModifier { enum CType { Ident(name: String, ?modifiers: Array<CModifier>); Pointer(t: CType, ?modifiers: Array<CModifier>); - FunctionPointer(name: String, argTypes: Array<CType>, ret: CType, ?modifiers: Array<CModifier>); + FunctionPointer(name: String, argNames: Array<String>, argTypes: Array<CType>, ret: CType, ?modifiers: Array<CModifier>); InlineStruct(struct: CStruct); Enum(name: String); } @@ -1205,13 +1208,18 @@ class CPrinter { (hasModifiers(modifiers) ? (printModifiers(modifiers) + ' ') : '') + name + (argName == null ? '' : ' $argName'); case Pointer(t, modifiers): printType(t) + '*' + (hasModifiers(modifiers) ? (' ' + printModifiers(modifiers)) : '') + (argName == null ? '' : ' $argName'); - case FunctionPointer(name, argTypes, ret): + case FunctionPointer(name, argNames, argTypes, ret): + final args = []; + for (i in 0...argTypes.length) { + final atype = printType(argTypes[i]); + args.push(argNames[i] == "" ? atype : ~/\*$/.replace(atype, " *") + " " + argNames[i]); + } if (argName == "") { - '${printType(ret)}(${argTypes.length > 0 ? argTypes.map((t) -> printType(t)).join(', ') : 'void'})'; + '${printType(ret)}(${argTypes.length > 0 ? args.join(', ') : 'void'})'; } else if (argName == null) { - '${printType(ret)} (* $name) (${argTypes.length > 0 ? argTypes.map((t) -> printType(t)).join(', ') : 'void'})'; + '${printType(ret)} (* $name) (${argTypes.length > 0 ? args.join(', ') : 'void'})'; } else { - '${printType(ret)} (* $argName) (${argTypes.length > 0 ? argTypes.map((t) -> printType(t)).join(', ') : 'void'})'; + '${printType(ret)} (* $argName) (${argTypes.length > 0 ? args.join(', ') : 'void'})'; } case InlineStruct(struct): 'struct {${printFields(struct.fields, false)}}' + (argName == null ? '' : ' $argName'); @@ -1653,7 +1661,7 @@ class CConverterContext { return switch cType { case Ident(name, modifiers): Ident(name, _setModifier(modifiers)); case Pointer(type, modifiers): Pointer(type, _setModifier(modifiers)); - case FunctionPointer(name, argTypes, ret, modifiers): FunctionPointer(name, argTypes, ret, _setModifier(modifiers)); + case FunctionPointer(name, argNames, argTypes, ret, modifiers): FunctionPointer(name, argNames, argTypes, ret, _setModifier(modifiers)); case InlineStruct(struct): cType; case Enum(name): cType; } @@ -1731,6 +1739,7 @@ class CConverterContext { var ident = safeIdent('function_' + args.map(arg -> typeDeclarationIdent(arg.t, false)).concat([typeDeclarationIdent(ret, false)]).join('_')); var funcPointer: CType = FunctionPointer( ident, + args.map(arg -> arg.name), args.map(arg -> convertType(arg.t, false, false, pos)), convertType(ret, false, false, pos) ); diff --git a/HaxeSwiftBridge.hx b/HaxeSwiftBridge.hx index 3889780..883c78f 100644 --- a/HaxeSwiftBridge.hx +++ b/HaxeSwiftBridge.hx @@ -424,7 +424,14 @@ class HaxeSwiftBridge { cFuncName = hx.strings.Strings.toLowerUnderscore(cFuncName); - final cleanDoc = f.doc != null ? StringTools.trim(removeIndentation(f.doc)) : null; + var cleanDoc = f.doc != null ? StringTools.trim(removeIndentation(f.doc)) : null; + if (cleanDoc != null) { + switch tret { + case TType(_.get().name => "Promise", params): + cleanDoc = ~/@returns Promise resolving to/.replace(cleanDoc, "@returns"); + default: + } + } if (cleanDoc != null) builder.add('\t/**\n${cleanDoc.split('\n').map(l -> '\t ' + l).join('\n')}\n\t */\n'); switch kind { diff --git a/borogove/Chat.hx b/borogove/Chat.hx index 8828e79..e0f397d 100644 --- a/borogove/Chat.hx +++ b/borogove/Chat.hx @@ -77,6 +77,9 @@ abstract class Chat { **/ @:allow(borogove) public var uiState(default, null): UiState = Open; + /** + Is this chat blocked? + **/ public var isBlocked(default, null): Bool = false; @:allow(borogove) private var extensions: Stanza; @@ -121,7 +124,7 @@ abstract class Chat { @param beforeId id of the message to look before @param beforeTime timestamp of the message to look before, String in format YYYY-MM-DDThh:mm:ss[.sss]+00:00 - @returns Promise of an array of ChatMessage that are found + @returns Promise resolving to an array of ChatMessage that are found **/ abstract public function getMessagesBefore(beforeId:Null<String>, beforeTime:Null<String>):Promise<Array<ChatMessage>>; @@ -131,7 +134,7 @@ abstract class Chat { @param afterId id of the message to look after @param afterTime timestamp of the message to look after, String in format YYYY-MM-DDThh:mm:ss[.sss]+00:00 - @returns Promise of an array of ChatMessage that are found + @returns Promise resolving to an array of ChatMessage that are found **/ abstract public function getMessagesAfter(afterId:Null<String>, afterTime:Null<String>):Promise<Array<ChatMessage>>; @@ -141,7 +144,7 @@ abstract class Chat { @param aroundId id of the message to look around @param aroundTime timestamp of the message to look around, String in format YYYY-MM-DDThh:mm:ss[.sss]+00:00 - @returns Promise of an array of ChatMessage that are found + @returns Promise resolving to an array of ChatMessage that are found **/ abstract public function getMessagesAround(aroundId:Null<String>, aroundTime:Null<String>):Promise<Array<ChatMessage>>; @@ -224,11 +227,12 @@ abstract class Chat { reaction.render( (text) -> { toSend.text = text.replace("\u{fe0f}", ""); - return; + return ""; }, (text, uri) -> { final hash = Hash.fromUri(uri); toSend.setHtml('<img alt="' + Util.xmlEscape(text) + '" src="' + Util.xmlEscape(hash == null ? uri : hash.bobUri()) + '" />'); + return ""; } ); sendMessage(toSend); @@ -483,8 +487,13 @@ abstract class Chat { lastMessage = message; } - public function setDisplayName(fn:String) { - this.displayName = fn; + /** + Set the display name to use for this chat + + @param displayName String to use as display name + **/ + public function setDisplayName(displayName: String) { + this.displayName = displayName; bookmark(); } @@ -550,6 +559,11 @@ abstract class Chat { this.avatarSha1 = sha1; } + /** + Set if this chat is to be trusted with our presence, etc + + @param trusted Bool if trusted or not + **/ public function setTrusted(trusted:Bool) { this.trusted = trusted; } @@ -566,6 +580,9 @@ abstract class Chat { return true; } + /** + @returns if this chat is currently syncing with the server + **/ public function syncing() { return !client.inSync; } diff --git a/borogove/ChatMessage.hx b/borogove/ChatMessage.hx index 5a14895..f496667 100644 --- a/borogove/ChatMessage.hx +++ b/borogove/ChatMessage.hx @@ -30,10 +30,25 @@ import borogove.Util; @:build(HaxeSwiftBridge.expose()) #end class ChatAttachment { + /** + Filename + **/ public final name: Null<String>; + /** + MIME Type + **/ public final mime: String; + /** + Size in bytes + **/ public final size: Null<Int>; + /** + URIs to data + **/ public final uris: ReadOnlyArray<String>; + /** + Hashes of data + **/ public final hashes: ReadOnlyArray<Hash>; #if cpp @@ -51,6 +66,14 @@ class ChatAttachment { } #if cpp + /** + Create a new attachment for adding to a ChatMessage + + @param name Optional filename + @param mime MIME type + @param size Size in bytes + @param uri URI to attachment + **/ public static function create(name: Null<String>, mime: String, size: Int, uri: String) { return new ChatAttachment(name, mime, size > 0 ? size : null, [uri], []); } @@ -261,7 +284,8 @@ class ChatMessage { return m; } - public function getReplyId() { + @:allow(borogove) + private function getReplyId() { if (replyId != null) return replyId; return type == MessageChannel || type == MessageChannelPrivate ? serverId : localId; } diff --git a/borogove/ChatMessageBuilder.hx b/borogove/ChatMessageBuilder.hx index 73b5a5c..4f5eba4 100644 --- a/borogove/ChatMessageBuilder.hx +++ b/borogove/ChatMessageBuilder.hx @@ -72,6 +72,9 @@ class ChatMessageBuilder { @:allow(borogove) private var replyTo: Array<JID> = []; + /** + The ID of the message sender + **/ public var senderId (get, default): Null<String> = null; /** @@ -234,6 +237,11 @@ class ChatMessageBuilder { if (uris.length > 0) attachments.push(new ChatAttachment(name, mime, size == null ? null : Std.parseInt(size), uris, hashes)); } + /** + Add an attachment to this message + + @param attachment The ChatAttachment to add + **/ public function addAttachment(attachment: ChatAttachment) { attachments.push(attachment); } @@ -282,7 +290,7 @@ class ChatMessageBuilder { throw "node was neither text nor element?"; } - /** + /** The ID of the Chat this message is associated with **/ public function chatId():String { @@ -300,10 +308,16 @@ class ChatMessageBuilder { return senderId ?? sender?.asString() ?? throw "sender is null"; } - public function isIncoming():Bool { + @:allow(borogove) + private function isIncoming():Bool { return direction == MessageReceived; } + /** + Build this builder into an immutable ChatMessage + + @returns the ChatMessage + **/ public function build() { if (serverId == null && localId == null) throw "Cannot build a ChatMessage with no id"; final to = this.to; diff --git a/borogove/Client.hx b/borogove/Client.hx index ae3b363..c1fff97 100644 --- a/borogove/Client.hx +++ b/borogove/Client.hx @@ -641,6 +641,8 @@ class Client extends EventEmitter { /** Gets the client ready to use but does not connect to the server + + @returns Promise resolving to true once the Client is ready **/ public function startOffline(): Promise<Bool> { #if cpp @@ -813,6 +815,9 @@ class Client extends EventEmitter { /** Turn a file into a ChatAttachment for attaching to a ChatMessage + + @param source The AttachmentSource to use + @returns Promise resolving to a ChatAttachment or null **/ public function prepareAttachment(source: AttachmentSource): Promise<Null<ChatAttachment>> { return persistence.findServicesWithFeature(accountId(), "urn:xmpp:http:upload:0").then((services) -> { diff --git a/borogove/Hash.hx b/borogove/Hash.hx index 27c6c37..b96f6e1 100644 --- a/borogove/Hash.hx +++ b/borogove/Hash.hx @@ -20,6 +20,9 @@ import HaxeCBridge; @:build(HaxeSwiftBridge.expose()) #end class Hash { + /** + Hash algorithm name + **/ public final algorithm: String; @:allow(borogove) private final hash: BytesData; @@ -30,6 +33,13 @@ class Hash { this.hash = hash; } + /** + Create a new Hash from a hex string + + @param algorithm name per https://xmpp.org/extensions/xep-0300.html + @param hash in hex format + @returns Hash or null on error + **/ public static function fromHex(algorithm: String, hash: String): Null<Hash> { try { return new Hash(algorithm, Bytes.ofHex(hash).getData()); @@ -38,6 +48,12 @@ class Hash { } } + /** + Create a new Hash from a ni:, cid: or similar URI + + @param uri The URI + @returns Hash or null on error + **/ public static function fromUri(uri: String): Null<Hash> { if (uri.startsWith("cid:") && uri.endsWith("@bob.xmpp.org") && uri.contains("+")) { final parts = uri.substr(4).split("@")[0].split("+"); @@ -64,6 +80,11 @@ class Hash { return new Hash("sha-256", Sha256.make(bytes).getData()); } + /** + Represent this Hash as a URI + + @returns URI as a string + **/ public function toUri() { if (Config.relativeHashUri) { return "/.well-known/ni/" + algorithm.urlEncode() + "/" + toBase64Url(); @@ -82,14 +103,29 @@ class Hash { return "ni:///" + algorithm.urlEncode() + ";" + toBase64Url(); } + /** + Represent this Hash as a hex string + + @returns hex string + **/ public function toHex() { return Bytes.ofData(hash).toHex(); } + /** + Represent this Hash as a Base64 string + + @returns Base64-encoded string + **/ public function toBase64() { return Base64.encode(Bytes.ofData(hash), true); } + /** + Represent this Hash as a Base64url string + + @returns Base64url-encoded string + **/ public function toBase64Url() { return Base64.urlEncode(Bytes.ofData(hash)); } diff --git a/borogove/Notification.hx b/borogove/Notification.hx index d0dc567..fe91a4e 100644 --- a/borogove/Notification.hx +++ b/borogove/Notification.hx @@ -14,17 +14,53 @@ import HaxeCBridge; @:build(HaxeSwiftBridge.expose()) #end class Notification { + /** + The title + **/ public final title: String; + /** + The body text + **/ public final body: String; + /** + The ID of the associated account + **/ public final accountId: String; + /** + The ID of the associated chat + **/ public final chatId: String; + /** + The ID of the message sender + **/ public final senderId: String; + /** + The serverId of the message + **/ public final messageId: String; + /** + The type of the message + **/ public final type: MessageType; + /** + If this is a call notification, the call status + **/ public final callStatus: Null<String>; + /** + If this is a call notification, the call session ID + **/ public final callSid: Null<String>; + /** + Optional image URI + **/ public final imageUri: Null<String>; + /** + Optional language code + **/ public final lang: Null<String>; + /** + Optional date and time of the event + **/ public final timestamp: Null<String>; @:allow(borogove) diff --git a/borogove/Push.hx b/borogove/Push.hx index fcacbe6..1390996 100644 --- a/borogove/Push.hx +++ b/borogove/Push.hx @@ -18,6 +18,13 @@ import HaxeCBridge; @:build(HaxeSwiftBridge.expose()) #end class Push { + /** + Receive a new push notification from some external system + + @param data the raw data from the push + @param persistence the persistence layer to write into + @returns a Notification representing the push data + **/ public static function receive(data: String, persistence: Persistence) { var stanza = Stanza.parse(data); if (stanza == null) return null; diff --git a/borogove/Reaction.hx b/borogove/Reaction.hx index d8bc0e9..a105955 100644 --- a/borogove/Reaction.hx +++ b/borogove/Reaction.hx @@ -13,14 +13,24 @@ import HaxeCBridge; @:build(HaxeSwiftBridge.expose()) #end class Reaction { + /** + ID of who sent this Reaction + **/ public final senderId: String; + /** + Date and time when this Reaction was sent, + in format YYYY-MM-DDThh:mm:ss[.sss]+00:00 + **/ public final timestamp: String; - public final text: String; - public final key: String; - public final envelopeId: Null<String>; + @:allow(borogove) + private final text: String; + @:allow(borogove) + private final key: String; + @:allow(borogove) + private final envelopeId: Null<String>; @:allow(borogove) - public function new(senderId: String, timestamp: String, text: String, envelopeId: Null<String> = null, key: Null<String> = null) { + private function new(senderId: String, timestamp: String, text: String, envelopeId: Null<String> = null, key: Null<String> = null) { this.senderId = senderId; this.timestamp = timestamp; this.text = text.replace("\u{fe0f}", ""); @@ -32,13 +42,26 @@ class Reaction { Create a new Unicode reaction to send @param unicode emoji of the reaction + @returns Reaction **/ public static function unicode(unicode: String) { return new Reaction("", "", unicode); } - @:allow(borogove) - private function render<T>(forText: (String) -> T, forImage: (String, String) -> T) { + /** + Create a new Unicode reaction to send + + @param forText Callback called if this is a textual reaction. + Called with the unicode String. + @param forImage Callback called if this is a custom/image reaction. + Called with the name and the URI to the image. + @returns the return value of the callback + **/ + #if cpp + public function render(forText: (String) -> String, forImage: (String, String) -> String) { + #else + public function render<T>(forText: (String) -> T, forImage: (String, String) -> T) { + #end return forText(text + "\u{fe0f}"); } } @@ -62,13 +85,17 @@ class CustomEmojiReaction extends Reaction { @param text name of custom emoji @param uri URI for media of custom emoji + @returns Reaction **/ public static function custom(text: String, uri: String) { return new CustomEmojiReaction("", "", text, uri); } - @:allow(borogove) + #if cpp + override public function render(forText: (String) -> String, forImage: (String, String) -> String) { + #else override public function render<T>(forText: (String) -> T, forImage: (String, String) -> T) { + #end final hash = Hash.fromUri(uri); return forImage(text, hash?.toUri() ?? uri); } diff --git a/borogove/persistence/MediaStoreFS.hx b/borogove/persistence/MediaStoreFS.hx index ef2300e..c52d2cf 100644 --- a/borogove/persistence/MediaStoreFS.hx +++ b/borogove/persistence/MediaStoreFS.hx @@ -12,11 +12,17 @@ import thenshim.Promise; #if cpp @:build(HaxeCBridge.expose()) @:build(HaxeSwiftBridge.expose()) +@HaxeCBridge.name("borogove_persistence_media_store_fs") #end class MediaStoreFS implements MediaStore { private final blobpath: String; private var kv: Null<KeyValueStore> = null; + /** + Store media on the filesystem + + @param path where on filesystem to store media + **/ public function new(path: String) { blobpath = path; } @@ -26,6 +32,12 @@ class MediaStoreFS implements MediaStore { this.kv = kv; } + /** + Get absolute path on filesystem to a particular piece of media + + @param uri The URI to the media (ni:// or similar) + @returns Promise resolving to the path or null + **/ public function getMediaPath(uri: String): Promise<Null<String>> { final hash = Hash.fromUri(uri); if (hash.algorithm == "sha-256") { diff --git a/borogove/persistence/Sqlite.hx b/borogove/persistence/Sqlite.hx index 8d38c27..68994c8 100644 --- a/borogove/persistence/Sqlite.hx +++ b/borogove/persistence/Sqlite.hx @@ -341,6 +341,15 @@ class Sqlite implements Persistence implements KeyValueStore { storeMessages(accountId, [message]); } + /** + Get a single message + + @param accountId the account the message was sent or received on + @param chatId the chat the message was sent or received on + @param serverId the serverId of the message (optional if localId is specified) + @param localId the localId of the message (optional if serverId is specified) + @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=?"; final params = [accountId, chatId]; @@ -628,6 +637,12 @@ class Sqlite implements Persistence implements KeyValueStore { }); } + /** + Remove an account from storage + + @param accountId the account to remove + @param completely if message history, etc should be removed also + **/ public function removeAccount(accountId:String, completely:Bool) { db.exec("DELETE FROM accounts WHERE account_id=?", [accountId]); @@ -639,6 +654,11 @@ class Sqlite implements Persistence implements KeyValueStore { } + /** + List all known accounts + + @returns Promise resolving to array of account IDs + **/ public function listAccounts(): Promise<Array<String>> { return db.exec("SELECT account_id FROM accounts").then(result -> result == null ? [] : { iterator: () -> result }.map(row -> row.account_id)