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