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