package com.nostra13.socialsharing.twitter.extpack.winterwell.jtwitter;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.EnumMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.nostra13.socialsharing.twitter.extpack.winterwell.json.JSONArray;
import com.nostra13.socialsharing.twitter.extpack.winterwell.json.JSONException;
import com.nostra13.socialsharing.twitter.extpack.winterwell.json.JSONObject;
import com.nostra13.socialsharing.twitter.extpack.winterwell.jtwitter.Twitter.ITweet;
import com.nostra13.socialsharing.twitter.extpack.winterwell.jtwitter.Twitter.KEntityType;
import com.nostra13.socialsharing.twitter.extpack.winterwell.jtwitter.Twitter.TweetEntity;
/**
* A Twitter status post. .toString() returns the status text.
* <p>
* Notes: This is a finalised data object. It exposes its fields for convenient
* access. If you want to change your status, use
* {@link Twitter#setStatus(String)} and {@link Twitter#destroyStatus(Status)}.
*/
public final class Status implements ITweet {
/**
* regex for @you mentions
*/
static final Pattern AT_YOU_SIR = Pattern.compile("@(\\w+)");
private static final String FAKE = "fake";
private static final long serialVersionUID = 1L;
/**
* Convert from a json array of objects into a list of tweets.
*
* @param json
* can be empty, must not be null
* @throws TwitterException
*/
static List<Status> getStatuses(String json) throws TwitterException {
if (json.trim().equals(""))
return Collections.emptyList();
try {
List<Status> tweets = new ArrayList<Status>();
JSONArray arr = new JSONArray(json);
for (int i = 0; i < arr.length(); i++) {
Object ai = arr.get(i);
if (JSONObject.NULL.equals(ai)) {
continue;
}
JSONObject obj = (JSONObject) ai;
Status tweet = new Status(obj, null);
tweets.add(tweet);
}
return tweets;
} catch (JSONException e) {
throw new TwitterException.Parsing(json, e);
}
}
/**
* Search results use a slightly different protocol! In particular w.r.t.
* user ids and info.
*
* @param searchResults
* @return search results as Status objects - but with dummy users! The
* dummy users have a screenname and a profile image url, but no
* other information. This reflects the current behaviour of the
* Twitter API.
*/
static List<Status> getStatusesFromSearch(Twitter tw, String json) {
try {
JSONObject searchResults = new JSONObject(json);
List<Status> users = new ArrayList<Status>();
JSONArray arr = searchResults.getJSONArray("results");
for (int i = 0; i < arr.length(); i++) {
JSONObject obj = arr.getJSONObject(i);
String userScreenName = obj.getString("from_user");
String profileImgUrl = obj.getString("profile_image_url");
User user = new User(userScreenName);
user.profileImageUrl = InternalUtils.URI(profileImgUrl);
Status s = new Status(obj, user);
users.add(s);
}
return users;
} catch (JSONException e) {
throw new TwitterException.Parsing(json, e);
}
}
/**
* @param object
* @return place, location, failing which geo coordinates
* @throws JSONException
*/
static Object jsonGetLocn(JSONObject object) throws JSONException {
String _location = InternalUtils.jsonGet("location", object);
// no blank strings
if (_location != null && _location.length() == 0) {
_location = null;
}
JSONObject _place = object.optJSONObject("place");
if (_location != null) {
// normalise UT (UberTwitter?) locations
Matcher m = InternalUtils.latLongLocn.matcher(_location);
if (m.matches()) {
_location = m.group(2) + "," + m.group(3);
}
return _location; // should we also check geo and place for extra
// info??
}
// Twitter place
if (_place != null) {
Place place = new Place(_place);
return place;
}
JSONObject geo = object.optJSONObject("geo");
if (geo != null && geo != JSONObject.NULL) {
JSONArray latLong = geo.getJSONArray("coordinates");
_location = latLong.get(0) + "," + latLong.get(1);
}
// TODO place (when is this set?)
return _location;
}
public final Date createdAt;
private EnumMap<KEntityType, List<TweetEntity>> entities;
private boolean favorited;
/**
* Warning: use equals() not == to compare these!
*/
public final BigInteger id;
/**
* Often null (even when this Status is a reply). This is the in-reply-to
* status id as reported by Twitter.
*/
public final BigInteger inReplyToStatusId;
private String location;
/**
* null, except for official retweets when this is the original retweeted
* Status.
*/
private Status original;
private Place place;
/**
* Represents the number of times a status has been retweeted using
* _new-style_ retweets. -1 if unknown.
*/
public final int retweetCount;
boolean sensitive;
/**
* E.g. "web" vs. "im"
* <p>
* "fake" if this Status was made locally or from an RSS feed rather than
* retrieved from Twitter json (as normal).
*/
public final String source;
/** The actual status text. */
public final String text;
/**
* Rarely null.
* <p>
* When can this be null?<br>
* - If creating a "fake" tweet via
* {@link Status#Status(User, String, long, Date)} and supplying a null
* User!
*/
public final User user;
// private String[] withheldIn;
// /**
// Should we have this??
// * @return usually null!
// * Otherwise, a list of country codes for where this tweet has been censored.
// */
// public String[] getWithheldIn() {
// return withheldIn;
// }
/**
* @param object
* @param user
* Set when parsing the json returned for a User. null when
* parsing the json returned for a Status.
* @throws TwitterException
*/
@SuppressWarnings("deprecation")
Status(JSONObject object, User user) throws TwitterException {
try {
String _id = object.optString("id_str");
id = new BigInteger(_id == "" ? object.get("id").toString() : _id);
// retweet?
JSONObject retweeted = object.optJSONObject("retweeted_status");
if (retweeted != null) {
original = new Status(retweeted, null);
}
// text!
String _text = InternalUtils.jsonGet("text", object);
// Twitter have started truncating RTs -- let's fix the text up if we can
boolean truncated = object.optBoolean("truncated");
String rtStart = null;
if (truncated && original!=null && _text.startsWith("RT ")) {
rtStart = "RT @"+original.getUser()+": ";
_text = rtStart+original.getText();
} else {
_text = InternalUtils.unencode(_text); // bugger - this screws up the indices in tweet entities
}
text = _text;
// date
String c = InternalUtils.jsonGet("created_at", object);
createdAt = InternalUtils.parseDate(c);
// source - sometimes encoded (search), sometimes not
// (timelines)!
String src = InternalUtils.jsonGet("source", object);
source = src.contains("<") ? InternalUtils.unencode(src) : src;
// threading
String irt = InternalUtils.jsonGet("in_reply_to_status_id", object);
if (irt == null) {
// Twitter doesn't give in-reply-to for retweets
// - but since we have the info, let's make it available
inReplyToStatusId = original == null ? null : original.getId();
} else {
inReplyToStatusId = new BigInteger(irt);
}
favorited = object.optBoolean("favorited");
// set user
if (user != null) {
this.user = user;
} else {
JSONObject jsonUser = object.optJSONObject("user");
// null user happens in very rare circumstances, which I
// have not pinned down yet.
if (jsonUser == null) {
this.user = null;
} else if (jsonUser.length() < 3) {
// TODO seen a bug where the jsonUser is just
// {"id":24147187,"id_str":"24147187"}
// Not sure when/why this happens
String _uid = jsonUser.optString("id_str");
BigInteger userId = new BigInteger(_uid == "" ? object.get(
"id").toString() : _uid);
try {
user = new Twitter().show(userId);
} catch (Exception e) {
// ignore
}
this.user = user;
} else {
// normal JSON case
this.user = new User(jsonUser, this);
}
}
// location if geocoding is on
Object _locn = Status.jsonGetLocn(object);
location = _locn == null ? null : _locn.toString();
if (_locn instanceof Place) {
place = (Place) _locn;
}
retweetCount = object.optInt("retweet_count", -1);
// ignore this as it can be misleading: true is reliable, false isn't
// retweeted = object.optBoolean("retweeted");
// Entities (switched on by Twitter.setIncludeTweetEntities(true))
JSONObject jsonEntities = object.optJSONObject("entities");
// Note: Twitter filters out dud @names
if (jsonEntities != null) {
entities = new EnumMap<Twitter.KEntityType, List<TweetEntity>>(
KEntityType.class);
if (rtStart!=null) {
// truncation! the entities returned are likely to be duds -- adjust from the original instead
int rt = rtStart.length();
for (KEntityType type : KEntityType.values()) {
List<TweetEntity> es = original.getTweetEntities(type);
if (es==null) continue;
ArrayList rtEs = new ArrayList(es.size());
for (TweetEntity e : es) {
TweetEntity rte = new TweetEntity(this, e.type,
/* safety checks on length are paranoia (could be removed) */
Math.min(rt+e.start, text.length()), Math.min(rt+e.end, text.length()), e.display);
rtEs.add(rte);
}
entities.put(type, rtEs);
}
} else {
// normal case
for (KEntityType type : KEntityType.values()) {
List<TweetEntity> es = TweetEntity.parse(this, _text, type,
jsonEntities);
entities.put(type, es);
}
}
}
// censorship flags
// Should we have this??
// String withheld = object.optString("withheld_in_countries");
// if (withheld!=null && withheld.length()!=0) {
// withheldIn = withheld.split(", ");
// }
// "withheld_scope": "status" or "user"
sensitive = object.optBoolean("possibly_sensitive");
} catch (JSONException e) {
throw new TwitterException.Parsing(null, e);
}
}
/**
* Create a *fake* Status object. This does not represent a real tweet!
* Uses: few and far between. There is no real contract as to how objects
* made in this way will behave.
* <p>
* If you want to post a tweet (and hence get a real Status object), use
* {@link Twitter#setStatus(String)}.
*
* @param user
* Can be null or bogus -- provided that's OK with your code.
* @param text
* Can be null or bogus -- provided that's OK with your code.
* @param id
* Can be null or bogus -- provided that's OK with your code.
* @param createdAt
* Can be null -- provided that's OK with your code.
*/
@Deprecated
public Status(User user, String text, Number id, Date createdAt) {
this.text = text;
this.user = user;
this.createdAt = createdAt;
this.id = id == null ? null
: (id instanceof BigInteger ? (BigInteger) id : new BigInteger(
id.toString()));
inReplyToStatusId = null;
source = FAKE;
retweetCount = -1;
}
/**
* Tests by class=Status and tweet id number
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Status other = (Status) obj;
return id.equals(other.id);
}
@Override
public Date getCreatedAt() {
return createdAt;
}
/**
* @return The Twitter id for this post. This is used by some API methods.
*/
@Override
public BigInteger getId() {
return id;
}
@Override
public String getLocation() {
return location;
}
/**
* @return list of \@mentioned people (there is no guarantee that these
* mentions are for correct Twitter screen-names). May be empty,
* never null. Screen-names are always lowercased -- unless
* {@link Twitter#CASE_SENSITIVE_SCREENNAMES} is switched on.
*/
@Override
public List<String> getMentions() {
// TODO test & use this
// List<TweetEntity> ms = entities.get(KEntityType.user_mentions);
Matcher m = AT_YOU_SIR.matcher(text);
List<String> list = new ArrayList<String>(2);
while (m.find()) {
// skip email addresses (and other poorly formatted things)
if (m.start() != 0
&& Character.isLetterOrDigit(text.charAt(m.start() - 1))) {
continue;
}
String mention = m.group(1);
// enforce lower case? (normally yes)
if (!Twitter.CASE_SENSITIVE_SCREENNAMES) {
mention = mention.toLowerCase();
}
list.add(mention);
}
return list;
}
/**
* Only set for official new-style retweets. This is the original retweeted
* Status. null otherwise.
*/
public Status getOriginal() {
return original;
}
@Override
public Place getPlace() {
return place;
}
/** The actual status text. This is also returned by {@link #toString()}.
* NB: This can be longer than 140 chars for a retweet. */
@Override
public String getText() {
return text;
}
@Override
public List<TweetEntity> getTweetEntities(KEntityType type) {
return entities == null ? null : entities.get(type);
}
@Override
public User getUser() {
return user;
}
@Override
public int hashCode() {
return id.hashCode();
}
/**
* true if this has been marked as a favourite by the authenticating user
*/
public boolean isFavorite() {
return favorited;
}
/**
* A <i>self-applied</i> label for sensitive content (eg. X-rated images).
* Obviously, you can only rely on this label if the tweeter is reliably
* setting it.
*
* @return true=kinky, false=family-friendly
*/
public boolean isSensitive() {
return sensitive;
}
/**
* @return The text of this status. E.g. "Kicking fommil's arse at
* Civilisation."
*/
@Override
public String toString() {
return text;
}
/**
* @return text, with the t.co urls replaced.
* Use-case: for filtering based on text contents, when we want to
* match against the full url.
* Note: this does NOT resolve short urls from bit.ly etc.
*/
public String getDisplayText() {
return getDisplayText2(this);
}
static String getDisplayText2(ITweet tweet) {
List<TweetEntity> es = tweet.getTweetEntities(KEntityType.urls);
String _text = tweet.getText();
if (es==null || es.size()==0) return _text;
StringBuilder sb = new StringBuilder(200);
int i=0;
for (TweetEntity entity : es) {
sb.append(_text.substring(i, entity.start));
sb.append(entity.displayVersion());
i = entity.end;
}
if (i < _text.length()) {
sb.append(_text.substring(i));
}
return sb.toString();
}
}