borogove

created pr with borogove/muc-create.1 on 2026-10-28T20:49:38Z · by 9e834e0d
added borogove/muc-create.2 on 2026-10-30T12:26:53Z · by 3c31bc8c
1: 7eacfb3 < -: ------- Initial channel creation
2: 4b5a982 ! 1: d85d8d1 review
3: 8c9556b < -: ------- Support intant room creation
4: 941aad7 < -: ------- Add tests
added borogove/muc-create.3 on 2026-11-03T03:40:00Z · by 9e834e0d
-: ------- > 1: 0aa766b Initial channel creation
1: d85d8d1 ! 2: a7f33d5 After config form is submitted, creation is complete
-: ------- > 3: e6042b0 Support intant room creation
-: ------- > 4: 366bac7 Add tests
-: ------- > 5: fece298 review
-: ------- > 6: 322b2bd Review replies
added borogove/muc-create.4 on 2026-11-03T03:42:40Z · by 9e834e0d
1: 0aa766b = 1: 0aa766b Initial channel creation
2: a7f33d5 = 2: a7f33d5 After config form is submitted, creation is complete
3: e6042b0 = 3: e6042b0 Support intant room creation
4: 366bac7 ! 4: 0da1c7e Add tests
5: fece298 = 5: 86bad8e review
6: 322b2bd = 6: 997cf5a Review replies
commented on 2026-11-09T14:24:06Z · by 3c31bc8c
lgtm!
cmds
checkout latest patchset:
ssh pr.pico.sh pull borogove:muc-create | git am -3
checkout specific patchset revision:
ssh pr.pico.sh pull borogove:muc-create [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh borogove:muc-create
add comment to patch request:
ssh pr.pico.sh comment borogove:muc-create "lgtm!"

Patchset borogove/muc-create.3 on 2026-11-03T03:40:00Z · commit e6042b0

Support intant room creation
Stephen Paul Weber 2026-10-28T20:42:50Z
Not blindly using the server defaults, but using a small number of
things specified by the user (name and isPrivate)

Also prefill the form with the name for non-instant creation.
Semantic diff summary
0 added, 0 modified, 0 signature changed, 0 removed across 0 analyzed files (5 files skipped: unsupported file type)
+0 -72 borogove/ChannelCreateOptions.hx #
......@@ -1,72 +0,0 @@
1-package borogove;
2-
3-using StringTools;
4-
5-#if cpp
6-import HaxeCBridge;
7-#end
8-
9-@:expose
10-@:nullSafety(StrictThreaded)
11-#if cpp
12-@:build(HaxeCBridge.expose())
13-@:build(HaxeSwiftBridge.expose())
14-#end
15-class ChannelCreateOptions {
16- /**
17- Service to create the channel on
18-
19- Will use the account default if not set
20- **/
21- public var service: Null<String> = null;
22-
23- /**
24- Human-readable name for the channel.
25- **/
26- public var name: Null<String> = null;
27-
28- /**
29- Localpart of the room ID
30- **/
31- public var localpart: Null<String> = null;
32-
33- /**
34- Whether the room should be members only
35- **/
36- public var isPrivate: Bool = true;
37-
38- /**
39- When true, leave the room locked after creation and let the caller
40- submit the full owner configuration form before it is unlocked.
41- **/
42- public var deferConfig: Bool = false;
43-
44- public function new() {}
45-
46- /**
47- Build the room JID for this channel on a MUC service.
48-
49- Uses `localpart` when supplied, otherwise derives one from `name`, and
50- finally falls back to a generated UUID.
51-
52- @param service MUC service domain, for example `groups.example.com`
53- @returns the room JID, for example `team@groups.example.com`
54- **/
55- public function jid(service: String) {
56- final requested = localpart;
57- var local = requested != null && requested.trim() != "" ? requested : slug(name);
58- if (local == null || local == "") local = ID.unique();
59-
60- return new JID(local, service);
61- }
62-
63- private static function slug(name: Null<String>): Null<String> {
64- if (name == null) return null;
65-
66- var result = name.trim().toLowerCase();
67- result = ~/[^a-z0-9]+/g.replace(result, "-");
68- result = ~/^-+|-+$/g.replace(result, "");
69-
70- return result == "" ? null : result;
71- }
72-}
+56 -1 borogove/Chat.hx #
......@@ -9,6 +9,7 @@ import borogove.ChatMessage;
99 import borogove.Color;
1010 import borogove.GenericStream;
1111 import borogove.ID;
12+import borogove.Form;
1213 import borogove.Message;
1314 import borogove.MessageSync;
1415 import borogove.Outbox;
......@@ -1521,6 +1522,10 @@ class Channel extends Chat {
15211522 @:allow(borogove)
15221523 private var joinFailed = null;
15231524 @:allow(borogove)
1525+ private var creationFormPrefill: Null<Map<String, Array<String>>> = null;
1526+ @:allow(borogove)
1527+ private var creationFormSubmission: Null<Map<String, Array<String>>> = null;
1528+ @:allow(borogove)
15241529 private var sync: Null<MessageSync> = null;
15251530 private var forceLive = false;
15261531 @:allow(borogove)
......@@ -1670,6 +1675,54 @@ class Channel extends Chat {
16701675 client.trigger("chats/update", [this]);
16711676 }
16721677
1678+ private function maybeConfigureCreation(): Void {
1679+ if (uiState != Creating || creationFormSubmission == null) return;
1680+
1681+ final command = new MucSettingsCommand(
1682+ client,
1683+ JID.parse(chatId),
1684+ () -> completeCreation(),
1685+ creationFormPrefill
1686+ );
1687+ command.execute().then(session -> {
1688+ if (session.status != "executing" || session.forms.length < 1) {
1689+ failCreationConfig(session);
1690+ return null;
1691+ }
1692+
1693+ #if js
1694+ final submitData = creationFormSubmission;
1695+ #else
1696+ final submitData = FormSubmitBuilder.fromMap(creationFormSubmission);
1697+ #end
1698+
1699+ session.execute(null, submitData, 0).then(nextSession -> {
1700+ if (nextSession.status != "completed") failCreationConfig(nextSession);
1701+ return null;
1702+ }, err -> {
1703+ failCreationConfig(err);
1704+ return null;
1705+ });
1706+ return null;
1707+ }, err -> {
1708+ failCreationConfig(err);
1709+ return null;
1710+ });
1711+ }
1712+
1713+ private function failCreationConfig(error: Dynamic): Void {
1714+ if (uiState != Creating) return;
1715+
1716+ final stanza: Stanza = Std.isOfType(error, Stanza)
1717+ ? (cast error : Stanza)
1718+ : new Stanza("presence", { type: "error" })
1719+ .tag("error", { type: "cancel" })
1720+ .tag("internal-server-error", { xmlns: "urn:ietf:params:xml:ns:xmpp-stanzas" });
1721+
1722+ setJoinFailed(stanza);
1723+ client.trigger("chats/update", [this]);
1724+ }
1725+
16731726 private function selfPingSuccess() {
16741727 if (nickInUse() != client.displayName()) {
16751728 final desiredFullJid = JID.parse(chatId).withResource(client.displayName());
......@@ -1798,8 +1851,9 @@ class Channel extends Chat {
17981851 override public function commands(): Promise<Array<Command>> {
17991852 return super.commands().then((cmds: Array<Command>) -> {
18001853 if (isOwner()) {
1854+ final prefill = uiState == Creating ? creationFormPrefill : null;
18011855 final onCompleted = uiState == Creating ? () -> completeCreation() : null;
1802- cmds.push(new MucSettingsCommand(client, JID.parse(chatId), onCompleted));
1856+ cmds.push(new MucSettingsCommand(client, JID.parse(chatId), onCompleted, prefill));
18031857 }
18041858 return cmds;
18051859 });
......@@ -1867,6 +1921,7 @@ class Channel extends Chat {
18671921 .tag("conflict", { xmlns: "urn:ietf:params:xml:ns:xmpp-stanzas" })
18681922 );
18691923 } else {
1924+ if (!isJoined) maybeConfigureCreation();
18701925 isJoined = true;
18711926 }
18721927 client.trigger("chats/update", [this]);
+72 -7 borogove/Client.hx #
......@@ -1557,24 +1557,71 @@ class Client extends EventEmitter {
15571557 }
15581558
15591559 /**
1560- Create a new Channel
1560+ Create a new unconfigured Channel
15611561
1562- @param options channel creation options
1562+ The returned channel starts in the `Creating` state and stays locked until
1563+ the caller submits the muc-settings command.
1564+
1565+ @param name human-readable name for the channel
1566+ @param localpart localpart of the room ID
1567+ @param service MUC service domain; uses the account default if omitted
1568+ @returns A promise resolving to the newly created Channel, in the `Creating` state
1569+ **/
1570+ public function createChannel(
1571+ ?name: Null<String>,
1572+ ?localpart: Null<String>,
1573+ ?service: Null<String>
1574+ ): Promise<Channel> {
1575+ return createChannelInternal(name, localpart, service, name == null ? null : ["muc#roomconfig_roomname" => [name]], null);
1576+ }
1577+
1578+ /**
1579+ Create a new Channel and configure it
1580+
1581+ @param name human-readable name for the channel
1582+ @param isPrivate whether the room should be members only
1583+ @param localpart localpart of the room ID
1584+ @param service MUC service domain; uses the account default if omitted
15631585 @returns A promise resolving to the newly created Channel, in the `Creating` state
15641586 **/
1565- public function createChannel(options: ChannelCreateOptions): Promise<Channel> {
1566- return (if (options.service != null) {
1567- Promise.resolve(options.service);
1587+ public function createChannelInstant(
1588+ ?name: Null<String>,
1589+ isPrivate: Bool = true,
1590+ ?localpart: Null<String>,
1591+ ?service: Null<String>
1592+ ): Promise<Channel> {
1593+ final submission = new Map<String, Array<String>>();
1594+ if (name != null && name.trim() != "") {
1595+ submission.set("muc#roomconfig_roomname", [name]);
1596+ }
1597+ submission.set("muc#roomconfig_membersonly", [isPrivate ? "1" : "0"]);
1598+ submission.set("muc#roomconfig_persistentroom", ["1"]);
1599+ submission.set("muc#roomconfig_whois", ["anyone"]);
1600+ submission.set("muc#roomconfig_allowinvites", ["1"]);
1601+ return createChannelInternal(name, localpart, service, null, submission);
1602+ }
1603+
1604+ private function createChannelInternal(
1605+ ?name: Null<String>,
1606+ ?localpart: Null<String>,
1607+ ?service: Null<String>,
1608+ ?prefill: Null<Map<String, Array<String>>>,
1609+ ?submission: Null<Map<String, Array<String>>>
1610+ ): Promise<Channel> {
1611+ return (if (service != null) {
1612+ Promise.resolve(service);
15681613 } else {
15691614 persistence.findServicesWithFeature(accountId(), "http://jabber.org/protocol/muc").then(services -> services[0]?.serviceId);
15701615 }).then((service) -> {
15711616 if (service == null) throw "No default service on which to make a channel";
15721617
1573- final chatId = options.jid(service).asString();
1618+ final chatId = channelJid(service, name, localpart).asString();
15741619 if (getChat(chatId) != null) throw 'Chat already exists: ${chatId}';
15751620
15761621 final channel = new Channel(this, this.stream, this.persistence, chatId, Creating, false, false, null);
1577- channel.displayName = options.name ?? options.localpart ?? chatId;
1622+ channel.displayName = name ?? localpart ?? chatId;
1623+ channel.creationFormPrefill = prefill;
1624+ channel.creationFormSubmission = submission;
15781625 channel.join();
15791626
15801627 chats.unshift(channel);
......@@ -1585,6 +1632,24 @@ class Client extends EventEmitter {
15851632 });
15861633 }
15871634
1635+ private static function channelJid(service: String, name: Null<String>, localpart: Null<String>): JID {
1636+ final requested = localpart;
1637+ var local = requested != null && requested.trim() != "" ? requested : channelSlug(name);
1638+ if (local == null || local == "") local = ID.unique();
1639+
1640+ return new JID(local, service);
1641+ }
1642+
1643+ private static function channelSlug(name: Null<String>): Null<String> {
1644+ if (name == null) return null;
1645+
1646+ var result = name.trim().toLowerCase();
1647+ result = ~/[^a-z0-9]+/g.replace(result, "-");
1648+ result = ~/^-+|-+$/g.replace(result, "");
1649+
1650+ return result == "" ? null : result;
1651+ }
1652+
15881653 /**
15891654 Find a chat by id
15901655
+9 -0 borogove/Form.hx #
......@@ -78,6 +78,15 @@ class FormSubmitBuilder {
7878 **/
7979 public function new() { }
8080
81+ @:allow(borogove)
82+ private static function fromMap(data: Map<String, Array<String>>): FormSubmitBuilder {
83+ final builder = new FormSubmitBuilder();
84+ for (k => values in data) {
85+ for (v in values) builder.add(k, v);
86+ }
87+ return builder;
88+ }
89+
8190 /**
8291 Add a submitted value for a field.
8392
+13 -1 borogove/MucSettingsCommand.hx #
......@@ -13,11 +13,13 @@ import HaxeCBridge;
1313
1414 class MucSettingsCommand extends Command {
1515 private var onCompleted: Null<()->Void>;
16+ private var prefill: Null<Map<String, Array<String>>>;
1617
1718 @:allow(borogove)
18- public function new(client: Client, jid: JID, ?onCompleted: ()->Void) {
19+ public function new(client: Client, jid: JID, ?onCompleted: ()->Void, ?prefill: Null<Map<String, Array<String>>>) {
1920 super(client, { jid: jid, name: "Settings", node: "muc-settings" });
2021 this.onCompleted = onCompleted;
22+ this.prefill = prefill;
2123 }
2224
2325 @:allow(borogove.MucSettingsCommandSession)
......@@ -27,6 +29,15 @@ class MucSettingsCommand extends Command {
2729 if (cb != null) cb();
2830 }
2931
32+ private static function applyPrefill(form: DataForm, prefill: Null<Map<String, Array<String>>>): Void {
33+ if (prefill == null) return;
34+
35+ for (name => values in prefill) {
36+ final field = form.field(name);
37+ if (field != null) field.value = values;
38+ }
39+ }
40+
3041 override public function execute(): Promise<CommandSession> {
3142 return new Promise<CommandSession>((resolve, reject) -> {
3243 final q = new MucSettingsGet(this.jid.asString());
......@@ -43,6 +54,7 @@ class MucSettingsCommand extends Command {
4354 return;
4455 }
4556
57+ applyPrefill(form, prefill);
4658 resolve(new MucSettingsCommandSession("executing", null, [new FormOption("Submit", "execute"), new FormOption("Cancel", "cancel")], [new Form(form, null)], this));
4759 });
4860 this.client.sendQuery(q);
Back to top