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