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