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