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 public String getAvatar() {
394 return account.getRoster().getContact(conversation.getJid()).getAvatar();
395 }
396
397 private void updateFormData(Data form) {
398 this.form = form;
399 }
400
401 public boolean hasFeature(String feature) {
402 return this.features.contains(feature);
403 }
404
405 public boolean canInvite() {
406 Field field = this.form.getFieldByName("muc#roomconfig_allowinvites");
407 return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
408 }
409
410 public boolean canChangeSubject() {
411 Field field = this.form.getFieldByName("muc#roomconfig_changesubject");
412 return self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
413 }
414
415 public boolean allowPm() {
416 return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_ALLOW_PM, false);
417 }
418
419 public boolean participating() {
420 return !online()
421 || self.getRole().ranks(Role.PARTICIPANT)
422 || hasFeature("muc_unmoderated");
423 }
424
425 public boolean membersOnly() {
426 return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, false);
427 }
428
429 public boolean mamSupport() {
430 return hasFeature(Namespace.MAM) || hasFeature(Namespace.MAM_LEGACY);
431 }
432
433 public boolean mamLegacy() {
434 return hasFeature(Namespace.MAM_LEGACY) && !hasFeature(Namespace.MAM);
435 }
436
437 public boolean nonanonymous() {
438 return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, false);
439 }
440
441 public boolean isPrivateAndNonAnonymous() {
442 return membersOnly() && nonanonymous();
443 }
444
445 public boolean moderated() {
446 return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MODERATED, false);
447 }
448
449 public User deleteUser(Jid jid) {
450 User user = findUserByFullJid(jid);
451 if (user != null) {
452 synchronized (users) {
453 users.remove(user);
454 boolean realJidInMuc = false;
455 for (User u : users) {
456 if (user.realJid != null && user.realJid.equals(u.realJid)) {
457 realJidInMuc = true;
458 break;
459 }
460 }
461 boolean self = user.realJid != null && user.realJid.equals(account.getJid().asBareJid());
462 if (membersOnly()
463 && nonanonymous()
464 && user.affiliation.ranks(Affiliation.MEMBER)
465 && user.realJid != null
466 && !realJidInMuc
467 && !self) {
468 user.role = Role.NONE;
469 user.avatar = null;
470 user.fullJid = null;
471 users.add(user);
472 }
473 }
474 }
475 return user;
476 }
477
478 //returns true if real jid was new;
479 public boolean updateUser(User user) {
480 User old;
481 boolean realJidFound = false;
482 if (user.fullJid == null && user.realJid != null) {
483 old = findUserByRealJid(user.realJid);
484 realJidFound = old != null;
485 if (old != null) {
486 if (old.fullJid != null) {
487 return false; //don't add. user already exists
488 } else {
489 synchronized (users) {
490 users.remove(old);
491 }
492 }
493 }
494 } else if (user.realJid != null) {
495 old = findUserByRealJid(user.realJid);
496 realJidFound = old != null;
497 synchronized (users) {
498 if (old != null && old.fullJid == null) {
499 users.remove(old);
500 }
501 }
502 }
503 old = findUserByFullJid(user.getFullJid());
504 synchronized (this.users) {
505 if (old != null) {
506 users.remove(old);
507 }
508 boolean fullJidIsSelf = isOnline && user.getFullJid() != null && user.getFullJid().equals(self.getFullJid());
509 if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER))
510 && user.getAffiliation().outranks(Affiliation.OUTCAST)
511 && !fullJidIsSelf) {
512 this.users.add(user);
513 return !realJidFound && user.realJid != null;
514 }
515 }
516 return false;
517 }
518
519 public User findUserByFullJid(Jid jid) {
520 if (jid == null) {
521 return null;
522 }
523 synchronized (users) {
524 for (User user : users) {
525 if (jid.equals(user.getFullJid())) {
526 return user;
527 }
528 }
529 }
530 return null;
531 }
532
533 public User findUserByRealJid(Jid jid) {
534 if (jid == null) {
535 return null;
536 }
537 synchronized (users) {
538 for (User user : users) {
539 if (jid.equals(user.realJid)) {
540 return user;
541 }
542 }
543 }
544 return null;
545 }
546
547 public User findUser(ReadByMarker readByMarker) {
548 if (readByMarker.getRealJid() != null) {
549 User user = findUserByRealJid(readByMarker.getRealJid().asBareJid());
550 if (user == null) {
551 user = new User(this, readByMarker.getFullJid());
552 user.setRealJid(readByMarker.getRealJid());
553 }
554 return user;
555 } else if (readByMarker.getFullJid() != null) {
556 return findUserByFullJid(readByMarker.getFullJid());
557 } else {
558 return null;
559 }
560 }
561
562 public boolean isContactInRoom(Contact contact) {
563 return findUserByRealJid(contact.getJid().asBareJid()) != null;
564 }
565
566 public boolean isUserInRoom(Jid jid) {
567 return findUserByFullJid(jid) != null;
568 }
569
570 public void setError(Error error) {
571 this.isOnline = isOnline && error == Error.NONE;
572 this.error = error;
573 }
574
575 public boolean setOnline() {
576 boolean before = this.isOnline;
577 this.isOnline = true;
578 return !before;
579 }
580
581 public ArrayList<User> getUsers() {
582 return getUsers(true);
583 }
584
585 public ArrayList<User> getUsers(boolean includeOffline) {
586 synchronized (users) {
587 if (includeOffline) {
588 return new ArrayList<>(users);
589 } else {
590 ArrayList<User> onlineUsers = new ArrayList<>();
591 for (User user : users) {
592 if (user.getRole().ranks(Role.PARTICIPANT)) {
593 onlineUsers.add(user);
594 }
595 }
596 return onlineUsers;
597 }
598 }
599 }
600
601 public ArrayList<User> getUsersWithChatState(ChatState state, int max) {
602 synchronized (users) {
603 ArrayList<User> list = new ArrayList<>();
604 for (User user : users) {
605 if (user.chatState == state) {
606 list.add(user);
607 if (list.size() >= max) {
608 break;
609 }
610 }
611 }
612 return list;
613 }
614 }
615
616 public List<User> getUsers(int max) {
617 ArrayList<User> subset = new ArrayList<>();
618 HashSet<Jid> jids = new HashSet<>();
619 jids.add(account.getJid().asBareJid());
620 synchronized (users) {
621 for (User user : users) {
622 if (user.getRealJid() == null || jids.add(user.getRealJid())) {
623 subset.add(user);
624 }
625 if (subset.size() >= max) {
626 break;
627 }
628 }
629 }
630 return subset;
631 }
632
633 public int getUserCount() {
634 synchronized (users) {
635 return users.size();
636 }
637 }
638
639 private String getProposedNick() {
640 if (conversation.getBookmark() != null
641 && conversation.getBookmark().getNick() != null
642 && !conversation.getBookmark().getNick().trim().isEmpty()) {
643 return conversation.getBookmark().getNick().trim();
644 } else if (!conversation.getJid().isBareJid()) {
645 return conversation.getJid().getResource();
646 } else {
647 return JidHelper.localPartOrFallback(account.getJid());
648 }
649 }
650
651 public String getActualNick() {
652 if (this.self.getName() != null) {
653 return this.self.getName();
654 } else {
655 return this.getProposedNick();
656 }
657 }
658
659 public boolean online() {
660 return this.isOnline;
661 }
662
663 public Error getError() {
664 return this.error;
665 }
666
667 public void setOnRenameListener(OnRenameListener listener) {
668 this.onRenameListener = listener;
669 }
670
671 public void setOffline() {
672 synchronized (users) {
673 this.users.clear();
674 }
675 this.error = Error.NO_RESPONSE;
676 this.isOnline = false;
677 }
678
679 public User getSelf() {
680 return self;
681 }
682
683 public boolean setSubject(String subject) {
684 return this.conversation.setAttribute("subject", subject);
685 }
686
687 public String getSubject() {
688 return this.conversation.getAttribute("subject");
689 }
690
691 public List<User> getFallbackUsersFromCryptoTargets() {
692 List<User> users = new ArrayList<>();
693 for (Jid jid : conversation.getAcceptedCryptoTargets()) {
694 User user = new User(this, null);
695 user.setRealJid(jid);
696 users.add(user);
697 }
698 return users;
699 }
700
701 public List<User> getUsersRelevantForNameAndAvatar() {
702 final List<User> users;
703 if (isOnline) {
704 users = getUsers(5);
705 } else {
706 users = getFallbackUsersFromCryptoTargets();
707 }
708 return users;
709 }
710
711 public String createNameFromParticipants() {
712 List<User> users = getUsersRelevantForNameAndAvatar();
713 if (users.size() >= 2) {
714 StringBuilder builder = new StringBuilder();
715 for (User user : users) {
716 if (builder.length() != 0) {
717 builder.append(", ");
718 }
719 String name = UIHelper.getDisplayName(user);
720 if (name != null) {
721 builder.append(name.split("\\s+")[0]);
722 }
723 }
724 return builder.toString();
725 } else {
726 return null;
727 }
728 }
729
730 public long[] getPgpKeyIds() {
731 List<Long> ids = new ArrayList<>();
732 for (User user : this.users) {
733 if (user.getPgpKeyId() != 0) {
734 ids.add(user.getPgpKeyId());
735 }
736 }
737 ids.add(account.getPgpId());
738 long[] primitiveLongArray = new long[ids.size()];
739 for (int i = 0; i < ids.size(); ++i) {
740 primitiveLongArray[i] = ids.get(i);
741 }
742 return primitiveLongArray;
743 }
744
745 public boolean pgpKeysInUse() {
746 synchronized (users) {
747 for (User user : users) {
748 if (user.getPgpKeyId() != 0) {
749 return true;
750 }
751 }
752 }
753 return false;
754 }
755
756 public boolean everybodyHasKeys() {
757 synchronized (users) {
758 for (User user : users) {
759 if (user.getPgpKeyId() == 0) {
760 return false;
761 }
762 }
763 }
764 return true;
765 }
766
767 public Jid createJoinJid(String nick) {
768 try {
769 return Jid.of(this.conversation.getJid().asBareJid().toString() + "/" + nick);
770 } catch (final IllegalArgumentException e) {
771 return null;
772 }
773 }
774
775 public Jid getTrueCounterpart(Jid jid) {
776 if (jid.equals(getSelf().getFullJid())) {
777 return account.getJid().asBareJid();
778 }
779 User user = findUserByFullJid(jid);
780 return user == null ? null : user.realJid;
781 }
782
783 public String getPassword() {
784 this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
785 if (this.password == null && conversation.getBookmark() != null
786 && conversation.getBookmark().getPassword() != null) {
787 return conversation.getBookmark().getPassword();
788 } else {
789 return this.password;
790 }
791 }
792
793 public void setPassword(String password) {
794 if (conversation.getBookmark() != null) {
795 conversation.getBookmark().setPassword(password);
796 } else {
797 this.password = password;
798 }
799 conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
800 }
801
802 public Conversation getConversation() {
803 return this.conversation;
804 }
805
806 public List<Jid> getMembers() {
807 ArrayList<Jid> members = new ArrayList<>();
808 synchronized (users) {
809 for (User user : users) {
810 if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null) {
811 members.add(user.realJid);
812 }
813 }
814 }
815 return members;
816 }
817}