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