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