1package eu.siacs.conversations.entities;
2
3import android.support.annotation.NonNull;
4import android.support.annotation.Nullable;
5
6import java.util.ArrayList;
7import java.util.Collections;
8import java.util.HashSet;
9import java.util.List;
10import java.util.Locale;
11import java.util.Set;
12
13import eu.siacs.conversations.Config;
14import eu.siacs.conversations.R;
15import eu.siacs.conversations.services.MessageArchiveService;
16import eu.siacs.conversations.utils.JidHelper;
17import eu.siacs.conversations.utils.UIHelper;
18import eu.siacs.conversations.xmpp.chatstate.ChatState;
19import eu.siacs.conversations.xmpp.forms.Data;
20import eu.siacs.conversations.xmpp.forms.Field;
21import eu.siacs.conversations.xmpp.pep.Avatar;
22import rocks.xmpp.addr.Jid;
23
24public class MucOptions {
25
26 public static final String STATUS_CODE_SELF_PRESENCE = "110";
27 public static final String STATUS_CODE_ROOM_CREATED = "201";
28 public static final String STATUS_CODE_BANNED = "301";
29 public static final String STATUS_CODE_CHANGED_NICK = "303";
30 public static final String STATUS_CODE_KICKED = "307";
31 public static final String STATUS_CODE_AFFILIATION_CHANGE = "321";
32 public static final String STATUS_CODE_LOST_MEMBERSHIP = "322";
33 public static final String STATUS_CODE_SHUTDOWN = "332";
34 private final Set<User> users = new HashSet<>();
35 private final Conversation conversation;
36 public OnRenameListener onRenameListener = null;
37 private boolean mAutoPushConfiguration = true;
38 private Account account;
39 private ServiceDiscoveryResult serviceDiscoveryResult;
40 private boolean isOnline = false;
41 private Error error = Error.NONE;
42 private User self;
43 private String password = null;
44 public MucOptions(Conversation conversation) {
45 this.account = conversation.getAccount();
46 this.conversation = conversation;
47 this.self = new User(this, createJoinJid(getProposedNick()));
48 this.self.affiliation = Affiliation.of(conversation.getAttribute("affiliation"));
49 this.self.role = Role.of(conversation.getAttribute("role"));
50 }
51
52 public Account getAccount() {
53 return this.conversation.getAccount();
54 }
55
56 public boolean setSelf(User user) {
57 this.self = user;
58 final boolean roleChanged = this.conversation.setAttribute("role", user.role.toString());
59 final boolean affiliationChanged = this.conversation.setAttribute("affiliation", user.affiliation.toString());
60 return roleChanged || affiliationChanged;
61 }
62
63 public void changeAffiliation(Jid jid, Affiliation affiliation) {
64 User user = findUserByRealJid(jid);
65 synchronized (users) {
66 if (user != null && user.getRole() == Role.NONE) {
67 users.remove(user);
68 if (affiliation.ranks(Affiliation.MEMBER)) {
69 user.affiliation = affiliation;
70 users.add(user);
71 }
72 }
73 }
74 }
75
76 public void flagNoAutoPushConfiguration() {
77 mAutoPushConfiguration = false;
78 }
79
80 public boolean autoPushConfiguration() {
81 return mAutoPushConfiguration;
82 }
83
84 public boolean isSelf(Jid counterpart) {
85 return counterpart.equals(self.getFullJid());
86 }
87
88 public void resetChatState() {
89 synchronized (users) {
90 for (User user : users) {
91 user.chatState = Config.DEFAULT_CHATSTATE;
92 }
93 }
94 }
95
96 public boolean mamSupport() {
97 return MessageArchiveService.Version.has(getFeatures());
98 }
99
100 public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) {
101 this.serviceDiscoveryResult = serviceDiscoveryResult;
102 String name;
103 Field roomConfigName = getRoomInfoForm().getFieldByName("muc#roomconfig_roomname");
104 if (roomConfigName != null) {
105 name = roomConfigName.getValue();
106 } else {
107 List<ServiceDiscoveryResult.Identity> identities = serviceDiscoveryResult.getIdentities();
108 String identityName = identities.size() > 0 ? identities.get(0).getName() : null;
109 final Jid jid = conversation.getJid();
110 if (identityName != null && !identityName.equals(jid == null ? null : jid.getEscapedLocal())) {
111 name = identityName;
112 } else {
113 name = null;
114 }
115 }
116 boolean changed = conversation.setAttribute("muc_name", name);
117 changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, this.hasFeature("muc_membersonly"));
118 changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MODERATED, this.hasFeature("muc_moderated"));
119 changed |= conversation.setAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, this.hasFeature("muc_nonanonymous"));
120 return changed;
121 }
122
123 private Data getRoomInfoForm() {
124 final List<Data> forms = serviceDiscoveryResult == null ? Collections.emptyList() : serviceDiscoveryResult.forms;
125 return forms.size() == 0 ? new Data() : forms.get(0);
126 }
127
128 public String getAvatar() {
129 return account.getRoster().getContact(conversation.getJid()).getAvatarFilename();
130 }
131
132 public boolean hasFeature(String feature) {
133 return this.serviceDiscoveryResult != null && this.serviceDiscoveryResult.features.contains(feature);
134 }
135
136 public boolean hasVCards() {
137 return hasFeature("vcard-temp");
138 }
139
140 public boolean canInvite() {
141 Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowinvites");
142 return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
143 }
144
145 public boolean canChangeSubject() {
146 Field field = getRoomInfoForm().getFieldByName("muc#roominfo_changesubject");
147 return self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
148 }
149
150 public boolean allowPm() {
151 final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowpm");
152 if (field == null) {
153 return true; //fall back if field does not exists
154 }
155 if ("anyone".equals(field.getValue())) {
156 return true;
157 } else if ("participants".equals(field.getValue())) {
158 return self.getRole().ranks(Role.PARTICIPANT);
159 } else if ("moderators".equals(field.getValue())) {
160 return self.getRole().ranks(Role.MODERATOR);
161 } else {
162 return false;
163 }
164 }
165
166 public boolean participating() {
167 return self.getRole().ranks(Role.PARTICIPANT) || !moderated();
168 }
169
170 public boolean membersOnly() {
171 return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, false);
172 }
173
174 public List<String> getFeatures() {
175 return this.serviceDiscoveryResult != null ? this.serviceDiscoveryResult.features : Collections.emptyList();
176 }
177
178 public boolean nonanonymous() {
179 return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, false);
180 }
181
182 public boolean isPrivateAndNonAnonymous() {
183 return membersOnly() && nonanonymous();
184 }
185
186 public boolean moderated() {
187 return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MODERATED, false);
188 }
189
190 public User deleteUser(Jid jid) {
191 User user = findUserByFullJid(jid);
192 if (user != null) {
193 synchronized (users) {
194 users.remove(user);
195 boolean realJidInMuc = false;
196 for (User u : users) {
197 if (user.realJid != null && user.realJid.equals(u.realJid)) {
198 realJidInMuc = true;
199 break;
200 }
201 }
202 boolean self = user.realJid != null && user.realJid.equals(account.getJid().asBareJid());
203 if (membersOnly()
204 && nonanonymous()
205 && user.affiliation.ranks(Affiliation.MEMBER)
206 && user.realJid != null
207 && !realJidInMuc
208 && !self) {
209 user.role = Role.NONE;
210 user.avatar = null;
211 user.fullJid = null;
212 users.add(user);
213 }
214 }
215 }
216 return user;
217 }
218
219 //returns true if real jid was new;
220 public boolean updateUser(User user) {
221 User old;
222 boolean realJidFound = false;
223 if (user.fullJid == null && user.realJid != null) {
224 old = findUserByRealJid(user.realJid);
225 realJidFound = old != null;
226 if (old != null) {
227 if (old.fullJid != null) {
228 return false; //don't add. user already exists
229 } else {
230 synchronized (users) {
231 users.remove(old);
232 }
233 }
234 }
235 } else if (user.realJid != null) {
236 old = findUserByRealJid(user.realJid);
237 realJidFound = old != null;
238 synchronized (users) {
239 if (old != null && old.fullJid == null) {
240 users.remove(old);
241 }
242 }
243 }
244 old = findUserByFullJid(user.getFullJid());
245 synchronized (this.users) {
246 if (old != null) {
247 users.remove(old);
248 }
249 boolean fullJidIsSelf = isOnline && user.getFullJid() != null && user.getFullJid().equals(self.getFullJid());
250 if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER))
251 && user.getAffiliation().outranks(Affiliation.OUTCAST)
252 && !fullJidIsSelf) {
253 this.users.add(user);
254 return !realJidFound && user.realJid != null;
255 }
256 }
257 return false;
258 }
259
260 public User findUserByFullJid(Jid jid) {
261 if (jid == null) {
262 return null;
263 }
264 synchronized (users) {
265 for (User user : users) {
266 if (jid.equals(user.getFullJid())) {
267 return user;
268 }
269 }
270 }
271 return null;
272 }
273
274 public User findUserByRealJid(Jid jid) {
275 if (jid == null) {
276 return null;
277 }
278 synchronized (users) {
279 for (User user : users) {
280 if (jid.equals(user.realJid)) {
281 return user;
282 }
283 }
284 }
285 return null;
286 }
287
288 public User findOrCreateUserByRealJid(Jid jid, Jid fullJid) {
289 User user = findUserByRealJid(jid);
290 if (user == null) {
291 user = new User(this, fullJid);
292 user.setRealJid(jid);
293 }
294 return user;
295 }
296
297 public User findUser(ReadByMarker readByMarker) {
298 if (readByMarker.getRealJid() != null) {
299 return findOrCreateUserByRealJid(readByMarker.getRealJid().asBareJid(), readByMarker.getFullJid());
300 } else if (readByMarker.getFullJid() != null) {
301 return findUserByFullJid(readByMarker.getFullJid());
302 } else {
303 return null;
304 }
305 }
306
307 public boolean isContactInRoom(Contact contact) {
308 return findUserByRealJid(contact.getJid().asBareJid()) != null;
309 }
310
311 public boolean isUserInRoom(Jid jid) {
312 return findUserByFullJid(jid) != null;
313 }
314
315 public boolean setOnline() {
316 boolean before = this.isOnline;
317 this.isOnline = true;
318 return !before;
319 }
320
321 public ArrayList<User> getUsers() {
322 return getUsers(true);
323 }
324
325 public ArrayList<User> getUsers(boolean includeOffline) {
326 synchronized (users) {
327 ArrayList<User> users = new ArrayList<>();
328 for (User user : this.users) {
329 if (!user.isDomain() && (includeOffline || user.getRole().ranks(Role.PARTICIPANT))) {
330 users.add(user);
331 }
332 }
333 return users;
334 }
335 }
336
337 public ArrayList<User> getUsersWithChatState(ChatState state, int max) {
338 synchronized (users) {
339 ArrayList<User> list = new ArrayList<>();
340 for (User user : users) {
341 if (user.chatState == state) {
342 list.add(user);
343 if (list.size() >= max) {
344 break;
345 }
346 }
347 }
348 return list;
349 }
350 }
351
352 public List<User> getUsers(int max) {
353 ArrayList<User> subset = new ArrayList<>();
354 HashSet<Jid> jids = new HashSet<>();
355 jids.add(account.getJid().asBareJid());
356 synchronized (users) {
357 for (User user : users) {
358 if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
359 subset.add(user);
360 }
361 if (subset.size() >= max) {
362 break;
363 }
364 }
365 }
366 return subset;
367 }
368
369 public int getUserCount() {
370 synchronized (users) {
371 return users.size();
372 }
373 }
374
375 private String getProposedNick() {
376 if (conversation.getBookmark() != null
377 && conversation.getBookmark().getNick() != null
378 && !conversation.getBookmark().getNick().trim().isEmpty()) {
379 return conversation.getBookmark().getNick().trim();
380 } else if (!conversation.getJid().isBareJid()) {
381 return conversation.getJid().getResource();
382 } else {
383 return JidHelper.localPartOrFallback(account.getJid());
384 }
385 }
386
387 public String getActualNick() {
388 if (this.self.getName() != null) {
389 return this.self.getName();
390 } else {
391 return this.getProposedNick();
392 }
393 }
394
395 public boolean online() {
396 return this.isOnline;
397 }
398
399 public Error getError() {
400 return this.error;
401 }
402
403 public void setError(Error error) {
404 this.isOnline = isOnline && error == Error.NONE;
405 this.error = error;
406 }
407
408 public void setOnRenameListener(OnRenameListener listener) {
409 this.onRenameListener = listener;
410 }
411
412 public void setOffline() {
413 synchronized (users) {
414 this.users.clear();
415 }
416 this.error = Error.NO_RESPONSE;
417 this.isOnline = false;
418 }
419
420 public User getSelf() {
421 return self;
422 }
423
424 public boolean setSubject(String subject) {
425 return this.conversation.setAttribute("subject", subject);
426 }
427
428 public String getSubject() {
429 return this.conversation.getAttribute("subject");
430 }
431
432 public String getName() {
433 return this.conversation.getAttribute("muc_name");
434 }
435
436 private List<User> getFallbackUsersFromCryptoTargets() {
437 List<User> users = new ArrayList<>();
438 for (Jid jid : conversation.getAcceptedCryptoTargets()) {
439 User user = new User(this, null);
440 user.setRealJid(jid);
441 users.add(user);
442 }
443 return users;
444 }
445
446 public List<User> getUsersRelevantForNameAndAvatar() {
447 final List<User> users;
448 if (isOnline) {
449 users = getUsers(5);
450 } else {
451 users = getFallbackUsersFromCryptoTargets();
452 }
453 return users;
454 }
455
456 public String createNameFromParticipants() {
457 List<User> users = getUsersRelevantForNameAndAvatar();
458 if (users.size() >= 2) {
459 StringBuilder builder = new StringBuilder();
460 for (User user : users) {
461 if (builder.length() != 0) {
462 builder.append(", ");
463 }
464 String name = UIHelper.getDisplayName(user);
465 if (name != null) {
466 builder.append(name.split("\\s+")[0]);
467 }
468 }
469 return builder.toString();
470 } else {
471 return null;
472 }
473 }
474
475 public long[] getPgpKeyIds() {
476 List<Long> ids = new ArrayList<>();
477 for (User user : this.users) {
478 if (user.getPgpKeyId() != 0) {
479 ids.add(user.getPgpKeyId());
480 }
481 }
482 ids.add(account.getPgpId());
483 long[] primitiveLongArray = new long[ids.size()];
484 for (int i = 0; i < ids.size(); ++i) {
485 primitiveLongArray[i] = ids.get(i);
486 }
487 return primitiveLongArray;
488 }
489
490 public boolean pgpKeysInUse() {
491 synchronized (users) {
492 for (User user : users) {
493 if (user.getPgpKeyId() != 0) {
494 return true;
495 }
496 }
497 }
498 return false;
499 }
500
501 public boolean everybodyHasKeys() {
502 synchronized (users) {
503 for (User user : users) {
504 if (user.getPgpKeyId() == 0) {
505 return false;
506 }
507 }
508 }
509 return true;
510 }
511
512 public Jid createJoinJid(String nick) {
513 try {
514 return Jid.of(this.conversation.getJid().asBareJid().toString() + "/" + nick);
515 } catch (final IllegalArgumentException e) {
516 return null;
517 }
518 }
519
520 public Jid getTrueCounterpart(Jid jid) {
521 if (jid.equals(getSelf().getFullJid())) {
522 return account.getJid().asBareJid();
523 }
524 User user = findUserByFullJid(jid);
525 return user == null ? null : user.realJid;
526 }
527
528 public String getPassword() {
529 this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
530 if (this.password == null && conversation.getBookmark() != null
531 && conversation.getBookmark().getPassword() != null) {
532 return conversation.getBookmark().getPassword();
533 } else {
534 return this.password;
535 }
536 }
537
538 public void setPassword(String password) {
539 if (conversation.getBookmark() != null) {
540 conversation.getBookmark().setPassword(password);
541 } else {
542 this.password = password;
543 }
544 conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
545 }
546
547 public Conversation getConversation() {
548 return this.conversation;
549 }
550
551 public List<Jid> getMembers(final boolean includeDomains) {
552 ArrayList<Jid> members = new ArrayList<>();
553 synchronized (users) {
554 for (User user : users) {
555 if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null && (!user.isDomain() || includeDomains)) {
556 members.add(user.realJid);
557 }
558 }
559 }
560 return members;
561 }
562
563 public enum Affiliation {
564 OWNER(4, R.string.owner),
565 ADMIN(3, R.string.admin),
566 MEMBER(2, R.string.member),
567 OUTCAST(0, R.string.outcast),
568 NONE(1, R.string.no_affiliation);
569
570 private int resId;
571 private int rank;
572 Affiliation(int rank, int resId) {
573 this.resId = resId;
574 this.rank = rank;
575 }
576
577 public static Affiliation of(@Nullable String value) {
578 if (value == null) {
579 return NONE;
580 }
581 try {
582 return Affiliation.valueOf(value.toUpperCase(Locale.US));
583 } catch (IllegalArgumentException e) {
584 return NONE;
585 }
586 }
587
588 public int getResId() {
589 return resId;
590 }
591
592 @Override
593 public String toString() {
594 return name().toLowerCase(Locale.US);
595 }
596
597 public boolean outranks(Affiliation affiliation) {
598 return rank > affiliation.rank;
599 }
600
601 public boolean ranks(Affiliation affiliation) {
602 return rank >= affiliation.rank;
603 }
604 }
605
606 public enum Role {
607 MODERATOR(R.string.moderator, 3),
608 VISITOR(R.string.visitor, 1),
609 PARTICIPANT(R.string.participant, 2),
610 NONE(R.string.no_role, 0);
611
612 private int resId;
613 private int rank;
614 Role(int resId, int rank) {
615 this.resId = resId;
616 this.rank = rank;
617 }
618
619 public static Role of(@Nullable String value) {
620 if (value == null) {
621 return NONE;
622 }
623 try {
624 return Role.valueOf(value.toUpperCase(Locale.US));
625 } catch (IllegalArgumentException e) {
626 return NONE;
627 }
628 }
629
630 public int getResId() {
631 return resId;
632 }
633
634 @Override
635 public String toString() {
636 return name().toLowerCase(Locale.US);
637 }
638
639 public boolean ranks(Role role) {
640 return rank >= role.rank;
641 }
642 }
643
644 public enum Error {
645 NO_RESPONSE,
646 SERVER_NOT_FOUND,
647 REMOTE_SERVER_TIMEOUT,
648 NONE,
649 NICK_IN_USE,
650 PASSWORD_REQUIRED,
651 BANNED,
652 MEMBERS_ONLY,
653 RESOURCE_CONSTRAINT,
654 KICKED,
655 SHUTDOWN,
656 DESTROYED,
657 INVALID_NICK,
658 UNKNOWN
659 }
660
661 private interface OnEventListener {
662 void onSuccess();
663
664 void onFailure();
665 }
666
667 public interface OnRenameListener extends OnEventListener {
668
669 }
670
671 public static class User implements Comparable<User> {
672 private Role role = Role.NONE;
673 private Affiliation affiliation = Affiliation.NONE;
674 private Jid realJid;
675 private Jid fullJid;
676 private long pgpKeyId = 0;
677 private Avatar avatar;
678 private MucOptions options;
679 private ChatState chatState = Config.DEFAULT_CHATSTATE;
680
681 public User(MucOptions options, Jid fullJid) {
682 this.options = options;
683 this.fullJid = fullJid;
684 }
685
686 public String getName() {
687 return fullJid == null ? null : fullJid.getResource();
688 }
689
690 public Role getRole() {
691 return this.role;
692 }
693
694 public void setRole(String role) {
695 this.role = Role.of(role);
696 }
697
698 public Affiliation getAffiliation() {
699 return this.affiliation;
700 }
701
702 public void setAffiliation(String affiliation) {
703 this.affiliation = Affiliation.of(affiliation);
704 }
705
706 public long getPgpKeyId() {
707 if (this.pgpKeyId != 0) {
708 return this.pgpKeyId;
709 } else if (realJid != null) {
710 return getAccount().getRoster().getContact(realJid).getPgpKeyId();
711 } else {
712 return 0;
713 }
714 }
715
716 public void setPgpKeyId(long id) {
717 this.pgpKeyId = id;
718 }
719
720 public Contact getContact() {
721 if (fullJid != null) {
722 return getAccount().getRoster().getContactFromRoster(realJid);
723 } else if (realJid != null) {
724 return getAccount().getRoster().getContact(realJid);
725 } else {
726 return null;
727 }
728 }
729
730 public boolean setAvatar(Avatar avatar) {
731 if (this.avatar != null && this.avatar.equals(avatar)) {
732 return false;
733 } else {
734 this.avatar = avatar;
735 return true;
736 }
737 }
738
739 public String getAvatar() {
740 if (avatar != null) {
741 return avatar.getFilename();
742 }
743 Avatar avatar = realJid != null ? getAccount().getRoster().getContact(realJid).getAvatar() : null;
744 return avatar == null ? null : avatar.getFilename();
745 }
746
747 public Account getAccount() {
748 return options.getAccount();
749 }
750
751 public Conversation getConversation() {
752 return options.getConversation();
753 }
754
755 public Jid getFullJid() {
756 return fullJid;
757 }
758
759 @Override
760 public boolean equals(Object o) {
761 if (this == o) return true;
762 if (o == null || getClass() != o.getClass()) return false;
763
764 User user = (User) o;
765
766 if (role != user.role) return false;
767 if (affiliation != user.affiliation) return false;
768 if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null)
769 return false;
770 return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null;
771
772 }
773
774 public boolean isDomain() {
775 return realJid != null && realJid.getLocal() == null && role == Role.NONE;
776 }
777
778 @Override
779 public int hashCode() {
780 int result = role != null ? role.hashCode() : 0;
781 result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
782 result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
783 result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
784 return result;
785 }
786
787 @Override
788 public String toString() {
789 return "[fulljid:" + String.valueOf(fullJid) + ",realjid:" + String.valueOf(realJid) + ",affiliation" + affiliation.toString() + "]";
790 }
791
792 public boolean realJidMatchesAccount() {
793 return realJid != null && realJid.equals(options.account.getJid().asBareJid());
794 }
795
796 @Override
797 public int compareTo(@NonNull User another) {
798 if (another.getAffiliation().outranks(getAffiliation())) {
799 return 1;
800 } else if (getAffiliation().outranks(another.getAffiliation())) {
801 return -1;
802 } else {
803 return getComparableName().compareToIgnoreCase(another.getComparableName());
804 }
805 }
806
807 private String getComparableName() {
808 Contact contact = getContact();
809 if (contact != null) {
810 return contact.getDisplayName();
811 } else {
812 String name = getName();
813 return name == null ? "" : name;
814 }
815 }
816
817 public Jid getRealJid() {
818 return realJid;
819 }
820
821 public void setRealJid(Jid jid) {
822 this.realJid = jid != null ? jid.asBareJid() : null;
823 }
824
825 public boolean setChatState(ChatState chatState) {
826 if (this.chatState == chatState) {
827 return false;
828 }
829 this.chatState = chatState;
830 return true;
831 }
832 }
833}