MucOptions.java

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