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