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