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