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