/**
* Note represents a single WordPress.com notification
*/
package org.wordpress.android.models;
import android.text.Spannable;
import android.text.TextUtils;
import android.util.Base64;
import org.apache.commons.lang3.time.DateUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.wordpress.android.fluxc.model.CommentModel;
import org.wordpress.android.fluxc.model.CommentStatus;
import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.DateTimeUtils;
import org.wordpress.android.util.JSONUtils;
import org.wordpress.android.util.StringUtils;
import java.io.UnsupportedEncodingException;
import java.util.Comparator;
import java.util.Date;
import java.util.EnumSet;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
public class Note {
private static final String TAG = "NoteModel";
// Maximum character length for a comment preview
static private final int MAX_COMMENT_PREVIEW_LENGTH = 200;
// Note types
public static final String NOTE_FOLLOW_TYPE = "follow";
public static final String NOTE_LIKE_TYPE = "like";
public static final String NOTE_COMMENT_TYPE = "comment";
public static final String NOTE_MATCHER_TYPE = "automattcher";
public static final String NOTE_COMMENT_LIKE_TYPE = "comment_like";
public static final String NOTE_REBLOG_TYPE = "reblog";
public static final String NOTE_NEW_POST_TYPE = "new_post";
public static final String NOTE_UNKNOWN_TYPE = "unknown";
// JSON action keys
private static final String ACTION_KEY_REPLY = "replyto-comment";
private static final String ACTION_KEY_APPROVE = "approve-comment";
private static final String ACTION_KEY_SPAM = "spam-comment";
private static final String ACTION_KEY_LIKE = "like-comment";
private JSONObject mActions;
private JSONObject mNoteJSON;
private final String mKey;
private final Object mSyncLock = new Object();
private String mLocalStatus;
public enum EnabledActions {
ACTION_REPLY,
ACTION_APPROVE,
ACTION_UNAPPROVE,
ACTION_SPAM,
ACTION_LIKE
}
public enum NoteTimeGroup {
GROUP_TODAY,
GROUP_YESTERDAY,
GROUP_OLDER_TWO_DAYS,
GROUP_OLDER_WEEK,
GROUP_OLDER_MONTH
}
public Note(String key, JSONObject noteJSON) {
mKey = key;
mNoteJSON = noteJSON;
}
public Note(JSONObject noteJSON){
mNoteJSON = noteJSON;
mKey = mNoteJSON.optString("id", "");
}
public JSONObject getJSON() {
return mNoteJSON != null ? mNoteJSON : new JSONObject();
}
public String getId() {
return mKey;
}
public String getType() {
return queryJSON("type", NOTE_UNKNOWN_TYPE);
}
private Boolean isType(String type) {
return getType().equals(type);
}
public Boolean isCommentType() {
synchronized (mSyncLock) {
return (isAutomattcherType() && JSONUtils.queryJSON(mNoteJSON, "meta.ids.comment", -1) != -1) ||
isType(NOTE_COMMENT_TYPE);
}
}
public Boolean isAutomattcherType() {
return isType(NOTE_MATCHER_TYPE);
}
public Boolean isNewPostType() {
return isType(NOTE_NEW_POST_TYPE);
}
public Boolean isFollowType() {
return isType(NOTE_FOLLOW_TYPE);
}
public Boolean isLikeType() {
return isType(NOTE_LIKE_TYPE);
}
public Boolean isCommentLikeType() {
return isType(NOTE_COMMENT_LIKE_TYPE);
}
public Boolean isReblogType() {
return isType(NOTE_REBLOG_TYPE);
}
public Boolean isCommentReplyType() {
return isCommentType() && getParentCommentId() > 0;
}
// Returns true if the user has replied to this comment note
public Boolean isCommentWithUserReply() {
return isCommentType() && !TextUtils.isEmpty(getCommentSubjectNoticon());
}
public Boolean isUserList() {
return isLikeType() || isCommentLikeType() || isFollowType() || isReblogType();
}
/*
* does user have permission to moderate/reply/spam this comment?
*/
public boolean canModerate() {
EnumSet<EnabledActions> enabledActions = getEnabledActions();
return enabledActions != null && (enabledActions.contains(EnabledActions.ACTION_APPROVE) || enabledActions.contains(EnabledActions.ACTION_UNAPPROVE));
}
public boolean canMarkAsSpam() {
EnumSet<EnabledActions> enabledActions = getEnabledActions();
return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_SPAM));
}
public boolean canReply() {
EnumSet<EnabledActions> enabledActions = getEnabledActions();
return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_REPLY));
}
public boolean canTrash() {
return canModerate();
}
public boolean canLike() {
EnumSet<EnabledActions> enabledActions = getEnabledActions();
return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_LIKE));
}
public String getLocalStatus() {
return StringUtils.notNullStr(mLocalStatus);
}
public void setLocalStatus(String localStatus) {
mLocalStatus = localStatus;
}
public JSONObject getSubject() {
try {
synchronized (mSyncLock) {
JSONArray subjectArray = mNoteJSON.getJSONArray("subject");
if (subjectArray.length() > 0) {
return subjectArray.getJSONObject(0);
}
}
} catch (JSONException e) {
return null;
}
return null;
}
public Spannable getFormattedSubject() {
return NotificationsUtils.getSpannableContentForRanges(getSubject());
}
public String getTitle() {
return queryJSON("title", "");
}
public String getIconURL() {
return queryJSON("icon", "");
}
public String getCommentSubject() {
synchronized (mSyncLock) {
JSONArray subjectArray = mNoteJSON.optJSONArray("subject");
if (subjectArray != null) {
String commentSubject = JSONUtils.queryJSON(subjectArray, "subject[1].text", "");
// Trim down the comment preview if the comment text is too large.
if (commentSubject != null && commentSubject.length() > MAX_COMMENT_PREVIEW_LENGTH) {
commentSubject = commentSubject.substring(0, MAX_COMMENT_PREVIEW_LENGTH - 1);
}
return commentSubject;
}
}
return "";
}
public String getCommentSubjectNoticon() {
JSONArray subjectRanges = queryJSON("subject[0].ranges", new JSONArray());
if (subjectRanges != null) {
for (int i=0; i < subjectRanges.length(); i++) {
try {
JSONObject rangeItem = subjectRanges.getJSONObject(i);
if (rangeItem.has("type") && rangeItem.optString("type").equals("noticon")) {
return rangeItem.optString("value", "");
}
} catch (JSONException e) {
return "";
}
}
}
return "";
}
public long getCommentReplyId() {
return queryJSON("meta.ids.reply_comment", 0);
}
/**
* Compare note timestamp to now and return a time grouping
*/
public static NoteTimeGroup getTimeGroupForTimestamp(long timestamp) {
Date today = new Date();
Date then = new Date(timestamp * 1000);
if (then.compareTo(DateUtils.addMonths(today, -1)) < 0) {
return NoteTimeGroup.GROUP_OLDER_MONTH;
} else if (then.compareTo(DateUtils.addWeeks(today, -1)) < 0) {
return NoteTimeGroup.GROUP_OLDER_WEEK;
} else if (then.compareTo(DateUtils.addDays(today, -2)) < 0
|| DateUtils.isSameDay(DateUtils.addDays(today, -2), then)) {
return NoteTimeGroup.GROUP_OLDER_TWO_DAYS;
} else if (DateUtils.isSameDay(DateUtils.addDays(today, -1), then)) {
return NoteTimeGroup.GROUP_YESTERDAY;
} else {
return NoteTimeGroup.GROUP_TODAY;
}
}
public static class TimeStampComparator implements Comparator<Note> {
@Override
public int compare(Note a, Note b) {
return b.getTimestampString().compareTo(a.getTimestampString());
}
}
/**
* The inverse of isRead
*/
public Boolean isUnread() {
return !isRead();
}
private Boolean isRead() {
return queryJSON("read", 0) == 1;
}
public void setRead(){
try {
mNoteJSON.putOpt("read", 1);
} catch (JSONException e){
AppLog.e(AppLog.T.NOTIFS, "Failed to set 'read' property", e);
}
}
/**
* Get the timestamp provided by the API for the note
*/
public long getTimestamp() {
return DateTimeUtils.timestampFromIso8601(getTimestampString());
}
public String getTimestampString() {
return queryJSON("timestamp", "");
}
public JSONArray getBody() {
try {
synchronized (mSyncLock) {
return mNoteJSON.getJSONArray("body");
}
} catch (JSONException e) {
return new JSONArray();
}
}
// returns character code for notification font
public String getNoticonCharacter() {
return queryJSON("noticon", "");
}
private JSONObject getCommentActions() {
if (mActions == null) {
// Find comment block that matches the root note comment id
long commentId = getCommentId();
JSONArray bodyArray = getBody();
for (int i = 0; i < bodyArray.length(); i++) {
try {
JSONObject bodyItem = bodyArray.getJSONObject(i);
if (bodyItem.has("type") && bodyItem.optString("type").equals("comment")
&& commentId == JSONUtils.queryJSON(bodyItem, "meta.ids.comment", 0)) {
mActions = JSONUtils.queryJSON(bodyItem, "actions", new JSONObject());
break;
}
} catch (JSONException e) {
break;
}
}
if (mActions == null) {
mActions = new JSONObject();
}
}
return mActions;
}
/*
* returns the actions allowed on this note, assumes it's a comment notification
*/
public EnumSet<EnabledActions> getEnabledActions() {
EnumSet<EnabledActions> actions = EnumSet.noneOf(EnabledActions.class);
JSONObject jsonActions = getCommentActions();
if (jsonActions == null || jsonActions.length() == 0) {
return actions;
}
if (jsonActions.has(ACTION_KEY_REPLY)) {
actions.add(EnabledActions.ACTION_REPLY);
}
if (jsonActions.has(ACTION_KEY_APPROVE) && jsonActions.optBoolean(ACTION_KEY_APPROVE, false)) {
actions.add(EnabledActions.ACTION_UNAPPROVE);
}
if (jsonActions.has(ACTION_KEY_APPROVE) && !jsonActions.optBoolean(ACTION_KEY_APPROVE, false)) {
actions.add(EnabledActions.ACTION_APPROVE);
}
if (jsonActions.has(ACTION_KEY_SPAM)) {
actions.add(EnabledActions.ACTION_SPAM);
}
if (jsonActions.has(ACTION_KEY_LIKE)) {
actions.add(EnabledActions.ACTION_LIKE);
}
return actions;
}
public int getSiteId() {
return queryJSON("meta.ids.site", 0);
}
public int getPostId() {
return queryJSON("meta.ids.post", 0);
}
public long getCommentId() {
return queryJSON("meta.ids.comment", 0);
}
public long getParentCommentId() {
return queryJSON("meta.ids.parent_comment", 0);
}
/**
* Rudimentary system for pulling an item out of a JSON object hierarchy
*/
private <U> U queryJSON(String query, U defaultObject) {
synchronized (mSyncLock) {
if (mNoteJSON == null) return defaultObject;
return JSONUtils.queryJSON(mNoteJSON, query, defaultObject);
}
}
/**
* Constructs a new Comment object based off of data in a Note
*/
public CommentModel buildComment() {
CommentModel comment = new CommentModel();
comment.setRemotePostId(getPostId());
comment.setRemoteCommentId(getCommentId());
comment.setAuthorName(getCommentAuthorName());
comment.setDatePublished(DateTimeUtils.iso8601FromTimestamp(getTimestamp()));
comment.setContent(getCommentText());
comment.setStatus(getCommentStatus().toString());
comment.setAuthorUrl(getCommentAuthorUrl());
comment.setPostTitle(getTitle()); // unavailable in note model
comment.setAuthorEmail(""); // unavailable in note model
comment.setAuthorProfileImageUrl(getIconURL());
comment.setILike(hasLikedComment());
return comment;
}
public String getCommentAuthorName() {
JSONArray bodyArray = getBody();
for (int i=0; i < bodyArray.length(); i++) {
try {
JSONObject bodyItem = bodyArray.getJSONObject(i);
if (bodyItem.has("type") && bodyItem.optString("type").equals("user")) {
return bodyItem.optString("text");
}
} catch (JSONException e) {
return "";
}
}
return "";
}
private String getCommentText() {
return queryJSON("body[last].text", "");
}
private String getCommentAuthorUrl() {
JSONArray bodyArray = getBody();
for (int i=0; i < bodyArray.length(); i++) {
try {
JSONObject bodyItem = bodyArray.getJSONObject(i);
if (bodyItem.has("type") && bodyItem.optString("type").equals("user")) {
return JSONUtils.queryJSON(bodyItem, "meta.links.home", "");
}
} catch (JSONException e) {
return "";
}
}
return "";
}
public CommentStatus getCommentStatus() {
EnumSet<EnabledActions> enabledActions = getEnabledActions();
if (enabledActions.contains(EnabledActions.ACTION_UNAPPROVE)) {
return CommentStatus.APPROVED;
} else if (enabledActions.contains(EnabledActions.ACTION_APPROVE)) {
return CommentStatus.UNAPPROVED;
}
return CommentStatus.ALL;
}
public boolean hasLikedComment() {
JSONObject jsonActions = getCommentActions();
return !(jsonActions == null || jsonActions.length() == 0) && jsonActions.optBoolean(ACTION_KEY_LIKE);
}
public String getUrl() {
return queryJSON("url", "");
}
public JSONArray getHeader() {
synchronized (mSyncLock) {
return mNoteJSON.optJSONArray("header");
}
}
public static synchronized Note buildFromBase64EncodedData(String noteId, String base64FullNoteData) {
Note note = null;
if (base64FullNoteData == null) return null;
byte[] b64DecodedPayload = Base64.decode(base64FullNoteData, Base64.DEFAULT);
// Decompress the payload
Inflater decompresser = new Inflater();
decompresser.setInput(b64DecodedPayload, 0, b64DecodedPayload.length);
byte[] result = new byte[4096]; //max length an Android PN payload can have
int resultLength = 0;
try {
resultLength = decompresser.inflate(result);
decompresser.end();
} catch (DataFormatException e) {
AppLog.e(AppLog.T.NOTIFS, "Can't decompress the PN Payload. It could be > 4K", e);
}
String out = null;
try {
out = new String(result, 0, resultLength, "UTF8");
} catch (UnsupportedEncodingException e) {
AppLog.e(AppLog.T.NOTIFS, "Notification data contains non UTF8 characters.", e);
}
if (out != null) {
try {
JSONObject jsonObject = new JSONObject(out);
if (jsonObject.has("notes")) {
JSONArray jsonArray = jsonObject.getJSONArray("notes");
if (jsonArray != null && jsonArray.length() == 1) {
jsonObject = jsonArray.getJSONObject(0);
}
}
note = new Note(noteId, jsonObject);
} catch (JSONException e) {
AppLog.e(AppLog.T.NOTIFS, "Can't parse the Note JSON received in the PN", e);
}
}
return note;
}
}