| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-12-16 15:00:21 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-12-16 15:00:33 UTC |
| parent | 0c31c6ad0fb426b6fe16b9b762a554e259c688d5 |
| borogove/Client.hx | +32 | -10 |
| borogove/Participant.hx | +5 | -5 |
| borogove/Profile.hx | +147 | -35 |
| borogove/Stanza.hx | +17 | -0 |
| npm/index.ts | +1 | -0 |
diff --git a/borogove/Client.hx b/borogove/Client.hx index be701a8..c42cba7 100644 --- a/borogove/Client.hx +++ b/borogove/Client.hx @@ -15,6 +15,7 @@ import borogove.EncryptionPolicy; #if !NO_OMEMO import borogove.OMEMO; #end +import borogove.Profile; import borogove.PubsubEvent; import borogove.Stream; import borogove.Util; @@ -756,21 +757,42 @@ class Client extends EventEmitter { } /** - Set the current display name for this account on the server + Set the current profile for this account on the server - @param display name to set (ignored if empty or NULL) + @param profile to set + @param publicAccess set the access for the profile to public **/ - public function setDisplayName(displayName: String) { - if (displayName == null || displayName == "" || displayName == this.displayName()) return; + public function setProfile(profile: ProfileBuilder, publicAccess: Bool) { + final fn = profile.build().items.find(item -> item.key == "fn"); + if (fn != null) { + final fnText = fn.text()[0]; + if (fnText != null && fnText != "" && fnText != this.displayName()) { + stream.sendIq( + new Stanza("iq", { type: "set" }) + .tag("pubsub", { xmlns: "http://jabber.org/protocol/pubsub" }) + .tag("publish", { node: "http://jabber.org/protocol/nick" }) + .tag("item") + .textTag("nick", fnText, { xmlns: "http://jabber.org/protocol/nick" }) + .up().up().up(), + (response) -> { } + ); + } + } - stream.sendIq( + publishWithOptions( new Stanza("iq", { type: "set" }) .tag("pubsub", { xmlns: "http://jabber.org/protocol/pubsub" }) - .tag("publish", { node: "http://jabber.org/protocol/nick" }) - .tag("item") - .textTag("nick", displayName, { xmlns: "http://jabber.org/protocol/nick" }) - .up().up().up(), - (response) -> { } + .tag("publish", { node: "urn:xmpp:vcard4" }) + .tag("item", { id: ID.long() }) + .addChild(profile.buildStanza()), + new Stanza("x", { xmlns: "jabber:x:data", type: "submit" }) + .tag("field", { "var": "FORM_TYPE", type: "hidden" }).textTag("value", "http://jabber.org/protocol/pubsub#publish-options").up() + .tag("field", { "var": "pubsub#title" }).textTag("value", "Profile").up() + .tag("field", { "var": "pubsub#type" }).textTag("value", "urn:ietf:params:xml:ns:vcard-4.0").up() + .tag("field", { "var": "pubsub#deliver_payloads" }).textTag("value", "false").up() + .tag("field", { "var": "pubsub#persist_items" }).textTag("value", "true").up() + .tag("field", { "var": "pubsub#max_items" }).textTag("value", "1").up() + .tag("field", { "var": "pubsub#access_model" }).textTag("value", publicAccess ? "open" : "presence").up(), ); } diff --git a/borogove/Participant.hx b/borogove/Participant.hx index a700638..a870db6 100644 --- a/borogove/Participant.hx +++ b/borogove/Participant.hx @@ -35,12 +35,12 @@ class Participant { final get = new PubsubGet(jid.asString(), "urn:xmpp:vcard4"); get.onFinished(() -> { final item = get.getResult()[0]; - final vcard = item?.getChild("vcard", "urn:ietf:params:xml:ns:vcard-4.0"); - if (vcard == null) { - resolve(new Profile(new Stanza("vcard"))); - } else { - resolve(new Profile(vcard)); + final fromItem = item?.getChild("vcard", "urn:ietf:params:xml:ns:vcard-4.0"); + final vcard = fromItem == null ? new Stanza("vcard", { xmlns: "urn:ietf:params:xml:ns:vcard-4.0" }) : fromItem; + if (!vcard.hasChild("fn")) { + vcard.insertChild(0, new Stanza("fn").textTag("text", displayName)); } + resolve(new Profile(vcard)); }); client.sendQuery(get); }); diff --git a/borogove/Profile.hx b/borogove/Profile.hx index dca49e1..bc4d65d 100644 --- a/borogove/Profile.hx +++ b/borogove/Profile.hx @@ -1,5 +1,8 @@ package borogove; +import haxe.ds.ReadOnlyArray; +using Lambda; + import borogove.Stanza; #if cpp @@ -13,40 +16,20 @@ import HaxeCBridge; @:build(HaxeSwiftBridge.expose()) #end class Profile { + @:allow(borogove.ProfileBuilder) private final vcard: Stanza; - @:allow(borogove) - private function new(vcard: Stanza) { - this.vcard = vcard; - } - - /** - Get a property from the profile - - @param key what property to get - @returns the property value - **/ - public function get(key: String): Array<ProfileItem> { - return vcard.allTags(key).map(child -> new ProfileItem(child)); - } - /** - List the regular properties which can be represented by a ProfileItem + All items in the profile **/ - public function properties(): Array<String> { - final names = vcard.allTags().map(el -> el.name); - final result = []; - final seen: Map<String, Bool> = []; - - for (name in names) { - // Compound properties that don't follow the normal rules - if (seen[name] != true && !["n", "adr", "gender"].contains(name)) { - seen[name] = true; - result.push(name); - } - } + public final items: ReadOnlyArray<ProfileItem>; - return result; + @:allow(borogove) + private function new(vcard: Stanza, ?items: ReadOnlyArray<ProfileItem>) { + this.vcard = vcard; + this.items = items != null ? items : vcard.allTags().filter(el -> + TYPES[el.name] != null // remove unknown or compound property + ).map(child -> new ProfileItem(child, child.name + "/" + ID.short())); } } @@ -57,20 +40,22 @@ class Profile { @:build(HaxeSwiftBridge.expose()) #end class ProfileItem { + public final id: String; + public final key: String; + + @:allow(borogove.ProfileBuilder) private final item: Stanza; @:allow(borogove.Profile) - private function new(item: Stanza) { + private function new(item: Stanza, id: String) { this.item = item; - } - - public function key() { - return item.name; + this.id = id; + this.key = item.name; } public function parameters(): Array<ProfileItem> { final params = item.getChild("parameters")?.allTags() ?? []; - return params.map(param -> new ProfileItem(param)); + return params.map(param -> new ProfileItem(param, id + "/" + ID.short())); } public function text(): Array<String> { @@ -107,3 +92,130 @@ class ProfileItem { return item.allTags("language-tag").map(s -> s.getText()); } } + +final TYPES = [ + "source" => "uri", + "kind" => "text", + "fn" => "text", + "nickname" => "text", // text list is allowed + "photo" => "uri", + "bday" => "date", // spec says date and/or time or text + "anniversary" => "date", // same as bday + "tel" => "uri", // spec says text is allowed for compatibility but SHOULD be URI + "email" => "text", + "impp" => "uri", + "lang" => "language-tag", + "tz" => "text", // spec allows utc-offset NOT RECOMMENDED and URI + "geo" => "uri", + "title" => "text", + "role" => "text", + "logo" => "uri", + "org" => "text", // text list says spec. non-xml spec says structured + "member" => "uri", + "related" => "uri", // spec says text is allowed + "categories" => "text", // text list is allowed + "note" => "text", + "prodid" => "text", + "rev" => "timestamp", + "sound" => "uri", + "uid" => "uri", // MAY be text + "url" => "uri", + "version" => "text", // always 4 for now... + "key" => "uri", // spec says may be text + "fburl" => "uri", + "caladruri" => "uri", + "caluri" => "uri", + "pronouns" => "text", +]; + +@:expose +@:nullSafety(Strict) +#if cpp +@:build(HaxeCBridge.expose()) +@:build(HaxeSwiftBridge.expose()) +#end +class ProfileBuilder { + private final vcard: Stanza; + private var items: Array<ProfileItem> = []; + + public function new(profile: Profile) { + vcard = profile.vcard.clone(); + final els = vcard.allTags().filter(el -> + // Compound properties that don't follow the normal item rules + !["n", "adr", "gender"].contains(el.name) + ); + for (item in profile.items) { + final el = els.shift(); + if (el == null || el.name != item.key) throw "els/items mismatch"; + items.push(new ProfileItem(el, item.id)); + } + } + + /** + Add a new field to this profile + **/ + public function add(k: String, v: String) { + final type = TYPES[k]; + if (type != null) { + final el = new Stanza(k).textTag(type, v); + vcard.addChild(el); + items.push(new ProfileItem(el, k + "/" + ID.short())); + } else { + throw 'Unknown profile property ${k}'; + } + } + + /** + Set the value of an existing field on this profile + **/ + public function set(id: String, v: String) { + final parts = id.split("/"); + final k = parts[0]; + final prop = items.find(item -> item.id == id)?.item; + if (prop == null) throw 'prop not found for ${id}'; + + final type = TYPES[k]; + if (type != null) { + prop.removeChildren(); + prop.textTag(type, v); + } else { + throw 'Unknown profile property ${k}'; + } + } + + /** + Move a profile item + + @param id the item to move + @param moveTo the item currently in the position where it should move to + **/ + public function move(id: String, moveTo: String) { + final move = items.find(item -> item.id == id); + if (move == null) throw 'item ${id} not found'; + + final idx = items.findIndex(item -> item.id == moveTo); + remove(id); + items.insert(idx, move); + vcard.insertChild(idx, move.item); + } + + /** + Remove a field from this profile + **/ + public function remove(id: String) { + final prop = items.find(item -> item.id == id); + if (prop == null) return; + + items = items.filter(item -> item.id != id); + vcard.removeChild(prop.item); + } + + public function build() { + return new Profile(vcard.clone(), items.array()); + } + + @:allow(borogove) + private function buildStanza() { + return vcard.clone(); + } +} diff --git a/borogove/Stanza.hx b/borogove/Stanza.hx index d7d043d..49b2c6f 100644 --- a/borogove/Stanza.hx +++ b/borogove/Stanza.hx @@ -207,6 +207,12 @@ class Stanza { return this; } + public function insertChild(idx: Int, stanza:Stanza) { + serialized = null; + this.last_added.children.insert(idx, Element(stanza)); + return this; + } + public function addChild(stanza:Stanza) { serialized = null; this.last_added.children.push(Element(stanza)); @@ -390,6 +396,17 @@ class Stanza { ); } + public function removeChild(remove: Stanza) { + children = children.filter((child) -> { + switch(child) { + case Element(c): + return remove != c; + default: + return true; + } + }); + } + public function removeChildren(?name: String, ?xmlns_:String):Void { serialized = null; diff --git a/npm/index.ts b/npm/index.ts index b8db24a..f1aa81a 100644 --- a/npm/index.ts +++ b/npm/index.ts @@ -32,6 +32,7 @@ export { borogove_Participant as Participant, borogove_Persistence as Persistence, borogove_Profile as Profile, + borogove_ProfileBuilder as ProfileBuilder, borogove_ProfileItem as ProfileItem, borogove_Push as Push, borogove_Reaction as Reaction,