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