MucOptions.java

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