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                if (old.nick != null && user.nick == null && old.getName().equals(user.getName())) user.nick = old.nick;
 287                if (old.hats != null && user.hats == null) user.hats = old.hats;
 288                if (old.avatar != null && user.avatar == null) user.avatar = old.avatar;
 289            }
 290            boolean fullJidIsSelf = isOnline && user.getFullJid() != null && user.getFullJid().equals(self.getFullJid());
 291            if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER))
 292                    && user.getAffiliation().outranks(Affiliation.OUTCAST)
 293                    && !fullJidIsSelf) {
 294                this.users.add(user);
 295                return !realJidFound && user.realJid != null;
 296            }
 297        }
 298        return false;
 299    }
 300
 301    public User findUserByName(final String name) {
 302        if (name == null) {
 303            return null;
 304        }
 305        synchronized (users) {
 306            for (User user : users) {
 307                if (name.equals(user.getName())) {
 308                    return user;
 309                }
 310            }
 311        }
 312        return null;
 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.asBareJid().equals(user.realJid)) {
 336                    return user;
 337                }
 338            }
 339        }
 340        return null;
 341    }
 342
 343    public User findOrCreateUserByRealJid(Jid jid, Jid fullJid) {
 344        User user = findUserByRealJid(jid);
 345        if (user == null) {
 346            user = new User(this, fullJid, null, new HashSet<>());
 347            user.setRealJid(jid);
 348        }
 349        return user;
 350    }
 351
 352    public User findUser(ReadByMarker readByMarker) {
 353        if (readByMarker.getRealJid() != null) {
 354            return findOrCreateUserByRealJid(readByMarker.getRealJid().asBareJid(), readByMarker.getFullJid());
 355        } else if (readByMarker.getFullJid() != null) {
 356            return findUserByFullJid(readByMarker.getFullJid());
 357        } else {
 358            return null;
 359        }
 360    }
 361
 362    public boolean isContactInRoom(Contact contact) {
 363        return contact != null && findUserByRealJid(contact.getJid().asBareJid()) != null;
 364    }
 365
 366    public boolean isUserInRoom(Jid jid) {
 367        return findUserByFullJid(jid) != null;
 368    }
 369
 370    public boolean setOnline() {
 371        boolean before = this.isOnline;
 372        this.isOnline = true;
 373        return !before;
 374    }
 375
 376    public ArrayList<User> getUsers() {
 377        return getUsers(true);
 378    }
 379
 380    public ArrayList<User> getUsers(boolean includeOffline) {
 381        synchronized (users) {
 382            ArrayList<User> users = new ArrayList<>();
 383            for (User user : this.users) {
 384                if (!user.isDomain() && (includeOffline || user.getRole().ranks(Role.PARTICIPANT))) {
 385                    users.add(user);
 386                }
 387            }
 388            return users;
 389        }
 390    }
 391
 392    public ArrayList<User> getUsersWithChatState(ChatState state, int max) {
 393        synchronized (users) {
 394            ArrayList<User> list = new ArrayList<>();
 395            for (User user : users) {
 396                if (user.chatState == state) {
 397                    list.add(user);
 398                    if (list.size() >= max) {
 399                        break;
 400                    }
 401                }
 402            }
 403            return list;
 404        }
 405    }
 406
 407    public List<User> getUsers(int max) {
 408        ArrayList<User> subset = new ArrayList<>();
 409        HashSet<Jid> jids = new HashSet<>();
 410        jids.add(account.getJid().asBareJid());
 411        synchronized (users) {
 412            for (User user : users) {
 413                if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
 414                    subset.add(user);
 415                }
 416                if (subset.size() >= max) {
 417                    break;
 418                }
 419            }
 420        }
 421        return subset;
 422    }
 423
 424    public static List<User> sub(List<User> users, int max) {
 425        ArrayList<User> subset = new ArrayList<>();
 426        HashSet<Jid> jids = new HashSet<>();
 427        for (User user : users) {
 428            jids.add(user.getAccount().getJid().asBareJid());
 429            if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
 430                subset.add(user);
 431            }
 432            if (subset.size() >= max) {
 433                break;
 434            }
 435        }
 436        return subset;
 437    }
 438
 439    public int getUserCount() {
 440        synchronized (users) {
 441            return users.size();
 442        }
 443    }
 444
 445    public String getProposedNick() {
 446        return getProposedNick(null);
 447    }
 448
 449    public String getProposedNick(final String mucNick) {
 450        final Bookmark bookmark = this.conversation.getBookmark();
 451        final String bookmarkedNick = normalize(account.getJid(), bookmark == null ? null : bookmark.getNick());
 452        if (bookmarkedNick != null) {
 453            this.tookProposedNickFromBookmark = true;
 454            return bookmarkedNick;
 455        } else if (mucNick != null) {
 456            return mucNick;
 457        } else if (!conversation.getJid().isBareJid()) {
 458            return conversation.getJid().getResource();
 459        } else {
 460            return defaultNick(account);
 461        }
 462    }
 463
 464    public static String defaultNick(final Account account) {
 465        final String displayName = normalize(account.getJid(), account.getDisplayName());
 466        if (displayName == null) {
 467            return JidHelper.localPartOrFallback(account.getJid());
 468        } else {
 469            return displayName;
 470        }
 471    }
 472
 473    private static String normalize(Jid account, String nick) {
 474        if (account == null || TextUtils.isEmpty(nick)) {
 475            return null;
 476        }
 477
 478        try {
 479            return account.withResource(nick).getResource();
 480        } catch (IllegalArgumentException e) {
 481            return nick;
 482        }
 483    }
 484
 485    public String getActualNick() {
 486        if (this.self.getNick() != null) {
 487            return this.self.getNick();
 488        } else {
 489            return this.getProposedNick();
 490        }
 491    }
 492
 493    public String getActualName() {
 494        if (this.self.getName() != null) {
 495            return this.self.getName();
 496        } else {
 497            return this.getProposedNick();
 498        }
 499    }
 500
 501    public boolean online() {
 502        return this.isOnline;
 503    }
 504
 505    public Error getError() {
 506        return this.error;
 507    }
 508
 509    public void setError(Error error) {
 510        this.isOnline = isOnline && error == Error.NONE;
 511        this.error = error;
 512    }
 513
 514    public void setOnRenameListener(OnRenameListener listener) {
 515        this.onRenameListener = listener;
 516    }
 517
 518    public void setOffline() {
 519        synchronized (users) {
 520            this.users.clear();
 521        }
 522        this.error = Error.NO_RESPONSE;
 523        this.isOnline = false;
 524    }
 525
 526    public User getSelf() {
 527        return self;
 528    }
 529
 530    public boolean setSubject(String subject) {
 531        return this.conversation.setAttribute("subject", subject);
 532    }
 533
 534    public String getSubject() {
 535        return this.conversation.getAttribute("subject");
 536    }
 537
 538    public String getName() {
 539        return this.conversation.getAttribute("muc_name");
 540    }
 541
 542    private List<User> getFallbackUsersFromCryptoTargets() {
 543        List<User> users = new ArrayList<>();
 544        for (Jid jid : conversation.getAcceptedCryptoTargets()) {
 545            User user = new User(this, null, null, new HashSet<>());
 546            user.setRealJid(jid);
 547            users.add(user);
 548        }
 549        return users;
 550    }
 551
 552    public List<User> getUsersRelevantForNameAndAvatar() {
 553        final List<User> users;
 554        if (isOnline) {
 555            users = getUsers(5);
 556        } else {
 557            users = getFallbackUsersFromCryptoTargets();
 558        }
 559        return users;
 560    }
 561
 562    String createNameFromParticipants() {
 563        List<User> users = getUsersRelevantForNameAndAvatar();
 564        if (users.size() >= 2) {
 565            StringBuilder builder = new StringBuilder();
 566            for (User user : users) {
 567                if (builder.length() != 0) {
 568                    builder.append(", ");
 569                }
 570                String name = UIHelper.getDisplayName(user);
 571                if (name != null) {
 572                    builder.append(name.split("\\s+")[0]);
 573                }
 574            }
 575            return builder.toString();
 576        } else {
 577            return null;
 578        }
 579    }
 580
 581    public long[] getPgpKeyIds() {
 582        List<Long> ids = new ArrayList<>();
 583        for (User user : this.users) {
 584            if (user.getPgpKeyId() != 0) {
 585                ids.add(user.getPgpKeyId());
 586            }
 587        }
 588        ids.add(account.getPgpId());
 589        long[] primitiveLongArray = new long[ids.size()];
 590        for (int i = 0; i < ids.size(); ++i) {
 591            primitiveLongArray[i] = ids.get(i);
 592        }
 593        return primitiveLongArray;
 594    }
 595
 596    public boolean pgpKeysInUse() {
 597        synchronized (users) {
 598            for (User user : users) {
 599                if (user.getPgpKeyId() != 0) {
 600                    return true;
 601                }
 602            }
 603        }
 604        return false;
 605    }
 606
 607    public boolean everybodyHasKeys() {
 608        synchronized (users) {
 609            for (User user : users) {
 610                if (user.getPgpKeyId() == 0) {
 611                    return false;
 612                }
 613            }
 614        }
 615        return true;
 616    }
 617
 618    public Jid createJoinJid(String nick) {
 619        return createJoinJid(nick, true);
 620    }
 621
 622    private Jid createJoinJid(String nick, boolean tryFix) {
 623        try {
 624            return conversation.getJid().withResource(nick);
 625        } catch (final IllegalArgumentException e) {
 626            try {
 627                return tryFix ? createJoinJid(gnu.inet.encoding.Punycode.encode(nick), false) : null;
 628            } catch (final gnu.inet.encoding.PunycodeException | ArrayIndexOutOfBoundsException e2) {
 629                return null;
 630            }
 631        }
 632    }
 633
 634    public Jid getTrueCounterpart(Jid jid) {
 635        if (jid.equals(getSelf().getFullJid())) {
 636            return account.getJid().asBareJid();
 637        }
 638        User user = findUserByFullJid(jid);
 639        return user == null ? null : user.realJid;
 640    }
 641
 642    public String getPassword() {
 643        this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
 644        if (this.password == null && conversation.getBookmark() != null
 645                && conversation.getBookmark().getPassword() != null) {
 646            return conversation.getBookmark().getPassword();
 647        } else {
 648            return this.password;
 649        }
 650    }
 651
 652    public void setPassword(String password) {
 653        if (conversation.getBookmark() != null) {
 654            conversation.getBookmark().setPassword(password);
 655        } else {
 656            this.password = password;
 657        }
 658        conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
 659    }
 660
 661    public Conversation getConversation() {
 662        return this.conversation;
 663    }
 664
 665    public List<Jid> getMembers(final boolean includeDomains) {
 666        ArrayList<Jid> members = new ArrayList<>();
 667        synchronized (users) {
 668            for (User user : users) {
 669                if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null && !user.realJid.asBareJid().equals(conversation.account.getJid().asBareJid()) && (!user.isDomain() || includeDomains)) {
 670                    members.add(user.realJid);
 671                }
 672            }
 673        }
 674        return members;
 675    }
 676
 677    public enum Affiliation {
 678        OWNER(4, R.string.owner),
 679        ADMIN(3, R.string.admin),
 680        MEMBER(2, R.string.member),
 681        OUTCAST(0, R.string.outcast),
 682        NONE(1, R.string.no_affiliation);
 683
 684        private final int resId;
 685        private final int rank;
 686
 687        Affiliation(int rank, int resId) {
 688            this.resId = resId;
 689            this.rank = rank;
 690        }
 691
 692        public static Affiliation of(@Nullable String value) {
 693            if (value == null) {
 694                return NONE;
 695            }
 696            try {
 697                return Affiliation.valueOf(value.toUpperCase(Locale.US));
 698            } catch (IllegalArgumentException e) {
 699                return NONE;
 700            }
 701        }
 702
 703        public int getResId() {
 704            return resId;
 705        }
 706
 707        @Override
 708        public String toString() {
 709            return name().toLowerCase(Locale.US);
 710        }
 711
 712        public boolean outranks(Affiliation affiliation) {
 713            return rank > affiliation.rank;
 714        }
 715
 716        public boolean ranks(Affiliation affiliation) {
 717            return rank >= affiliation.rank;
 718        }
 719    }
 720
 721    public enum Role {
 722        MODERATOR(R.string.moderator, 3),
 723        VISITOR(R.string.visitor, 1),
 724        PARTICIPANT(R.string.participant, 2),
 725        NONE(R.string.no_role, 0);
 726
 727        private final int resId;
 728        private final int rank;
 729
 730        Role(int resId, int rank) {
 731            this.resId = resId;
 732            this.rank = rank;
 733        }
 734
 735        public static Role of(@Nullable String value) {
 736            if (value == null) {
 737                return NONE;
 738            }
 739            try {
 740                return Role.valueOf(value.toUpperCase(Locale.US));
 741            } catch (IllegalArgumentException e) {
 742                return NONE;
 743            }
 744        }
 745
 746        public int getResId() {
 747            return resId;
 748        }
 749
 750        @Override
 751        public String toString() {
 752            return name().toLowerCase(Locale.US);
 753        }
 754
 755        public boolean ranks(Role role) {
 756            return rank >= role.rank;
 757        }
 758    }
 759
 760    public enum Error {
 761        NO_RESPONSE,
 762        SERVER_NOT_FOUND,
 763        REMOTE_SERVER_TIMEOUT,
 764        NONE,
 765        NICK_IN_USE,
 766        PASSWORD_REQUIRED,
 767        BANNED,
 768        MEMBERS_ONLY,
 769        RESOURCE_CONSTRAINT,
 770        KICKED,
 771        SHUTDOWN,
 772        DESTROYED,
 773        INVALID_NICK,
 774        TECHNICAL_PROBLEMS,
 775        UNKNOWN,
 776        NON_ANONYMOUS
 777    }
 778
 779    private interface OnEventListener {
 780        void onSuccess();
 781
 782        void onFailure();
 783    }
 784
 785    public interface OnRenameListener extends OnEventListener {
 786
 787    }
 788
 789    public static class Hat implements Comparable<Hat> {
 790        private final Uri uri;
 791        private final String title;
 792
 793        public Hat(final Element el) {
 794            Uri parseUri = null; // null hat uri is invaild per spec
 795            try {
 796                parseUri = Uri.parse(el.getAttribute("uri"));
 797            } catch (final Exception e) { }
 798            uri = parseUri;
 799
 800            title = el.getAttribute("title");
 801        }
 802
 803        public Hat(final Uri uri, final String title) {
 804            this.uri = uri;
 805            this.title = title;
 806        }
 807
 808        public String toString() {
 809            return title;
 810        }
 811
 812        public int getColor() {
 813            return UIHelper.getColorForName(uri == null ? title : uri.toString());
 814        }
 815
 816        @Override
 817        public int compareTo(@NonNull Hat another) {
 818            return title.compareTo(another.title);
 819        }
 820    }
 821
 822    public static class User implements Comparable<User>, AvatarService.Avatarable {
 823        private Role role = Role.NONE;
 824        private Affiliation affiliation = Affiliation.NONE;
 825        private Jid realJid;
 826        private Jid fullJid;
 827        protected String nick;
 828        private long pgpKeyId = 0;
 829        protected Avatar avatar;
 830        private final MucOptions options;
 831        private ChatState chatState = Config.DEFAULT_CHAT_STATE;
 832        protected Set<Hat> hats;
 833
 834        public User(MucOptions options, Jid fullJid, final String nick, final Set<Hat> hats) {
 835            this.options = options;
 836            this.fullJid = fullJid;
 837            this.nick = nick;
 838            this.hats = hats;
 839        }
 840
 841        public String getName() {
 842            return fullJid == null ? null : fullJid.getResource();
 843        }
 844
 845        public String getNick() {
 846            return nick == null ? getName() : nick;
 847        }
 848
 849        public Role getRole() {
 850            return this.role;
 851        }
 852
 853        public void setRole(String role) {
 854            this.role = Role.of(role);
 855        }
 856
 857        public Affiliation getAffiliation() {
 858            return this.affiliation;
 859        }
 860
 861        public void setAffiliation(String affiliation) {
 862            this.affiliation = Affiliation.of(affiliation);
 863        }
 864
 865        public Set<Hat> getHats() {
 866            return this.hats == null ? new HashSet<>() : hats;
 867        }
 868
 869        public long getPgpKeyId() {
 870            if (this.pgpKeyId != 0) {
 871                return this.pgpKeyId;
 872            } else if (realJid != null) {
 873                return getAccount().getRoster().getContact(realJid).getPgpKeyId();
 874            } else {
 875                return 0;
 876            }
 877        }
 878
 879        public void setPgpKeyId(long id) {
 880            this.pgpKeyId = id;
 881        }
 882
 883        public Contact getContact() {
 884            if (fullJid != null) {
 885                return getAccount().getRoster().getContactFromContactList(realJid);
 886            } else if (realJid != null) {
 887                return getAccount().getRoster().getContact(realJid);
 888            } else {
 889                return null;
 890            }
 891        }
 892
 893        public boolean setAvatar(Avatar avatar) {
 894            if (this.avatar != null && this.avatar.equals(avatar)) {
 895                return false;
 896            } else {
 897                this.avatar = avatar;
 898                return true;
 899            }
 900        }
 901
 902        public String getAvatar() {
 903            if (avatar != null) {
 904                return avatar.getFilename();
 905            }
 906            Avatar avatar = realJid != null ? getAccount().getRoster().getContact(realJid).getAvatar() : null;
 907            return avatar == null ? null : avatar.getFilename();
 908        }
 909
 910        public Cid getAvatarCid() {
 911            if (avatar != null) {
 912                return avatar.cid();
 913            }
 914            Avatar avatar = realJid != null ? getAccount().getRoster().getContact(realJid).getAvatar() : null;
 915            return avatar == null ? null : avatar.cid();
 916        }
 917
 918        public Account getAccount() {
 919            return options.getAccount();
 920        }
 921
 922        public Conversation getConversation() {
 923            return options.getConversation();
 924        }
 925
 926        public Jid getFullJid() {
 927            return fullJid;
 928        }
 929
 930        @Override
 931        public boolean equals(Object o) {
 932            if (this == o) return true;
 933            if (o == null || getClass() != o.getClass()) return false;
 934
 935            User user = (User) o;
 936
 937            if (role != user.role) return false;
 938            if (affiliation != user.affiliation) return false;
 939            if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null)
 940                return false;
 941            return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null;
 942
 943        }
 944
 945        public boolean isDomain() {
 946            return realJid != null && realJid.getLocal() == null && role == Role.NONE;
 947        }
 948
 949        @Override
 950        public int hashCode() {
 951            int result = role != null ? role.hashCode() : 0;
 952            result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
 953            result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
 954            result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
 955            return result;
 956        }
 957
 958        @Override
 959        public String toString() {
 960            return "[fulljid:" + fullJid + ",realjid:" + realJid + ",nick:" + nick + ",affiliation" + affiliation.toString() + "]";
 961        }
 962
 963        public boolean realJidMatchesAccount() {
 964            return realJid != null && realJid.equals(options.account.getJid().asBareJid());
 965        }
 966
 967        @Override
 968        public int compareTo(@NonNull User another) {
 969            if (another.getAffiliation().outranks(getAffiliation())) {
 970                return 1;
 971            } else if (getAffiliation().outranks(another.getAffiliation())) {
 972                return -1;
 973            } else {
 974                return getComparableName().compareToIgnoreCase(another.getComparableName());
 975            }
 976        }
 977
 978        public String getComparableName() {
 979            Contact contact = getContact();
 980            if (contact != null) {
 981                return contact.getDisplayName();
 982            } else {
 983                String name = getName();
 984                return name == null ? "" : name;
 985            }
 986        }
 987
 988        public Jid getRealJid() {
 989            return realJid;
 990        }
 991
 992        public void setRealJid(Jid jid) {
 993            this.realJid = jid != null ? jid.asBareJid() : null;
 994        }
 995
 996        public boolean setChatState(ChatState chatState) {
 997            if (this.chatState == chatState) {
 998                return false;
 999            }
1000            this.chatState = chatState;
1001            return true;
1002        }
1003
1004        @Override
1005        public int getAvatarBackgroundColor() {
1006            final String seed = realJid != null ? realJid.asBareJid().toString() : null;
1007            return UIHelper.getColorForName(seed == null ? getName() : seed);
1008        }
1009
1010        @Override
1011        public String getAvatarName() {
1012            return getConversation().getName().toString();
1013        }
1014    }
1015}