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(final 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(final Jid jid, final Affiliation affiliation) {
  72        final 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(final int max) {
 444        final ArrayList<User> subset = new ArrayList<>();
 445        final HashSet<Jid> addresses = new HashSet<>();
 446        addresses.add(account.getJid().asBareJid());
 447        synchronized (users) {
 448            for (User user : users) {
 449                if (user.getRealJid() == null
 450                        || (user.getRealJid().getLocal() != null
 451                                && addresses.add(user.getRealJid()))) {
 452                    subset.add(user);
 453                }
 454                if (subset.size() >= max) {
 455                    break;
 456                }
 457            }
 458        }
 459        return subset;
 460    }
 461
 462    public static List<User> sub(List<User> users, int max) {
 463        ArrayList<User> subset = new ArrayList<>();
 464        HashSet<Jid> jids = new HashSet<>();
 465        for (User user : users) {
 466            jids.add(user.getAccount().getJid().asBareJid());
 467            if (user.getRealJid() == null
 468                    || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
 469                subset.add(user);
 470            }
 471            if (subset.size() >= max) {
 472                break;
 473            }
 474        }
 475        return subset;
 476    }
 477
 478    public int getUserCount() {
 479        synchronized (users) {
 480            return users.size();
 481        }
 482    }
 483
 484    private String getProposedNick() {
 485        final Bookmark bookmark = this.conversation.getBookmark();
 486        if (bookmark != null) {
 487            // if we already have a bookmark we consider this the source of truth
 488            return getProposedNickPure();
 489        }
 490        final var storedJid = conversation.getJid();
 491        if (storedJid.isBareJid()) {
 492            return defaultNick(account);
 493        } else {
 494            return storedJid.getResource();
 495        }
 496    }
 497
 498    public String getProposedNickPure() {
 499        final Bookmark bookmark = this.conversation.getBookmark();
 500        final String bookmarkedNick =
 501                normalize(account.getJid(), bookmark == null ? null : bookmark.getNick());
 502        if (bookmarkedNick != null) {
 503            return bookmarkedNick;
 504        } else {
 505            return defaultNick(account);
 506        }
 507    }
 508
 509    public static String defaultNick(final Account account) {
 510        final String displayName = normalize(account.getJid(), account.getDisplayName());
 511        if (displayName == null) {
 512            return JidHelper.localPartOrFallback(account.getJid());
 513        } else {
 514            return displayName;
 515        }
 516    }
 517
 518    private static String normalize(final Jid account, final String nick) {
 519        if (account == null || Strings.isNullOrEmpty(nick)) {
 520            return null;
 521        }
 522        try {
 523            return account.withResource(nick).getResource();
 524        } catch (final IllegalArgumentException e) {
 525            return null;
 526        }
 527    }
 528
 529    public String getActualNick() {
 530        if (this.self.getName() != null) {
 531            return this.self.getName();
 532        } else {
 533            return this.getProposedNick();
 534        }
 535    }
 536
 537    public boolean online() {
 538        return this.isOnline;
 539    }
 540
 541    public Error getError() {
 542        return this.error;
 543    }
 544
 545    public void setError(Error error) {
 546        this.isOnline = isOnline && error == Error.NONE;
 547        this.error = error;
 548    }
 549
 550    public void setOnRenameListener(OnRenameListener listener) {
 551        this.onRenameListener = listener;
 552    }
 553
 554    public void setOffline() {
 555        synchronized (users) {
 556            this.users.clear();
 557        }
 558        this.error = Error.NO_RESPONSE;
 559        this.isOnline = false;
 560    }
 561
 562    public User getSelf() {
 563        return self;
 564    }
 565
 566    public boolean setSubject(String subject) {
 567        return this.conversation.setAttribute("subject", subject);
 568    }
 569
 570    public String getSubject() {
 571        return this.conversation.getAttribute("subject");
 572    }
 573
 574    public String getName() {
 575        return this.conversation.getAttribute("muc_name");
 576    }
 577
 578    private List<User> getFallbackUsersFromCryptoTargets() {
 579        List<User> users = new ArrayList<>();
 580        for (Jid jid : conversation.getAcceptedCryptoTargets()) {
 581            User user = new User(this, null);
 582            user.setRealJid(jid);
 583            users.add(user);
 584        }
 585        return users;
 586    }
 587
 588    public List<User> getUsersRelevantForNameAndAvatar() {
 589        final List<User> users;
 590        if (isOnline) {
 591            users = getUsers(5);
 592        } else {
 593            users = getFallbackUsersFromCryptoTargets();
 594        }
 595        return users;
 596    }
 597
 598    String createNameFromParticipants() {
 599        List<User> users = getUsersRelevantForNameAndAvatar();
 600        if (users.size() >= 2) {
 601            StringBuilder builder = new StringBuilder();
 602            for (User user : users) {
 603                if (builder.length() != 0) {
 604                    builder.append(", ");
 605                }
 606                String name = UIHelper.getDisplayName(user);
 607                if (name != null) {
 608                    builder.append(name.split("\\s+")[0]);
 609                }
 610            }
 611            return builder.toString();
 612        } else {
 613            return null;
 614        }
 615    }
 616
 617    public long[] getPgpKeyIds() {
 618        List<Long> ids = new ArrayList<>();
 619        for (User user : this.users) {
 620            if (user.getPgpKeyId() != 0) {
 621                ids.add(user.getPgpKeyId());
 622            }
 623        }
 624        ids.add(account.getPgpId());
 625        long[] primitiveLongArray = new long[ids.size()];
 626        for (int i = 0; i < ids.size(); ++i) {
 627            primitiveLongArray[i] = ids.get(i);
 628        }
 629        return primitiveLongArray;
 630    }
 631
 632    public boolean pgpKeysInUse() {
 633        synchronized (users) {
 634            for (User user : users) {
 635                if (user.getPgpKeyId() != 0) {
 636                    return true;
 637                }
 638            }
 639        }
 640        return false;
 641    }
 642
 643    public boolean everybodyHasKeys() {
 644        synchronized (users) {
 645            for (User user : users) {
 646                if (user.getPgpKeyId() == 0) {
 647                    return false;
 648                }
 649            }
 650        }
 651        return true;
 652    }
 653
 654    public Jid createJoinJid(String nick) {
 655        try {
 656            return conversation.getJid().withResource(nick);
 657        } catch (final IllegalArgumentException e) {
 658            return null;
 659        }
 660    }
 661
 662    public Jid getTrueCounterpart(Jid jid) {
 663        if (jid.equals(getSelf().getFullJid())) {
 664            return account.getJid().asBareJid();
 665        }
 666        User user = findUserByFullJid(jid);
 667        return user == null ? null : user.realJid;
 668    }
 669
 670    public String getPassword() {
 671        this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
 672        if (this.password == null
 673                && conversation.getBookmark() != null
 674                && conversation.getBookmark().getPassword() != null) {
 675            return conversation.getBookmark().getPassword();
 676        } else {
 677            return this.password;
 678        }
 679    }
 680
 681    public void setPassword(String password) {
 682        if (conversation.getBookmark() != null) {
 683            conversation.getBookmark().setPassword(password);
 684        } else {
 685            this.password = password;
 686        }
 687        conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
 688    }
 689
 690    public Conversation getConversation() {
 691        return this.conversation;
 692    }
 693
 694    public List<Jid> getMembers(final boolean includeDomains) {
 695        ArrayList<Jid> members = new ArrayList<>();
 696        synchronized (users) {
 697            for (User user : users) {
 698                if (user.affiliation.ranks(Affiliation.MEMBER)
 699                        && user.realJid != null
 700                        && !user.realJid
 701                                .asBareJid()
 702                                .equals(conversation.account.getJid().asBareJid())
 703                        && (!user.isDomain() || includeDomains)) {
 704                    members.add(user.realJid);
 705                }
 706            }
 707        }
 708        return members;
 709    }
 710
 711    public enum Affiliation {
 712        OWNER(4, R.string.owner),
 713        ADMIN(3, R.string.admin),
 714        MEMBER(2, R.string.member),
 715        OUTCAST(0, R.string.outcast),
 716        NONE(1, R.string.no_affiliation);
 717
 718        private final int resId;
 719        private final int rank;
 720
 721        Affiliation(int rank, int resId) {
 722            this.resId = resId;
 723            this.rank = rank;
 724        }
 725
 726        public static Affiliation of(@Nullable String value) {
 727            if (value == null) {
 728                return NONE;
 729            }
 730            try {
 731                return Affiliation.valueOf(value.toUpperCase(Locale.US));
 732            } catch (IllegalArgumentException e) {
 733                return NONE;
 734            }
 735        }
 736
 737        public int getResId() {
 738            return resId;
 739        }
 740
 741        @Override
 742        public String toString() {
 743            return name().toLowerCase(Locale.US);
 744        }
 745
 746        public boolean outranks(Affiliation affiliation) {
 747            return rank > affiliation.rank;
 748        }
 749
 750        public boolean ranks(Affiliation affiliation) {
 751            return rank >= affiliation.rank;
 752        }
 753    }
 754
 755    public enum Role {
 756        MODERATOR(R.string.moderator, 3),
 757        VISITOR(R.string.visitor, 1),
 758        PARTICIPANT(R.string.participant, 2),
 759        NONE(R.string.no_role, 0);
 760
 761        private final int resId;
 762        private final int rank;
 763
 764        Role(int resId, int rank) {
 765            this.resId = resId;
 766            this.rank = rank;
 767        }
 768
 769        public static Role of(@Nullable String value) {
 770            if (value == null) {
 771                return NONE;
 772            }
 773            try {
 774                return Role.valueOf(value.toUpperCase(Locale.US));
 775            } catch (IllegalArgumentException e) {
 776                return NONE;
 777            }
 778        }
 779
 780        public int getResId() {
 781            return resId;
 782        }
 783
 784        @Override
 785        public String toString() {
 786            return name().toLowerCase(Locale.US);
 787        }
 788
 789        public boolean ranks(Role role) {
 790            return rank >= role.rank;
 791        }
 792    }
 793
 794    public enum Error {
 795        NO_RESPONSE,
 796        SERVER_NOT_FOUND,
 797        REMOTE_SERVER_TIMEOUT,
 798        NONE,
 799        NICK_IN_USE,
 800        PASSWORD_REQUIRED,
 801        BANNED,
 802        MEMBERS_ONLY,
 803        RESOURCE_CONSTRAINT,
 804        KICKED,
 805        SHUTDOWN,
 806        DESTROYED,
 807        INVALID_NICK,
 808        TECHNICAL_PROBLEMS,
 809        UNKNOWN,
 810        NON_ANONYMOUS
 811    }
 812
 813    private interface OnEventListener {
 814        void onSuccess();
 815
 816        void onFailure();
 817    }
 818
 819    public interface OnRenameListener extends OnEventListener {}
 820
 821    public static class User implements Comparable<User>, AvatarService.Avatarable {
 822        private Role role = Role.NONE;
 823        private Affiliation affiliation = Affiliation.NONE;
 824        private Jid realJid;
 825        private Jid fullJid;
 826        private long pgpKeyId = 0;
 827        private Avatar avatar;
 828        private final MucOptions options;
 829        private ChatState chatState = Config.DEFAULT_CHAT_STATE;
 830        private String occupantId;
 831
 832        public User(MucOptions options, Jid fullJid) {
 833            this.options = options;
 834            this.fullJid = fullJid;
 835        }
 836
 837        public String getName() {
 838            return fullJid == null ? null : fullJid.getResource();
 839        }
 840
 841        public Role getRole() {
 842            return this.role;
 843        }
 844
 845        public void setRole(String role) {
 846            this.role = Role.of(role);
 847        }
 848
 849        public Affiliation getAffiliation() {
 850            return this.affiliation;
 851        }
 852
 853        public void setAffiliation(String affiliation) {
 854            this.affiliation = Affiliation.of(affiliation);
 855        }
 856
 857        public long getPgpKeyId() {
 858            if (this.pgpKeyId != 0) {
 859                return this.pgpKeyId;
 860            } else if (realJid != null) {
 861                return getAccount().getRoster().getContact(realJid).getPgpKeyId();
 862            } else {
 863                return 0;
 864            }
 865        }
 866
 867        public void setPgpKeyId(long id) {
 868            this.pgpKeyId = id;
 869        }
 870
 871        public Contact getContact() {
 872            if (fullJid != null) {
 873                return getAccount().getRoster().getContactFromContactList(realJid);
 874            } else if (realJid != null) {
 875                return getAccount().getRoster().getContact(realJid);
 876            } else {
 877                return null;
 878            }
 879        }
 880
 881        public boolean setAvatar(final Avatar avatar) {
 882            if (this.avatar != null && this.avatar.equals(avatar)) {
 883                return false;
 884            } else {
 885                this.avatar = avatar;
 886                return true;
 887            }
 888        }
 889
 890        public String getAvatar() {
 891            if (avatar != null) {
 892                return avatar.getFilename();
 893            }
 894            Avatar avatar =
 895                    realJid != null
 896                            ? getAccount().getRoster().getContact(realJid).getAvatar()
 897                            : null;
 898            return avatar == null ? null : avatar.getFilename();
 899        }
 900
 901        public Account getAccount() {
 902            return options.getAccount();
 903        }
 904
 905        public Conversation getConversation() {
 906            return options.getConversation();
 907        }
 908
 909        public Jid getFullJid() {
 910            return fullJid;
 911        }
 912
 913        @Override
 914        public boolean equals(Object o) {
 915            if (this == o) return true;
 916            if (o == null || getClass() != o.getClass()) return false;
 917
 918            User user = (User) o;
 919
 920            if (role != user.role) return false;
 921            if (affiliation != user.affiliation) return false;
 922            if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null)
 923                return false;
 924            return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null;
 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}