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 user.setRole("visitor");
568 }
569 return user;
570 }
571
572 public User findUser(ReadByMarker readByMarker) {
573 if (readByMarker.getRealJid() != null) {
574 User user = findUserByRealJid(readByMarker.getRealJid().asBareJid());
575 if (user == null) {
576 user = new User(this, readByMarker.getFullJid());
577 user.setRealJid(readByMarker.getRealJid());
578 }
579 return user;
580 } else if (readByMarker.getFullJid() != null) {
581 return findUserByFullJid(readByMarker.getFullJid());
582 } else {
583 return null;
584 }
585 }
586
587 public boolean isContactInRoom(Contact contact) {
588 return findUserByRealJid(contact.getJid().asBareJid()) != null;
589 }
590
591 public boolean isUserInRoom(Jid jid) {
592 return findUserByFullJid(jid) != null;
593 }
594
595 public void setError(Error error) {
596 this.isOnline = isOnline && error == Error.NONE;
597 this.error = error;
598 }
599
600 public boolean setOnline() {
601 boolean before = this.isOnline;
602 this.isOnline = true;
603 return !before;
604 }
605
606 public ArrayList<User> getUsers() {
607 return getUsers(true);
608 }
609
610 public ArrayList<User> getUsers(boolean includeOffline) {
611 synchronized (users) {
612 ArrayList<User> users = new ArrayList<>();
613 for (User user : this.users) {
614 if (!user.isDomain() && (includeOffline || user.getRole().ranks(Role.PARTICIPANT))) {
615 users.add(user);
616 }
617 }
618 return users;
619 }
620 }
621
622 public ArrayList<User> getUsersWithChatState(ChatState state, int max) {
623 synchronized (users) {
624 ArrayList<User> list = new ArrayList<>();
625 for (User user : users) {
626 if (user.chatState == state) {
627 list.add(user);
628 if (list.size() >= max) {
629 break;
630 }
631 }
632 }
633 return list;
634 }
635 }
636
637 public List<User> getUsers(int max) {
638 ArrayList<User> subset = new ArrayList<>();
639 HashSet<Jid> jids = new HashSet<>();
640 jids.add(account.getJid().asBareJid());
641 synchronized (users) {
642 for (User user : users) {
643 if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
644 subset.add(user);
645 }
646 if (subset.size() >= max) {
647 break;
648 }
649 }
650 }
651 return subset;
652 }
653
654 public int getUserCount() {
655 synchronized (users) {
656 return users.size();
657 }
658 }
659
660 private String getProposedNick() {
661 if (conversation.getBookmark() != null
662 && conversation.getBookmark().getNick() != null
663 && !conversation.getBookmark().getNick().trim().isEmpty()) {
664 return conversation.getBookmark().getNick().trim();
665 } else if (!conversation.getJid().isBareJid()) {
666 return conversation.getJid().getResource();
667 } else {
668 return JidHelper.localPartOrFallback(account.getJid());
669 }
670 }
671
672 public String getActualNick() {
673 if (this.self.getName() != null) {
674 return this.self.getName();
675 } else {
676 return this.getProposedNick();
677 }
678 }
679
680 public boolean online() {
681 return this.isOnline;
682 }
683
684 public Error getError() {
685 return this.error;
686 }
687
688 public void setOnRenameListener(OnRenameListener listener) {
689 this.onRenameListener = listener;
690 }
691
692 public void setOffline() {
693 synchronized (users) {
694 this.users.clear();
695 }
696 this.error = Error.NO_RESPONSE;
697 this.isOnline = false;
698 }
699
700 public User getSelf() {
701 return self;
702 }
703
704 public boolean setSubject(String subject) {
705 return this.conversation.setAttribute("subject", subject);
706 }
707
708 public String getSubject() {
709 return this.conversation.getAttribute("subject");
710 }
711
712 public String getName() {
713 return this.conversation.getAttribute("muc_name");
714 }
715
716 private List<User> getFallbackUsersFromCryptoTargets() {
717 List<User> users = new ArrayList<>();
718 for (Jid jid : conversation.getAcceptedCryptoTargets()) {
719 User user = new User(this, null);
720 user.setRealJid(jid);
721 users.add(user);
722 }
723 return users;
724 }
725
726 public List<User> getUsersRelevantForNameAndAvatar() {
727 final List<User> users;
728 if (isOnline) {
729 users = getUsers(5);
730 } else {
731 users = getFallbackUsersFromCryptoTargets();
732 }
733 return users;
734 }
735
736 public String createNameFromParticipants() {
737 List<User> users = getUsersRelevantForNameAndAvatar();
738 if (users.size() >= 2) {
739 StringBuilder builder = new StringBuilder();
740 for (User user : users) {
741 if (builder.length() != 0) {
742 builder.append(", ");
743 }
744 String name = UIHelper.getDisplayName(user);
745 if (name != null) {
746 builder.append(name.split("\\s+")[0]);
747 }
748 }
749 return builder.toString();
750 } else {
751 return null;
752 }
753 }
754
755 public long[] getPgpKeyIds() {
756 List<Long> ids = new ArrayList<>();
757 for (User user : this.users) {
758 if (user.getPgpKeyId() != 0) {
759 ids.add(user.getPgpKeyId());
760 }
761 }
762 ids.add(account.getPgpId());
763 long[] primitiveLongArray = new long[ids.size()];
764 for (int i = 0; i < ids.size(); ++i) {
765 primitiveLongArray[i] = ids.get(i);
766 }
767 return primitiveLongArray;
768 }
769
770 public boolean pgpKeysInUse() {
771 synchronized (users) {
772 for (User user : users) {
773 if (user.getPgpKeyId() != 0) {
774 return true;
775 }
776 }
777 }
778 return false;
779 }
780
781 public boolean everybodyHasKeys() {
782 synchronized (users) {
783 for (User user : users) {
784 if (user.getPgpKeyId() == 0) {
785 return false;
786 }
787 }
788 }
789 return true;
790 }
791
792 public Jid createJoinJid(String nick) {
793 try {
794 return Jid.of(this.conversation.getJid().asBareJid().toString() + "/" + nick);
795 } catch (final IllegalArgumentException e) {
796 return null;
797 }
798 }
799
800 public Jid getTrueCounterpart(Jid jid) {
801 if (jid.equals(getSelf().getFullJid())) {
802 return account.getJid().asBareJid();
803 }
804 User user = findUserByFullJid(jid);
805 return user == null ? null : user.realJid;
806 }
807
808 public String getPassword() {
809 this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
810 if (this.password == null && conversation.getBookmark() != null
811 && conversation.getBookmark().getPassword() != null) {
812 return conversation.getBookmark().getPassword();
813 } else {
814 return this.password;
815 }
816 }
817
818 public void setPassword(String password) {
819 if (conversation.getBookmark() != null) {
820 conversation.getBookmark().setPassword(password);
821 } else {
822 this.password = password;
823 }
824 conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
825 }
826
827 public Conversation getConversation() {
828 return this.conversation;
829 }
830
831 public List<Jid> getMembers(final boolean includeDomains) {
832 ArrayList<Jid> members = new ArrayList<>();
833 synchronized (users) {
834 for (User user : users) {
835 if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null && (!user.isDomain() || includeDomains)) {
836 members.add(user.realJid);
837 }
838 }
839 }
840 return members;
841 }
842}