MucOptions.java

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