MucOptions.java

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