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