MucOptions.java

  1package eu.siacs.conversations.entities;
  2
  3import android.support.annotation.NonNull;
  4import android.support.annotation.Nullable;
  5
  6import java.util.ArrayList;
  7import java.util.Collections;
  8import java.util.HashSet;
  9import java.util.List;
 10import java.util.Locale;
 11import java.util.Set;
 12
 13import eu.siacs.conversations.Config;
 14import eu.siacs.conversations.R;
 15import eu.siacs.conversations.services.MessageArchiveService;
 16import eu.siacs.conversations.utils.JidHelper;
 17import eu.siacs.conversations.utils.UIHelper;
 18import eu.siacs.conversations.xmpp.chatstate.ChatState;
 19import eu.siacs.conversations.xmpp.forms.Data;
 20import eu.siacs.conversations.xmpp.forms.Field;
 21import eu.siacs.conversations.xmpp.pep.Avatar;
 22import rocks.xmpp.addr.Jid;
 23
 24public class MucOptions {
 25
 26	private boolean mAutoPushConfiguration = true;
 27
 28	public Account getAccount() {
 29		return this.conversation.getAccount();
 30	}
 31
 32	public boolean setSelf(User user) {
 33		this.self = user;
 34		final boolean roleChanged = this.conversation.setAttribute("role",user.role.toString());
 35		final boolean affiliationChanged = this.conversation.setAttribute("affiliation",user.affiliation.toString());
 36		return roleChanged || affiliationChanged;
 37	}
 38
 39	public void changeAffiliation(Jid jid, Affiliation affiliation) {
 40		User user = findUserByRealJid(jid);
 41		synchronized (users) {
 42			if (user != null && user.getRole() == Role.NONE) {
 43				users.remove(user);
 44				if (affiliation.ranks(Affiliation.MEMBER)) {
 45					user.affiliation = affiliation;
 46					users.add(user);
 47				}
 48			}
 49		}
 50	}
 51
 52	public void flagNoAutoPushConfiguration() {
 53		mAutoPushConfiguration = false;
 54	}
 55
 56	public boolean autoPushConfiguration() {
 57		return mAutoPushConfiguration;
 58	}
 59
 60	public boolean isSelf(Jid counterpart) {
 61		return counterpart.equals(self.getFullJid());
 62	}
 63
 64	public void resetChatState() {
 65		synchronized (users) {
 66			for (User user : users) {
 67				user.chatState = Config.DEFAULT_CHATSTATE;
 68			}
 69		}
 70	}
 71
 72	public boolean mamSupport() {
 73		return MessageArchiveService.Version.has(getFeatures());
 74	}
 75
 76	public enum Affiliation {
 77		OWNER(4, R.string.owner),
 78		ADMIN(3, R.string.admin),
 79		MEMBER(2, R.string.member),
 80		OUTCAST(0, R.string.outcast),
 81		NONE(1, R.string.no_affiliation);
 82
 83		Affiliation(int rank, int resId) {
 84			this.resId = resId;
 85			this.rank = rank;
 86		}
 87
 88		private int resId;
 89		private int rank;
 90
 91		public int getResId() {
 92			return resId;
 93		}
 94
 95		@Override
 96		public String toString() {
 97			return name().toLowerCase(Locale.US);
 98		}
 99
100		public boolean outranks(Affiliation affiliation) {
101			return rank > affiliation.rank;
102		}
103
104		public boolean ranks(Affiliation affiliation) {
105			return rank >= affiliation.rank;
106		}
107
108		public static Affiliation of(@Nullable String value) {
109			if (value == null) {
110				return NONE;
111			}
112			try {
113				return Affiliation.valueOf(value.toUpperCase(Locale.US));
114			} catch (IllegalArgumentException e) {
115				return NONE;
116			}
117		}
118	}
119
120	public enum Role {
121		MODERATOR(R.string.moderator, 3),
122		VISITOR(R.string.visitor, 1),
123		PARTICIPANT(R.string.participant, 2),
124		NONE(R.string.no_role, 0);
125
126		Role(int resId, int rank) {
127			this.resId = resId;
128			this.rank = rank;
129		}
130
131		private int resId;
132		private int rank;
133
134		public int getResId() {
135			return resId;
136		}
137
138		@Override
139		public String toString() {
140			return name().toLowerCase(Locale.US);
141		}
142
143		public boolean ranks(Role role) {
144			return rank >= role.rank;
145		}
146
147		public static Role of(@Nullable String value) {
148			if (value == null) {
149				return NONE;
150			}
151			try {
152				return Role.valueOf(value.toUpperCase(Locale.US));
153			} catch (IllegalArgumentException e) {
154				return NONE;
155			}
156		}
157	}
158
159	public enum Error {
160		NO_RESPONSE,
161		SERVER_NOT_FOUND,
162		NONE,
163		NICK_IN_USE,
164		PASSWORD_REQUIRED,
165		BANNED,
166		MEMBERS_ONLY,
167		RESOURCE_CONSTRAINT,
168		KICKED,
169		SHUTDOWN,
170		INVALID_NICK,
171		UNKNOWN
172	}
173
174	public static final String STATUS_CODE_SELF_PRESENCE = "110";
175	public static final String STATUS_CODE_ROOM_CREATED = "201";
176	public static final String STATUS_CODE_BANNED = "301";
177	public static final String STATUS_CODE_CHANGED_NICK = "303";
178	public static final String STATUS_CODE_KICKED = "307";
179	public static final String STATUS_CODE_AFFILIATION_CHANGE = "321";
180	public static final String STATUS_CODE_LOST_MEMBERSHIP = "322";
181	public static final String STATUS_CODE_SHUTDOWN = "332";
182
183	private interface OnEventListener {
184		void onSuccess();
185
186		void onFailure();
187	}
188
189	public interface OnRenameListener extends OnEventListener {
190
191	}
192
193	public static class User implements Comparable<User> {
194		private Role role = Role.NONE;
195		private Affiliation affiliation = Affiliation.NONE;
196		private Jid realJid;
197		private Jid fullJid;
198		private long pgpKeyId = 0;
199		private Avatar avatar;
200		private MucOptions options;
201		private ChatState chatState = Config.DEFAULT_CHATSTATE;
202
203		public User(MucOptions options, Jid from) {
204			this.options = options;
205			this.fullJid = from;
206		}
207
208		public String getName() {
209			return fullJid == null ? null : fullJid.getResource();
210		}
211
212		public void setRealJid(Jid jid) {
213			this.realJid = jid != null ? jid.asBareJid() : null;
214		}
215
216		public Role getRole() {
217			return this.role;
218		}
219
220		public void setRole(String role) {
221			this.role = Role.of(role);
222		}
223
224		public Affiliation getAffiliation() {
225			return this.affiliation;
226		}
227
228		public void setAffiliation(String affiliation) {
229			this.affiliation = Affiliation.of(affiliation);
230		}
231
232		public void setPgpKeyId(long id) {
233			this.pgpKeyId = id;
234		}
235
236		public long getPgpKeyId() {
237			if (this.pgpKeyId != 0) {
238				return this.pgpKeyId;
239			} else if (realJid != null) {
240				return getAccount().getRoster().getContact(realJid).getPgpKeyId();
241			} else {
242				return 0;
243			}
244		}
245
246		public Contact getContact() {
247			if (fullJid != null) {
248				return getAccount().getRoster().getContactFromRoster(realJid);
249			} else if (realJid != null) {
250				return getAccount().getRoster().getContact(realJid);
251			} else {
252				return null;
253			}
254		}
255
256		public boolean setAvatar(Avatar avatar) {
257			if (this.avatar != null && this.avatar.equals(avatar)) {
258				return false;
259			} else {
260				this.avatar = avatar;
261				return true;
262			}
263		}
264
265		public String getAvatar() {
266			return avatar == null ? null : avatar.getFilename();
267		}
268
269		public Account getAccount() {
270			return options.getAccount();
271		}
272
273		public Conversation getConversation() {
274			return options.getConversation();
275		}
276
277		public Jid getFullJid() {
278			return fullJid;
279		}
280
281		@Override
282		public boolean equals(Object o) {
283			if (this == o) return true;
284			if (o == null || getClass() != o.getClass()) return false;
285
286			User user = (User) o;
287
288			if (role != user.role) return false;
289			if (affiliation != user.affiliation) return false;
290			if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null)
291				return false;
292			return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null;
293
294		}
295
296		public boolean isDomain() {
297			return realJid != null && realJid.getLocal() == null && role == Role.NONE;
298		}
299
300		@Override
301		public int hashCode() {
302			int result = role != null ? role.hashCode() : 0;
303			result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
304			result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
305			result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
306			return result;
307		}
308
309		@Override
310		public String toString() {
311			return "[fulljid:" + String.valueOf(fullJid) + ",realjid:" + String.valueOf(realJid) + ",affiliation" + affiliation.toString() + "]";
312		}
313
314		public boolean realJidMatchesAccount() {
315			return realJid != null && realJid.equals(options.account.getJid().asBareJid());
316		}
317
318		@Override
319		public int compareTo(@NonNull User another) {
320			if (another.getAffiliation().outranks(getAffiliation())) {
321				return 1;
322			} else if (getAffiliation().outranks(another.getAffiliation())) {
323				return -1;
324			} else {
325				return getComparableName().compareToIgnoreCase(another.getComparableName());
326			}
327		}
328
329
330		private String getComparableName() {
331			Contact contact = getContact();
332			if (contact != null) {
333				return contact.getDisplayName();
334			} else {
335				String name = getName();
336				return name == null ? "" : name;
337			}
338		}
339
340		public Jid getRealJid() {
341			return realJid;
342		}
343
344		public boolean setChatState(ChatState chatState) {
345			if (this.chatState == chatState) {
346				return false;
347			}
348			this.chatState = chatState;
349			return true;
350		}
351	}
352
353	private Account account;
354	private final Set<User> users = new HashSet<>();
355	private ServiceDiscoveryResult serviceDiscoveryResult;
356	private final Conversation conversation;
357	private boolean isOnline = false;
358	private Error error = Error.NONE;
359	public OnRenameListener onRenameListener = null;
360	private User self;
361	private String password = null;
362
363	public MucOptions(Conversation conversation) {
364		this.account = conversation.getAccount();
365		this.conversation = conversation;
366		this.self = new User(this, createJoinJid(getProposedNick()));
367		this.self.affiliation = Affiliation.of(conversation.getAttribute("affiliation"));
368		this.self.role = Role.of(conversation.getAttribute("role"));
369	}
370
371	public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) {
372		this.serviceDiscoveryResult = serviceDiscoveryResult;
373		String name;
374		Field roomConfigName = getRoomInfoForm().getFieldByName("muc#roomconfig_roomname");
375		if (roomConfigName != null) {
376			name = roomConfigName.getValue();
377		} else {
378			List<ServiceDiscoveryResult.Identity> identities = serviceDiscoveryResult.getIdentities();
379			String identityName = identities.size() > 0 ? identities.get(0).getName() : null;
380			final Jid jid = conversation.getJid();
381			if (identityName != null && !identityName.equals(jid == null ? null : jid.getEscapedLocal())) {
382				name = identityName;
383			} else {
384				name = null;
385			}
386		}
387		boolean changed = conversation.setAttribute("muc_name", name);
388		changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, this.hasFeature("muc_membersonly"));
389		changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MODERATED, this.hasFeature("muc_moderated"));
390		changed |= conversation.setAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, this.hasFeature("muc_nonanonymous"));
391		return changed;
392	}
393
394
395	private Data getRoomInfoForm() {
396		final List<Data> forms = serviceDiscoveryResult == null ? Collections.emptyList() : serviceDiscoveryResult.forms;
397		return forms.size() == 0 ? new Data() : forms.get(0);
398	}
399
400	public String getAvatar() {
401		return account.getRoster().getContact(conversation.getJid()).getAvatar();
402	}
403
404	public boolean hasFeature(String feature) {
405		return this.serviceDiscoveryResult != null && this.serviceDiscoveryResult.features.contains(feature);
406	}
407
408	public boolean hasVCards() {
409	    return hasFeature("vcard-temp");
410    }
411
412	public boolean canInvite() {
413		Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowinvites");
414		return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
415	}
416
417	public boolean canChangeSubject() {
418		Field field = getRoomInfoForm().getFieldByName("muc#roominfo_changesubject");
419		return self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
420	}
421
422	public boolean allowPm() {
423		final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowpm");
424		if (field == null) {
425			return true; //fall back if field does not exists
426		}
427		if ("anyone".equals(field.getValue())) {
428			return true;
429		} else if ("participants".equals(field.getValue())) {
430			return self.getRole().ranks(Role.PARTICIPANT);
431		} else if ("moderators".equals(field.getValue())) {
432			return self.getRole().ranks(Role.MODERATOR);
433		} else {
434			return false;
435		}
436	}
437
438	public boolean participating() {
439		return self.getRole().ranks(Role.PARTICIPANT) || !moderated();
440	}
441
442	public boolean membersOnly() {
443		return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, false);
444	}
445
446
447	public List<String> getFeatures() {
448		return this.serviceDiscoveryResult != null ? this.serviceDiscoveryResult.features : Collections.emptyList();
449	}
450
451	public boolean nonanonymous() {
452		return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, false);
453	}
454
455	public boolean isPrivateAndNonAnonymous() {
456		return membersOnly() && nonanonymous();
457	}
458
459	public boolean moderated() {
460		return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MODERATED, false);
461	}
462
463	public User deleteUser(Jid jid) {
464		User user = findUserByFullJid(jid);
465		if (user != null) {
466			synchronized (users) {
467				users.remove(user);
468				boolean realJidInMuc = false;
469				for (User u : users) {
470					if (user.realJid != null && user.realJid.equals(u.realJid)) {
471						realJidInMuc = true;
472						break;
473					}
474				}
475				boolean self = user.realJid != null && user.realJid.equals(account.getJid().asBareJid());
476				if (membersOnly()
477						&& nonanonymous()
478						&& user.affiliation.ranks(Affiliation.MEMBER)
479						&& user.realJid != null
480						&& !realJidInMuc
481						&& !self) {
482					user.role = Role.NONE;
483					user.avatar = null;
484					user.fullJid = null;
485					users.add(user);
486				}
487			}
488		}
489		return user;
490	}
491
492	//returns true if real jid was new;
493	public boolean updateUser(User user) {
494		User old;
495		boolean realJidFound = false;
496		if (user.fullJid == null && user.realJid != null) {
497			old = findUserByRealJid(user.realJid);
498			realJidFound = old != null;
499			if (old != null) {
500				if (old.fullJid != null) {
501					return false; //don't add. user already exists
502				} else {
503					synchronized (users) {
504						users.remove(old);
505					}
506				}
507			}
508		} else if (user.realJid != null) {
509			old = findUserByRealJid(user.realJid);
510			realJidFound = old != null;
511			synchronized (users) {
512				if (old != null && old.fullJid == null) {
513					users.remove(old);
514				}
515			}
516		}
517		old = findUserByFullJid(user.getFullJid());
518		synchronized (this.users) {
519			if (old != null) {
520				users.remove(old);
521			}
522			boolean fullJidIsSelf = isOnline && user.getFullJid() != null && user.getFullJid().equals(self.getFullJid());
523			if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER))
524					&& user.getAffiliation().outranks(Affiliation.OUTCAST)
525					&& !fullJidIsSelf) {
526				this.users.add(user);
527				return !realJidFound && user.realJid != null;
528			}
529		}
530		return false;
531	}
532
533	public User findUserByFullJid(Jid jid) {
534		if (jid == null) {
535			return null;
536		}
537		synchronized (users) {
538			for (User user : users) {
539				if (jid.equals(user.getFullJid())) {
540					return user;
541				}
542			}
543		}
544		return null;
545	}
546
547	public User findUserByRealJid(Jid jid) {
548		if (jid == null) {
549			return null;
550		}
551		synchronized (users) {
552			for (User user : users) {
553				if (jid.equals(user.realJid)) {
554					return user;
555				}
556			}
557		}
558		return null;
559	}
560
561	public User findUser(ReadByMarker readByMarker) {
562		if (readByMarker.getRealJid() != null) {
563			User user = findUserByRealJid(readByMarker.getRealJid().asBareJid());
564			if (user == null) {
565				user = new User(this, readByMarker.getFullJid());
566				user.setRealJid(readByMarker.getRealJid());
567			}
568			return user;
569		} else if (readByMarker.getFullJid() != null) {
570			return findUserByFullJid(readByMarker.getFullJid());
571		} else {
572			return null;
573		}
574	}
575
576	public boolean isContactInRoom(Contact contact) {
577		return findUserByRealJid(contact.getJid().asBareJid()) != null;
578	}
579
580	public boolean isUserInRoom(Jid jid) {
581		return findUserByFullJid(jid) != null;
582	}
583
584	public void setError(Error error) {
585		this.isOnline = isOnline && error == Error.NONE;
586		this.error = error;
587	}
588
589	public boolean setOnline() {
590		boolean before = this.isOnline;
591		this.isOnline = true;
592		return !before;
593	}
594
595	public ArrayList<User> getUsers() {
596		return getUsers(true);
597	}
598
599	public ArrayList<User> getUsers(boolean includeOffline) {
600		synchronized (users) {
601				ArrayList<User> users = new ArrayList<>();
602				for (User user : this.users) {
603					if (!user.isDomain() && (includeOffline || user.getRole().ranks(Role.PARTICIPANT))) {
604						users.add(user);
605					}
606				}
607				return users;
608		}
609	}
610
611	public ArrayList<User> getUsersWithChatState(ChatState state, int max) {
612		synchronized (users) {
613			ArrayList<User> list = new ArrayList<>();
614			for (User user : users) {
615				if (user.chatState == state) {
616					list.add(user);
617					if (list.size() >= max) {
618						break;
619					}
620				}
621			}
622			return list;
623		}
624	}
625
626	public List<User> getUsers(int max) {
627		ArrayList<User> subset = new ArrayList<>();
628		HashSet<Jid> jids = new HashSet<>();
629		jids.add(account.getJid().asBareJid());
630		synchronized (users) {
631			for (User user : users) {
632				if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
633					subset.add(user);
634				}
635				if (subset.size() >= max) {
636					break;
637				}
638			}
639		}
640		return subset;
641	}
642
643	public int getUserCount() {
644		synchronized (users) {
645			return users.size();
646		}
647	}
648
649	private String getProposedNick() {
650		if (conversation.getBookmark() != null
651				&& conversation.getBookmark().getNick() != null
652				&& !conversation.getBookmark().getNick().trim().isEmpty()) {
653			return conversation.getBookmark().getNick().trim();
654		} else if (!conversation.getJid().isBareJid()) {
655			return conversation.getJid().getResource();
656		} else {
657			return JidHelper.localPartOrFallback(account.getJid());
658		}
659	}
660
661	public String getActualNick() {
662		if (this.self.getName() != null) {
663			return this.self.getName();
664		} else {
665			return this.getProposedNick();
666		}
667	}
668
669	public boolean online() {
670		return this.isOnline;
671	}
672
673	public Error getError() {
674		return this.error;
675	}
676
677	public void setOnRenameListener(OnRenameListener listener) {
678		this.onRenameListener = listener;
679	}
680
681	public void setOffline() {
682		synchronized (users) {
683			this.users.clear();
684		}
685		this.error = Error.NO_RESPONSE;
686		this.isOnline = false;
687	}
688
689	public User getSelf() {
690		return self;
691	}
692
693	public boolean setSubject(String subject) {
694		return this.conversation.setAttribute("subject", subject);
695	}
696
697	public String getSubject() {
698		return this.conversation.getAttribute("subject");
699	}
700
701	public String getName() {
702		return this.conversation.getAttribute("muc_name");
703	}
704
705	private List<User> getFallbackUsersFromCryptoTargets() {
706		List<User> users = new ArrayList<>();
707		for (Jid jid : conversation.getAcceptedCryptoTargets()) {
708			User user = new User(this, null);
709			user.setRealJid(jid);
710			users.add(user);
711		}
712		return users;
713	}
714
715	public List<User> getUsersRelevantForNameAndAvatar() {
716		final List<User> users;
717		if (isOnline) {
718			users = getUsers(5);
719		} else {
720			users = getFallbackUsersFromCryptoTargets();
721		}
722		return users;
723	}
724
725	public String createNameFromParticipants() {
726		List<User> users = getUsersRelevantForNameAndAvatar();
727		if (users.size() >= 2) {
728			StringBuilder builder = new StringBuilder();
729			for (User user : users) {
730				if (builder.length() != 0) {
731					builder.append(", ");
732				}
733				String name = UIHelper.getDisplayName(user);
734				if (name != null) {
735					builder.append(name.split("\\s+")[0]);
736				}
737			}
738			return builder.toString();
739		} else {
740			return null;
741		}
742	}
743
744	public long[] getPgpKeyIds() {
745		List<Long> ids = new ArrayList<>();
746		for (User user : this.users) {
747			if (user.getPgpKeyId() != 0) {
748				ids.add(user.getPgpKeyId());
749			}
750		}
751		ids.add(account.getPgpId());
752		long[] primitiveLongArray = new long[ids.size()];
753		for (int i = 0; i < ids.size(); ++i) {
754			primitiveLongArray[i] = ids.get(i);
755		}
756		return primitiveLongArray;
757	}
758
759	public boolean pgpKeysInUse() {
760		synchronized (users) {
761			for (User user : users) {
762				if (user.getPgpKeyId() != 0) {
763					return true;
764				}
765			}
766		}
767		return false;
768	}
769
770	public boolean everybodyHasKeys() {
771		synchronized (users) {
772			for (User user : users) {
773				if (user.getPgpKeyId() == 0) {
774					return false;
775				}
776			}
777		}
778		return true;
779	}
780
781	public Jid createJoinJid(String nick) {
782		try {
783			return Jid.of(this.conversation.getJid().asBareJid().toString() + "/" + nick);
784		} catch (final IllegalArgumentException e) {
785			return null;
786		}
787	}
788
789	public Jid getTrueCounterpart(Jid jid) {
790		if (jid.equals(getSelf().getFullJid())) {
791			return account.getJid().asBareJid();
792		}
793		User user = findUserByFullJid(jid);
794		return user == null ? null : user.realJid;
795	}
796
797	public String getPassword() {
798		this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
799		if (this.password == null && conversation.getBookmark() != null
800				&& conversation.getBookmark().getPassword() != null) {
801			return conversation.getBookmark().getPassword();
802		} else {
803			return this.password;
804		}
805	}
806
807	public void setPassword(String password) {
808		if (conversation.getBookmark() != null) {
809			conversation.getBookmark().setPassword(password);
810		} else {
811			this.password = password;
812		}
813		conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
814	}
815
816	public Conversation getConversation() {
817		return this.conversation;
818	}
819
820	public List<Jid> getMembers(final boolean includeDomains) {
821		ArrayList<Jid> members = new ArrayList<>();
822		synchronized (users) {
823			for (User user : users) {
824				if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null && (!user.isDomain() || includeDomains)) {
825					members.add(user.realJid);
826				}
827			}
828		}
829		return members;
830	}
831}