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