MucOptions.java

   1package eu.siacs.conversations.entities;
   2
   3import androidx.annotation.NonNull;
   4import com.google.common.base.Strings;
   5import com.google.common.collect.ImmutableList;
   6import com.google.common.collect.ImmutableMap;
   7import com.google.common.collect.Iterables;
   8import de.gultsch.common.IntMap;
   9import eu.siacs.conversations.Config;
  10import eu.siacs.conversations.services.AvatarService;
  11import eu.siacs.conversations.services.MessageArchiveService;
  12import eu.siacs.conversations.utils.JidHelper;
  13import eu.siacs.conversations.utils.UIHelper;
  14import eu.siacs.conversations.xml.Namespace;
  15import eu.siacs.conversations.xmpp.Jid;
  16import eu.siacs.conversations.xmpp.chatstate.ChatState;
  17import im.conversations.android.xmpp.model.data.Data;
  18import im.conversations.android.xmpp.model.data.Field;
  19import im.conversations.android.xmpp.model.disco.info.InfoQuery;
  20import im.conversations.android.xmpp.model.muc.Affiliation;
  21import im.conversations.android.xmpp.model.muc.Item;
  22import im.conversations.android.xmpp.model.muc.Role;
  23import java.util.ArrayList;
  24import java.util.Arrays;
  25import java.util.Collection;
  26import java.util.Collections;
  27import java.util.HashSet;
  28import java.util.List;
  29import java.util.Objects;
  30import java.util.Set;
  31
  32public class MucOptions {
  33
  34    private static final IntMap<Affiliation> AFFILIATION_RANKS =
  35            new IntMap<>(
  36                    new ImmutableMap.Builder<Affiliation, Integer>()
  37                            .put(Affiliation.OWNER, 4)
  38                            .put(Affiliation.ADMIN, 3)
  39                            .put(Affiliation.MEMBER, 2)
  40                            .put(Affiliation.NONE, 1)
  41                            .put(Affiliation.OUTCAST, 0)
  42                            .build());
  43
  44    private static final IntMap<Role> ROLE_RANKS =
  45            new IntMap<>(
  46                    new ImmutableMap.Builder<Role, Integer>()
  47                            .put(Role.MODERATOR, 3)
  48                            .put(Role.PARTICIPANT, 2)
  49                            .put(Role.VISITOR, 1)
  50                            .put(Role.NONE, 0)
  51                            .build());
  52
  53    public static final String STATUS_CODE_SELF_PRESENCE = "110";
  54    public static final String STATUS_CODE_ROOM_CREATED = "201";
  55    public static final String STATUS_CODE_BANNED = "301";
  56    public static final String STATUS_CODE_CHANGED_NICK = "303";
  57    public static final String STATUS_CODE_KICKED = "307";
  58    public static final String STATUS_CODE_AFFILIATION_CHANGE = "321";
  59    public static final String STATUS_CODE_LOST_MEMBERSHIP = "322";
  60    public static final String STATUS_CODE_SHUTDOWN = "332";
  61    public static final String STATUS_CODE_TECHNICAL_REASONS = "333";
  62    // TODO this should be a list
  63    private final Set<User> users = new HashSet<>();
  64    private final Conversation conversation;
  65    public OnRenameListener onRenameListener = null;
  66    private boolean mAutoPushConfiguration = true;
  67    private final Account account;
  68    private InfoQuery infoQuery;
  69    private boolean isOnline = false;
  70    private Error error = Error.NONE;
  71    private User self;
  72    private String password = null;
  73
  74    public MucOptions(final Conversation conversation) {
  75        this.account = conversation.getAccount();
  76        this.conversation = conversation;
  77        this.self = new User(this, createJoinJid(getProposedNick()));
  78        this.self.affiliation = Item.affiliationOrNone(conversation.getAttribute("affiliation"));
  79        this.self.role = Item.roleOrNone(conversation.getAttribute("role"));
  80    }
  81
  82    public Account getAccount() {
  83        return this.conversation.getAccount();
  84    }
  85
  86    public boolean setSelf(final User user) {
  87        this.self = user;
  88        final boolean roleChanged = this.conversation.setAttribute("role", user.role.toString());
  89        final boolean affiliationChanged =
  90                this.conversation.setAttribute("affiliation", user.affiliation.toString());
  91        return roleChanged || affiliationChanged;
  92    }
  93
  94    public void changeAffiliation(final Jid jid, final Affiliation affiliation) {
  95        final User user = findUserByRealJid(jid);
  96        synchronized (users) {
  97            if (user != null && user.getRole() == Role.NONE) {
  98                users.remove(user);
  99                if (AFFILIATION_RANKS.getInt(affiliation)
 100                        >= AFFILIATION_RANKS.getInt(Affiliation.MEMBER)) {
 101                    user.affiliation = affiliation;
 102                    users.add(user);
 103                }
 104            }
 105        }
 106    }
 107
 108    public void setAutoPushConfiguration(final boolean auto) {
 109        this.mAutoPushConfiguration = auto;
 110    }
 111
 112    public boolean autoPushConfiguration() {
 113        return mAutoPushConfiguration;
 114    }
 115
 116    public boolean isSelf(final Jid counterpart) {
 117        return counterpart.equals(self.getFullJid());
 118    }
 119
 120    public boolean isSelf(final String occupantId) {
 121        return occupantId.equals(self.getOccupantId());
 122    }
 123
 124    public void resetChatState() {
 125        synchronized (users) {
 126            for (User user : users) {
 127                user.chatState = Config.DEFAULT_CHAT_STATE;
 128            }
 129        }
 130    }
 131
 132    public boolean mamSupport() {
 133        return MessageArchiveService.Version.has(getFeatures());
 134    }
 135
 136    private InfoQuery getServiceDiscoveryResult() {
 137        return this.infoQuery;
 138    }
 139
 140    public boolean updateConfiguration(final InfoQuery serviceDiscoveryResult) {
 141        this.infoQuery = serviceDiscoveryResult;
 142        final String name = getName(serviceDiscoveryResult);
 143        boolean changed = conversation.setAttribute("muc_name", name);
 144        changed |=
 145                conversation.setAttribute(
 146                        Conversation.ATTRIBUTE_MEMBERS_ONLY, this.hasFeature("muc_membersonly"));
 147        changed |=
 148                conversation.setAttribute(
 149                        Conversation.ATTRIBUTE_MODERATED, this.hasFeature("muc_moderated"));
 150        changed |=
 151                conversation.setAttribute(
 152                        Conversation.ATTRIBUTE_NON_ANONYMOUS, this.hasFeature("muc_nonanonymous"));
 153        return changed;
 154    }
 155
 156    private String getName(final InfoQuery serviceDiscoveryResult) {
 157        final var roomInfo =
 158                serviceDiscoveryResult.getServiceDiscoveryExtension(
 159                        "http://jabber.org/protocol/muc#roominfo");
 160        final Field roomConfigName =
 161                roomInfo == null ? null : roomInfo.getFieldByName("muc#roomconfig_roomname");
 162        if (roomConfigName != null) {
 163            return roomConfigName.getValue();
 164        } else {
 165            final var identities = serviceDiscoveryResult.getIdentities();
 166            final String identityName =
 167                    !identities.isEmpty()
 168                            ? Iterables.getFirst(identities, null).getIdentityName()
 169                            : null;
 170            final Jid jid = conversation.getJid();
 171            if (identityName != null && !identityName.equals(jid == null ? null : jid.getLocal())) {
 172                return identityName;
 173            } else {
 174                return null;
 175            }
 176        }
 177    }
 178
 179    private Data getRoomInfoForm() {
 180        final var serviceDiscoveryResult = getServiceDiscoveryResult();
 181        return serviceDiscoveryResult == null
 182                ? null
 183                : serviceDiscoveryResult.getServiceDiscoveryExtension(Namespace.MUC_ROOM_INFO);
 184    }
 185
 186    public String getAvatar() {
 187        return account.getRoster().getContact(conversation.getJid()).getAvatar();
 188    }
 189
 190    public boolean hasFeature(String feature) {
 191        final var serviceDiscoveryResult = getServiceDiscoveryResult();
 192        return serviceDiscoveryResult != null
 193                && serviceDiscoveryResult.getFeatureStrings().contains(feature);
 194    }
 195
 196    public boolean hasVCards() {
 197        return hasFeature("vcard-temp");
 198    }
 199
 200    public boolean canInvite() {
 201        final boolean hasPermission =
 202                !membersOnly() || self.ranks(Role.MODERATOR) || allowInvites();
 203        return hasPermission && online();
 204    }
 205
 206    public boolean allowInvites() {
 207        final var roomInfo = getRoomInfoForm();
 208        if (roomInfo == null) {
 209            return false;
 210        }
 211        final var field = roomInfo.getFieldByName("muc#roomconfig_allowinvites");
 212        return field != null && "1".equals(field.getValue());
 213    }
 214
 215    public boolean canChangeSubject() {
 216        return self.ranks(Role.MODERATOR) || participantsCanChangeSubject();
 217    }
 218
 219    public boolean participantsCanChangeSubject() {
 220        final var roomInfo = getRoomInfoForm();
 221        if (roomInfo == null) {
 222            return false;
 223        }
 224        final Field configField = roomInfo.getFieldByName("muc#roomconfig_changesubject");
 225        final Field infoField = roomInfo.getFieldByName("muc#roominfo_changesubject");
 226        final Field field = configField != null ? configField : infoField;
 227        return field != null && "1".equals(field.getValue());
 228    }
 229
 230    public boolean allowPm() {
 231        final var roomInfo = getRoomInfoForm();
 232        if (roomInfo == null) {
 233            return true;
 234        }
 235        final Field field = roomInfo.getFieldByName("muc#roomconfig_allowpm");
 236        if (field == null) {
 237            return true; // fall back if field does not exists
 238        }
 239        if ("anyone".equals(field.getValue())) {
 240            return true;
 241        } else if ("participants".equals(field.getValue())) {
 242            return self.ranks(Role.PARTICIPANT);
 243        } else if ("moderators".equals(field.getValue())) {
 244            return self.ranks(Role.MODERATOR);
 245        } else {
 246            return false;
 247        }
 248    }
 249
 250    public boolean allowPmRaw() {
 251        final var roomInfo = getRoomInfoForm();
 252        final Field field =
 253                roomInfo == null ? null : roomInfo.getFieldByName("muc#roomconfig_allowpm");
 254        return field == null || Arrays.asList("anyone", "participants").contains(field.getValue());
 255    }
 256
 257    public boolean participating() {
 258        return self.ranks(Role.PARTICIPANT) || !moderated();
 259    }
 260
 261    public boolean membersOnly() {
 262        return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, false);
 263    }
 264
 265    public Collection<String> getFeatures() {
 266        final var serviceDiscoveryResult = getServiceDiscoveryResult();
 267        return serviceDiscoveryResult != null
 268                ? serviceDiscoveryResult.getFeatureStrings()
 269                : Collections.emptyList();
 270    }
 271
 272    public boolean nonanonymous() {
 273        return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, false);
 274    }
 275
 276    public boolean isPrivateAndNonAnonymous() {
 277        return membersOnly() && nonanonymous();
 278    }
 279
 280    public boolean moderated() {
 281        return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MODERATED, false);
 282    }
 283
 284    public boolean stableId() {
 285        return getFeatures().contains("http://jabber.org/protocol/muc#stable_id");
 286    }
 287
 288    public boolean occupantId() {
 289        final var features = getFeatures();
 290        return features.contains(Namespace.OCCUPANT_ID);
 291    }
 292
 293    public User deleteUser(Jid jid) {
 294        User user = findUserByFullJid(jid);
 295        if (user != null) {
 296            synchronized (users) {
 297                users.remove(user);
 298                boolean realJidInMuc = false;
 299                for (User u : users) {
 300                    if (user.realJid != null && user.realJid.equals(u.realJid)) {
 301                        realJidInMuc = true;
 302                        break;
 303                    }
 304                }
 305                boolean self =
 306                        user.realJid != null && user.realJid.equals(account.getJid().asBareJid());
 307                if (membersOnly()
 308                        && nonanonymous()
 309                        && user.ranks(Affiliation.MEMBER)
 310                        && user.realJid != null
 311                        && !realJidInMuc
 312                        && !self) {
 313                    user.role = Role.NONE;
 314                    user.avatar = null;
 315                    user.fullJid = null;
 316                    users.add(user);
 317                }
 318            }
 319        }
 320        return user;
 321    }
 322
 323    // returns true if real jid was new;
 324    public boolean updateUser(User user) {
 325        User old;
 326        boolean realJidFound = false;
 327        if (user.fullJid == null && user.realJid != null) {
 328            old = findUserByRealJid(user.realJid);
 329            realJidFound = old != null;
 330            if (old != null) {
 331                if (old.fullJid != null) {
 332                    return false; // don't add. user already exists
 333                } else {
 334                    synchronized (users) {
 335                        users.remove(old);
 336                    }
 337                }
 338            }
 339        } else if (user.realJid != null) {
 340            old = findUserByRealJid(user.realJid);
 341            realJidFound = old != null;
 342            synchronized (users) {
 343                if (old != null && (old.fullJid == null || old.role == Role.NONE)) {
 344                    users.remove(old);
 345                }
 346            }
 347        }
 348        old = findUserByFullJid(user.getFullJid());
 349
 350        synchronized (this.users) {
 351            if (old != null) {
 352                users.remove(old);
 353            }
 354            boolean fullJidIsSelf =
 355                    isOnline
 356                            && user.getFullJid() != null
 357                            && user.getFullJid().equals(self.getFullJid());
 358            if ((!membersOnly() || user.ranks(Affiliation.MEMBER))
 359                    && user.outranks(Affiliation.OUTCAST)
 360                    && !fullJidIsSelf) {
 361                this.users.add(user);
 362                return !realJidFound && user.realJid != null;
 363            }
 364        }
 365        return false;
 366    }
 367
 368    public User findUserByFullJid(Jid jid) {
 369        if (jid == null) {
 370            return null;
 371        }
 372        synchronized (users) {
 373            for (User user : users) {
 374                if (jid.equals(user.getFullJid())) {
 375                    return user;
 376                }
 377            }
 378        }
 379        return null;
 380    }
 381
 382    public User findUserByRealJid(Jid jid) {
 383        if (jid == null) {
 384            return null;
 385        }
 386        synchronized (users) {
 387            for (User user : users) {
 388                if (jid.equals(user.realJid)) {
 389                    return user;
 390                }
 391            }
 392        }
 393        return null;
 394    }
 395
 396    public User findUserByOccupantId(final String occupantId) {
 397        synchronized (this.users) {
 398            return Strings.isNullOrEmpty(occupantId)
 399                    ? null
 400                    : Iterables.find(this.users, u -> occupantId.equals(u.occupantId), null);
 401        }
 402    }
 403
 404    public User findOrCreateUserByRealJid(Jid jid, Jid fullJid) {
 405        final User existing = findUserByRealJid(jid);
 406        if (existing != null) {
 407            return existing;
 408        }
 409        final var user = new User(this, fullJid);
 410        user.setRealJid(jid);
 411        return user;
 412    }
 413
 414    public User findUser(ReadByMarker readByMarker) {
 415        if (readByMarker.getRealJid() != null) {
 416            return findOrCreateUserByRealJid(
 417                    readByMarker.getRealJid().asBareJid(), readByMarker.getFullJid());
 418        } else if (readByMarker.getFullJid() != null) {
 419            return findUserByFullJid(readByMarker.getFullJid());
 420        } else {
 421            return null;
 422        }
 423    }
 424
 425    private User findUser(final Reaction reaction) {
 426        if (reaction.trueJid != null) {
 427            return findOrCreateUserByRealJid(reaction.trueJid.asBareJid(), reaction.from);
 428        }
 429        final var existing = findUserByOccupantId(reaction.occupantId);
 430        if (existing != null) {
 431            return existing;
 432        } else if (reaction.from != null) {
 433            return new User(this, reaction.from);
 434        } else {
 435            return null;
 436        }
 437    }
 438
 439    public List<User> findUsers(final Collection<Reaction> reactions) {
 440        final ImmutableList.Builder<User> builder = new ImmutableList.Builder<>();
 441        for (final Reaction reaction : reactions) {
 442            final var user = findUser(reaction);
 443            if (user != null) {
 444                builder.add(user);
 445            }
 446        }
 447        return builder.build();
 448    }
 449
 450    public boolean isContactInRoom(Contact contact) {
 451        return contact != null && findUserByRealJid(contact.getJid().asBareJid()) != null;
 452    }
 453
 454    public boolean isUserInRoom(Jid jid) {
 455        return findUserByFullJid(jid) != null;
 456    }
 457
 458    public boolean setOnline() {
 459        boolean before = this.isOnline;
 460        this.isOnline = true;
 461        return !before;
 462    }
 463
 464    public ArrayList<User> getUsers() {
 465        return getUsers(true);
 466    }
 467
 468    public ArrayList<User> getUsers(boolean includeOffline) {
 469        synchronized (users) {
 470            ArrayList<User> users = new ArrayList<>();
 471            for (User user : this.users) {
 472                if (!user.isDomain() && (includeOffline || user.ranks(Role.PARTICIPANT))) {
 473                    users.add(user);
 474                }
 475            }
 476            return users;
 477        }
 478    }
 479
 480    public ArrayList<User> getUsersWithChatState(ChatState state, int max) {
 481        synchronized (users) {
 482            ArrayList<User> list = new ArrayList<>();
 483            for (User user : users) {
 484                if (user.chatState == state) {
 485                    list.add(user);
 486                    if (list.size() >= max) {
 487                        break;
 488                    }
 489                }
 490            }
 491            return list;
 492        }
 493    }
 494
 495    public List<User> getUsers(final int max) {
 496        final ArrayList<User> subset = new ArrayList<>();
 497        final HashSet<Jid> addresses = new HashSet<>();
 498        addresses.add(account.getJid().asBareJid());
 499        synchronized (users) {
 500            for (User user : users) {
 501                if (user.getRealJid() == null
 502                        || (user.getRealJid().getLocal() != null
 503                                && addresses.add(user.getRealJid()))) {
 504                    subset.add(user);
 505                }
 506                if (subset.size() >= max) {
 507                    break;
 508                }
 509            }
 510        }
 511        return subset;
 512    }
 513
 514    public static List<User> sub(final List<User> users, final int max) {
 515        final var subset = new ArrayList<User>();
 516        final var addresses = new HashSet<Jid>();
 517        for (final var user : users) {
 518            addresses.add(user.getAccount().getJid().asBareJid());
 519            final var address = user.getRealJid();
 520            if (address == null || (address.getLocal() != null && addresses.add(address))) {
 521                subset.add(user);
 522            }
 523            if (subset.size() >= max) {
 524                return subset;
 525            }
 526        }
 527        return subset;
 528    }
 529
 530    public int getUserCount() {
 531        synchronized (users) {
 532            return users.size();
 533        }
 534    }
 535
 536    private String getProposedNick() {
 537        final Bookmark bookmark = this.conversation.getBookmark();
 538        if (bookmark != null) {
 539            // if we already have a bookmark we consider this the source of truth
 540            return getProposedNickPure();
 541        }
 542        final var storedJid = conversation.getJid();
 543        if (storedJid.isBareJid()) {
 544            return defaultNick(account);
 545        } else {
 546            return storedJid.getResource();
 547        }
 548    }
 549
 550    public String getProposedNickPure() {
 551        final Bookmark bookmark = this.conversation.getBookmark();
 552        final String bookmarkedNick =
 553                normalize(account.getJid(), bookmark == null ? null : bookmark.getNick());
 554        if (bookmarkedNick != null) {
 555            return bookmarkedNick;
 556        } else {
 557            return defaultNick(account);
 558        }
 559    }
 560
 561    public static String defaultNick(final Account account) {
 562        final String displayName = normalize(account.getJid(), account.getDisplayName());
 563        if (displayName == null) {
 564            return JidHelper.localPartOrFallback(account.getJid());
 565        } else {
 566            return displayName;
 567        }
 568    }
 569
 570    private static String normalize(final Jid account, final String nick) {
 571        if (account == null || Strings.isNullOrEmpty(nick)) {
 572            return null;
 573        }
 574        try {
 575            return account.withResource(nick).getResource();
 576        } catch (final IllegalArgumentException e) {
 577            return null;
 578        }
 579    }
 580
 581    public String getActualNick() {
 582        if (this.self.getName() != null) {
 583            return this.self.getName();
 584        } else {
 585            return this.getProposedNick();
 586        }
 587    }
 588
 589    public boolean online() {
 590        return this.isOnline;
 591    }
 592
 593    public Error getError() {
 594        return this.error;
 595    }
 596
 597    public void setError(Error error) {
 598        this.isOnline = isOnline && error == Error.NONE;
 599        this.error = error;
 600    }
 601
 602    public void setOnRenameListener(OnRenameListener listener) {
 603        this.onRenameListener = listener;
 604    }
 605
 606    public void setOffline() {
 607        synchronized (users) {
 608            this.users.clear();
 609        }
 610        this.error = Error.NO_RESPONSE;
 611        this.isOnline = false;
 612    }
 613
 614    public User getSelf() {
 615        return self;
 616    }
 617
 618    public boolean setSubject(String subject) {
 619        return this.conversation.setAttribute("subject", subject);
 620    }
 621
 622    public String getSubject() {
 623        return this.conversation.getAttribute("subject");
 624    }
 625
 626    public String getName() {
 627        return this.conversation.getAttribute("muc_name");
 628    }
 629
 630    private List<User> getFallbackUsersFromCryptoTargets() {
 631        List<User> users = new ArrayList<>();
 632        for (Jid jid : conversation.getAcceptedCryptoTargets()) {
 633            User user = new User(this, null);
 634            user.setRealJid(jid);
 635            users.add(user);
 636        }
 637        return users;
 638    }
 639
 640    public List<User> getUsersRelevantForNameAndAvatar() {
 641        final List<User> users;
 642        if (isOnline) {
 643            users = getUsers(5);
 644        } else {
 645            users = getFallbackUsersFromCryptoTargets();
 646        }
 647        return users;
 648    }
 649
 650    String createNameFromParticipants() {
 651        List<User> users = getUsersRelevantForNameAndAvatar();
 652        if (users.size() >= 2) {
 653            StringBuilder builder = new StringBuilder();
 654            for (User user : users) {
 655                if (builder.length() != 0) {
 656                    builder.append(", ");
 657                }
 658                String name = UIHelper.getDisplayName(user);
 659                if (name != null) {
 660                    builder.append(name.split("\\s+")[0]);
 661                }
 662            }
 663            return builder.toString();
 664        } else {
 665            return null;
 666        }
 667    }
 668
 669    public long[] getPgpKeyIds() {
 670        List<Long> ids = new ArrayList<>();
 671        for (User user : this.users) {
 672            if (user.getPgpKeyId() != 0) {
 673                ids.add(user.getPgpKeyId());
 674            }
 675        }
 676        ids.add(account.getPgpId());
 677        long[] primitiveLongArray = new long[ids.size()];
 678        for (int i = 0; i < ids.size(); ++i) {
 679            primitiveLongArray[i] = ids.get(i);
 680        }
 681        return primitiveLongArray;
 682    }
 683
 684    public boolean pgpKeysInUse() {
 685        synchronized (users) {
 686            for (User user : users) {
 687                if (user.getPgpKeyId() != 0) {
 688                    return true;
 689                }
 690            }
 691        }
 692        return false;
 693    }
 694
 695    public boolean everybodyHasKeys() {
 696        synchronized (users) {
 697            for (User user : users) {
 698                if (user.getPgpKeyId() == 0) {
 699                    return false;
 700                }
 701            }
 702        }
 703        return true;
 704    }
 705
 706    public Jid createJoinJid(String nick) {
 707        try {
 708            return conversation.getJid().withResource(nick);
 709        } catch (final IllegalArgumentException e) {
 710            return null;
 711        }
 712    }
 713
 714    public Jid getTrueCounterpart(Jid jid) {
 715        if (jid.equals(getSelf().getFullJid())) {
 716            return account.getJid().asBareJid();
 717        }
 718        User user = findUserByFullJid(jid);
 719        return user == null ? null : user.realJid;
 720    }
 721
 722    public String getPassword() {
 723        this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
 724        if (this.password == null
 725                && conversation.getBookmark() != null
 726                && conversation.getBookmark().getPassword() != null) {
 727            return conversation.getBookmark().getPassword();
 728        } else {
 729            return this.password;
 730        }
 731    }
 732
 733    public void setPassword(String password) {
 734        if (conversation.getBookmark() != null) {
 735            conversation.getBookmark().setPassword(password);
 736        } else {
 737            this.password = password;
 738        }
 739        conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
 740    }
 741
 742    public Conversation getConversation() {
 743        return this.conversation;
 744    }
 745
 746    public List<Jid> getMembers(final boolean includeDomains) {
 747        ArrayList<Jid> members = new ArrayList<>();
 748        synchronized (users) {
 749            for (User user : users) {
 750                if (user.ranks(Affiliation.MEMBER)
 751                        && user.realJid != null
 752                        && !user.realJid
 753                                .asBareJid()
 754                                .equals(conversation.account.getJid().asBareJid())
 755                        && (!user.isDomain() || includeDomains)) {
 756                    members.add(user.realJid);
 757                }
 758            }
 759        }
 760        return members;
 761    }
 762
 763    public enum Error {
 764        NO_RESPONSE,
 765        SERVER_NOT_FOUND,
 766        REMOTE_SERVER_TIMEOUT,
 767        NONE,
 768        NICK_IN_USE,
 769        PASSWORD_REQUIRED,
 770        BANNED,
 771        MEMBERS_ONLY,
 772        RESOURCE_CONSTRAINT,
 773        KICKED,
 774        SHUTDOWN,
 775        DESTROYED,
 776        INVALID_NICK,
 777        TECHNICAL_PROBLEMS,
 778        UNKNOWN,
 779        NON_ANONYMOUS
 780    }
 781
 782    private interface OnEventListener {
 783        void onSuccess();
 784
 785        void onFailure();
 786    }
 787
 788    public interface OnRenameListener extends OnEventListener {}
 789
 790    public static class User implements Comparable<User>, AvatarService.Avatarable {
 791        private Role role = Role.NONE;
 792        private Affiliation affiliation = Affiliation.NONE;
 793        private Jid realJid;
 794        private Jid fullJid;
 795        private long pgpKeyId = 0;
 796        private String avatar;
 797        private final MucOptions options;
 798        private ChatState chatState = Config.DEFAULT_CHAT_STATE;
 799        private String occupantId;
 800
 801        public User(final MucOptions options, final Jid fullJid) {
 802            this.options = options;
 803            this.fullJid = fullJid;
 804        }
 805
 806        public String getName() {
 807            return fullJid == null ? null : fullJid.getResource();
 808        }
 809
 810        public Role getRole() {
 811            return this.role;
 812        }
 813
 814        public void setRole(final Role role) {
 815            this.role = role;
 816        }
 817
 818        public Affiliation getAffiliation() {
 819            return this.affiliation;
 820        }
 821
 822        public void setAffiliation(final Affiliation affiliation) {
 823            this.affiliation = affiliation;
 824        }
 825
 826        public long getPgpKeyId() {
 827            if (this.pgpKeyId != 0) {
 828                return this.pgpKeyId;
 829            } else if (realJid != null) {
 830                return getAccount().getRoster().getContact(realJid).getPgpKeyId();
 831            } else {
 832                return 0;
 833            }
 834        }
 835
 836        public void setPgpKeyId(long id) {
 837            this.pgpKeyId = id;
 838        }
 839
 840        public Contact getContact() {
 841            if (fullJid != null) {
 842                return realJid == null
 843                        ? null
 844                        : getAccount().getRoster().getContactFromContactList(realJid);
 845            } else if (realJid != null) {
 846                return getAccount().getRoster().getContact(realJid);
 847            } else {
 848                return null;
 849            }
 850        }
 851
 852        public boolean setAvatar(final String avatar) {
 853            if (this.avatar != null && this.avatar.equals(avatar)) {
 854                return false;
 855            } else {
 856                this.avatar = avatar;
 857                return true;
 858            }
 859        }
 860
 861        public String getAvatar() {
 862
 863            // TODO prefer potentially better quality avatars from contact
 864            // TODO use getContact and if that’s not null and avatar is set use that
 865
 866            getContact();
 867
 868            if (avatar != null) {
 869                return avatar;
 870            }
 871            if (realJid == null) {
 872                return null;
 873            }
 874            final var contact = getAccount().getRoster().getContact(realJid);
 875            return contact.getAvatar();
 876        }
 877
 878        public Account getAccount() {
 879            return options.getAccount();
 880        }
 881
 882        public MucOptions getMucOptions() {
 883            return this.options;
 884        }
 885
 886        public Conversation getConversation() {
 887            return options.getConversation();
 888        }
 889
 890        public Jid getFullJid() {
 891            return fullJid;
 892        }
 893
 894        @Override
 895        public boolean equals(Object o) {
 896            if (this == o) return true;
 897            if (o == null || getClass() != o.getClass()) return false;
 898
 899            User user = (User) o;
 900
 901            if (role != user.role) return false;
 902            if (affiliation != user.affiliation) return false;
 903            if (!Objects.equals(realJid, user.realJid)) return false;
 904            return Objects.equals(fullJid, user.fullJid);
 905        }
 906
 907        public boolean isDomain() {
 908            return realJid != null && realJid.getLocal() == null && role == Role.NONE;
 909        }
 910
 911        @Override
 912        public int hashCode() {
 913            int result = role != null ? role.hashCode() : 0;
 914            result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
 915            result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
 916            result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
 917            return result;
 918        }
 919
 920        @Override
 921        public String toString() {
 922            return "[fulljid:"
 923                    + fullJid
 924                    + ",realjid:"
 925                    + realJid
 926                    + ",affiliation"
 927                    + affiliation.toString()
 928                    + "]";
 929        }
 930
 931        public boolean realJidMatchesAccount() {
 932            return realJid != null && realJid.equals(options.account.getJid().asBareJid());
 933        }
 934
 935        @Override
 936        public int compareTo(@NonNull User another) {
 937            if (another.outranks(getAffiliation())) {
 938                return 1;
 939            } else if (outranks(another.getAffiliation())) {
 940                return -1;
 941            } else {
 942                return getComparableName().compareToIgnoreCase(another.getComparableName());
 943            }
 944        }
 945
 946        public String getComparableName() {
 947            Contact contact = getContact();
 948            if (contact != null) {
 949                return contact.getDisplayName();
 950            } else {
 951                String name = getName();
 952                return name == null ? "" : name;
 953            }
 954        }
 955
 956        public Jid getRealJid() {
 957            return realJid;
 958        }
 959
 960        public void setRealJid(Jid jid) {
 961            this.realJid = jid != null ? jid.asBareJid() : null;
 962        }
 963
 964        public boolean setChatState(ChatState chatState) {
 965            if (this.chatState == chatState) {
 966                return false;
 967            }
 968            this.chatState = chatState;
 969            return true;
 970        }
 971
 972        @Override
 973        public int getAvatarBackgroundColor() {
 974            final String seed = realJid != null ? realJid.asBareJid().toString() : null;
 975            return UIHelper.getColorForName(seed == null ? getName() : seed);
 976        }
 977
 978        @Override
 979        public String getAvatarName() {
 980            return getConversation().getName().toString();
 981        }
 982
 983        public void setOccupantId(final String occupantId) {
 984            this.occupantId = occupantId;
 985        }
 986
 987        public String getOccupantId() {
 988            return this.occupantId;
 989        }
 990
 991        public boolean ranks(final Role role) {
 992            return ROLE_RANKS.getInt(this.role) >= ROLE_RANKS.getInt(role);
 993        }
 994
 995        public boolean ranks(final Affiliation affiliation) {
 996            return AFFILIATION_RANKS.getInt(this.affiliation)
 997                    >= AFFILIATION_RANKS.getInt(affiliation);
 998        }
 999
1000        public boolean outranks(final Affiliation affiliation) {
1001            return AFFILIATION_RANKS.getInt(this.affiliation)
1002                    > AFFILIATION_RANKS.getInt(affiliation);
1003        }
1004    }
1005}