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 class User {
111 private Role role = Role.NONE;
112 private Affiliation affiliation = Affiliation.NONE;
113 private String name;
114 private Jid jid;
115 private long pgpKeyId = 0;
116
117 public String getName() {
118 return name;
119 }
120
121 public void setName(String user) {
122 this.name = user;
123 }
124
125 public void setJid(Jid jid) {
126 this.jid = jid;
127 }
128
129 public Jid getJid() {
130 return this.jid;
131 }
132
133 public Role getRole() {
134 return this.role;
135 }
136
137 public void setRole(String role) {
138 role = role.toLowerCase();
139 switch (role) {
140 case "moderator":
141 this.role = Role.MODERATOR;
142 break;
143 case "participant":
144 this.role = Role.PARTICIPANT;
145 break;
146 case "visitor":
147 this.role = Role.VISITOR;
148 break;
149 default:
150 this.role = Role.NONE;
151 break;
152 }
153 }
154
155 @Override
156 public boolean equals(Object other) {
157 if (this == other) {
158 return true;
159 } else if (!(other instanceof User)) {
160 return false;
161 } else {
162 User o = (User) other;
163 return name != null && name.equals(o.name)
164 && jid != null && jid.equals(o.jid)
165 && affiliation == o.affiliation
166 && role == o.role;
167 }
168 }
169
170 public Affiliation getAffiliation() {
171 return this.affiliation;
172 }
173
174 public void setAffiliation(String affiliation) {
175 affiliation = affiliation.toLowerCase();
176 switch (affiliation) {
177 case "admin":
178 this.affiliation = Affiliation.ADMIN;
179 break;
180 case "owner":
181 this.affiliation = Affiliation.OWNER;
182 break;
183 case "member":
184 this.affiliation = Affiliation.MEMBER;
185 break;
186 case "outcast":
187 this.affiliation = Affiliation.OUTCAST;
188 break;
189 default:
190 this.affiliation = Affiliation.NONE;
191 }
192 }
193
194 public void setPgpKeyId(long id) {
195 this.pgpKeyId = id;
196 }
197
198 public long getPgpKeyId() {
199 return this.pgpKeyId;
200 }
201
202 public Contact getContact() {
203 return account.getRoster().getContactFromRoster(getJid());
204 }
205 }
206
207 private Account account;
208 private List<User> users = new CopyOnWriteArrayList<>();
209 private List<String> features = new ArrayList<>();
210 private Conversation conversation;
211 private boolean isOnline = false;
212 private int error = ERROR_UNKNOWN;
213 private OnRenameListener onRenameListener = null;
214 private User self = new User();
215 private String subject = null;
216 private String password = null;
217 private boolean mNickChangingInProgress = false;
218
219 public MucOptions(Conversation conversation) {
220 this.account = conversation.getAccount();
221 this.conversation = conversation;
222 }
223
224 public void updateFeatures(ArrayList<String> features) {
225 this.features.clear();
226 this.features.addAll(features);
227 }
228
229 public boolean hasFeature(String feature) {
230 return this.features.contains(feature);
231 }
232
233 public boolean canInvite() {
234 return !membersOnly() || self.getAffiliation().ranks(Affiliation.ADMIN);
235 }
236
237 public boolean participating() {
238 return !online() || self.getRole().ranks(Role.PARTICIPANT);
239 }
240
241 public boolean membersOnly() {
242 return hasFeature("muc_membersonly");
243 }
244
245 public boolean mamSupport() {
246 // Update with "urn:xmpp:mam:1" once we support it
247 return hasFeature("urn:xmpp:mam:0");
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 if (onRenameListener != null) {
312 onRenameListener.onSuccess();
313 }
314 mNickChangingInProgress = false;
315 }
316 } else {
317 addUser(user);
318 }
319 if (pgp != null) {
320 Element signed = packet.findChild("x", "jabber:x:signed");
321 if (signed != null) {
322 Element status = packet.findChild("status");
323 String msg = status == null ? "" : status.getContent();
324 long keyId = pgp.fetchKeyId(account, msg, signed.getContent());
325 if (keyId != 0) {
326 user.setPgpKeyId(keyId);
327 }
328 }
329 }
330 }
331 }
332 } else if (type.equals("unavailable")) {
333 if (codes.contains(STATUS_CODE_SELF_PRESENCE) ||
334 packet.getFrom().equals(this.conversation.getJid())) {
335 if (codes.contains(STATUS_CODE_CHANGED_NICK)) {
336 this.mNickChangingInProgress = true;
337 } else if (codes.contains(STATUS_CODE_KICKED)) {
338 setError(KICKED_FROM_ROOM);
339 } else if (codes.contains(STATUS_CODE_BANNED)) {
340 setError(ERROR_BANNED);
341 } else if (codes.contains(STATUS_CODE_LOST_MEMBERSHIP)) {
342 setError(ERROR_MEMBERS_ONLY);
343 } else {
344 setError(ERROR_UNKNOWN);
345 }
346 } else {
347 deleteUser(name);
348 }
349 } else if (type.equals("error")) {
350 Element error = packet.findChild("error");
351 if (error != null && error.hasChild("conflict")) {
352 if (isOnline) {
353 if (onRenameListener != null) {
354 onRenameListener.onFailure();
355 }
356 } else {
357 setError(ERROR_NICK_IN_USE);
358 }
359 } else if (error != null && error.hasChild("not-authorized")) {
360 setError(ERROR_PASSWORD_REQUIRED);
361 } else if (error != null && error.hasChild("forbidden")) {
362 setError(ERROR_BANNED);
363 } else if (error != null && error.hasChild("registration-required")) {
364 setError(ERROR_MEMBERS_ONLY);
365 }
366 }
367 }
368 }
369
370 private void setError(int error) {
371 this.isOnline = false;
372 this.error = error;
373 }
374
375 private List<String> getStatusCodes(Element x) {
376 List<String> codes = new ArrayList<>();
377 if (x != null) {
378 for (Element child : x.getChildren()) {
379 if (child.getName().equals("status")) {
380 String code = child.getAttribute("code");
381 if (code != null) {
382 codes.add(code);
383 }
384 }
385 }
386 }
387 return codes;
388 }
389
390 public List<User> getUsers() {
391 return this.users;
392 }
393
394 public String getProposedNick() {
395 if (conversation.getBookmark() != null
396 && conversation.getBookmark().getNick() != null
397 && !conversation.getBookmark().getNick().isEmpty()) {
398 return conversation.getBookmark().getNick();
399 } else if (!conversation.getJid().isBareJid()) {
400 return conversation.getJid().getResourcepart();
401 } else {
402 return account.getUsername();
403 }
404 }
405
406 public String getActualNick() {
407 if (this.self.getName() != null) {
408 return this.self.getName();
409 } else {
410 return this.getProposedNick();
411 }
412 }
413
414 public boolean online() {
415 return this.isOnline;
416 }
417
418 public int getError() {
419 return this.error;
420 }
421
422 public void setOnRenameListener(OnRenameListener listener) {
423 this.onRenameListener = listener;
424 }
425
426 public void setOffline() {
427 this.users.clear();
428 this.error = 0;
429 this.isOnline = false;
430 }
431
432 public User getSelf() {
433 return self;
434 }
435
436 public void setSubject(String content) {
437 this.subject = content;
438 }
439
440 public String getSubject() {
441 return this.subject;
442 }
443
444 public String createNameFromParticipants() {
445 if (users.size() >= 2) {
446 List<String> names = new ArrayList<String>();
447 for (User user : users) {
448 Contact contact = user.getContact();
449 if (contact != null && !contact.getDisplayName().isEmpty()) {
450 names.add(contact.getDisplayName().split("\\s+")[0]);
451 } else {
452 names.add(user.getName());
453 }
454 }
455 StringBuilder builder = new StringBuilder();
456 for (int i = 0; i < names.size(); ++i) {
457 builder.append(names.get(i));
458 if (i != names.size() - 1) {
459 builder.append(", ");
460 }
461 }
462 return builder.toString();
463 } else {
464 return null;
465 }
466 }
467
468 public long[] getPgpKeyIds() {
469 List<Long> ids = new ArrayList<>();
470 for (User user : getUsers()) {
471 if (user.getPgpKeyId() != 0) {
472 ids.add(user.getPgpKeyId());
473 }
474 }
475 long[] primitivLongArray = new long[ids.size()];
476 for (int i = 0; i < ids.size(); ++i) {
477 primitivLongArray[i] = ids.get(i);
478 }
479 return primitivLongArray;
480 }
481
482 public boolean pgpKeysInUse() {
483 for (User user : getUsers()) {
484 if (user.getPgpKeyId() != 0) {
485 return true;
486 }
487 }
488 return false;
489 }
490
491 public boolean everybodyHasKeys() {
492 for (User user : getUsers()) {
493 if (user.getPgpKeyId() == 0) {
494 return false;
495 }
496 }
497 return true;
498 }
499
500 public Jid createJoinJid(String nick) {
501 try {
502 return Jid.fromString(this.conversation.getJid().toBareJid().toString() + "/" + nick);
503 } catch (final InvalidJidException e) {
504 return null;
505 }
506 }
507
508 public Jid getTrueCounterpart(String counterpart) {
509 for (User user : this.getUsers()) {
510 if (user.getName().equals(counterpart)) {
511 return user.getJid();
512 }
513 }
514 return null;
515 }
516
517 public String getPassword() {
518 this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
519 if (this.password == null && conversation.getBookmark() != null
520 && conversation.getBookmark().getPassword() != null) {
521 return conversation.getBookmark().getPassword();
522 } else {
523 return this.password;
524 }
525 }
526
527 public void setPassword(String password) {
528 if (conversation.getBookmark() != null) {
529 conversation.getBookmark().setPassword(password);
530 } else {
531 this.password = password;
532 }
533 conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
534 }
535
536 public Conversation getConversation() {
537 return this.conversation;
538 }
539}