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 mamSupport() {
251 // Update with "urn:xmpp:mam:1" once we support it
252 return hasFeature("urn:xmpp:mam:0");
253 }
254
255 public boolean nonanonymous() {
256 return hasFeature("muc_nonanonymous");
257 }
258
259 public boolean persistent() {
260 return hasFeature("muc_persistent");
261 }
262
263 public boolean moderated() {
264 return hasFeature("muc_moderated");
265 }
266
267 public void deleteUser(String name) {
268 for (int i = 0; i < users.size(); ++i) {
269 if (users.get(i).getName().equals(name)) {
270 users.remove(i);
271 return;
272 }
273 }
274 }
275
276 public void addUser(User user) {
277 for (int i = 0; i < users.size(); ++i) {
278 if (users.get(i).getName().equals(user.getName())) {
279 users.set(i, user);
280 return;
281 }
282 }
283 users.add(user);
284 }
285
286 public boolean isUserInRoom(String name) {
287 for (int i = 0; i < users.size(); ++i) {
288 if (users.get(i).getName().equals(name)) {
289 return true;
290 }
291 }
292 return false;
293 }
294
295 public void processPacket(PresencePacket packet, PgpEngine pgp) {
296 final Jid from = packet.getFrom();
297 if (!from.isBareJid()) {
298 final String name = from.getResourcepart();
299 final String type = packet.getAttribute("type");
300 final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user");
301 final List<String> codes = getStatusCodes(x);
302 if (type == null) {
303 User user = new User();
304 if (x != null) {
305 Element item = x.findChild("item");
306 if (item != null && name != null) {
307 user.setName(name);
308 user.setAffiliation(item.getAttribute("affiliation"));
309 user.setRole(item.getAttribute("role"));
310 user.setJid(item.getAttributeAsJid("jid"));
311 if (codes.contains(STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(this.conversation.getJid())) {
312 this.isOnline = true;
313 this.error = ERROR_NO_ERROR;
314 self = user;
315 if (mNickChangingInProgress) {
316 if (onRenameListener != null) {
317 onRenameListener.onSuccess();
318 }
319 mNickChangingInProgress = false;
320 } else if (this.onJoinListener != null) {
321 this.onJoinListener.onSuccess();
322 this.onJoinListener = null;
323 }
324 } else {
325 addUser(user);
326 }
327 if (pgp != null) {
328 Element signed = packet.findChild("x", "jabber:x:signed");
329 if (signed != null) {
330 Element status = packet.findChild("status");
331 String msg;
332 if (status != null) {
333 msg = status.getContent();
334 } else {
335 msg = "";
336 }
337 user.setPgpKeyId(pgp.fetchKeyId(account, msg,
338 signed.getContent()));
339 }
340 }
341 }
342 }
343 } else if (type.equals("unavailable")) {
344 if (codes.contains(STATUS_CODE_SELF_PRESENCE) ||
345 packet.getFrom().equals(this.conversation.getJid())) {
346 if (codes.contains(STATUS_CODE_CHANGED_NICK)) {
347 this.mNickChangingInProgress = true;
348 } else if (codes.contains(STATUS_CODE_KICKED)) {
349 setError(KICKED_FROM_ROOM);
350 } else if (codes.contains(STATUS_CODE_BANNED)) {
351 setError(ERROR_BANNED);
352 } else if (codes.contains(STATUS_CODE_LOST_MEMBERSHIP)) {
353 setError(ERROR_MEMBERS_ONLY);
354 } else {
355 setError(ERROR_UNKNOWN);
356 }
357 } else {
358 deleteUser(name);
359 }
360 } else if (type.equals("error")) {
361 Element error = packet.findChild("error");
362 if (error != null && error.hasChild("conflict")) {
363 if (isOnline) {
364 if (onRenameListener != null) {
365 onRenameListener.onFailure();
366 }
367 } else {
368 setError(ERROR_NICK_IN_USE);
369 }
370 } else if (error != null && error.hasChild("not-authorized")) {
371 setError(ERROR_PASSWORD_REQUIRED);
372 } else if (error != null && error.hasChild("forbidden")) {
373 setError(ERROR_BANNED);
374 } else if (error != null && error.hasChild("registration-required")) {
375 setError(ERROR_MEMBERS_ONLY);
376 }
377 }
378 }
379 }
380
381 private void setError(int error) {
382 this.isOnline = false;
383 this.error = error;
384 if (onJoinListener != null) {
385 onJoinListener.onFailure();
386 onJoinListener = null;
387 }
388 }
389
390 private List<String> getStatusCodes(Element x) {
391 List<String> codes = new ArrayList<>();
392 if (x != null) {
393 for (Element child : x.getChildren()) {
394 if (child.getName().equals("status")) {
395 String code = child.getAttribute("code");
396 if (code != null) {
397 codes.add(code);
398 }
399 }
400 }
401 }
402 return codes;
403 }
404
405 public List<User> getUsers() {
406 return this.users;
407 }
408
409 public String getProposedNick() {
410 if (conversation.getBookmark() != null
411 && conversation.getBookmark().getNick() != null
412 && !conversation.getBookmark().getNick().isEmpty()) {
413 return conversation.getBookmark().getNick();
414 } else if (!conversation.getJid().isBareJid()) {
415 return conversation.getJid().getResourcepart();
416 } else {
417 return account.getUsername();
418 }
419 }
420
421 public String getActualNick() {
422 if (this.self.getName() != null) {
423 return this.self.getName();
424 } else {
425 return this.getProposedNick();
426 }
427 }
428
429 public boolean online() {
430 return this.isOnline;
431 }
432
433 public int getError() {
434 return this.error;
435 }
436
437 public void setOnRenameListener(OnRenameListener listener) {
438 this.onRenameListener = listener;
439 }
440
441 public void setOnJoinListener(OnJoinListener listener) {
442 this.onJoinListener = listener;
443 }
444
445 public void setOffline() {
446 this.users.clear();
447 this.error = 0;
448 this.isOnline = false;
449 }
450
451 public User getSelf() {
452 return self;
453 }
454
455 public void setSubject(String content) {
456 this.subject = content;
457 }
458
459 public String getSubject() {
460 return this.subject;
461 }
462
463 public String createNameFromParticipants() {
464 if (users.size() >= 2) {
465 List<String> names = new ArrayList<String>();
466 for (User user : users) {
467 Contact contact = user.getContact();
468 if (contact != null && !contact.getDisplayName().isEmpty()) {
469 names.add(contact.getDisplayName().split("\\s+")[0]);
470 } else {
471 names.add(user.getName());
472 }
473 }
474 StringBuilder builder = new StringBuilder();
475 for (int i = 0; i < names.size(); ++i) {
476 builder.append(names.get(i));
477 if (i != names.size() - 1) {
478 builder.append(", ");
479 }
480 }
481 return builder.toString();
482 } else {
483 return null;
484 }
485 }
486
487 public long[] getPgpKeyIds() {
488 List<Long> ids = new ArrayList<>();
489 for (User user : getUsers()) {
490 if (user.getPgpKeyId() != 0) {
491 ids.add(user.getPgpKeyId());
492 }
493 }
494 long[] primitivLongArray = new long[ids.size()];
495 for (int i = 0; i < ids.size(); ++i) {
496 primitivLongArray[i] = ids.get(i);
497 }
498 return primitivLongArray;
499 }
500
501 public boolean pgpKeysInUse() {
502 for (User user : getUsers()) {
503 if (user.getPgpKeyId() != 0) {
504 return true;
505 }
506 }
507 return false;
508 }
509
510 public boolean everybodyHasKeys() {
511 for (User user : getUsers()) {
512 if (user.getPgpKeyId() == 0) {
513 return false;
514 }
515 }
516 return true;
517 }
518
519 public Jid createJoinJid(String nick) {
520 try {
521 return Jid.fromString(this.conversation.getJid().toBareJid().toString() + "/" + nick);
522 } catch (final InvalidJidException e) {
523 return null;
524 }
525 }
526
527 public Jid getTrueCounterpart(String counterpart) {
528 for (User user : this.getUsers()) {
529 if (user.getName().equals(counterpart)) {
530 return user.getJid();
531 }
532 }
533 return null;
534 }
535
536 public String getPassword() {
537 this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
538 if (this.password == null && conversation.getBookmark() != null
539 && conversation.getBookmark().getPassword() != null) {
540 return conversation.getBookmark().getPassword();
541 } else {
542 return this.password;
543 }
544 }
545
546 public void setPassword(String password) {
547 if (conversation.getBookmark() != null) {
548 conversation.getBookmark().setPassword(password);
549 } else {
550 this.password = password;
551 }
552 conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
553 }
554
555 public Conversation getConversation() {
556 return this.conversation;
557 }
558}