borogove
created pr with
borogove/muc-create.1
added borogove/muc-create.2
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
-: ------- > 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
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
lgtm!
cmds
checkout latest patchset:
ssh pr.pico.sh pull borogove:muc-create | git am -3checkout specific patchset revision:
ssh pr.pico.sh pull borogove:muc-create [rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh borogove:muc-createadd comment to patch request:
ssh pr.pico.sh comment borogove:muc-create "lgtm!"
Patchset
borogove/muc-create.4
Initial channel creation
Stephen Paul Weber
After config form is submitted, creation is complete
2026-10-28T19:34:47ZStephen Paul Weber
→ Support intant room creation
2026-10-28T19:56:35ZStephen Paul Weber
Add tests
2026-10-28T20:42:50ZStephen Paul Weber
review
2026-10-28T20:49:11ZEric Roberts
Review replies
2026-10-30T12:26:06ZStephen Paul Weber
2026-11-03T03:39:25Z
Support intant room creation
Stephen Paul Weber
2026-10-28T20:42:50ZNot 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
#
| ... | ... | @@ -1521,6 +1522,10 @@ class Channel extends Chat { | |
| 1521 | 1522 | @:allow(borogove) | |
| 1522 | 1523 | private var joinFailed = null; | |
| 1523 | 1524 | @: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) | |
| 1524 | 1529 | private var sync: Null<MessageSync> = null; | |
| 1525 | 1530 | private var forceLive = false; | |
| 1526 | 1531 | @:allow(borogove) |
| ... | ... | @@ -1670,6 +1675,54 @@ class Channel extends Chat { | |
| 1670 | 1675 | client.trigger("chats/update", [this]); | |
| 1671 | 1676 | } | |
| 1672 | 1677 | ||
| 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 | + | ||
| 1673 | 1726 | private function selfPingSuccess() { | |
| 1674 | 1727 | if (nickInUse() != client.displayName()) { | |
| 1675 | 1728 | final desiredFullJid = JID.parse(chatId).withResource(client.displayName()); |
| ... | ... | @@ -1798,8 +1851,9 @@ class Channel extends Chat { | |
| 1798 | 1851 | override public function commands(): Promise<Array<Command>> { | |
| 1799 | 1852 | return super.commands().then((cmds: Array<Command>) -> { | |
| 1800 | 1853 | if (isOwner()) { | |
| 1854 | + | final prefill = uiState == Creating ? creationFormPrefill : null; | |
| 1801 | 1855 | 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)); | |
| 1803 | 1857 | } | |
| 1804 | 1858 | return cmds; | |
| 1805 | 1859 | }); |
+72
-7
borogove/Client.hx
#
| ... | ... | @@ -1557,24 +1557,71 @@ class Client extends EventEmitter { | |
| 1557 | 1557 | } | |
| 1558 | 1558 | ||
| 1559 | 1559 | /** | |
| 1560 | - | Create a new Channel | |
| 1560 | + | Create a new unconfigured Channel | |
| 1561 | 1561 | ||
| 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 | |
| 1563 | 1585 | @returns A promise resolving to the newly created Channel, in the `Creating` state | |
| 1564 | 1586 | **/ | |
| 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); | |
| 1568 | 1613 | } else { | |
| 1569 | 1614 | persistence.findServicesWithFeature(accountId(), "http://jabber.org/protocol/muc").then(services -> services[0]?.serviceId); | |
| 1570 | 1615 | }).then((service) -> { | |
| 1571 | 1616 | if (service == null) throw "No default service on which to make a channel"; | |
| 1572 | 1617 | ||
| 1573 | - | final chatId = options.jid(service).asString(); | |
| 1618 | + | final chatId = channelJid(service, name, localpart).asString(); | |
| 1574 | 1619 | if (getChat(chatId) != null) throw 'Chat already exists: ${chatId}'; | |
| 1575 | 1620 | ||
| 1576 | 1621 | 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; | |
| 1578 | 1625 | channel.join(); | |
| 1579 | 1626 | ||
| 1580 | 1627 | chats.unshift(channel); |
| ... | ... | @@ -1585,6 +1632,24 @@ class Client extends EventEmitter { | |
| 1585 | 1632 | }); | |
| 1586 | 1633 | } | |
| 1587 | 1634 | ||
| 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 | + | ||
| 1588 | 1653 | /** | |
| 1589 | 1654 | Find a chat by id | |
| 1590 | 1655 |
+9
-0
borogove/Form.hx
#
| ... | ... | @@ -78,6 +78,15 @@ class FormSubmitBuilder { | |
| 78 | 78 | **/ | |
| 79 | 79 | public function new() { } | |
| 80 | 80 | ||
| 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 | + | ||
| 81 | 90 | /** | |
| 82 | 91 | Add a submitted value for a field. | |
| 83 | 92 |
+13
-1
borogove/MucSettingsCommand.hx
#
| ... | ... | @@ -13,11 +13,13 @@ import HaxeCBridge; | |
| 13 | 13 | ||
| 14 | 14 | class MucSettingsCommand extends Command { | |
| 15 | 15 | private var onCompleted: Null<()->Void>; | |
| 16 | + | private var prefill: Null<Map<String, Array<String>>>; | |
| 16 | 17 | ||
| 17 | 18 | @: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>>>) { | |
| 19 | 20 | super(client, { jid: jid, name: "Settings", node: "muc-settings" }); | |
| 20 | 21 | this.onCompleted = onCompleted; | |
| 22 | + | this.prefill = prefill; | |
| 21 | 23 | } | |
| 22 | 24 | ||
| 23 | 25 | @:allow(borogove.MucSettingsCommandSession) |
| ... | ... | @@ -27,6 +29,15 @@ class MucSettingsCommand extends Command { | |
| 27 | 29 | if (cb != null) cb(); | |
| 28 | 30 | } | |
| 29 | 31 | ||
| 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 | + | ||
| 30 | 41 | override public function execute(): Promise<CommandSession> { | |
| 31 | 42 | return new Promise<CommandSession>((resolve, reject) -> { | |
| 32 | 43 | final q = new MucSettingsGet(this.jid.asString()); |
| ... | ... | @@ -43,6 +54,7 @@ class MucSettingsCommand extends Command { | |
| 43 | 54 | return; | |
| 44 | 55 | } | |
| 45 | 56 | ||
| 57 | + | applyPrefill(form, prefill); | |
| 46 | 58 | resolve(new MucSettingsCommandSession("executing", null, [new FormOption("Submit", "execute"), new FormOption("Cancel", "cancel")], [new Form(form, null)], this)); | |
| 47 | 59 | }); | |
| 48 | 60 | this.client.sendQuery(q); |