1package eu.siacs.conversations.entities;
2
3import android.annotation.SuppressLint;
4
5import java.util.ArrayList;
6import java.util.List;
7import java.util.concurrent.CopyOnWriteArrayList;
8
9import eu.siacs.conversations.R;
10import eu.siacs.conversations.crypto.PgpEngine;
11import eu.siacs.conversations.xml.Element;
12import eu.siacs.conversations.xmpp.jid.InvalidJidException;
13import eu.siacs.conversations.xmpp.jid.Jid;
14import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
15
16@SuppressLint("DefaultLocale")
17public class MucOptions {
18
19 public enum Affiliation {
20 OWNER("owner", 4, R.string.owner),
21 ADMIN("admin", 3, R.string.admin),
22 MEMBER("member", 2, R.string.member),
23 OUTCAST("outcast", 0, R.string.outcast),
24 NONE("none", 1, R.string.no_affiliation);
25
26 Affiliation(String string, int rank, int resId) {
27 this.string = string;
28 this.resId = resId;
29 this.rank = rank;
30 }
31
32 private String string;
33 private int resId;
34 private int rank;
35
36 public int getResId() {
37 return resId;
38 }
39
40 @Override
41 public String toString() {
42 return this.string;
43 }
44
45 public boolean outranks(Affiliation affiliation) {
46 return rank > affiliation.rank;
47 }
48
49 public boolean ranks(Affiliation affiliation) {
50 return rank >= affiliation.rank;
51 }
52 }
53
54 public enum Role {
55 MODERATOR("moderator", R.string.moderator,3),
56 VISITOR("visitor", R.string.visitor,1),
57 PARTICIPANT("participant", R.string.participant,2),
58 NONE("none", R.string.no_role,0);
59
60 private Role(String string, int resId, int rank) {
61 this.string = string;
62 this.resId = resId;
63 this.rank = rank;
64 }
65
66 private String string;
67 private int resId;
68 private int rank;
69
70 public int getResId() {
71 return resId;
72 }
73
74 @Override
75 public String toString() {
76 return this.string;
77 }
78
79 public boolean ranks(Role role) {
80 return rank >= role.rank;
81 }
82 }
83
84 public static final int ERROR_NO_ERROR = 0;
85 public static final int ERROR_NICK_IN_USE = 1;
86 public static final int ERROR_UNKNOWN = 2;
87 public static final int ERROR_PASSWORD_REQUIRED = 3;
88 public static final int ERROR_BANNED = 4;
89 public static final int ERROR_MEMBERS_ONLY = 5;
90
91 public static final int KICKED_FROM_ROOM = 9;
92
93 public static final String STATUS_CODE_ROOM_CONFIG_CHANGED = "104";
94 public static final String STATUS_CODE_SELF_PRESENCE = "110";
95 public static final String STATUS_CODE_BANNED = "301";
96 public static final String STATUS_CODE_CHANGED_NICK = "303";
97 public static final String STATUS_CODE_KICKED = "307";
98 public static final String STATUS_CODE_LOST_MEMBERSHIP = "321";
99
100 private interface OnEventListener {
101 public void onSuccess();
102
103 public void onFailure();
104 }
105
106 public interface OnRenameListener extends OnEventListener {
107
108 }
109
110 public interface OnJoinListener extends OnEventListener {
111
112 }
113
114 public class User {
115 private Role role = Role.NONE;
116 private Affiliation affiliation = Affiliation.NONE;
117 private String name;
118 private Jid jid;
119 private long pgpKeyId = 0;
120
121 public String getName() {
122 return name;
123 }
124
125 public void setName(String user) {
126 this.name = user;
127 }
128
129 public void setJid(Jid jid) {
130 this.jid = jid;
131 }
132
133 public Jid getJid() {
134 return this.jid;
135 }
136
137 public Role getRole() {
138 return this.role;
139 }
140
141 public void setRole(String role) {
142 role = role.toLowerCase();
143 switch (role) {
144 case "moderator":
145 this.role = Role.MODERATOR;
146 break;
147 case "participant":
148 this.role = Role.PARTICIPANT;
149 break;
150 case "visitor":
151 this.role = Role.VISITOR;
152 break;
153 default:
154 this.role = Role.NONE;
155 break;
156 }
157 }
158
159 @Override
160 public boolean equals(Object other) {
161 if (this == other) {
162 return true;
163 } else if (!(other instanceof User)) {
164 return false;
165 } else {
166 User o = (User) other;
167 return name != null && name.equals(o.name)
168 && jid != null && jid.equals(o.jid)
169 && affiliation == o.affiliation
170 && role == o.role;
171 }
172 }
173
174 public Affiliation getAffiliation() {
175 return this.affiliation;
176 }
177
178 public void setAffiliation(String affiliation) {
179 affiliation = affiliation.toLowerCase();
180 switch (affiliation) {
181 case "admin":
182 this.affiliation = Affiliation.ADMIN;
183 break;
184 case "owner":
185 this.affiliation = Affiliation.OWNER;
186 break;
187 case "member":
188 this.affiliation = Affiliation.MEMBER;
189 break;
190 case "outcast":
191 this.affiliation = Affiliation.OUTCAST;
192 break;
193 default:
194 this.affiliation = Affiliation.NONE;
195 }
196 }
197
198 public void setPgpKeyId(long id) {
199 this.pgpKeyId = id;
200 }
201
202 public long getPgpKeyId() {
203 return this.pgpKeyId;
204 }
205
206 public Contact getContact() {
207 return account.getRoster().getContactFromRoster(getJid());
208 }
209 }
210
211 private Account account;
212 private List<User> users = new CopyOnWriteArrayList<>();
213 private List<String> features = new ArrayList<>();
214 private Conversation conversation;
215 private boolean isOnline = false;
216 private int error = ERROR_UNKNOWN;
217 private OnRenameListener onRenameListener = null;
218 private OnJoinListener onJoinListener = null;
219 private User self = new User();
220 private String subject = null;
221 private String password = null;
222 private boolean mNickChangingInProgress = false;
223
224 public MucOptions(Conversation conversation) {
225 this.account = conversation.getAccount();
226 this.conversation = conversation;
227 }
228
229 public void updateFeatures(ArrayList<String> features) {
230 this.features.clear();
231 this.features.addAll(features);
232 }
233
234 public boolean hasFeature(String feature) {
235 return this.features.contains(feature);
236 }
237
238 public boolean canInvite() {
239 return !membersOnly() || self.getAffiliation().ranks(Affiliation.ADMIN);
240 }
241
242 public boolean participating() {
243 return !online() || self.getRole().ranks(Role.PARTICIPANT);
244 }
245
246 public boolean membersOnly() {
247 return hasFeature("muc_membersonly");
248 }
249
250 public boolean nonanonymous() {
251 return hasFeature("muc_nonanonymous");
252 }
253
254 public boolean persistent() {
255 return hasFeature("muc_persistent");
256 }
257
258 public boolean moderated() {
259 return hasFeature("muc_moderated");
260 }
261
262 public void deleteUser(String name) {
263 for (int i = 0; i < users.size(); ++i) {
264 if (users.get(i).getName().equals(name)) {
265 users.remove(i);
266 return;
267 }
268 }
269 }
270
271 public void addUser(User user) {
272 for (int i = 0; i < users.size(); ++i) {
273 if (users.get(i).getName().equals(user.getName())) {
274 users.set(i, user);
275 return;
276 }
277 }
278 users.add(user);
279 }
280
281 public boolean isUserInRoom(String name) {
282 for (int i = 0; i < users.size(); ++i) {
283 if (users.get(i).getName().equals(name)) {
284 return true;
285 }
286 }
287 return false;
288 }
289
290 public void processPacket(PresencePacket packet, PgpEngine pgp) {
291 final Jid from = packet.getFrom();
292 if (!from.isBareJid()) {
293 final String name = from.getResourcepart();
294 final String type = packet.getAttribute("type");
295 final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user");
296 final List<String> codes = getStatusCodes(x);
297 if (type == null) {
298 User user = new User();
299 if (x != null) {
300 Element item = x.findChild("item");
301 if (item != null && name != null) {
302 user.setName(name);
303 user.setAffiliation(item.getAttribute("affiliation"));
304 user.setRole(item.getAttribute("role"));
305 user.setJid(item.getAttributeAsJid("jid"));
306 if (codes.contains(STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(this.conversation.getJid())) {
307 this.isOnline = true;
308 this.error = ERROR_NO_ERROR;
309 self = user;
310 if (mNickChangingInProgress) {
311 onRenameListener.onSuccess();
312 mNickChangingInProgress = false;
313 } else if (this.onJoinListener != null) {
314 this.onJoinListener.onSuccess();
315 this.onJoinListener = null;
316 }
317 } else {
318 addUser(user);
319 }
320 if (pgp != null) {
321 Element signed = packet.findChild("x", "jabber:x:signed");
322 if (signed != null) {
323 Element status = packet.findChild("status");
324 String msg;
325 if (status != null) {
326 msg = status.getContent();
327 } else {
328 msg = "";
329 }
330 user.setPgpKeyId(pgp.fetchKeyId(account, msg,
331 signed.getContent()));
332 }
333 }
334 }
335 }
336 } else if (type.equals("unavailable")) {
337 if (codes.contains(STATUS_CODE_SELF_PRESENCE) ||
338 packet.getFrom().equals(this.conversation.getJid())) {
339 if (codes.contains(STATUS_CODE_CHANGED_NICK)) {
340 this.mNickChangingInProgress = true;
341 } else if (codes.contains(STATUS_CODE_KICKED)) {
342 setError(KICKED_FROM_ROOM);
343 } else if (codes.contains(STATUS_CODE_BANNED)) {
344 setError(ERROR_BANNED);
345 } else if (codes.contains(STATUS_CODE_LOST_MEMBERSHIP)) {
346 setError(ERROR_MEMBERS_ONLY);
347 } else {
348 setError(ERROR_UNKNOWN);
349 }
350 } else {
351 deleteUser(name);
352 }
353 } else if (type.equals("error")) {
354 Element error = packet.findChild("error");
355 if (error != null && error.hasChild("conflict")) {
356 if (isOnline) {
357 if (onRenameListener != null) {
358 onRenameListener.onFailure();
359 }
360 } else {
361 setError(ERROR_NICK_IN_USE);
362 }
363 } else if (error != null && error.hasChild("not-authorized")) {
364 setError(ERROR_PASSWORD_REQUIRED);
365 } else if (error != null && error.hasChild("forbidden")) {
366 setError(ERROR_BANNED);
367 } else if (error != null && error.hasChild("registration-required")) {
368 setError(ERROR_MEMBERS_ONLY);
369 }
370 }
371 }
372 }
373
374 private void setError(int error) {
375 this.isOnline = false;
376 this.error = error;
377 if (onJoinListener != null) {
378 onJoinListener.onFailure();
379 onJoinListener = null;
380 }
381 }
382
383 private List<String> getStatusCodes(Element x) {
384 List<String> codes = new ArrayList<>();
385 if (x != null) {
386 for (Element child : x.getChildren()) {
387 if (child.getName().equals("status")) {
388 String code = child.getAttribute("code");
389 if (code != null) {
390 codes.add(code);
391 }
392 }
393 }
394 }
395 return codes;
396 }
397
398 public List<User> getUsers() {
399 return this.users;
400 }
401
402 public String getProposedNick() {
403 if (conversation.getBookmark() != null
404 && conversation.getBookmark().getNick() != null
405 && !conversation.getBookmark().getNick().isEmpty()) {
406 return conversation.getBookmark().getNick();
407 } else if (!conversation.getJid().isBareJid()) {
408 return conversation.getJid().getResourcepart();
409 } else {
410 return account.getUsername();
411 }
412 }
413
414 public String getActualNick() {
415 if (this.self.getName() != null) {
416 return this.self.getName();
417 } else {
418 return this.getProposedNick();
419 }
420 }
421
422 public boolean online() {
423 return this.isOnline;
424 }
425
426 public int getError() {
427 return this.error;
428 }
429
430 public void setOnRenameListener(OnRenameListener listener) {
431 this.onRenameListener = listener;
432 }
433
434 public void setOnJoinListener(OnJoinListener listener) {
435 this.onJoinListener = listener;
436 }
437
438 public void setOffline() {
439 this.users.clear();
440 this.error = 0;
441 this.isOnline = false;
442 }
443
444 public User getSelf() {
445 return self;
446 }
447
448 public void setSubject(String content) {
449 this.subject = content;
450 }
451
452 public String getSubject() {
453 return this.subject;
454 }
455
456 public String createNameFromParticipants() {
457 if (users.size() >= 2) {
458 List<String> names = new ArrayList<String>();
459 for (User user : users) {
460 Contact contact = user.getContact();
461 if (contact != null && !contact.getDisplayName().isEmpty()) {
462 names.add(contact.getDisplayName().split("\\s+")[0]);
463 } else {
464 names.add(user.getName());
465 }
466 }
467 StringBuilder builder = new StringBuilder();
468 for (int i = 0; i < names.size(); ++i) {
469 builder.append(names.get(i));
470 if (i != names.size() - 1) {
471 builder.append(", ");
472 }
473 }
474 return builder.toString();
475 } else {
476 return null;
477 }
478 }
479
480 public long[] getPgpKeyIds() {
481 List<Long> ids = new ArrayList<>();
482 for (User user : getUsers()) {
483 if (user.getPgpKeyId() != 0) {
484 ids.add(user.getPgpKeyId());
485 }
486 }
487 long[] primitivLongArray = new long[ids.size()];
488 for (int i = 0; i < ids.size(); ++i) {
489 primitivLongArray[i] = ids.get(i);
490 }
491 return primitivLongArray;
492 }
493
494 public boolean pgpKeysInUse() {
495 for (User user : getUsers()) {
496 if (user.getPgpKeyId() != 0) {
497 return true;
498 }
499 }
500 return false;
501 }
502
503 public boolean everybodyHasKeys() {
504 for (User user : getUsers()) {
505 if (user.getPgpKeyId() == 0) {
506 return false;
507 }
508 }
509 return true;
510 }
511
512 public Jid createJoinJid(String nick) {
513 try {
514 return Jid.fromString(this.conversation.getJid().toBareJid().toString() + "/" + nick);
515 } catch (final InvalidJidException e) {
516 return null;
517 }
518 }
519
520 public Jid getTrueCounterpart(String counterpart) {
521 for (User user : this.getUsers()) {
522 if (user.getName().equals(counterpart)) {
523 return user.getJid();
524 }
525 }
526 return null;
527 }
528
529 public String getPassword() {
530 this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
531 if (this.password == null && conversation.getBookmark() != null
532 && conversation.getBookmark().getPassword() != null) {
533 return conversation.getBookmark().getPassword();
534 } else {
535 return this.password;
536 }
537 }
538
539 public void setPassword(String password) {
540 if (conversation.getBookmark() != null) {
541 conversation.getBookmark().setPassword(password);
542 } else {
543 this.password = password;
544 }
545 conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
546 }
547
548 public Conversation getConversation() {
549 return this.conversation;
550 }
551}