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 synchronized (users) {
417 ArrayList<User> users = new ArrayList<>();
418 for (User user : this.users) {
419 if (!user.isDomain() && (includeOffline ? user.getAffiliation().ranks(Affiliation.NONE) : user.getRole().ranks(Role.PARTICIPANT))) {
420 users.add(user);
421 }
422 }
423 return users;
424 }
425 }
426
427 public ArrayList<User> getUsersByRole(Role role) {
428 synchronized (users) {
429 ArrayList<User> list = new ArrayList<>();
430 for (User user : users) {
431 if (user.getRole().ranks(role)) {
432 list.add(user);
433 }
434 }
435 return list;
436 }
437 }
438
439 public ArrayList<User> getUsersWithChatState(ChatState state, int max) {
440 synchronized (users) {
441 ArrayList<User> list = new ArrayList<>();
442 for (User user : users) {
443 if (user.chatState == state) {
444 list.add(user);
445 if (list.size() >= max) {
446 break;
447 }
448 }
449 }
450 return list;
451 }
452 }
453
454 public List<User> getUsers(int max) {
455 ArrayList<User> subset = new ArrayList<>();
456 HashSet<Jid> jids = new HashSet<>();
457 jids.add(account.getJid().asBareJid());
458 synchronized (users) {
459 for (User user : users) {
460 if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
461 subset.add(user);
462 }
463 if (subset.size() >= max) {
464 break;
465 }
466 }
467 }
468 return subset;
469 }
470
471 public static List<User> sub(List<User> users, int max) {
472 ArrayList<User> subset = new ArrayList<>();
473 HashSet<Jid> jids = new HashSet<>();
474 for (User user : users) {
475 jids.add(user.getAccount().getJid().asBareJid());
476 if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
477 subset.add(user);
478 }
479 if (subset.size() >= max) {
480 break;
481 }
482 }
483 return subset;
484 }
485
486 public int getUserCount() {
487 synchronized (users) {
488 return users.size();
489 }
490 }
491
492 public String getProposedNick() {
493 return getProposedNick(null);
494 }
495
496 public String getProposedNick(final String mucNick) {
497 final Bookmark bookmark = this.conversation.getBookmark();
498 final String bookmarkedNick = normalize(account.getJid(), bookmark == null ? null : bookmark.getNick());
499 if (bookmarkedNick != null) {
500 this.tookProposedNickFromBookmark = true;
501 return bookmarkedNick;
502 } else if (mucNick != null) {
503 return mucNick;
504 } else if (!conversation.getJid().isBareJid()) {
505 return conversation.getJid().getResource();
506 } else {
507 return defaultNick(account);
508 }
509 }
510
511 public static String defaultNick(final Account account) {
512 final String displayName = normalize(account.getJid(), account.getDisplayName());
513 if (displayName == null) {
514 return JidHelper.localPartOrFallback(account.getJid());
515 } else {
516 return displayName;
517 }
518 }
519
520 private static String normalize(Jid account, String nick) {
521 if (account == null || TextUtils.isEmpty(nick)) {
522 return null;
523 }
524
525 try {
526 return account.withResource(nick).getResource();
527 } catch (IllegalArgumentException e) {
528 return nick;
529 }
530 }
531
532 public String getActualNick() {
533 if (this.self.getNick() != null) {
534 return this.self.getNick();
535 } else {
536 return this.getProposedNick();
537 }
538 }
539
540 public String getActualName() {
541 if (this.self.getName() != null) {
542 return this.self.getName();
543 } else {
544 return this.getProposedNick();
545 }
546 }
547
548 public boolean online() {
549 return this.isOnline;
550 }
551
552 public Error getError() {
553 return this.error;
554 }
555
556 public void setError(Error error) {
557 this.isOnline = isOnline && error == Error.NONE;
558 this.error = error;
559 }
560
561 public void setOnRenameListener(OnRenameListener listener) {
562 this.onRenameListener = listener;
563 }
564
565 public void setOffline() {
566 synchronized (users) {
567 this.users.clear();
568 }
569 this.error = Error.NO_RESPONSE;
570 this.isOnline = false;
571 }
572
573 public User getSelf() {
574 return self;
575 }
576
577 public boolean setSubject(String subject) {
578 return this.conversation.setAttribute("subject", subject);
579 }
580
581 public String getSubject() {
582 return this.conversation.getAttribute("subject");
583 }
584
585 public String getName() {
586 return this.conversation.getAttribute("muc_name");
587 }
588
589 private List<User> getFallbackUsersFromCryptoTargets() {
590 List<User> users = new ArrayList<>();
591 for (Jid jid : conversation.getAcceptedCryptoTargets()) {
592 User user = new User(this, null, null, null, new HashSet<>());
593 user.setRealJid(jid);
594 users.add(user);
595 }
596 return users;
597 }
598
599 public List<User> getUsersRelevantForNameAndAvatar() {
600 final List<User> users;
601 if (isOnline) {
602 users = getUsers(5);
603 } else {
604 users = getFallbackUsersFromCryptoTargets();
605 }
606 return users;
607 }
608
609 String createNameFromParticipants() {
610 List<User> users = getUsersRelevantForNameAndAvatar();
611 if (users.size() >= 2) {
612 StringBuilder builder = new StringBuilder();
613 for (User user : users) {
614 if (builder.length() != 0) {
615 builder.append(", ");
616 }
617 String name = UIHelper.getDisplayName(user);
618 if (name != null) {
619 builder.append(name.split("\\s+")[0]);
620 }
621 }
622 return builder.toString();
623 } else {
624 return null;
625 }
626 }
627
628 public long[] getPgpKeyIds() {
629 List<Long> ids = new ArrayList<>();
630 for (User user : this.users) {
631 if (user.getPgpKeyId() != 0) {
632 ids.add(user.getPgpKeyId());
633 }
634 }
635 ids.add(account.getPgpId());
636 long[] primitiveLongArray = new long[ids.size()];
637 for (int i = 0; i < ids.size(); ++i) {
638 primitiveLongArray[i] = ids.get(i);
639 }
640 return primitiveLongArray;
641 }
642
643 public boolean pgpKeysInUse() {
644 synchronized (users) {
645 for (User user : users) {
646 if (user.getPgpKeyId() != 0) {
647 return true;
648 }
649 }
650 }
651 return false;
652 }
653
654 public boolean everybodyHasKeys() {
655 synchronized (users) {
656 for (User user : users) {
657 if (user.getPgpKeyId() == 0) {
658 return false;
659 }
660 }
661 }
662 return true;
663 }
664
665 public Jid createJoinJid(String nick) {
666 return createJoinJid(nick, true);
667 }
668
669 private Jid createJoinJid(String nick, boolean tryFix) {
670 try {
671 return conversation.getJid().withResource(nick);
672 } catch (final IllegalArgumentException e) {
673 try {
674 return tryFix ? createJoinJid(gnu.inet.encoding.Punycode.encode(nick), false) : null;
675 } catch (final Exception e2) {
676 return null;
677 }
678 }
679 }
680
681 public Jid getTrueCounterpart(Jid jid) {
682 if (jid.equals(getSelf().getFullJid())) {
683 return account.getJid().asBareJid();
684 }
685 User user = findUserByFullJid(jid);
686 return user == null ? null : user.realJid;
687 }
688
689 public String getPassword() {
690 this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
691 if (this.password == null && conversation.getBookmark() != null
692 && conversation.getBookmark().getPassword() != null) {
693 return conversation.getBookmark().getPassword();
694 } else {
695 return this.password;
696 }
697 }
698
699 public void setPassword(String password) {
700 if (conversation.getBookmark() != null) {
701 conversation.getBookmark().setPassword(password);
702 } else {
703 this.password = password;
704 }
705 conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
706 }
707
708 public Conversation getConversation() {
709 return this.conversation;
710 }
711
712 public List<Jid> getMembers(final boolean includeDomains) {
713 ArrayList<Jid> members = new ArrayList<>();
714 synchronized (users) {
715 for (User user : users) {
716 if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null && !user.realJid.asBareJid().equals(conversation.account.getJid().asBareJid()) && (!user.isDomain() || includeDomains)) {
717 members.add(user.realJid);
718 }
719 }
720 }
721 return members;
722 }
723
724 public enum Affiliation {
725 OWNER(4, R.string.owner),
726 ADMIN(3, R.string.admin),
727 MEMBER(2, R.string.member),
728 OUTCAST(0, R.string.outcast),
729 NONE(1, R.string.no_affiliation);
730
731 private final int resId;
732 private final int rank;
733
734 Affiliation(int rank, int resId) {
735 this.resId = resId;
736 this.rank = rank;
737 }
738
739 public static Affiliation of(@Nullable String value) {
740 if (value == null) {
741 return NONE;
742 }
743 try {
744 return Affiliation.valueOf(value.toUpperCase(Locale.US));
745 } catch (IllegalArgumentException e) {
746 return NONE;
747 }
748 }
749
750 public int getResId() {
751 return resId;
752 }
753
754 @Override
755 public String toString() {
756 return name().toLowerCase(Locale.US);
757 }
758
759 public boolean outranks(Affiliation affiliation) {
760 return rank > affiliation.rank;
761 }
762
763 public boolean ranks(Affiliation affiliation) {
764 return rank >= affiliation.rank;
765 }
766 }
767
768 public enum Role {
769 MODERATOR(R.string.moderator, 3),
770 VISITOR(R.string.visitor, 1),
771 PARTICIPANT(R.string.participant, 2),
772 NONE(R.string.no_role, 0);
773
774 private final int resId;
775 private final int rank;
776
777 Role(int resId, int rank) {
778 this.resId = resId;
779 this.rank = rank;
780 }
781
782 public static Role of(@Nullable String value) {
783 if (value == null) {
784 return NONE;
785 }
786 try {
787 return Role.valueOf(value.toUpperCase(Locale.US));
788 } catch (IllegalArgumentException e) {
789 return NONE;
790 }
791 }
792
793 public int getResId() {
794 return resId;
795 }
796
797 @Override
798 public String toString() {
799 return name().toLowerCase(Locale.US);
800 }
801
802 public boolean ranks(Role role) {
803 return rank >= role.rank;
804 }
805 }
806
807 public enum Error {
808 NO_RESPONSE,
809 SERVER_NOT_FOUND,
810 REMOTE_SERVER_TIMEOUT,
811 NONE,
812 NICK_IN_USE,
813 PASSWORD_REQUIRED,
814 BANNED,
815 MEMBERS_ONLY,
816 RESOURCE_CONSTRAINT,
817 KICKED,
818 SHUTDOWN,
819 DESTROYED,
820 INVALID_NICK,
821 TECHNICAL_PROBLEMS,
822 UNKNOWN,
823 NON_ANONYMOUS
824 }
825
826 private interface OnEventListener {
827 void onSuccess();
828
829 void onFailure();
830 }
831
832 public interface OnRenameListener extends OnEventListener {
833
834 }
835
836 public static class Hat implements Comparable<Hat> {
837 private final Uri uri;
838 private final String title;
839
840 public Hat(final Element el) {
841 Uri parseUri = null; // null hat uri is invaild per spec
842 try {
843 parseUri = Uri.parse(el.getAttribute("uri"));
844 } catch (final Exception e) { }
845 uri = parseUri;
846
847 title = el.getAttribute("title");
848 }
849
850 public Hat(final Uri uri, final String title) {
851 this.uri = uri;
852 this.title = title;
853 }
854
855 public String toString() {
856 return title == null ? "" : title;
857 }
858
859 public int getColor() {
860 return UIHelper.getColorForName(uri == null ? toString() : uri.toString());
861 }
862
863 @Override
864 public int compareTo(@NonNull Hat another) {
865 return toString().compareTo(another.toString());
866 }
867 }
868
869 public static class User implements Comparable<User>, AvatarService.Avatarable {
870 private Role role = Role.NONE;
871 private Affiliation affiliation = Affiliation.NONE;
872 private Jid realJid;
873 private Jid fullJid;
874 protected String nick;
875 private long pgpKeyId = 0;
876 protected Avatar avatar;
877 private final MucOptions options;
878 private ChatState chatState = Config.DEFAULT_CHAT_STATE;
879 protected Set<Hat> hats;
880 protected String occupantId;
881 protected boolean online = true;
882
883 public User(MucOptions options, Jid fullJid, final String occupantId, final String nick, final Set<Hat> hats) {
884 this.options = options;
885 this.fullJid = fullJid;
886 this.occupantId = occupantId;
887 this.nick = nick;
888 this.hats = hats;
889
890 if (occupantId != null && options != null) {
891 final var sha1sum = options.getConversation().getAttribute("occupantAvatar/" + occupantId);
892 if (sha1sum != null) {
893 avatar = new Avatar();
894 avatar.sha1sum = sha1sum;
895 avatar.owner = fullJid;
896 }
897
898 if (nick == null) {
899 this.nick = options.getConversation().getAttribute("occupantNick/" + occupantId);
900 } else if (!getNick().equals(getName())) {
901 options.getConversation().setAttribute("occupantNick/" + occupantId, nick);
902 } else {
903 options.getConversation().setAttribute("occupantNick/" + occupantId, (String) null);
904 }
905 }
906 }
907
908 public String getName() {
909 return fullJid == null ? null : fullJid.getResource();
910 }
911
912 public Jid getMuc() {
913 return fullJid == null ? (options.getConversation().getJid().asBareJid()) : fullJid.asBareJid();
914 }
915
916 public String getOccupantId() {
917 return occupantId;
918 }
919
920 public String getNick() {
921 return nick == null ? getName() : nick;
922 }
923
924 public void setOnline(final boolean o) {
925 online = o;
926 }
927
928 public boolean isOnline() {
929 return fullJid != null && online;
930 }
931
932 public Role getRole() {
933 return this.role;
934 }
935
936 public void setRole(String role) {
937 this.role = Role.of(role);
938 }
939
940 public Affiliation getAffiliation() {
941 return this.affiliation;
942 }
943
944 public void setAffiliation(String affiliation) {
945 this.affiliation = Affiliation.of(affiliation);
946 }
947
948 public Set<Hat> getHats() {
949 return this.hats == null ? new HashSet<>() : hats;
950 }
951
952 public List<MucOptions.Hat> getPseudoHats(Context context) {
953 List<MucOptions.Hat> hats = new ArrayList<>();
954 if (getAffiliation() != MucOptions.Affiliation.NONE) {
955 hats.add(new MucOptions.Hat(null, context.getString(getAffiliation().getResId())));
956 }
957 if (getRole() != MucOptions.Role.PARTICIPANT) {
958 hats.add(new MucOptions.Hat(null, context.getString(getRole().getResId())));
959 }
960 return hats;
961 }
962
963 public long getPgpKeyId() {
964 if (this.pgpKeyId != 0) {
965 return this.pgpKeyId;
966 } else if (realJid != null) {
967 return getAccount().getRoster().getContact(realJid).getPgpKeyId();
968 } else {
969 return 0;
970 }
971 }
972
973 public void setPgpKeyId(long id) {
974 this.pgpKeyId = id;
975 }
976
977 public Contact getContact() {
978 if (fullJid != null) {
979 return getAccount().getRoster().getContactFromContactList(realJid);
980 } else if (realJid != null) {
981 return getAccount().getRoster().getContact(realJid);
982 } else {
983 return null;
984 }
985 }
986
987 public boolean setAvatar(Avatar avatar) {
988 if (occupantId != null) {
989 options.getConversation().setAttribute("occupantAvatar/" + occupantId, getContact() == null && avatar != null ? avatar.sha1sum : null);
990 }
991 if (this.avatar != null && this.avatar.equals(avatar)) {
992 return false;
993 } else {
994 this.avatar = avatar;
995 return true;
996 }
997 }
998
999 public String getAvatar() {
1000 if (avatar != null) {
1001 return avatar.getFilename();
1002 }
1003 Avatar avatar = realJid != null ? getAccount().getRoster().getContact(realJid).getAvatar() : null;
1004 return avatar == null ? null : avatar.getFilename();
1005 }
1006
1007 public Cid getAvatarCid() {
1008 if (avatar != null) {
1009 return avatar.cid();
1010 }
1011 Avatar avatar = realJid != null ? getAccount().getRoster().getContact(realJid).getAvatar() : null;
1012 return avatar == null ? null : avatar.cid();
1013 }
1014
1015 public Account getAccount() {
1016 return options.getAccount();
1017 }
1018
1019 public Conversation getConversation() {
1020 return options.getConversation();
1021 }
1022
1023 public Jid getFullJid() {
1024 return fullJid;
1025 }
1026
1027 @Override
1028 public boolean equals(Object o) {
1029 if (this == o) return true;
1030 if (o == null || getClass() != o.getClass()) return false;
1031
1032 User user = (User) o;
1033
1034 if (role != user.role) return false;
1035 if (affiliation != user.affiliation) return false;
1036 if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null)
1037 return false;
1038 return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null;
1039
1040 }
1041
1042 public boolean isDomain() {
1043 return realJid != null && realJid.getLocal() == null && role == Role.NONE;
1044 }
1045
1046 @Override
1047 public int hashCode() {
1048 int result = role != null ? role.hashCode() : 0;
1049 result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
1050 result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
1051 result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
1052 return result;
1053 }
1054
1055 @Override
1056 public String toString() {
1057 return "[fulljid:" + fullJid + ",realjid:" + realJid + ",nick:" + nick + ",affiliation" + affiliation.toString() + "]";
1058 }
1059
1060 public boolean realJidMatchesAccount() {
1061 return realJid != null && realJid.equals(options.account.getJid().asBareJid());
1062 }
1063
1064 @Override
1065 public int compareTo(@NonNull User another) {
1066 final var anotherPseudoId = another.getOccupantId() != null && another.getOccupantId().charAt(0) == '\0';
1067 final var pseudoId = getOccupantId() != null && getOccupantId().charAt(0) == '\0';
1068 if (anotherPseudoId && !pseudoId) {
1069 return 1;
1070 }
1071 if (pseudoId && !anotherPseudoId) {
1072 return -1;
1073 }
1074 if (another.getAffiliation().outranks(getAffiliation())) {
1075 return 1;
1076 } else if (getAffiliation().outranks(another.getAffiliation())) {
1077 return -1;
1078 } else {
1079 return getComparableName().compareToIgnoreCase(another.getComparableName());
1080 }
1081 }
1082
1083 public String getComparableName() {
1084 Contact contact = getContact();
1085 if (contact != null) {
1086 return contact.getDisplayName();
1087 } else {
1088 String name = getName();
1089 return name == null ? "" : name;
1090 }
1091 }
1092
1093 public Jid getRealJid() {
1094 return realJid;
1095 }
1096
1097 public void setRealJid(Jid jid) {
1098 this.realJid = jid != null ? jid.asBareJid() : null;
1099 }
1100
1101 public boolean setChatState(ChatState chatState) {
1102 if (this.chatState == chatState) {
1103 return false;
1104 }
1105 this.chatState = chatState;
1106 return true;
1107 }
1108
1109 @Override
1110 public int getAvatarBackgroundColor() {
1111 final String seed = realJid != null ? realJid.asBareJid().toString() : null;
1112 return UIHelper.getColorForName(seed == null ? getName() : seed);
1113 }
1114
1115 @Override
1116 public String getAvatarName() {
1117 return getConversation().getName().toString();
1118 }
1119 }
1120}