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.xml.Namespace;
14import eu.siacs.conversations.xmpp.chatstate.ChatState;
15import eu.siacs.conversations.xmpp.forms.Data;
16import eu.siacs.conversations.xmpp.forms.Field;
17import eu.siacs.conversations.xmpp.jid.InvalidJidException;
18import eu.siacs.conversations.xmpp.jid.Jid;
19import eu.siacs.conversations.xmpp.pep.Avatar;
20
21@SuppressLint("DefaultLocale")
22public class MucOptions {
23
24 private static List<String> LOCALPART_BLACKLIST = Arrays.asList("xmpp","jabber");
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 Jid getFullJid() {
285 return fullJid;
286 }
287
288 @Override
289 public boolean equals(Object o) {
290 if (this == o) return true;
291 if (o == null || getClass() != o.getClass()) return false;
292
293 User user = (User) o;
294
295 if (role != user.role) return false;
296 if (affiliation != user.affiliation) return false;
297 if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null)
298 return false;
299 return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null;
300
301 }
302
303 @Override
304 public int hashCode() {
305 int result = role != null ? role.hashCode() : 0;
306 result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
307 result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
308 result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
309 return result;
310 }
311
312 @Override
313 public String toString() {
314 return "[fulljid:"+String.valueOf(fullJid)+",realjid:"+String.valueOf(realJid)+",affiliation"+affiliation.toString()+"]";
315 }
316
317 public boolean realJidMatchesAccount() {
318 return realJid != null && realJid.equals(options.account.getJid().toBareJid());
319 }
320
321 @Override
322 public int compareTo(User another) {
323 if (another.getAffiliation().outranks(getAffiliation())) {
324 return 1;
325 } else if (getAffiliation().outranks(another.getAffiliation())) {
326 return -1;
327 } else {
328 return getComparableName().compareToIgnoreCase(another.getComparableName());
329 }
330 }
331
332
333 private String getComparableName() {
334 Contact contact = getContact();
335 if (contact != null) {
336 return contact.getDisplayName();
337 } else {
338 String name = getName();
339 return name == null ? "" : name;
340 }
341 }
342
343 public Jid getRealJid() {
344 return realJid;
345 }
346
347 public boolean setChatState(ChatState chatState) {
348 if (this.chatState == chatState) {
349 return false;
350 }
351 this.chatState = chatState;
352 return true;
353 }
354 }
355
356 private Account account;
357 private final Set<User> users = new HashSet<>();
358 private final List<String> features = new ArrayList<>();
359 private Data form = new Data();
360 private Conversation conversation;
361 private boolean isOnline = false;
362 private Error error = Error.NONE;
363 public OnRenameListener onRenameListener = null;
364 private User self;
365 private String subject = null;
366 private String password = null;
367
368 public MucOptions(Conversation conversation) {
369 this.account = conversation.getAccount();
370 this.conversation = conversation;
371 this.self = new User(this,createJoinJid(getProposedNick()));
372 }
373
374 public void updateFeatures(ArrayList<String> features) {
375 this.features.clear();
376 this.features.addAll(features);
377 }
378
379 public void updateFormData(Data form) {
380 this.form = form;
381 }
382
383 public boolean hasFeature(String feature) {
384 return this.features.contains(feature);
385 }
386
387 public boolean canInvite() {
388 Field field = this.form.getFieldByName("muc#roomconfig_allowinvites");
389 return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
390 }
391
392 public boolean canChangeSubject() {
393 Field field = this.form.getFieldByName("muc#roomconfig_changesubject");
394 return self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
395 }
396
397 public boolean participating() {
398 return !online()
399 || self.getRole().ranks(Role.PARTICIPANT)
400 || hasFeature("muc_unmoderated");
401 }
402
403 public boolean membersOnly() {
404 return hasFeature("muc_membersonly");
405 }
406
407 public boolean mamSupport() {
408 return hasFeature(Namespace.MAM) || hasFeature(Namespace.MAM_LEGACY);
409 }
410
411 public boolean mamLegacy() {
412 return hasFeature(Namespace.MAM_LEGACY) && !hasFeature(Namespace.MAM);
413 }
414
415 public boolean nonanonymous() {
416 return hasFeature("muc_nonanonymous");
417 }
418
419 public boolean persistent() {
420 return hasFeature("muc_persistent");
421 }
422
423 public boolean moderated() {
424 return hasFeature("muc_moderated");
425 }
426
427 public User deleteUser(Jid jid) {
428 User user = findUserByFullJid(jid);
429 if (user != null) {
430 synchronized (users) {
431 users.remove(user);
432 boolean realJidInMuc = false;
433 for (User u : users) {
434 if (user.realJid != null && user.realJid.equals(u.realJid)) {
435 realJidInMuc = true;
436 break;
437 }
438 }
439 boolean self = user.realJid != null && user.realJid.equals(account.getJid().toBareJid());
440 if (membersOnly()
441 && nonanonymous()
442 && user.affiliation.ranks(Affiliation.MEMBER)
443 && user.realJid != null
444 && !realJidInMuc
445 && !self) {
446 user.role = Role.NONE;
447 user.avatar = null;
448 user.fullJid = null;
449 users.add(user);
450 }
451 }
452 }
453 return user;
454 }
455
456 //returns true if real jid was new;
457 public boolean updateUser(User user) {
458 User old;
459 boolean realJidFound = false;
460 if (user.fullJid == null && user.realJid != null) {
461 old = findUserByRealJid(user.realJid);
462 realJidFound = old != null;
463 if (old != null) {
464 if (old.fullJid != null) {
465 return false; //don't add. user already exists
466 } else {
467 synchronized (users) {
468 users.remove(old);
469 }
470 }
471 }
472 } else if (user.realJid != null) {
473 old = findUserByRealJid(user.realJid);
474 realJidFound = old != null;
475 synchronized (users) {
476 if (old != null && old.fullJid == null) {
477 users.remove(old);
478 }
479 }
480 }
481 old = findUserByFullJid(user.getFullJid());
482 synchronized (this.users) {
483 if (old != null) {
484 users.remove(old);
485 }
486 boolean fullJidIsSelf = isOnline && user.getFullJid() != null && user.getFullJid().equals(self.getFullJid());
487 if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER))
488 && user.getAffiliation().outranks(Affiliation.OUTCAST)
489 && !fullJidIsSelf){
490 this.users.add(user);
491 return !realJidFound && user.realJid != null;
492 }
493 }
494 return false;
495 }
496
497 public User findUserByFullJid(Jid jid) {
498 if (jid == null) {
499 return null;
500 }
501 synchronized (users) {
502 for (User user : users) {
503 if (jid.equals(user.getFullJid())) {
504 return user;
505 }
506 }
507 }
508 return null;
509 }
510
511 private User findUserByRealJid(Jid jid) {
512 if (jid == null) {
513 return null;
514 }
515 synchronized (users) {
516 for (User user : users) {
517 if (jid.equals(user.realJid)) {
518 return user;
519 }
520 }
521 }
522 return null;
523 }
524
525 public boolean isContactInRoom(Contact contact) {
526 return findUserByRealJid(contact.getJid().toBareJid()) != null;
527 }
528
529 public boolean isUserInRoom(Jid jid) {
530 return findUserByFullJid(jid) != null;
531 }
532
533 public void setError(Error error) {
534 this.isOnline = isOnline && error == Error.NONE;
535 this.error = error;
536 }
537
538 public void setOnline() {
539 this.isOnline = true;
540 }
541
542 public ArrayList<User> getUsers() {
543 return getUsers(true);
544 }
545
546 public ArrayList<User> getUsers(boolean includeOffline) {
547 synchronized (users) {
548 if (includeOffline) {
549 return new ArrayList<>(users);
550 } else {
551 ArrayList<User> onlineUsers = new ArrayList<>();
552 for (User user : users) {
553 if (user.getRole().ranks(Role.PARTICIPANT)) {
554 onlineUsers.add(user);
555 }
556 }
557 return onlineUsers;
558 }
559 }
560 }
561
562 public ArrayList<User> getUsersWithChatState(ChatState state, int max) {
563 synchronized (users) {
564 ArrayList<User> list = new ArrayList<>();
565 for(User user : users) {
566 if (user.chatState == state) {
567 list.add(user);
568 if (list.size() >= max) {
569 break;
570 }
571 }
572 }
573 return list;
574 }
575 }
576
577 public List<User> getUsers(int max) {
578 ArrayList<User> subset = new ArrayList<>();
579 HashSet<Jid> jids = new HashSet<>();
580 jids.add(account.getJid().toBareJid());
581 synchronized (users) {
582 for(User user : users) {
583 if (user.getRealJid() == null || jids.add(user.getRealJid())) {
584 subset.add(user);
585 }
586 if (subset.size() >= max) {
587 break;
588 }
589 }
590 }
591 return subset;
592 }
593
594 public int getUserCount() {
595 synchronized (users) {
596 return users.size();
597 }
598 }
599
600 private String getProposedNick() {
601 if (conversation.getBookmark() != null
602 && conversation.getBookmark().getNick() != null
603 && !conversation.getBookmark().getNick().trim().isEmpty()) {
604 return conversation.getBookmark().getNick().trim();
605 } else if (!conversation.getJid().isBareJid()) {
606 return conversation.getJid().getResourcepart();
607 } else {
608 Jid jid = account.getJid();
609 if (LOCALPART_BLACKLIST.contains(jid.getLocalpart())) {
610 final String domain = jid.getDomainpart();
611 final int index = domain.lastIndexOf('.');
612 return index > 1 ? domain.substring(0,index) : domain;
613 } else {
614 return jid.getLocalpart();
615 }
616 }
617 }
618
619 public String getActualNick() {
620 if (this.self.getName() != null) {
621 return this.self.getName();
622 } else {
623 return this.getProposedNick();
624 }
625 }
626
627 public boolean online() {
628 return this.isOnline;
629 }
630
631 public Error getError() {
632 return this.error;
633 }
634
635 public void setOnRenameListener(OnRenameListener listener) {
636 this.onRenameListener = listener;
637 }
638
639 public void setOffline() {
640 synchronized (users) {
641 this.users.clear();
642 }
643 this.error = Error.NO_RESPONSE;
644 this.isOnline = false;
645 }
646
647 public User getSelf() {
648 return self;
649 }
650
651 public void setSubject(String content) {
652 this.subject = content;
653 }
654
655 public String getSubject() {
656 return this.subject;
657 }
658
659 public String createNameFromParticipants() {
660 if (getUserCount() >= 2) {
661 StringBuilder builder = new StringBuilder();
662 for (User user : getUsers(5)) {
663 if (builder.length() != 0) {
664 builder.append(", ");
665 }
666 Contact contact = user.getContact();
667 if (contact != null && !contact.getDisplayName().isEmpty()) {
668 builder.append(contact.getDisplayName().split("\\s+")[0]);
669 } else {
670 final String name = user.getName();
671 final Jid jid = user.getRealJid();
672 if (name != null){
673 builder.append(name.split("\\s+")[0]);
674 } else if (jid != null) {
675 builder.append(jid.getLocalpart());
676 }
677 }
678 }
679 return builder.toString();
680 } else {
681 return null;
682 }
683 }
684
685 public long[] getPgpKeyIds() {
686 List<Long> ids = new ArrayList<>();
687 for (User user : this.users) {
688 if (user.getPgpKeyId() != 0) {
689 ids.add(user.getPgpKeyId());
690 }
691 }
692 ids.add(account.getPgpId());
693 long[] primitiveLongArray = new long[ids.size()];
694 for (int i = 0; i < ids.size(); ++i) {
695 primitiveLongArray[i] = ids.get(i);
696 }
697 return primitiveLongArray;
698 }
699
700 public boolean pgpKeysInUse() {
701 synchronized (users) {
702 for (User user : users) {
703 if (user.getPgpKeyId() != 0) {
704 return true;
705 }
706 }
707 }
708 return false;
709 }
710
711 public boolean everybodyHasKeys() {
712 synchronized (users) {
713 for (User user : users) {
714 if (user.getPgpKeyId() == 0) {
715 return false;
716 }
717 }
718 }
719 return true;
720 }
721
722 public Jid createJoinJid(String nick) {
723 try {
724 return Jid.fromString(this.conversation.getJid().toBareJid().toString() + "/" + nick);
725 } catch (final InvalidJidException e) {
726 return null;
727 }
728 }
729
730 public Jid getTrueCounterpart(Jid jid) {
731 if (jid.equals(getSelf().getFullJid())) {
732 return account.getJid().toBareJid();
733 }
734 User user = findUserByFullJid(jid);
735 return user == null ? null : user.realJid;
736 }
737
738 public String getPassword() {
739 this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
740 if (this.password == null && conversation.getBookmark() != null
741 && conversation.getBookmark().getPassword() != null) {
742 return conversation.getBookmark().getPassword();
743 } else {
744 return this.password;
745 }
746 }
747
748 public void setPassword(String password) {
749 if (conversation.getBookmark() != null) {
750 conversation.getBookmark().setPassword(password);
751 } else {
752 this.password = password;
753 }
754 conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
755 }
756
757 public Conversation getConversation() {
758 return this.conversation;
759 }
760
761 public List<Jid> getMembers() {
762 ArrayList<Jid> members = new ArrayList<>();
763 synchronized (users) {
764 for (User user : users) {
765 if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null) {
766 members.add(user.realJid);
767 }
768 }
769 }
770 return members;
771 }
772}