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