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