Conversation.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5
  6import net.java.otr4j.OtrException;
  7import net.java.otr4j.crypto.OtrCryptoException;
  8import net.java.otr4j.session.SessionID;
  9import net.java.otr4j.session.SessionImpl;
 10import net.java.otr4j.session.SessionStatus;
 11
 12import org.json.JSONException;
 13import org.json.JSONObject;
 14
 15import java.security.interfaces.DSAPublicKey;
 16import java.util.ArrayList;
 17import java.util.Collections;
 18import java.util.Comparator;
 19import java.util.Iterator;
 20import java.util.List;
 21
 22import eu.siacs.conversations.Config;
 23import eu.siacs.conversations.xmpp.chatstate.ChatState;
 24import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 25import eu.siacs.conversations.xmpp.jid.Jid;
 26
 27public class Conversation extends AbstractEntity implements Blockable {
 28	public static final String TABLENAME = "conversations";
 29
 30	public static final int STATUS_AVAILABLE = 0;
 31	public static final int STATUS_ARCHIVED = 1;
 32	public static final int STATUS_DELETED = 2;
 33
 34	public static final int MODE_MULTI = 1;
 35	public static final int MODE_SINGLE = 0;
 36
 37	public static final String NAME = "name";
 38	public static final String ACCOUNT = "accountUuid";
 39	public static final String CONTACT = "contactUuid";
 40	public static final String CONTACTJID = "contactJid";
 41	public static final String STATUS = "status";
 42	public static final String CREATED = "created";
 43	public static final String MODE = "mode";
 44	public static final String ATTRIBUTES = "attributes";
 45
 46	public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
 47	public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
 48	public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
 49	public static final String ATTRIBUTE_LAST_MESSAGE_TRANSMITTED = "last_message_transmitted";
 50
 51	private String name;
 52	private String contactUuid;
 53	private String accountUuid;
 54	private Jid contactJid;
 55	private int status;
 56	private long created;
 57	private int mode;
 58
 59	private JSONObject attributes = new JSONObject();
 60
 61	private Jid nextCounterpart;
 62
 63	protected final ArrayList<Message> messages = new ArrayList<>();
 64	protected Account account = null;
 65
 66	private transient SessionImpl otrSession;
 67
 68	private transient String otrFingerprint = null;
 69	private Smp mSmp = new Smp();
 70
 71	private String nextMessage;
 72
 73	private transient MucOptions mucOptions = null;
 74
 75	private byte[] symmetricKey;
 76
 77	private Bookmark bookmark;
 78
 79	private boolean messagesLeftOnServer = true;
 80	private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
 81	private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
 82	private String mLastReceivedOtrMessageId = null;
 83
 84	public boolean hasMessagesLeftOnServer() {
 85		return messagesLeftOnServer;
 86	}
 87
 88	public void setHasMessagesLeftOnServer(boolean value) {
 89		this.messagesLeftOnServer = value;
 90	}
 91
 92	public Message findUnsentMessageWithUuid(String uuid) {
 93		synchronized(this.messages) {
 94			for (final Message message : this.messages) {
 95				final int s = message.getStatus();
 96				if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
 97					return message;
 98				}
 99			}
100		}
101		return null;
102	}
103
104	public void findWaitingMessages(OnMessageFound onMessageFound) {
105		synchronized (this.messages) {
106			for(Message message : this.messages) {
107				if (message.getStatus() == Message.STATUS_WAITING) {
108					onMessageFound.onMessageFound(message);
109				}
110			}
111		}
112	}
113
114	public void findUnreadMessages(OnMessageFound onMessageFound) {
115		synchronized (this.messages) {
116			for(Message message : this.messages) {
117				if (!message.isRead()) {
118					onMessageFound.onMessageFound(message);
119				}
120			}
121		}
122	}
123
124	public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
125		synchronized (this.messages) {
126			for (final Message message : this.messages) {
127				if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
128						&& message.getEncryption() != Message.ENCRYPTION_PGP) {
129					onMessageFound.onMessageFound(message);
130						}
131			}
132		}
133	}
134
135	public Message findMessageWithFileAndUuid(final String uuid) {
136		synchronized (this.messages) {
137			for (final Message message : this.messages) {
138				if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
139						&& message.getEncryption() != Message.ENCRYPTION_PGP
140						&& message.getUuid().equals(uuid)) {
141					return message;
142				}
143			}
144		}
145		return null;
146	}
147
148	public void clearMessages() {
149		synchronized (this.messages) {
150			this.messages.clear();
151		}
152	}
153
154	public boolean setIncomingChatState(ChatState state) {
155		if (this.mIncomingChatState == state) {
156			return false;
157		}
158		this.mIncomingChatState = state;
159		return true;
160	}
161
162	public ChatState getIncomingChatState() {
163		return this.mIncomingChatState;
164	}
165
166	public boolean setOutgoingChatState(ChatState state) {
167		if (mode == MODE_MULTI) {
168			return false;
169		}
170		if (this.mOutgoingChatState != state) {
171			this.mOutgoingChatState = state;
172			return true;
173		} else {
174			return false;
175		}
176	}
177
178	public ChatState getOutgoingChatState() {
179		return this.mOutgoingChatState;
180	}
181
182	public void trim() {
183		synchronized (this.messages) {
184			final int size = messages.size();
185			final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
186			if (size > maxsize) {
187				this.messages.subList(0, size - maxsize).clear();
188			}
189		}
190	}
191
192	public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
193		synchronized (this.messages) {
194			for (Message message : this.messages) {
195				if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
196						&& (message.getEncryption() == encryptionType)) {
197					onMessageFound.onMessageFound(message);
198				}
199			}
200		}
201	}
202
203	public void findUnsentTextMessages(OnMessageFound onMessageFound) {
204		synchronized (this.messages) {
205			for (Message message : this.messages) {
206				if (message.getType() != Message.TYPE_IMAGE
207						&& message.getStatus() == Message.STATUS_UNSEND) {
208					onMessageFound.onMessageFound(message);
209						}
210			}
211		}
212	}
213
214	public Message findSentMessageWithUuidOrRemoteId(String id) {
215		synchronized (this.messages) {
216			for (Message message : this.messages) {
217				if (id.equals(message.getUuid())
218						|| (message.getStatus() >= Message.STATUS_SEND
219						&& id.equals(message.getRemoteMsgId()))) {
220					return message;
221				}
222			}
223		}
224		return null;
225	}
226
227	public Message findSentMessageWithUuid(String id) {
228		synchronized (this.messages) {
229			for (Message message : this.messages) {
230				if (id.equals(message.getUuid())) {
231					return message;
232				}
233			}
234		}
235		return null;
236	}
237
238	public void populateWithMessages(final List<Message> messages) {
239		synchronized (this.messages) {
240			messages.clear();
241			messages.addAll(this.messages);
242		}
243		for(Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) {
244			if (iterator.next().wasMergedIntoPrevious()) {
245				iterator.remove();
246			}
247		}
248	}
249
250	@Override
251	public boolean isBlocked() {
252		return getContact().isBlocked();
253	}
254
255	@Override
256	public boolean isDomainBlocked() {
257		return getContact().isDomainBlocked();
258	}
259
260	@Override
261	public Jid getBlockedJid() {
262		return getContact().getBlockedJid();
263	}
264
265	public String getLastReceivedOtrMessageId() {
266		return this.mLastReceivedOtrMessageId;
267	}
268
269	public void setLastReceivedOtrMessageId(String id) {
270		this.mLastReceivedOtrMessageId = id;
271	}
272
273	public int countMessages() {
274		synchronized (this.messages) {
275			return this.messages.size();
276		}
277	}
278
279	public interface OnMessageFound {
280		void onMessageFound(final Message message);
281	}
282
283	public Conversation(final String name, final Account account, final Jid contactJid,
284			final int mode) {
285		this(java.util.UUID.randomUUID().toString(), name, null, account
286				.getUuid(), contactJid, System.currentTimeMillis(),
287				STATUS_AVAILABLE, mode, "");
288		this.account = account;
289	}
290
291	public Conversation(final String uuid, final String name, final String contactUuid,
292			final String accountUuid, final Jid contactJid, final long created, final int status,
293			final int mode, final String attributes) {
294		this.uuid = uuid;
295		this.name = name;
296		this.contactUuid = contactUuid;
297		this.accountUuid = accountUuid;
298		this.contactJid = contactJid;
299		this.created = created;
300		this.status = status;
301		this.mode = mode;
302		try {
303			this.attributes = new JSONObject(attributes == null ? "" : attributes);
304		} catch (JSONException e) {
305			this.attributes = new JSONObject();
306		}
307	}
308
309	public boolean isRead() {
310		return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
311	}
312
313	public List<Message> markRead() {
314		final List<Message> unread = new ArrayList<>();
315		synchronized (this.messages) {
316			for(Message message : this.messages) {
317				if (!message.isRead()) {
318					message.markRead();
319					unread.add(message);
320				}
321			}
322		}
323		return unread;
324	}
325
326	public Message getLatestMarkableMessage() {
327		for (int i = this.messages.size() - 1; i >= 0; --i) {
328			if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
329					&& this.messages.get(i).markable) {
330				if (this.messages.get(i).isRead()) {
331					return null;
332				} else {
333					return this.messages.get(i);
334				}
335					}
336		}
337		return null;
338	}
339
340	public Message getLatestMessage() {
341		if (this.messages.size() == 0) {
342			Message message = new Message(this, "", Message.ENCRYPTION_NONE);
343			message.setTime(getCreated());
344			return message;
345		} else {
346			Message message = this.messages.get(this.messages.size() - 1);
347			message.setConversation(this);
348			return message;
349		}
350	}
351
352	public String getName() {
353		if (getMode() == MODE_MULTI) {
354			if (getMucOptions().getSubject() != null) {
355				return getMucOptions().getSubject();
356			} else if (bookmark != null && bookmark.getBookmarkName() != null) {
357				return bookmark.getBookmarkName();
358			} else {
359				String generatedName = getMucOptions().createNameFromParticipants();
360				if (generatedName != null) {
361					return generatedName;
362				} else {
363					return getJid().getLocalpart();
364				}
365			}
366		} else {
367			return this.getContact().getDisplayName();
368		}
369	}
370
371	public String getAccountUuid() {
372		return this.accountUuid;
373	}
374
375	public Account getAccount() {
376		return this.account;
377	}
378
379	public Contact getContact() {
380		return this.account.getRoster().getContact(this.contactJid);
381	}
382
383	public void setAccount(final Account account) {
384		this.account = account;
385	}
386
387	@Override
388	public Jid getJid() {
389		return this.contactJid;
390	}
391
392	public int getStatus() {
393		return this.status;
394	}
395
396	public long getCreated() {
397		return this.created;
398	}
399
400	public ContentValues getContentValues() {
401		ContentValues values = new ContentValues();
402		values.put(UUID, uuid);
403		values.put(NAME, name);
404		values.put(CONTACT, contactUuid);
405		values.put(ACCOUNT, accountUuid);
406		values.put(CONTACTJID, contactJid.toString());
407		values.put(CREATED, created);
408		values.put(STATUS, status);
409		values.put(MODE, mode);
410		values.put(ATTRIBUTES, attributes.toString());
411		return values;
412	}
413
414	public static Conversation fromCursor(Cursor cursor) {
415		Jid jid;
416		try {
417			jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
418		} catch (final InvalidJidException e) {
419			// Borked DB..
420			jid = null;
421		}
422		return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
423				cursor.getString(cursor.getColumnIndex(NAME)),
424				cursor.getString(cursor.getColumnIndex(CONTACT)),
425				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
426				jid,
427				cursor.getLong(cursor.getColumnIndex(CREATED)),
428				cursor.getInt(cursor.getColumnIndex(STATUS)),
429				cursor.getInt(cursor.getColumnIndex(MODE)),
430				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
431	}
432
433	public void setStatus(int status) {
434		this.status = status;
435	}
436
437	public int getMode() {
438		return this.mode;
439	}
440
441	public void setMode(int mode) {
442		this.mode = mode;
443	}
444
445	public SessionImpl startOtrSession(String presence, boolean sendStart) {
446		if (this.otrSession != null) {
447			return this.otrSession;
448		} else {
449			final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
450					presence,
451					"xmpp");
452			this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
453			try {
454				if (sendStart) {
455					this.otrSession.startSession();
456					return this.otrSession;
457				}
458				return this.otrSession;
459			} catch (OtrException e) {
460				return null;
461			}
462		}
463
464	}
465
466	public SessionImpl getOtrSession() {
467		return this.otrSession;
468	}
469
470	public void resetOtrSession() {
471		this.otrFingerprint = null;
472		this.otrSession = null;
473		this.mSmp.hint = null;
474		this.mSmp.secret = null;
475		this.mSmp.status = Smp.STATUS_NONE;
476	}
477
478	public Smp smp() {
479		return mSmp;
480	}
481
482	public void startOtrIfNeeded() {
483		if (this.otrSession != null
484				&& this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
485			try {
486				this.otrSession.startSession();
487			} catch (OtrException e) {
488				this.resetOtrSession();
489			}
490				}
491	}
492
493	public boolean endOtrIfNeeded() {
494		if (this.otrSession != null) {
495			if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
496				try {
497					this.otrSession.endSession();
498					this.resetOtrSession();
499					return true;
500				} catch (OtrException e) {
501					this.resetOtrSession();
502					return false;
503				}
504			} else {
505				this.resetOtrSession();
506				return false;
507			}
508		} else {
509			return false;
510		}
511	}
512
513	public boolean hasValidOtrSession() {
514		return this.otrSession != null;
515	}
516
517	public synchronized String getOtrFingerprint() {
518		if (this.otrFingerprint == null) {
519			try {
520				if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
521					return null;
522				}
523				DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
524				this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
525			} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
526				return null;
527			}
528		}
529		return this.otrFingerprint;
530	}
531
532	public boolean verifyOtrFingerprint() {
533		final String fingerprint = getOtrFingerprint();
534		if (fingerprint != null) {
535			getContact().addOtrFingerprint(fingerprint);
536			return true;
537		} else {
538			return false;
539		}
540	}
541
542	public boolean isOtrFingerprintVerified() {
543		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
544	}
545
546	/**
547	 * short for is Private and Non-anonymous
548	 */
549	public boolean isPnNA() {
550		return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
551	}
552
553	public synchronized MucOptions getMucOptions() {
554		if (this.mucOptions == null) {
555			this.mucOptions = new MucOptions(this);
556		}
557		return this.mucOptions;
558	}
559
560	public void resetMucOptions() {
561		this.mucOptions = null;
562	}
563
564	public void setContactJid(final Jid jid) {
565		this.contactJid = jid;
566	}
567
568	public void setNextCounterpart(Jid jid) {
569		this.nextCounterpart = jid;
570	}
571
572	public Jid getNextCounterpart() {
573		return this.nextCounterpart;
574	}
575
576	private int getMostRecentlyUsedOutgoingEncryption() {
577		synchronized (this.messages) {
578			for(int i = this.messages.size() -1; i >= 0; --i) {
579				final Message m = this.messages.get(i);
580				if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
581					final int e = m.getEncryption();
582					if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
583						return Message.ENCRYPTION_PGP;
584					} else {
585						return e;
586					}
587				}
588			}
589		}
590		return Message.ENCRYPTION_NONE;
591	}
592
593	private int getMostRecentlyUsedIncomingEncryption() {
594		synchronized (this.messages) {
595			for(int i = this.messages.size() -1; i >= 0; --i) {
596				final Message m = this.messages.get(i);
597				if (m.getStatus() == Message.STATUS_RECEIVED) {
598					final int e = m.getEncryption();
599					if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
600						return Message.ENCRYPTION_PGP;
601					} else {
602						return e;
603					}
604				}
605			}
606		}
607		return Message.ENCRYPTION_NONE;
608	}
609
610	public int getNextEncryption() {
611		int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
612		if (next == -1) {
613			int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
614			if (outgoing == Message.ENCRYPTION_NONE) {
615				next = this.getMostRecentlyUsedIncomingEncryption();
616			} else {
617				next = outgoing;
618			}
619		}
620		if (Config.PARANOID_MODE && mode == MODE_SINGLE && next <= 0) {
621			if (getAccount().getAxolotlService().isContactAxolotlCapable(getContact())) {
622				return Message.ENCRYPTION_AXOLOTL;
623			} else {
624				return Message.ENCRYPTION_OTR;
625			}
626		}
627		return next;
628	}
629
630	public void setNextEncryption(int encryption) {
631		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
632	}
633
634	public String getNextMessage() {
635		if (this.nextMessage == null) {
636			return "";
637		} else {
638			return this.nextMessage;
639		}
640	}
641
642	public boolean smpRequested() {
643		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
644	}
645
646	public void setNextMessage(String message) {
647		this.nextMessage = message;
648	}
649
650	public void setSymmetricKey(byte[] key) {
651		this.symmetricKey = key;
652	}
653
654	public byte[] getSymmetricKey() {
655		return this.symmetricKey;
656	}
657
658	public void setBookmark(Bookmark bookmark) {
659		this.bookmark = bookmark;
660		this.bookmark.setConversation(this);
661	}
662
663	public void deregisterWithBookmark() {
664		if (this.bookmark != null) {
665			this.bookmark.setConversation(null);
666		}
667	}
668
669	public Bookmark getBookmark() {
670		return this.bookmark;
671	}
672
673	public boolean hasDuplicateMessage(Message message) {
674		synchronized (this.messages) {
675			for (int i = this.messages.size() - 1; i >= 0; --i) {
676				if (this.messages.get(i).equals(message)) {
677					return true;
678				}
679			}
680		}
681		return false;
682	}
683
684	public Message findSentMessageWithBody(String body) {
685		synchronized (this.messages) {
686			for (int i = this.messages.size() - 1; i >= 0; --i) {
687				Message message = this.messages.get(i);
688				if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) {
689					return message;
690				}
691			}
692			return null;
693		}
694	}
695
696	public void resetLastMessageTransmitted() {
697		this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,String.valueOf(-1));
698	}
699
700	public boolean setLastMessageTransmitted(long value) {
701		long before = getLastMessageTransmitted();
702		if (value - before > 1000) {
703			this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value));
704			return true;
705		} else {
706			return false;
707		}
708	}
709
710	public long getLastMessageTransmitted() {
711		long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0);
712		if (timestamp == 0) {
713			synchronized (this.messages) {
714				for(int i = this.messages.size() - 1; i >= 0; --i) {
715					Message message = this.messages.get(i);
716					if (message.getStatus() == Message.STATUS_RECEIVED) {
717						return message.getTimeSent();
718					}
719				}
720			}
721		}
722		return timestamp;
723	}
724
725	public void setMutedTill(long value) {
726		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
727	}
728
729	public boolean isMuted() {
730		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
731	}
732
733	public boolean setAttribute(String key, String value) {
734		try {
735			this.attributes.put(key, value);
736			return true;
737		} catch (JSONException e) {
738			return false;
739		}
740	}
741
742	public String getAttribute(String key) {
743		try {
744			return this.attributes.getString(key);
745		} catch (JSONException e) {
746			return null;
747		}
748	}
749
750	public int getIntAttribute(String key, int defaultValue) {
751		String value = this.getAttribute(key);
752		if (value == null) {
753			return defaultValue;
754		} else {
755			try {
756				return Integer.parseInt(value);
757			} catch (NumberFormatException e) {
758				return defaultValue;
759			}
760		}
761	}
762
763	public long getLongAttribute(String key, long defaultValue) {
764		String value = this.getAttribute(key);
765		if (value == null) {
766			return defaultValue;
767		} else {
768			try {
769				return Long.parseLong(value);
770			} catch (NumberFormatException e) {
771				return defaultValue;
772			}
773		}
774	}
775
776	public void add(Message message) {
777		message.setConversation(this);
778		synchronized (this.messages) {
779			this.messages.add(message);
780		}
781	}
782
783	public void addAll(int index, List<Message> messages) {
784		synchronized (this.messages) {
785			this.messages.addAll(index, messages);
786		}
787		account.getPgpDecryptionService().addAll(messages);
788	}
789
790	public void sort() {
791		synchronized (this.messages) {
792			Collections.sort(this.messages, new Comparator<Message>() {
793				@Override
794				public int compare(Message left, Message right) {
795					if (left.getTimeSent() < right.getTimeSent()) {
796						return -1;
797					} else if (left.getTimeSent() > right.getTimeSent()) {
798						return 1;
799					} else {
800						return 0;
801					}
802				}
803			});
804			for(Message message : this.messages) {
805				message.untie();
806			}
807		}
808	}
809
810	public int unreadCount() {
811		synchronized (this.messages) {
812			int count = 0;
813			for(int i = this.messages.size() - 1; i >= 0; --i) {
814				if (this.messages.get(i).isRead()) {
815					return count;
816				}
817				++count;
818			}
819			return count;
820		}
821	}
822
823	public class Smp {
824		public static final int STATUS_NONE = 0;
825		public static final int STATUS_CONTACT_REQUESTED = 1;
826		public static final int STATUS_WE_REQUESTED = 2;
827		public static final int STATUS_FAILED = 3;
828		public static final int STATUS_VERIFIED = 4;
829
830		public String secret = null;
831		public String hint = null;
832		public int status = 0;
833	}
834}