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