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