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