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