MucOptions.java

  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}