MucOptions.java

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