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 final var subset = new ArrayList<User>();
572 final var addresses = new HashSet<Jid>();
573 for (final var user : users) {
574 addresses.add(user.getAccount().getJid().asBareJid());
575 final var address = user.getRealJid();
576 if (address == null || (address.getLocal() != null && addresses.add(address))) {
577 subset.add(user);
578 }
579 if (subset.size() >= max) {
580 return subset;
581 }
582 }
583 return subset;
584 }
585
586 public int getUserCount() {
587 synchronized (users) {
588 return users.size();
589 }
590 }
591
592 private String getProposedNick() {
593 return getProposedNick(null);
594 }
595
596 private String getProposedNick(final String mucNick) {
597 final Bookmark bookmark = this.conversation.getBookmark();
598 if (bookmark != null) {
599 // if we already have a bookmark we consider this the source of truth
600 return getProposedNickPure();
601 }
602 final var storedJid = conversation.getJid();
603 if (mucNick != null) {
604 return mucNick;
605 } else if (storedJid.isBareJid()) {
606 return defaultNick(account);
607 } else {
608 return storedJid.getResource();
609 }
610 }
611
612 public String getProposedNickPure() {
613 final Bookmark bookmark = this.conversation.getBookmark();
614 final String bookmarkedNick =
615 normalize(account.getJid(), bookmark == null ? null : bookmark.getNick());
616 if (bookmarkedNick != null) {
617 return bookmarkedNick;
618 } else {
619 return defaultNick(account);
620 }
621 }
622
623 public static String defaultNick(final Account account) {
624 final String displayName = normalize(account.getJid(), account.getDisplayName());
625 if (displayName == null) {
626 return JidHelper.localPartOrFallback(account.getJid());
627 } else {
628 return displayName;
629 }
630 }
631
632 private static String normalize(final Jid account, final String nick) {
633 if (account == null || Strings.isNullOrEmpty(nick)) {
634 return null;
635 }
636
637 try {
638 return account.withResource(nick).getResource();
639 } catch (final IllegalArgumentException e) {
640 return null;
641 }
642 }
643
644 public String getActualNick() {
645 if (this.self.getNick() != null) {
646 return this.self.getNick();
647 } else {
648 return this.getProposedNick();
649 }
650 }
651
652 public String getActualName() {
653 if (this.self.getName() != null) {
654 return this.self.getName();
655 } else {
656 return this.getProposedNick();
657 }
658 }
659
660 public boolean online() {
661 return this.isOnline;
662 }
663
664 public Error getError() {
665 return this.error;
666 }
667
668 public void setError(Error error) {
669 this.isOnline = isOnline && error == Error.NONE;
670 this.error = error;
671 }
672
673 public void setOnRenameListener(OnRenameListener listener) {
674 this.onRenameListener = listener;
675 }
676
677 public void setOffline() {
678 synchronized (users) {
679 this.users.clear();
680 }
681 this.error = Error.NO_RESPONSE;
682 this.isOnline = false;
683 }
684
685 public User getSelf() {
686 return self;
687 }
688
689 public boolean setSubject(String subject) {
690 return this.conversation.setAttribute("subject", subject);
691 }
692
693 public String getSubject() {
694 return this.conversation.getAttribute("subject");
695 }
696
697 public String getName() {
698 return this.conversation.getAttribute("muc_name");
699 }
700
701 private List<User> getFallbackUsersFromCryptoTargets() {
702 List<User> users = new ArrayList<>();
703 for (Jid jid : conversation.getAcceptedCryptoTargets()) {
704 User user = new User(this, null, null, null, new HashSet<>());
705 user.setRealJid(jid);
706 users.add(user);
707 }
708 return users;
709 }
710
711 public List<User> getUsersRelevantForNameAndAvatar() {
712 final List<User> users;
713 if (isOnline) {
714 users = getUsers(5);
715 } else {
716 users = getFallbackUsersFromCryptoTargets();
717 }
718 return users;
719 }
720
721 String createNameFromParticipants() {
722 List<User> users = getUsersRelevantForNameAndAvatar();
723 if (users.size() >= 2) {
724 StringBuilder builder = new StringBuilder();
725 for (User user : users) {
726 if (builder.length() != 0) {
727 builder.append(", ");
728 }
729 String name = UIHelper.getDisplayName(user);
730 if (name != null) {
731 builder.append(name.split("\\s+")[0]);
732 }
733 }
734 return builder.toString();
735 } else {
736 return null;
737 }
738 }
739
740 public long[] getPgpKeyIds() {
741 List<Long> ids = new ArrayList<>();
742 for (User user : this.users) {
743 if (user.getPgpKeyId() != 0) {
744 ids.add(user.getPgpKeyId());
745 }
746 }
747 ids.add(account.getPgpId());
748 long[] primitiveLongArray = new long[ids.size()];
749 for (int i = 0; i < ids.size(); ++i) {
750 primitiveLongArray[i] = ids.get(i);
751 }
752 return primitiveLongArray;
753 }
754
755 public boolean pgpKeysInUse() {
756 synchronized (users) {
757 for (User user : users) {
758 if (user.getPgpKeyId() != 0) {
759 return true;
760 }
761 }
762 }
763 return false;
764 }
765
766 public boolean everybodyHasKeys() {
767 synchronized (users) {
768 for (User user : users) {
769 if (user.getPgpKeyId() == 0) {
770 return false;
771 }
772 }
773 }
774 return true;
775 }
776
777 public Jid createJoinJid(String nick) {
778 return createJoinJid(nick, true);
779 }
780
781 private Jid createJoinJid(String nick, boolean tryFix) {
782 try {
783 return conversation.getJid().withResource(nick);
784 } catch (final IllegalArgumentException e) {
785 try {
786 return tryFix ? createJoinJid(gnu.inet.encoding.Punycode.encode(nick), false) : null;
787 } catch (final Exception e2) {
788 return null;
789 }
790 }
791 }
792
793 public Jid getTrueCounterpart(Jid jid) {
794 if (jid.equals(getSelf().getFullJid())) {
795 return account.getJid().asBareJid();
796 }
797 User user = findUserByFullJid(jid);
798 return user == null ? null : user.realJid;
799 }
800
801 public String getPassword() {
802 this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
803 if (this.password == null
804 && conversation.getBookmark() != null
805 && conversation.getBookmark().getPassword() != null) {
806 return conversation.getBookmark().getPassword();
807 } else {
808 return this.password;
809 }
810 }
811
812 public void setPassword(String password) {
813 if (conversation.getBookmark() != null) {
814 conversation.getBookmark().setPassword(password);
815 } else {
816 this.password = password;
817 }
818 conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
819 }
820
821 public Conversation getConversation() {
822 return this.conversation;
823 }
824
825 public List<Jid> getMembers(final boolean includeDomains) {
826 ArrayList<Jid> members = new ArrayList<>();
827 synchronized (users) {
828 for (User user : users) {
829 if (user.ranks(Affiliation.MEMBER)
830 && user.realJid != null
831 && !user.realJid
832 .asBareJid()
833 .equals(conversation.account.getJid().asBareJid())
834 && (!user.isDomain() || includeDomains)) {
835 members.add(user.realJid);
836 }
837 }
838 }
839 return members;
840 }
841
842 public enum Error {
843 NO_RESPONSE,
844 SERVER_NOT_FOUND,
845 REMOTE_SERVER_TIMEOUT,
846 NONE,
847 NICK_IN_USE,
848 PASSWORD_REQUIRED,
849 BANNED,
850 MEMBERS_ONLY,
851 RESOURCE_CONSTRAINT,
852 KICKED,
853 SHUTDOWN,
854 DESTROYED,
855 INVALID_NICK,
856 TECHNICAL_PROBLEMS,
857 UNKNOWN,
858 NON_ANONYMOUS
859 }
860
861 private interface OnEventListener {
862 void onSuccess();
863
864 void onFailure();
865 }
866
867 public interface OnRenameListener extends OnEventListener {}
868
869 public static class Hat implements Comparable<Hat> {
870 private final Uri uri;
871 private final String title;
872
873 public Hat(final Element el) {
874 Uri parseUri = null; // null hat uri is invaild per spec
875 try {
876 parseUri = Uri.parse(el.getAttribute("uri"));
877 } catch (final Exception e) { }
878 uri = parseUri;
879
880 title = el.getAttribute("title");
881 }
882
883 public Hat(final Uri uri, final String title) {
884 this.uri = uri;
885 this.title = title;
886 }
887
888 public String toString() {
889 return title == null ? "" : title;
890 }
891
892 public int getColor() {
893 return UIHelper.getColorForName(uri == null ? toString() : uri.toString());
894 }
895
896 @Override
897 public int compareTo(@NonNull Hat another) {
898 return toString().compareTo(another.toString());
899 }
900 }
901
902 public static class User implements Comparable<User>, AvatarService.Avatarable {
903 private Role role = Role.NONE;
904 private Affiliation affiliation = Affiliation.NONE;
905 private Jid realJid;
906 private Jid fullJid;
907 protected String nick;
908 private long pgpKeyId = 0;
909 private String avatar;
910 private final MucOptions options;
911 private ChatState chatState = Config.DEFAULT_CHAT_STATE;
912 protected Set<Hat> hats;
913 protected String occupantId;
914 protected boolean online = true;
915
916 public User(final MucOptions options, Jid fullJid, final String occupantId, final String nick, final Set<Hat> hats) {
917 this.options = options;
918 this.fullJid = fullJid;
919 this.occupantId = occupantId;
920 this.nick = nick;
921 this.hats = hats;
922
923 if (occupantId != null && options != null) {
924 avatar = options.getConversation().getAttribute("occupantAvatar/" + occupantId);
925
926 if (nick == null) {
927 this.nick = options.getConversation().getAttribute("occupantNick/" + occupantId);
928 } else if (!getNick().equals(getName())) {
929 options.getConversation().setAttribute("occupantNick/" + occupantId, nick);
930 } else {
931 options.getConversation().setAttribute("occupantNick/" + occupantId, (String) null);
932 }
933 }
934 }
935
936 public String getName() {
937 return fullJid == null ? null : fullJid.getResource();
938 }
939
940 public Jid getMuc() {
941 return fullJid == null ? (options.getConversation().getJid().asBareJid()) : fullJid.asBareJid();
942 }
943
944 public String getNick() {
945 return nick == null ? getName() : nick;
946 }
947
948 public void setOnline(final boolean o) {
949 online = o;
950 }
951
952 public boolean isOnline() {
953 return fullJid != null && online;
954 }
955
956 public Role getRole() {
957 return this.role;
958 }
959
960 public void setRole(final Role role) {
961 this.role = role;
962 }
963
964 public Affiliation getAffiliation() {
965 return this.affiliation;
966 }
967
968 public void setAffiliation(final Affiliation affiliation) {
969 this.affiliation = affiliation;
970 }
971
972 public Set<Hat> getHats() {
973 return this.hats == null ? new HashSet<>() : hats;
974 }
975
976 public List<MucOptions.Hat> getPseudoHats(Context context) {
977 List<MucOptions.Hat> hats = new ArrayList<>();
978 if (getAffiliation() != Affiliation.NONE) {
979 hats.add(new MucOptions.Hat(null, context.getString(ConferenceDetailsActivity.affiliationToStringRes(getAffiliation()))));
980 }
981 if (getRole() != Role.PARTICIPANT) {
982 hats.add(new MucOptions.Hat(null, context.getString(ConferenceDetailsActivity.roleToStringRes(getRole()))));
983 }
984 return hats;
985 }
986
987 public long getPgpKeyId() {
988 if (this.pgpKeyId != 0) {
989 return this.pgpKeyId;
990 } else if (realJid != null) {
991 return getAccount().getRoster().getContact(realJid).getPgpKeyId();
992 } else {
993 return 0;
994 }
995 }
996
997 public void setPgpKeyId(long id) {
998 this.pgpKeyId = id;
999 }
1000
1001 public Contact getContact() {
1002 if (fullJid != null) {
1003 return realJid == null
1004 ? null
1005 : getAccount().getRoster().getContactFromContactList(realJid);
1006 } else if (realJid != null) {
1007 return getAccount().getRoster().getContact(realJid);
1008 } else {
1009 return null;
1010 }
1011 }
1012
1013 public boolean setAvatar(final String avatar) {
1014 if (occupantId != null) {
1015 options.getConversation().setAttribute("occupantAvatar/" + occupantId, getContact() == null && avatar != null ? avatar : null);
1016 }
1017 if (this.avatar != null && this.avatar.equals(avatar)) {
1018 return false;
1019 } else {
1020 this.avatar = avatar;
1021 return true;
1022 }
1023 }
1024
1025 public String getAvatar() {
1026
1027 // TODO prefer potentially better quality avatars from contact
1028 // TODO use getContact and if that’s not null and avatar is set use that
1029
1030 getContact();
1031
1032 if (avatar != null) {
1033 return avatar;
1034 }
1035 if (realJid == null) {
1036 return null;
1037 }
1038 final var contact = getAccount().getRoster().getContact(realJid);
1039 return contact.getAvatar();
1040 }
1041
1042 public Cid getAvatarCid() {
1043 final var sha1 = getAvatar();
1044 if (sha1 == null) return null;
1045 try {
1046 return CryptoHelper.cid(CryptoHelper.hexToBytes(sha1), "sha-1");
1047 } catch (NoSuchAlgorithmException e) {
1048 Log.e(Config.LOGTAG, "" + e);
1049 return null;
1050 }
1051 }
1052
1053 public Account getAccount() {
1054 return options.getAccount();
1055 }
1056
1057 public MucOptions getMucOptions() {
1058 return this.options;
1059 }
1060
1061 public Conversation getConversation() {
1062 return options.getConversation();
1063 }
1064
1065 public Jid getFullJid() {
1066 return fullJid;
1067 }
1068
1069 @Override
1070 public boolean equals(Object o) {
1071 if (this == o) return true;
1072 if (o == null || getClass() != o.getClass()) return false;
1073
1074 User user = (User) o;
1075
1076 if (role != user.role) return false;
1077 if (affiliation != user.affiliation) return false;
1078 if (!Objects.equals(realJid, user.realJid)) return false;
1079 return Objects.equals(fullJid, user.fullJid);
1080 }
1081
1082 public boolean isDomain() {
1083 return realJid != null && realJid.getLocal() == null && role == Role.NONE;
1084 }
1085
1086 @Override
1087 public int hashCode() {
1088 int result = role != null ? role.hashCode() : 0;
1089 result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
1090 result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
1091 result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
1092 return result;
1093 }
1094
1095 @Override
1096 public String toString() {
1097 return "[fulljid:"
1098 + fullJid
1099 + ",realjid:"
1100 + realJid
1101 + ",affiliation"
1102 + affiliation.toString()
1103 + "]";
1104 }
1105
1106 public boolean realJidMatchesAccount() {
1107 return realJid != null && realJid.equals(options.account.getJid().asBareJid());
1108 }
1109
1110 @Override
1111 public int compareTo(@NonNull User another) {
1112 final var anotherPseudoId = another.getOccupantId() != null && another.getOccupantId().charAt(0) == '\0';
1113 final var pseudoId = getOccupantId() != null && getOccupantId().charAt(0) == '\0';
1114 if (anotherPseudoId && !pseudoId) {
1115 return 1;
1116 }
1117 if (pseudoId && !anotherPseudoId) {
1118 return -1;
1119 }
1120 if (another.outranks(getAffiliation())) {
1121 return 1;
1122 } else if (outranks(another.getAffiliation())) {
1123 return -1;
1124 } else {
1125 return getComparableName().compareToIgnoreCase(another.getComparableName());
1126 }
1127 }
1128
1129 public String getComparableName() {
1130 Contact contact = getContact();
1131 if (contact != null) {
1132 return contact.getDisplayName();
1133 } else {
1134 String name = getName();
1135 return name == null ? "" : name;
1136 }
1137 }
1138
1139 public Jid getRealJid() {
1140 return realJid;
1141 }
1142
1143 public void setRealJid(Jid jid) {
1144 this.realJid = jid != null ? jid.asBareJid() : null;
1145 }
1146
1147 public boolean setChatState(ChatState chatState) {
1148 if (this.chatState == chatState) {
1149 return false;
1150 }
1151 this.chatState = chatState;
1152 return true;
1153 }
1154
1155 @Override
1156 public int getAvatarBackgroundColor() {
1157 final String seed = realJid != null ? realJid.asBareJid().toString() : null;
1158 return UIHelper.getColorForName(seed == null ? getName() : seed);
1159 }
1160
1161 @Override
1162 public String getAvatarName() {
1163 return getConversation().getName().toString();
1164 }
1165
1166 public void setOccupantId(final String occupantId) {
1167 this.occupantId = occupantId;
1168 }
1169
1170 public String getOccupantId() {
1171 return this.occupantId;
1172 }
1173
1174 public boolean ranks(final Role role) {
1175 return ROLE_RANKS.getInt(this.role) >= ROLE_RANKS.getInt(role);
1176 }
1177
1178 public boolean ranks(final Affiliation affiliation) {
1179 return AFFILIATION_RANKS.getInt(this.affiliation)
1180 >= AFFILIATION_RANKS.getInt(affiliation);
1181 }
1182
1183 public boolean outranks(final Affiliation affiliation) {
1184 return AFFILIATION_RANKS.getInt(this.affiliation)
1185 > AFFILIATION_RANKS.getInt(affiliation);
1186 }
1187 }
1188}