001/*
002 *
003 * Copyright 2015-2025 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.muc;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.List;
022import java.util.logging.Level;
023import java.util.logging.Logger;
024import java.util.stream.Collectors;
025
026import org.jivesoftware.smack.SmackException.NoResponseException;
027import org.jivesoftware.smack.SmackException.NotConnectedException;
028import org.jivesoftware.smack.XMPPException.XMPPErrorException;
029
030import org.jivesoftware.smackx.muc.MultiUserChatException.MucConfigurationNotSupportedException;
031import org.jivesoftware.smackx.xdata.BooleanFormField;
032import org.jivesoftware.smackx.xdata.FormField;
033import org.jivesoftware.smackx.xdata.form.FillableForm;
034import org.jivesoftware.smackx.xdata.form.FilledForm;
035import org.jivesoftware.smackx.xdata.form.Form;
036import org.jivesoftware.smackx.xdata.packet.DataForm;
037
038import org.jxmpp.jid.Jid;
039import org.jxmpp.jid.util.JidUtil;
040
041/**
042 * Multi-User Chat configuration form manager is used to fill out and submit a {@link FilledForm} used to
043 * configure rooms.
044 * <p>
045 * Room configuration needs either be done right after the room is created and still locked. Or at
046 * any later point (see <a href="http://xmpp.org/extensions/xep-0045.html#roomconfig">XEP-45 § 10.2
047 * Subsequent Room Configuration</a>). When done with the configuration, call
048 * {@link #submitConfigurationForm()}.
049 * </p>
050 * <p>
051 * The manager may not provide all possible configuration options. If you want direct access to the
052 * configuration form, use {@link MultiUserChat#getConfigurationForm()} and
053 * {@link MultiUserChat#sendConfigurationForm(FillableForm)}.
054 * </p>
055 */
056public class MucConfigFormManager {
057
058    private static final String HASH_ROOMCONFIG = "#roomconfig";
059
060    private static final Logger LOGGER = Logger.getLogger(MucConfigFormManager.class.getName());
061
062    public static final String FORM_TYPE = MultiUserChatConstants.NAMESPACE + HASH_ROOMCONFIG;
063
064                    /**
065     * The constant String {@value}.
066     *
067     * @see <a href="http://xmpp.org/extensions/xep-0045.html#owner">XEP-0045 § 10. Owner Use Cases</a>
068     */
069    public static final String MUC_ROOMCONFIG_ROOMOWNERS = "muc#roomconfig_roomowners";
070
071    /**
072     * The constant String {@value}.
073     *
074     * @see <a href="http://xmpp.org/extensions/xep-0045.html#owner">XEP-0045 § 10. Owner Use Cases</a>
075     */
076    public static final String MUC_ROOMCONFIG_ROOMADMINS = "muc#roomconfig_roomadmins";
077
078    /**
079     * The constant String {@value}.
080     */
081    public static final String MUC_ROOMCONFIG_MEMBERSONLY = "muc#roomconfig_membersonly";
082
083    /**
084     * The constant String {@value}.
085     *
086     * @see <a href="http://xmpp.org/extensions/xep-0045.html#enter-pw">XEP-0045 § 7.2.6 Password-Protected Rooms</a>
087     */
088    public static final String MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM = "muc#roomconfig_passwordprotectedroom";
089
090    /**
091     * The constant String {@value}.
092     */
093    public static final String MUC_ROOMCONFIG_ROOMSECRET = "muc#roomconfig_roomsecret";
094
095    /**
096     * The constant String {@value}.
097     */
098    public static final String MUC_ROOMCONFIG_MODERATEDROOM = "muc#roomconfig_moderatedroom";
099
100    /**
101     * The constant String {@value}.
102     */
103    public static final String MUC_ROOMCONFIG_PUBLICLYSEARCHABLEROOM = "muc#roomconfig_publicroom";
104
105    /**
106     * The constant String {@value}.
107     */
108    public static final String MUC_ROOMCONFIG_ROOMNAME = "muc#roomconfig_roomname";
109
110    /**
111     * The constant String {@value}.
112     */
113    public static final String MUC_ROOMCONFIG_ENABLE_PUBLIC_LOGGING = "muc#roomconfig_enablelogging";
114
115    /**
116     * The constant String {@value}.
117     */
118    public static final String MUC_ROOMCONFIG_CHANGE_SUBJECT = "muc#roomconfig_changesubject";
119
120    public static final String MUC_ROOMCONFIG_WHOIS = "muc#roomconfig_whois";
121
122    public static final String MUC_ROOMCONFIG_MAXUSERS = "muc#roomconfig_maxusers";
123
124    private final MultiUserChat multiUserChat;
125    private final FillableForm answerForm;
126    private final List<Jid> owners;
127    private final List<Jid> admins;
128
129    /**
130     * Create a new MUC config form manager.
131     * <p>
132     * Note that the answerForm needs to be filled out with the defaults.
133     * </p>
134     *
135     * @param multiUserChat the MUC for this configuration form.
136     * @throws InterruptedException if the calling thread was interrupted.
137     * @throws NotConnectedException if the XMPP connection is not connected.
138     * @throws XMPPErrorException if there was an XMPP error returned.
139     * @throws NoResponseException if there was no response from the remote entity.
140     */
141    MucConfigFormManager(MultiUserChat multiUserChat) throws NoResponseException,
142                    XMPPErrorException, NotConnectedException, InterruptedException {
143        this.multiUserChat = multiUserChat;
144
145        // Set the answer form
146        Form configForm = multiUserChat.getConfigurationForm();
147        this.answerForm = configForm.getFillableForm();
148
149        // Set the local variables according to the fields found in the answer form
150        FormField roomOwnersFormField = answerForm.getDataForm().getField(MUC_ROOMCONFIG_ROOMOWNERS);
151        if (roomOwnersFormField != null) {
152            // Set 'owners' to the currently configured owners
153            List<? extends CharSequence> ownerStrings = roomOwnersFormField.getValues();
154            owners = new ArrayList<>(ownerStrings.size());
155            JidUtil.jidsFrom(ownerStrings, owners, null);
156        }
157        else {
158            // roomowners not supported, this should barely be the case
159            owners = null;
160        }
161
162        FormField roomAdminsFormField = answerForm.getDataForm().getField(MUC_ROOMCONFIG_ROOMADMINS);
163        if (roomAdminsFormField != null) {
164            // Set 'admins' to the currently configured admins
165            List<? extends CharSequence> adminStrings = roomAdminsFormField.getValues();
166            admins = new ArrayList<>(adminStrings.size());
167            JidUtil.jidsFrom(adminStrings, admins, null);
168        }
169        else {
170            // roomadmins not supported, this should barely be the case
171            admins = null;
172        }
173    }
174
175    /**
176     * Check if the room supports room owners.
177     * @return <code>true</code> if supported, <code>false</code> if not.
178     * @see #MUC_ROOMCONFIG_ROOMOWNERS
179     */
180    public boolean supportsRoomOwners() {
181        return owners != null;
182    }
183
184    /**
185     * Check if the room supports room admins.
186     * @return <code>true</code> if supported, <code>false</code> if not.
187     * @see #MUC_ROOMCONFIG_ROOMADMINS
188     */
189    public boolean supportsRoomAdmins() {
190        return admins != null;
191    }
192
193    /**
194     * Set the owners of the room.
195     *
196     * @param newOwners a collection of JIDs to become the new owners of the room.
197     * @return a reference to this object.
198     * @throws MucConfigurationNotSupportedException if the MUC service does not support this option.
199     * @see #MUC_ROOMCONFIG_ROOMOWNERS
200     */
201    public MucConfigFormManager setRoomOwners(Collection<? extends Jid> newOwners) throws MucConfigurationNotSupportedException {
202        if (!supportsRoomOwners()) {
203            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_ROOMOWNERS);
204        }
205        owners.clear();
206        owners.addAll(newOwners);
207        return this;
208    }
209
210    /**
211     * Set the admins of the room.
212     *
213     * @param newAdmins a collection of JIDs to become the new admins of the room.
214     * @return a reference to this object.
215     * @throws MucConfigurationNotSupportedException if the MUC service does not support this option.
216     * @see #MUC_ROOMCONFIG_ROOMADMINS
217     */
218    public MucConfigFormManager setRoomAdmins(Collection<? extends Jid> newAdmins) throws MucConfigurationNotSupportedException {
219        if (!supportsRoomAdmins()) {
220            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_ROOMADMINS);
221        }
222        admins.clear();
223        admins.addAll(newAdmins);
224        return this;
225    }
226
227    /**
228     * Check if the room supports a members only configuration.
229     *
230     * @return <code>true</code> if supported, <code>false</code> if not.
231     */
232    public boolean supportsMembersOnly() {
233        return answerForm.hasField(MUC_ROOMCONFIG_MEMBERSONLY);
234    }
235
236    /**
237     * Check if the room supports being moderated in the configuration.
238     *
239     * @return <code>true</code> if supported, <code>false</code> if not.
240     */
241    public boolean supportsModeration() {
242        return answerForm.hasField(MUC_ROOMCONFIG_MODERATEDROOM);
243    }
244
245    /**
246     * Make the room for members only.
247     *
248     * @return a reference to this object.
249     * @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service.
250     */
251    public MucConfigFormManager makeMembersOnly() throws MucConfigurationNotSupportedException {
252        return setMembersOnly(true);
253    }
254
255    /**
256     * Set if the room is members only. Rooms are not members only per default.
257     *
258     * @param isMembersOnly if the room should be members only.
259     * @return a reference to this object.
260     * @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service.
261     */
262    public MucConfigFormManager setMembersOnly(boolean isMembersOnly) throws MucConfigurationNotSupportedException {
263        if (!supportsMembersOnly()) {
264            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_MEMBERSONLY);
265        }
266        answerForm.setAnswer(MUC_ROOMCONFIG_MEMBERSONLY, isMembersOnly);
267        return this;
268    }
269
270
271    /**
272     * Make the room moderated.
273     *
274     * @return a reference to this object.
275     * @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service.
276     */
277    public MucConfigFormManager makeModerated() throws MucConfigurationNotSupportedException {
278        return setModerated(true);
279    }
280
281    /**
282     * Set if the room is members only. Rooms are not members only per default.
283     *
284     * @param isModerated if the room should be moderated.
285     * @return a reference to this object.
286     * @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service.
287     */
288    public MucConfigFormManager setModerated(boolean isModerated) throws MucConfigurationNotSupportedException {
289        if (!supportsModeration()) {
290            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_MODERATEDROOM);
291        }
292        answerForm.setAnswer(MUC_ROOMCONFIG_MODERATEDROOM, isModerated);
293        return this;
294    }
295
296
297    /**
298     * Check if the room supports its visibility being controlled via configuration.
299     *
300     * @return <code>true</code> if supported, <code>false</code> if not.
301     */
302    public boolean supportsPublicRoom() {
303        return answerForm.hasField(MUC_ROOMCONFIG_PUBLICLYSEARCHABLEROOM);
304    }
305
306    /**
307     * Make the room publicly searchable.
308     *
309     * @return a reference to this object.
310     * @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service.
311     */
312    public MucConfigFormManager makePublic() throws MucConfigurationNotSupportedException {
313        return setPublic(true);
314    }
315
316    /**
317     * Make the room hidden (not publicly searchable).
318     *
319     * @return a reference to this object.
320     * @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service.
321     */
322    public MucConfigFormManager makeHidden() throws MucConfigurationNotSupportedException {
323        return setPublic(false);
324    }
325
326    /**
327     * Set if the room is publicly searchable (i.e. visible via discovery requests to the MUC service).
328     *
329     * @param isPublic if the room should be publicly searchable.
330     * @return a reference to this object.
331     * @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service.
332     */
333    public MucConfigFormManager setPublic(boolean isPublic) throws MucConfigurationNotSupportedException {
334        if (!supportsPublicRoom()) {
335            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_PUBLICLYSEARCHABLEROOM);
336        }
337        answerForm.setAnswer(MUC_ROOMCONFIG_PUBLICLYSEARCHABLEROOM, isPublic);
338        return this;
339    }
340
341    public boolean supportsRoomname() {
342        return answerForm.hasField(MUC_ROOMCONFIG_ROOMNAME);
343    }
344
345    public MucConfigFormManager setRoomName(String roomName) throws MucConfigurationNotSupportedException {
346        if (!supportsRoomname()) {
347            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_ROOMNAME);
348        }
349        answerForm.setAnswer(MUC_ROOMCONFIG_ROOMNAME, roomName);
350        return this;
351    }
352
353    /**
354     * Check if the room supports password protection.
355     *
356     * @return <code>true</code> if supported, <code>false</code> if not.
357     */
358    public boolean supportsPasswordProtected() {
359        return answerForm.hasField(MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM);
360    }
361
362    /**
363     * Set a password and make the room password protected. Users will need to supply the password
364     * to join the room.
365     *
366     * @param password the password to set.
367     * @return a reference to this object.
368     * @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service.
369     */
370    public MucConfigFormManager setAndEnablePassword(String password)
371                    throws MucConfigurationNotSupportedException {
372        return setIsPasswordProtected(true).setRoomSecret(password);
373    }
374
375    /**
376     * Make the room password protected.
377     *
378     * @return a reference to this object.
379     * @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service.
380     */
381    public MucConfigFormManager makePasswordProtected() throws MucConfigurationNotSupportedException {
382        return setIsPasswordProtected(true);
383    }
384
385    /**
386     * Set if this room is password protected. Rooms are by default not password protected.
387     *
388     * @param isPasswordProtected TODO javadoc me please
389     * @return a reference to this object.
390     * @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service.
391     */
392    public MucConfigFormManager setIsPasswordProtected(boolean isPasswordProtected)
393                    throws MucConfigurationNotSupportedException {
394        if (!supportsPasswordProtected()) {
395            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM);
396        }
397        answerForm.setAnswer(MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM, isPasswordProtected);
398        return this;
399    }
400
401    public boolean supportsPublicLogging() {
402        return answerForm.hasField(MUC_ROOMCONFIG_ENABLE_PUBLIC_LOGGING);
403    }
404
405    public MucConfigFormManager setPublicLogging(boolean enabled) throws MucConfigurationNotSupportedException {
406        if (!supportsPublicLogging()) {
407            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_ENABLE_PUBLIC_LOGGING);
408        }
409        answerForm.setAnswer(MUC_ROOMCONFIG_ENABLE_PUBLIC_LOGGING, enabled);
410        return this;
411    }
412
413    public MucConfigFormManager enablePublicLogging() throws MucConfigurationNotSupportedException {
414        return setPublicLogging(true);
415    }
416
417    public MucConfigFormManager disablPublicLogging() throws MucConfigurationNotSupportedException {
418        return setPublicLogging(false);
419    }
420
421    /**
422     * Set the room secret, aka the room password. If set and enabled, the password is required to
423     * join the room. Note that this does only set it by does not enable password protection. Use
424     * {@link #setAndEnablePassword(String)} to set a password and make the room protected.
425     *
426     * @param secret the secret/password.
427     * @return a reference to this object.
428     * @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service.
429     */
430    public MucConfigFormManager setRoomSecret(String secret)
431                    throws MucConfigurationNotSupportedException {
432        if (!answerForm.hasField(MUC_ROOMCONFIG_ROOMSECRET)) {
433            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_ROOMSECRET);
434        }
435        answerForm.setAnswer(MUC_ROOMCONFIG_ROOMSECRET, secret);
436        return this;
437    }
438
439    public boolean supportsChangeSubjectByOccupant() {
440        return answerForm.hasField(MUC_ROOMCONFIG_CHANGE_SUBJECT);
441    }
442
443    public boolean occupantsAreAllowedToChangeSubject() throws MucConfigurationNotSupportedException {
444        if (!supportsChangeSubjectByOccupant()) {
445            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_CHANGE_SUBJECT);
446        }
447        return answerForm.getField(MUC_ROOMCONFIG_CHANGE_SUBJECT).ifPossibleAsOrThrow(BooleanFormField.class).getValueAsBoolean();
448    }
449
450    public MucConfigFormManager setChangeSubjectByOccupant(boolean enabled) throws MucConfigurationNotSupportedException {
451        if (!supportsChangeSubjectByOccupant()) {
452            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_CHANGE_SUBJECT);
453        }
454        answerForm.setAnswer(MUC_ROOMCONFIG_CHANGE_SUBJECT, enabled);
455        return this;
456    }
457
458    public MucConfigFormManager allowOccupantsToChangeSubject() throws MucConfigurationNotSupportedException {
459        return setChangeSubjectByOccupant(true);
460    }
461
462    public MucConfigFormManager disallowOccupantsToChangeSubject() throws MucConfigurationNotSupportedException {
463        return setChangeSubjectByOccupant(false);
464    }
465
466    enum WhoisAllowedBy {
467        moderators,
468        anyone,
469    }
470
471    public boolean supportsWhoisAllowedBy() {
472        return answerForm.hasField(MUC_ROOMCONFIG_WHOIS);
473    }
474
475    public MucConfigFormManager setWhoisAllowedBy(WhoisAllowedBy whoisAllowedBy)
476                    throws MucConfigurationNotSupportedException {
477        if (!supportsWhoisAllowedBy()) {
478            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_WHOIS);
479        }
480        answerForm.setAnswer(MUC_ROOMCONFIG_WHOIS, whoisAllowedBy.name());
481        return this;
482    }
483
484    public boolean supportsMaxUsers() {
485        return answerForm.hasField(MUC_ROOMCONFIG_MAXUSERS);
486    }
487
488    public List<Integer> getPossibleMaxUsersValues() throws MucConfigurationNotSupportedException {
489        if (!supportsMaxUsers()) {
490            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_MAXUSERS);
491        }
492        return answerForm.getField(MUC_ROOMCONFIG_MAXUSERS)
493                        .getValuesAsString()
494                        .stream()
495                        .map(s -> Integer.valueOf(s))
496                        .collect(Collectors.toList());
497    }
498
499    public MucConfigFormManager setMaxUsers(int maxUsers) throws MucConfigurationNotSupportedException {
500        if (!supportsMaxUsers()) {
501            throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_MAXUSERS);
502        }
503        if (maxUsers < 1) {
504            throw new IllegalArgumentException();
505        }
506        answerForm.setAnswer(MUC_ROOMCONFIG_MAXUSERS, maxUsers);
507        return this;
508    }
509
510    /**
511     * Submit the configuration as {@link FilledForm} to the room.
512     *
513     * @throws NoResponseException if there was no response from the room.
514     * @throws XMPPErrorException if there was an XMPP error returned.
515     * @throws NotConnectedException if the XMPP connection is not connected.
516     * @throws InterruptedException if the calling thread was interrupted.
517     */
518    public void submitConfigurationForm() throws NoResponseException, XMPPErrorException, NotConnectedException,
519                    InterruptedException {
520        if (owners != null) {
521            answerForm.setAnswer(MUC_ROOMCONFIG_ROOMOWNERS, JidUtil.toStringList(owners));
522        }
523        if (admins != null) {
524            answerForm.setAnswer(MUC_ROOMCONFIG_ROOMADMINS, JidUtil.toStringList(admins));
525        }
526        multiUserChat.sendConfigurationForm(answerForm);
527    }
528
529    public void cancel() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
530        var cancelDataForm = DataForm.builder(DataForm.Type.cancel).build();
531        multiUserChat.sendAsMucOwner(cancelDataForm);
532    }
533
534    public interface MucConfigApplier {
535        void apply(MucConfigFormManager manager)
536                        throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, MucConfigurationNotSupportedException;
537    }
538
539    public MultiUserChat applyAndSubmit(MucConfigApplier applier)
540                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, MucConfigurationNotSupportedException {
541        try {
542            applier.apply(this);
543            submitConfigurationForm();
544        } catch (XMPPErrorException | InterruptedException | MucConfigurationNotSupportedException e) {
545            try {
546                cancel();
547            } catch (NoResponseException | XMPPErrorException | NotConnectedException
548                            | InterruptedException cancelException) {
549                LOGGER.log(Level.SEVERE, "Exception while canceling MUC configuration for " + multiUserChat, e);
550            }
551            throw e;
552        }
553
554        return multiUserChat;
555    }
556}