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}