MucOptions.java

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