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