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