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 @Override
309 public int hashCode() {
310 int result = role != null ? role.hashCode() : 0;
311 result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
312 result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
313 result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
314 return result;
315 }
316
317 @Override
318 public String toString() {
319 return "[fulljid:" + String.valueOf(fullJid) + ",realjid:" + String.valueOf(realJid) + ",affiliation" + affiliation.toString() + "]";
320 }
321
322 public boolean realJidMatchesAccount() {
323 return realJid != null && realJid.equals(options.account.getJid().asBareJid());
324 }
325
326 @Override
327 public int compareTo(@NonNull User another) {
328 if (another.getAffiliation().outranks(getAffiliation())) {
329 return 1;
330 } else if (getAffiliation().outranks(another.getAffiliation())) {
331 return -1;
332 } else {
333 return getComparableName().compareToIgnoreCase(another.getComparableName());
334 }
335 }
336
337
338 private String getComparableName() {
339 Contact contact = getContact();
340 if (contact != null) {
341 return contact.getDisplayName();
342 } else {
343 String name = getName();
344 return name == null ? "" : name;
345 }
346 }
347
348 public Jid getRealJid() {
349 return realJid;
350 }
351
352 public boolean setChatState(ChatState chatState) {
353 if (this.chatState == chatState) {
354 return false;
355 }
356 this.chatState = chatState;
357 return true;
358 }
359 }
360
361 private Account account;
362 private final Set<User> users = new HashSet<>();
363 private ServiceDiscoveryResult serviceDiscoveryResult;
364 private final Conversation conversation;
365 private boolean isOnline = false;
366 private Error error = Error.NONE;
367 public OnRenameListener onRenameListener = null;
368 private User self;
369 private String password = null;
370
371 public MucOptions(Conversation conversation) {
372 this.account = conversation.getAccount();
373 this.conversation = conversation;
374 this.self = new User(this, createJoinJid(getProposedNick()));
375 }
376
377 public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) {
378 this.serviceDiscoveryResult = serviceDiscoveryResult;
379 String name;
380 Field roomInfoName = getRoomInfoForm().getFieldByName("muc#roominfo_name");
381 if (roomInfoName != null) {
382 name = roomInfoName.getValue();
383 } else {
384 List<ServiceDiscoveryResult.Identity> identities = serviceDiscoveryResult.getIdentities();
385 String identityName = identities.size() > 0 ? identities.get(0).getName() : null;
386 if (!conversation.getJid().getEscapedLocal().equals(identityName)) {
387 name = identityName;
388 } else {
389 name = null;
390 }
391 }
392 boolean changed = conversation.setAttribute("muc_name", name);
393 changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, this.hasFeature("muc_membersonly"));
394 changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MODERATED, this.hasFeature("muc_moderated"));
395 changed |= conversation.setAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, this.hasFeature("muc_nonanonymous"));
396 return changed;
397 }
398
399
400 private Data getRoomInfoForm() {
401 final List<Data> forms = serviceDiscoveryResult == null ? Collections.emptyList() : serviceDiscoveryResult.forms;
402 return forms.size() == 0 ? new Data() : forms.get(0);
403 }
404
405 public String getAvatar() {
406 return account.getRoster().getContact(conversation.getJid()).getAvatar();
407 }
408
409 public boolean hasFeature(String feature) {
410 return this.serviceDiscoveryResult != null && this.serviceDiscoveryResult.features.contains(feature);
411 }
412
413 public boolean hasVCards() {
414 return hasFeature("vcard-temp");
415 }
416
417 public boolean canInvite() {
418 Field field = getRoomInfoForm().getFieldByName("muc#roominfo_allowinvites");
419 return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
420 }
421
422 public boolean canChangeSubject() {
423 Field field = getRoomInfoForm().getFieldByName("muc#roominfo_changesubject");
424 return self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
425 }
426
427 public boolean allowPm() {
428 Field field = getRoomInfoForm().getFieldByName("muc#roominfo_allowpm");
429 return field != null && "1".equals(field.getValue());
430 }
431
432 public boolean participating() {
433 return !online()
434 || self.getRole().ranks(Role.PARTICIPANT)
435 || hasFeature("muc_unmoderated");
436 }
437
438 public boolean membersOnly() {
439 return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, false);
440 }
441
442 public boolean mamSupport() {
443 return hasFeature(Namespace.MAM) || hasFeature(Namespace.MAM_LEGACY);
444 }
445
446 public boolean mamLegacy() {
447 return hasFeature(Namespace.MAM_LEGACY) && !hasFeature(Namespace.MAM);
448 }
449
450 public boolean nonanonymous() {
451 return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, false);
452 }
453
454 public boolean isPrivateAndNonAnonymous() {
455 return membersOnly() && nonanonymous();
456 }
457
458 public boolean moderated() {
459 return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MODERATED, false);
460 }
461
462 public User deleteUser(Jid jid) {
463 User user = findUserByFullJid(jid);
464 if (user != null) {
465 synchronized (users) {
466 users.remove(user);
467 boolean realJidInMuc = false;
468 for (User u : users) {
469 if (user.realJid != null && user.realJid.equals(u.realJid)) {
470 realJidInMuc = true;
471 break;
472 }
473 }
474 boolean self = user.realJid != null && user.realJid.equals(account.getJid().asBareJid());
475 if (membersOnly()
476 && nonanonymous()
477 && user.affiliation.ranks(Affiliation.MEMBER)
478 && user.realJid != null
479 && !realJidInMuc
480 && !self) {
481 user.role = Role.NONE;
482 user.avatar = null;
483 user.fullJid = null;
484 users.add(user);
485 }
486 }
487 }
488 return user;
489 }
490
491 //returns true if real jid was new;
492 public boolean updateUser(User user) {
493 User old;
494 boolean realJidFound = false;
495 if (user.fullJid == null && user.realJid != null) {
496 old = findUserByRealJid(user.realJid);
497 realJidFound = old != null;
498 if (old != null) {
499 if (old.fullJid != null) {
500 return false; //don't add. user already exists
501 } else {
502 synchronized (users) {
503 users.remove(old);
504 }
505 }
506 }
507 } else if (user.realJid != null) {
508 old = findUserByRealJid(user.realJid);
509 realJidFound = old != null;
510 synchronized (users) {
511 if (old != null && old.fullJid == null) {
512 users.remove(old);
513 }
514 }
515 }
516 old = findUserByFullJid(user.getFullJid());
517 synchronized (this.users) {
518 if (old != null) {
519 users.remove(old);
520 }
521 boolean fullJidIsSelf = isOnline && user.getFullJid() != null && user.getFullJid().equals(self.getFullJid());
522 if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER))
523 && user.getAffiliation().outranks(Affiliation.OUTCAST)
524 && !fullJidIsSelf) {
525 this.users.add(user);
526 return !realJidFound && user.realJid != null;
527 }
528 }
529 return false;
530 }
531
532 public User findUserByFullJid(Jid jid) {
533 if (jid == null) {
534 return null;
535 }
536 synchronized (users) {
537 for (User user : users) {
538 if (jid.equals(user.getFullJid())) {
539 return user;
540 }
541 }
542 }
543 return null;
544 }
545
546 public User findUserByRealJid(Jid jid) {
547 if (jid == null) {
548 return null;
549 }
550 synchronized (users) {
551 for (User user : users) {
552 if (jid.equals(user.realJid)) {
553 return user;
554 }
555 }
556 }
557 return null;
558 }
559
560 public User findUser(ReadByMarker readByMarker) {
561 if (readByMarker.getRealJid() != null) {
562 User user = findUserByRealJid(readByMarker.getRealJid().asBareJid());
563 if (user == null) {
564 user = new User(this, readByMarker.getFullJid());
565 user.setRealJid(readByMarker.getRealJid());
566 }
567 return user;
568 } else if (readByMarker.getFullJid() != null) {
569 return findUserByFullJid(readByMarker.getFullJid());
570 } else {
571 return null;
572 }
573 }
574
575 public boolean isContactInRoom(Contact contact) {
576 return findUserByRealJid(contact.getJid().asBareJid()) != null;
577 }
578
579 public boolean isUserInRoom(Jid jid) {
580 return findUserByFullJid(jid) != null;
581 }
582
583 public void setError(Error error) {
584 this.isOnline = isOnline && error == Error.NONE;
585 this.error = error;
586 }
587
588 public boolean setOnline() {
589 boolean before = this.isOnline;
590 this.isOnline = true;
591 return !before;
592 }
593
594 public ArrayList<User> getUsers() {
595 return getUsers(true);
596 }
597
598 public ArrayList<User> getUsers(boolean includeOffline) {
599 synchronized (users) {
600 if (includeOffline) {
601 return new ArrayList<>(users);
602 } else {
603 ArrayList<User> onlineUsers = new ArrayList<>();
604 for (User user : users) {
605 if (user.getRole().ranks(Role.PARTICIPANT)) {
606 onlineUsers.add(user);
607 }
608 }
609 return onlineUsers;
610 }
611 }
612 }
613
614 public ArrayList<User> getUsersWithChatState(ChatState state, int max) {
615 synchronized (users) {
616 ArrayList<User> list = new ArrayList<>();
617 for (User user : users) {
618 if (user.chatState == state) {
619 list.add(user);
620 if (list.size() >= max) {
621 break;
622 }
623 }
624 }
625 return list;
626 }
627 }
628
629 public List<User> getUsers(int max) {
630 ArrayList<User> subset = new ArrayList<>();
631 HashSet<Jid> jids = new HashSet<>();
632 jids.add(account.getJid().asBareJid());
633 synchronized (users) {
634 for (User user : users) {
635 if (user.getRealJid() == null || jids.add(user.getRealJid())) {
636 subset.add(user);
637 }
638 if (subset.size() >= max) {
639 break;
640 }
641 }
642 }
643 return subset;
644 }
645
646 public int getUserCount() {
647 synchronized (users) {
648 return users.size();
649 }
650 }
651
652 private String getProposedNick() {
653 if (conversation.getBookmark() != null
654 && conversation.getBookmark().getNick() != null
655 && !conversation.getBookmark().getNick().trim().isEmpty()) {
656 return conversation.getBookmark().getNick().trim();
657 } else if (!conversation.getJid().isBareJid()) {
658 return conversation.getJid().getResource();
659 } else {
660 return JidHelper.localPartOrFallback(account.getJid());
661 }
662 }
663
664 public String getActualNick() {
665 if (this.self.getName() != null) {
666 return this.self.getName();
667 } else {
668 return this.getProposedNick();
669 }
670 }
671
672 public boolean online() {
673 return this.isOnline;
674 }
675
676 public Error getError() {
677 return this.error;
678 }
679
680 public void setOnRenameListener(OnRenameListener listener) {
681 this.onRenameListener = listener;
682 }
683
684 public void setOffline() {
685 synchronized (users) {
686 this.users.clear();
687 }
688 this.error = Error.NO_RESPONSE;
689 this.isOnline = false;
690 }
691
692 public User getSelf() {
693 return self;
694 }
695
696 public boolean setSubject(String subject) {
697 return this.conversation.setAttribute("subject", subject);
698 }
699
700 public String getSubject() {
701 return this.conversation.getAttribute("subject");
702 }
703
704 public String getName() {
705 String mucName = this.conversation.getAttribute("muc_name");
706 return conversation.getJid().getEscapedLocal().equals(mucName) ? null : mucName;
707 }
708
709 private List<User> getFallbackUsersFromCryptoTargets() {
710 List<User> users = new ArrayList<>();
711 for (Jid jid : conversation.getAcceptedCryptoTargets()) {
712 User user = new User(this, null);
713 user.setRealJid(jid);
714 users.add(user);
715 }
716 return users;
717 }
718
719 public List<User> getUsersRelevantForNameAndAvatar() {
720 final List<User> users;
721 if (isOnline) {
722 users = getUsers(5);
723 } else {
724 users = getFallbackUsersFromCryptoTargets();
725 }
726 return users;
727 }
728
729 public String createNameFromParticipants() {
730 List<User> users = getUsersRelevantForNameAndAvatar();
731 if (users.size() >= 2) {
732 StringBuilder builder = new StringBuilder();
733 for (User user : users) {
734 if (builder.length() != 0) {
735 builder.append(", ");
736 }
737 String name = UIHelper.getDisplayName(user);
738 if (name != null) {
739 builder.append(name.split("\\s+")[0]);
740 }
741 }
742 return builder.toString();
743 } else {
744 return null;
745 }
746 }
747
748 public long[] getPgpKeyIds() {
749 List<Long> ids = new ArrayList<>();
750 for (User user : this.users) {
751 if (user.getPgpKeyId() != 0) {
752 ids.add(user.getPgpKeyId());
753 }
754 }
755 ids.add(account.getPgpId());
756 long[] primitiveLongArray = new long[ids.size()];
757 for (int i = 0; i < ids.size(); ++i) {
758 primitiveLongArray[i] = ids.get(i);
759 }
760 return primitiveLongArray;
761 }
762
763 public boolean pgpKeysInUse() {
764 synchronized (users) {
765 for (User user : users) {
766 if (user.getPgpKeyId() != 0) {
767 return true;
768 }
769 }
770 }
771 return false;
772 }
773
774 public boolean everybodyHasKeys() {
775 synchronized (users) {
776 for (User user : users) {
777 if (user.getPgpKeyId() == 0) {
778 return false;
779 }
780 }
781 }
782 return true;
783 }
784
785 public Jid createJoinJid(String nick) {
786 try {
787 return Jid.of(this.conversation.getJid().asBareJid().toString() + "/" + nick);
788 } catch (final IllegalArgumentException e) {
789 return null;
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 && conversation.getBookmark() != null
804 && conversation.getBookmark().getPassword() != null) {
805 return conversation.getBookmark().getPassword();
806 } else {
807 return this.password;
808 }
809 }
810
811 public void setPassword(String password) {
812 if (conversation.getBookmark() != null) {
813 conversation.getBookmark().setPassword(password);
814 } else {
815 this.password = password;
816 }
817 conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
818 }
819
820 public Conversation getConversation() {
821 return this.conversation;
822 }
823
824 public List<Jid> getMembers() {
825 ArrayList<Jid> members = new ArrayList<>();
826 synchronized (users) {
827 for (User user : users) {
828 if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null) {
829 members.add(user.realJid);
830 }
831 }
832 }
833 return members;
834 }
835}