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