vars)
throws Exception;
/**
* Lower-level POST method - stringifies JSON body instead of encoding vars
* @param uri
* @param body
* @return a freshly opened authorised connection
* @throws TwitterException
*/
HttpURLConnection post2_connect(String uri, JSONObject body) throws Exception;
/**
* Lower-level POST method - takes raw string for body
* @param uri
* @param payload
* @return a freshly opened authorised connection
* @throws TwitterException
*/
HttpURLConnection post2_connect(String uri, String payload) throws Exception;
/**
* Set the timeout for a single get/post request. This is an optional
* method - implementations can ignore it!
*
* @param millisecs
*/
void setTimeout(int millisecs);
/**
* If true, will wait 1/2 second and make a 2nd request when presented with
* a server error (E50X). Only retries once -- a 2nd fail will throw an exception.
*
* This policy handles most Twitter server glitches.
*/
boolean isRetryOnError();
void setRetryOnError(boolean retryOnError);
}
/**
* This gives common access to features that are common to both
* {@link Message}s and {@link Status}es.
*
* @author daniel
*
*/
public static interface ITweet extends Serializable {
Date getCreatedAt();
/**
* Twitter IDs are numbers - but they can exceed the range of Java's
* signed long.
*
* @return The Twitter id for this post. This is used by some API
* methods.
*/
BigInteger getId();
/**
* @return the location of this tweet. Can be null, never blank. This
* can come from geo-tagging or the user's location. This may be
* a place name, or in the form "latitude,longitude" if it came
* from a geo-tagged source.
*
* Note: This will be set if Twitter supply any geo-information.
* We extract a location from geo and place objects.
*/
String getLocation();
/**
* @return list of screen-names this message is to. May be empty, never
* null. For Statuses, this is anyone mentioned in the message.
* For DMs, this is a wrapper round
* {@link Message#getRecipient()}.
*
* Notes: This method is in ITweet as a convenience to allow the
* same code to process both Statuses and Messages where
* possible. It would be better named "getRecipients()", but for
* historical reasons it isn't.
*/
List getMentions();
/**
* @return more information on the location of this tweet. This is
* usually null!
*/
Place getPlace();
/** The actual status text. This is also returned by {@link #toString()} */
String getText();
/**
* Twitter wrap urls with their own url-shortener (as a defence against
* malicious tweets). You are recommended to direct people to the
* Twitter-url, but use the original url for display.
*
* Entity support is off by default. Request entity support by setting
* {@link Twitter#setIncludeTweetEntities(boolean)}. Twitter do NOT
* support entities for search :(
*
* @param type
* urls, user_mentions, or hashtags
* @return the text entities in this tweet, or null if the info was not
* supplied.
*/
List getTweetEntities(KEntityType type);
/** The User who made the tweet */
User getUser();
/**
* @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.
*/
String getDisplayText();
}
public static enum KEntityType {
hashtags, urls, user_mentions, media, symbols, extended_entities
}
/**
* @deprecated Replaced in 1.1 with a more flexible family of resources.
*
* Kept here for backwards compatibility only.
* Will be removed: June 2013!
*/
public static enum KRequestType {
NORMAL(RateLimit.RES_USER_TIMELINE),
SEARCH(RateLimit.RES_SEARCH),
/** this is X-Feature Class "namesearch" in the response headers */
SEARCH_USERS("/users/search"),
SHOW_USER(RateLimit.RES_USERS_SHOW1),
UPLOAD_MEDIA("Media"),
STREAM_KEYWORD(""),
STREAM_USER("");
/**
* USed to find the X-?RateLimit header.
*/
final String rateLimit;
private KRequestType(String rateLimit) {
this.rateLimit = rateLimit;
}
}
/**
* A special slice of text within a tweet.
*
* @see Twitter#setIncludeTweetEntities(boolean)
*/
public final static class TweetEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
*
* @param tweet
* @param rawText
* @param type
* @param jsonEntities
* @return Can be null if no entities of this type are specified
* @throws JSONException
*/
static ArrayList parse(ITweet tweet, String rawText, KEntityType type,
JSONObject jsonEntities) throws JSONException
{
assert type != null && tweet != null && rawText != null && jsonEntities!=null
: tweet+"\t"+rawText+"\t"+type+"\t"+jsonEntities;
try {
JSONArray arr = jsonEntities.optJSONArray(type.toString());
// e.g. "user_mentions":[{"id":19720954,"name":"Lilly Hunter","indices":[0,10],"screen_name":"LillyLyle"}
if (arr==null || arr.length()==0) {
return null;
}
ArrayList list = new ArrayList(
arr.length());
for (int i = 0; i < arr.length(); i++) {
JSONObject obj = arr.getJSONObject(i);
TweetEntity te = new TweetEntity(tweet, rawText, type, obj, list);
list.add(te);
}
return list;
} catch (Throwable e) {
// whatever bogus data Twitter send, don't fail
return null;
}
}
final String display;
/**
* end of the entity in the contents String, exclusive
*/
public final int end;
/**
* start of the entity in the contents String, inclusive
*/
public final int start;
private final ITweet tweet;
public final KEntityType type;
/**
* Location of the actual image file (if there is one) - used when getting attached images from DMs
*/
final String mediaUrl;
/**
*
* @param tweet
* @param rawText Needed to undo the indexing errors created by entity encoding
* @param type
* @param obj
* @param previous Used to handle repeated entities
* @throws JSONException
*/
TweetEntity(ITweet tweet, String rawText, KEntityType type, JSONObject obj, ArrayList previous)
throws JSONException
{
this.tweet = tweet;
this.type = type;
switch (type) {
case urls:
Object eu = obj.opt("expanded_url");
display = JSONObject.NULL.equals(eu) ? null : (String) eu;
break;
case user_mentions:
display = obj.getString("name");
break;
case media:
display = obj.getString("display_url");
break;
default:
display = null;
}
// Init mediaUrl
if ( KEntityType.media.equals(this.type)) {
this.mediaUrl = obj.getString("media_url");
} else {
this.mediaUrl = null;
}
// start, end
JSONArray indices = obj.getJSONArray("indices");
int _start = indices.getInt(0);
int _end = indices.getInt(1);
assert _start >= 0 && _end >= _start : obj;
// Sadly, due to entity encoding, start/end may be off!
String text = tweet.getText();
if (rawText.regionMatches(_start, text, _start, _end - _start)) {
// normal case: all OK
start = _start; end = _end;
return;
}
// oh well - let's correct start/end
// Note: This correction can go wrong in a particular case:
// encoding has messed up the indices & we have a repeated entity.
// ??Do we care enough to fix such a rare corner case with moderately harmless side-effects?
// Protect against (rare) dud data from Twitter
_end = Math.min(_end, rawText.length());
_start = Math.min(_start, _end);
if (_start == _end) { // paranoia -- but it happens (last seen Oct 2012; see TwitterTest)
// Guess blindly by type!
switch(type) {
case hashtags:
break;
case urls:
Matcher m = Regex.VALID_URL.matcher(text);
if (m.find()) {
start = m.start();
end = m.end();
return;
}
break;
case user_mentions:
break;
}
// Fail
end = Math.min(_end, text.length());
start = Math.min(_start, end);
return;
}
String entityText = rawText.substring(_start, _end);
// Handle repeated entities -- eg same url / @name twice at different positions
int from = 0;
for(TweetEntity prev : previous) {
if (tweet.getText().regionMatches(prev.start, entityText, 0, entityText.length())) {
from = prev.end;
}
}
// Find where the referenced text is in the un-encoded version
int i = text.indexOf(entityText, from);
if (i==-1) {
// This can't legitimately happen, but handle it anyway 'cos it does (rare & random)
entityText = InternalUtils.unencode(entityText);
i = text.indexOf(entityText);
if (i==-1) i = _start; // give up gracefully
}
start = i;
end = start + _end - _start;
}
/**
* Constructor for when you know exactly what you want (rare).
*/
TweetEntity(ITweet tweet, KEntityType type, int start, int end, String display) {
this.tweet = tweet;
this.end = end;
this.start = start;
this.type = type;
this.display = display;
this.mediaUrl = null;
}
/**
* @return For a url: the expanded version For a user-mention: the
* user's name
*/
public String displayVersion() {
return display == null ? toString() : display;
}
public String mediaUrl() {
return mediaUrl;
}
/**
* The slice of text in the tweet. E.g. for a url, this will be the
* *shortened* version.
*
* @see #displayVersion()
*/
@Override
public String toString() {
// There is a strange bug where -- rarely -- end > tweet length!
// I think this is now fixed (it was an encoding issue).
String text = tweet.getText();
int e = Math.min(end, text.length());
int s = Math.min(start, e);
return text.substring(s, e);
}
}
/**
* This rather dangerous global toggle switches off lower-casing on Twitter
* screen-names.
*
* Screen-names are case insensitive as far as Twitter is concerned. However
* you might want to preserve the case people use for display purposes.
*
* false by default.
*/
public static boolean CASE_SENSITIVE_SCREENNAMES;
/**
* This global toggle switches on/off length-checking for tweets.
*
* To avoid wasting time or API rate-limit usage, JTwitter can check that outgoing
* tweets meet the maximum-length restriction. Set this to false to disable that
* check (Twitter will still apply their own check!).
*
* true by default.
* @see #countCharacters(String)
*/
public static boolean CHECK_TWEET_LENGTH = true;
/**
* The length of a url after t.co shortening. Currently 23 characters.
* (Used to be 22 for HTTP / 23 for HTTPS but now 23 for all)
*
* Use updateConfiguration()if you want to get the latest settings from
* Twitter.
*/
public static int LINK_LENGTH = 23;
/**
* The characters used up by an attached image. Currently 23 characters (ie = an https link).
*
* Use updateConfiguration()if you want to get the latest settings from
* Twitter.
*/
public static int MEDIA_LENGTH = 23;
/**
* 3mb
*
* https://dev.twitter.com/rest/media/uploading-media
* Note: "It is possible to upload a 5 MB image, but the Tweet creation requires images to be <= 3 MB"
*/
public static long PHOTO_SIZE_LIMIT = 3145728L; // 3mb
/**
* 15mb
*/
public static long VIDEO_SIZE_LIMIT = 10*15 * 1024L * 1024L; // 10*15mb
/**
* 5mb (conservative 1024 x 5000 - could probably be higher)
*/
private static final long MAX_CHUNK_SIZE = 5120000L;
public static final String SEARCH_MIXED = "mixed";
public static final String SEARCH_POPULAR = "popular";
/**
* return the most recent results in the response
*/
public static final String SEARCH_RECENT = "recent";
private static final long serialVersionUID = 1L;
/**
* JTwitter version
*/
public final static String version = "3.8.3";
/**
* The maximum number of characters that a tweet can contain.
*/
public final static int MAX_CHARS = 280;
/** Which version of Twitter API?
* The upgrade to v1.1 implemented here is necessary as of March 2013 */
static final String API_VERSION = "1.1";
static final String DEFAULT_TWITTER_URL = "https://api.twitter.com/"+API_VERSION;
static final String TWITTER_UPLOAD_URL = "https://upload.twitter.com/" + API_VERSION;
/**
* Uploaded media files will be available for use for 60 minutes before they are flushed from the servers (if not associated with a Tweet or Card).
*/
static final String MEDIA_UPLOAD_ENDPOINT = "/media/upload.json";
static final String DM_BASE_ENDPOINT = "/direct_messages/events";
public static int MAX_DM_LENGTH = 10000;
/**
* @deprecated Not used at present
* Set to true to perform extra error-handling & correction.
*/
public static boolean WORRIED_ABOUT_TWITTER = false;
/**
* Convenience method: Finds a user with the given screen-name from the
* list.
*
* @param screenName
* aka login name
* @param users
* @return User with the given name, or null.
*/
public static User getUser(String screenName, List users) {
assert screenName != null && users != null;
for (User user : users) {
if (screenName.equals(user.screenName))
return user;
}
return null;
}
/**
*
* @param args
* Can be used as a command-line tweet tool. To do so, enter 3
* arguments: name, password, tweet
*
* If empty, prints version info.
*/
public static void main(String[] args) {
// Post a tweet if we are handed a name, password and tweet
if (args.length == 3) {
Twitter tw = new Twitter(args[0], args[1]);
// int s = 0;
// List fids = tw.getFollowerIDs();
// for (Long fid : fids) {
// User f = tw.follow(""+fid);
// if (f!=null) s++;
// }
Status s = tw.setStatus(args[2]);
System.out.println(s);
return;
}
System.out.println("Java interface for Twitter");
System.out.println("--------------------------");
System.out.println("Version " + version);
System.out.println("Released under LGPL by Winterwell Associates Ltd.");
System.out
.println("See source code, JavaDoc, or http://winterwell.com for details on how to use.");
}
/**
* TODO merge with {@link #maxResults}??
*/
Integer count;
/**
* Used by search
*/
private String geocode;
final IHttpClient http;
private String appKey;
private String appSecret;
private String accessToken;
boolean includeRTs = true;
private String lang;
/**
* Provides support for fetching many pages of results.
* -1 = 1 page's worth
*/
private int maxResults = -1;
private double[] myLatLong;
/**
* Twitter login name. Can be null even if we have authentication when using
* OAuth.
*/
private String name;
private String resultType;
/**
* The user. Can be null. Can be a "fake-user" (screenname-only) object.
*/
User self;
private Date sinceDate;
private BigInteger sinceId;
private String sourceApp = "jtwitterlib";
boolean tweetEntities = true;
/** Turn off if your code is broken by seeing e.g. long tweets */
boolean extendedMode = true;
/** Turn off if your code only generates old-style tweets where every user tagged is explicitly mentioned in message text */
boolean autoPopulateReplyMetadata = true;
@Deprecated // Keeping for backwards compatibility of serialised form until Q2 2013
private transient String twitlongerApiKey;
@Deprecated // Keeping for backwards compatibility of serialised form until Q2 2013
private transient String twitlongerAppName;
/**
* E.g. "https://api.twitter.com/1.1"
*
* Change this to access sites other than Twitter that support the Twitter
* API, or to set which version of the API you want to use.
* Note: Does not include the final "/"
*/
String TWITTER_URL = DEFAULT_TWITTER_URL;
private Date untilDate;
private BigInteger untilId;
private Long placeId;
/**
* If set, this will place-id be sent with status-updates to geo-tag your tweets.
* @param placeId Can be null (which is the default)
* @see #setMyLocation(double[])
*/
public void setMyPlace(Long placeId) {
this.placeId = placeId;
}
/**
* @deprecated ALL twitter.com endpoints now require authentication.
* This method is kept for use with other services (e.g. identi.ca).
*
* Create a Twitter client without specifying a user.
*/
public Twitter() {
this(null, new URLConnectionHttpClient());
}
/**
* Java wrapper for the Twitter API.
*
* @param name
* the authenticating user's name, if known. Can be null.
* @param client e.g. OAuthSignpostClient
* @see OAuthSignpostClient
*/
public Twitter(String name, IHttpClient client) {
this.name = name;
http = client;
assert client != null;
}
/**
* WARNING: Twitter no longer supports name/password basic authentication.
* This constructor is only for non-Twitter sites, such as identi.ca.
*
* @param screenName
* The name of the user. Only used by some methods.
* @param password
* The password of the user.
*
* @Deprecated Twitter have switched off basic authentication! Use an OAuth
* client such as {@link OAuthSignpostClient} with
* {@link #Twitter(String, IHttpClient)}
*/
@Deprecated
public Twitter(String screenName, String password) {
this(screenName, new URLConnectionHttpClient(screenName, password));
}
/**
* Copy constructor. Use this to pass cloned Twitter objects for
* multi-threaded work. This will share rate-limit info between them :)
*
* @param jtwit
*/
public Twitter(Twitter jtwit) {
this(jtwit.getScreenName(), jtwit.http.copy());
this.accessToken = jtwit.accessToken;
this.appKey = jtwit.appKey;
this.appSecret = jtwit.appSecret;
}
/**
* API methods relating to your account.
*/
public Twitter_Account account() {
return new Twitter_Account(this);
}
/**
* API methods for Twitter stats.
*/
public Twitter_Analytics analytics() {
return new Twitter_Analytics(http);
}
/**
* Add in since_id, page and count, if set. For methods that
* return lists of statuses or messages.
*
* @param vars
* @return vars
*/
Map addListParameters(Map vars) {
if (sinceId != null && sinceId.doubleValue() != 0) {
String s = sinceId.toString();
vars.put("since_id", s);
}
if (untilId != null) {
vars.put("max_id", untilId.toString());
}
if (count != null) {
vars.put("count", count.toString());
}
return vars;
}
/**
* Adds include_entities, include_rts and tweet_mode. For methods
* that return single or lists of statuses or messages.
* @param vars
* @return
*/
Map addTweetParameters (Map vars) {
if (tweetEntities) {
vars.put("include_entities", "1"); // TODO remove after testing -- this is the new default
} else {
vars.put("include_entities", "0");
}
if ( ! includeRTs) {
vars.put("include_rts", "0"); // On is the new default
}
if (extendedMode) {
vars.put("tweet_mode", "extended"); // Ask for non-truncated tweets with full_text replacing text
}
return vars;
}
/**
* Adds all necessary parameters for retrieving lists of tweets.
*
* @param vars
* @return vars
*/
Map addStandardishParameters(
Map vars) {
addListParameters(vars);
addTweetParameters(vars);
return vars;
}
/**
* Equivalent to {@link #follow(String)}. C.f.
* http://apiwiki.twitter.com/Migrating-to-followers-terminology
*
* @param username
* Required. The screen name of the user to befriend.
* @return The befriended user.
* @deprecated Use {@link #follow(String)} instead, which is equivalent.
*/
@Deprecated
public User befriend(String username) throws TwitterException {
return follow(username);
}
/**
* Equivalent to {@link #stopFollowing(String)}.
*
* @deprecated Please use {@link #stopFollowing(String)} instead.
*/
@Deprecated
public User breakFriendship(String username) {
return stopFollowing(username);
}
/**
* @deprecated Use {@link Twitter_Users#show(List)} instead
*/
public List bulkShow(List screenNames) {
return users().show(screenNames);
}
/**
* @deprecated Use {@link #showById(List)} instead
*/
public List bulkShowById(List extends Number> userIds) {
return users().showById(userIds);
}
/**
* Filter keeping only those messages that come between sinceDate and
* untilDate (if either or both are set). The Twitter API used to offer
* this, but we now have to do it client side.
*
* @see #setSinceId(Number)
*
* @param list
* @return filtered list (a copy)
*/
private List dateFilter(List list) {
if (sinceDate == null && untilDate == null)
return list;
ArrayList filtered = new ArrayList(list.size());
for (T message : list) {
// assume OK if Twitter is being stingy on the info
if (message.getCreatedAt() == null) {
filtered.add(message);
continue;
}
if (untilDate != null && untilDate.before(message.getCreatedAt())) {
continue;
}
if (sinceDate != null && sinceDate.after(message.getCreatedAt())) {
continue;
}
// ok
filtered.add(message);
}
return filtered;
}
/**
* Deletes the given Status or Message. The authenticating user must be the
* author of the status post.
*/
public void destroy(ITweet tweet) throws TwitterException {
if (tweet instanceof Status) {
destroyStatus(tweet.getId());
} else {
destroyMessage((Message) tweet);
}
}
/**
* Destroy a direct message.
*
* @param dm
*/
private void destroyMessage(Message dm) {
destroyMessage(dm.id);
}
/**
* Deletes the direct message specified by the ID. The authenticating user
* must be the author of the specified status.
*
* @see #destroy(ITweet)
*/
public void destroyMessage(Number id) {
Map vars = new HashMap();
vars.put("id", id.toString());
String page = post(TWITTER_URL + DM_BASE_ENDPOINT + "/destroy.json", vars, true);
assert page != null;
}
/**
* Deletes the status specified by the required ID parameter. The
* authenticating user must be the author of the specified status.
*
* @see #destroy(ITweet)
*/
public void destroyStatus(Number id) throws TwitterException {
String page = post(TWITTER_URL + "/statuses/destroy/" + id + ".json",
null, true);
// Note: Sends two HTTP requests to Twitter rather than one: Twitter
// appears
// not to make deletions visible until the user's status page is
// requested.
flush();
assert page != null;
}
/**
* Deletes the given status. Equivalent to {@link #destroyStatus(int)}. The
* authenticating user must be the author of the status post.
*
* @deprecated in favour of {@link #destroy(ITweet)}. This method will be
* removed by the end of 2010.
* @see #destroy(ITweet)
*/
@Deprecated
public void destroyStatus(Status status) throws TwitterException {
destroyStatus(status.getId());
}
/**
* Have we got enough results for the current search?
*
* @param list
* @return always false if list is empty, true if maxResults is set to -1 (ie, one-page) or if list
* contains maxResults or more items.
*/
boolean enoughResults(List list) {
if (list.isEmpty()) return false;
// -1 = a default of one page
if (maxResults==-1) return true;
return list.size() >= maxResults;
}
// TODO is this still needed??
void flush() {
// This seems to prompt twitter to update in some cases!
http.getPage("https://twitter.com/" + name, null, true);
}
/**
* @see Twitter_Users#follow(String)
*/
@Deprecated
public User follow(String username) throws TwitterException {
return users().follow(username);
}
@Override
public String toString() {
return name==null? "Twitter" : "Twitter["+name+"]";
}
/**
* @see Twitter_Users#follow(User)
*/
@Deprecated
public User follow(User user) {
return follow(user.screenName);
}
/**
* Geo-location API methods.
* Doesn't require a logged in user.
*/
public Twitter_Geo geo() {
return new Twitter_Geo(this);
}
/**
* Returns a single direct message to the authenticating user, specified by ID
* @param id The DM ID.
*/
public Message getDirectMessage(Number id) {
boolean auth = InternalUtils.authoriseIn11(this);
Map vars = InternalUtils.asMap("id", id);
String json = http.getPage(TWITTER_URL + DM_BASE_ENDPOINT + "/show.json", vars, auth);
try {
JSONObject response = new JSONObject(json);
Message message = new Message(response.getJSONObject("event"));
return message;
} catch (JSONException e) {
throw new TwitterException.Parsing(json, e);
}
}
/**
* Returns a list of the direct messages to AND from the authenticating user.
*
* Note: the Twitter API makes this available in rss if that's of interest.
*/
public List getDirectMessages() {
InternalUtils.log("jtwitter.dm", "as:"+getScreenNameIfKnown()+"...");
return getMessages(standardishParameters());
}
/**
* The most recent 20 favourite tweets. (Note: This can use page - and page
* only - to fetch older favourites).
*/
public List getFavorites() {
return getFavorites(null);
}
/**
* The most recent 20 favourite tweets for the given user.
*
* @param screenName
* login-name.
*/
public List getFavorites(String screenName) {
Map vars = InternalUtils.asMap("screen_name",
screenName);
return getStatuses(TWITTER_URL + "/favorites/list.json",
addStandardishParameters(vars), http.canAuthenticate());
}
/**
* @see Twitter_Users#getFollowerIDs()
*/
@Deprecated
public List getFollowerIDs() throws TwitterException {
return users().getFollowerIDs();
}
/**
* @see Twitter_Users#getFollowerIDs(String)
*/
@Deprecated
public List getFollowerIDs(String screenName)
throws TwitterException {
return users().getFollowerIDs(screenName);
}
/**
* @see Twitter_Users#getFollowers()
*/
@Deprecated
public List getFollowers() throws TwitterException {
return users().getFollowers();
}
/**
* @see Twitter_Users#getFollowers(String)
*/
@Deprecated
public List getFollowers(String username) throws TwitterException {
return users().getFollowers(username);
}
/**
* @see Twitter_Users#getFriendIDs()
*/
@Deprecated
public List getFriendIDs() throws TwitterException {
return users().getFriendIDs();
}
/**
* @see Twitter_Users#getFriendIDs(String)
*/
@Deprecated
public List getFriendIDs(String screenName) throws TwitterException {
return users().getFriendIDs(screenName);
}
/**
* @see Twitter_Users#getFriends()
*/
@Deprecated
public List getFriends() throws TwitterException {
return users().getFriends();
}
/**
* @see Twitter_Users#getFriendss(String)
*/
@Deprecated
public List getFriends(String username) throws TwitterException {
return users().getFriends(username);
}
/**
* Returns the 20 most recent statuses posted in the last 24 hours from the
* authenticating user and that user's friends.
*
* @deprecated Replaced by {@link #getHomeTimeline()}
*/
@Deprecated
public List getFriendsTimeline() throws TwitterException {
return getHomeTimeline();
}
/**
* Returns the 20 most recent statuses posted in the last 24 hours from the
* authenticating user and that user's friends, including retweets.
*/
public List getHomeTimeline() throws TwitterException {
assert http.canAuthenticate();
return getStatuses(TWITTER_URL + "/statuses/home_timeline.json",
standardishParameters(), true);
}
/**
* Provides access to the {@link IHttpClient} which manages the low-level
* authentication, posts and gets.
*/
public IHttpClient getHttpClient() {
return http;
}
/**
* @return your lists, ie. the one's you made.
*/
public List getLists() {
return getLists(name);
}
/**
*
Returns all lists the authenticating or specified user subscribes to,
including their own.
@param user can be null for the authenticating user.
@see #getLists(String)
*/
public List getListsAll(User user) {
assert user!=null || http.canAuthenticate() : "No authenticating user";
try {
String url = TWITTER_URL + "/lists/all.json";
Map vars = user.screenName==null?
InternalUtils.asMap("user_id", user.id)
: InternalUtils.asMap("screen_name", user.screenName);
String listsJson = http.getPage(url, vars, http.canAuthenticate());
JSONObject wrapper = new JSONObject(listsJson);
JSONArray jarr = (JSONArray) wrapper.get("lists");
List lists = new ArrayList();
for (int i = 0; i < jarr.length(); i++) {
JSONObject li = jarr.getJSONObject(i);
TwitterList twList = new TwitterList(li, this);
lists.add(twList);
}
return lists;
} catch (JSONException e) {
throw new TwitterException.Parsing(null, e);
}
}
/**
* @param screenName
* @return the (first 20) lists created by the given user
*/
public List getLists(String screenName) {
assert screenName != null;
try {
String url = TWITTER_URL + "/lists/list.json";
Map vars = InternalUtils.asMap(
"screen_name", screenName);
String listsJson = http.getPage(url, vars, true);
// JSONObject wrapper = new JSONObject(listsJson);
JSONArray jarr = new JSONArray(listsJson); // wrapper.get("lists");
List lists = new ArrayList();
for (int i = 0; i < jarr.length(); i++) {
JSONObject li = jarr.getJSONObject(i);
TwitterList twList = new TwitterList(li, this);
lists.add(twList);
}
return lists;
} catch (JSONException e) {
throw new TwitterException.Parsing(null, e);
}
}
/**
* @param screenName
* @param filterToOwned
* If true, only return lists which the user owns.
* @return lists of which screenName is a member. NOTE: currently limited to
* a maximum of 20 lists!
*/
public List getListsContaining(String screenName,
boolean filterToOwned) {
assert screenName != null;
try {
String url = TWITTER_URL + "/lists/memberships.json";
Map vars = InternalUtils.asMap("screen_name",
screenName);
if (filterToOwned) {
assert http.canAuthenticate();
vars.put("filter_to_owned_lists", "1");
}
String listsJson = http.getPage(url, vars, http.canAuthenticate());
JSONObject wrapper = new JSONObject(listsJson);
JSONArray jarr = (JSONArray) wrapper.get("lists");
List lists = new ArrayList();
for (int i = 0; i < jarr.length(); i++) {
JSONObject li = jarr.getJSONObject(i);
TwitterList twList = new TwitterList(li, this);
lists.add(twList);
}
return lists;
} catch (JSONException e) {
throw new TwitterException.Parsing(null, e);
}
}
/**
* Convenience for {@link #getListsContaining(String, boolean)}.
*
* @return lists that you are a member of. Warning: currently limited to a
* maximum of 20 results.
*/
public List getListsContainingMe() {
return getListsContaining(name, false);
}
/**
* @deprecated Use {@link TwitLonger}
* @param truncatedStatus
* If this is a twitlonger.com truncated status, then call
* twitlonger to fetch the full text.
* @return the full status message. If this is not a twitlonger status, this
* will just return the status text as-is.
* @see #updateLongStatus(String, long)
*/
public String getLongStatus(Status truncatedStatus) {
TwitLonger tl = new TwitLonger();
return tl.getLongStatus(truncatedStatus);
}
/**
* Provides support for fetching many pages. -1 indicates "give me 1 page's worth"
*/
public int getMaxResults() {
return maxResults;
}
/**
* Returns the 20 most recent replies/mentions (status updates with
* \@username) to the authenticating user. Replies are only available to the
* authenticating user; you cannot request a list of replies to another
* user whether public or protected.
*
* This is exactly the same as {@link #getReplies()}
*
* When paging, this method can only go back up to 800 statuses.
*
* Does not include new-style retweets.
*/
public List getMentions() {
return getStatuses(TWITTER_URL + "/statuses/mentions_timeline.json",
standardishParameters(), true);
}
/**
*
* @param url
* @param vars
* @param isPublic
* Value to set for Message.isPublic
* @return
*/
private List getMessages(Map vars) {
String url = TWITTER_URL + DM_BASE_ENDPOINT + "/list.json";
// Twitter truncates DMs to 140 chars to maintain compatibility with older apps
// Add param "full_text=true" to query to get full text, unless already set
if(!vars.containsKey("full_text")) {
vars.put("full_text", "true");
}
/*
if (json.trim().equals(""))
return Collections.emptyList();
try {
JSONArray array = new JSONArray(json);
return getMessages(array);
} catch (JSONException e) {
throw new TwitterException.Parsing(json, e);
}
*/
// Default: 1 page
if (maxResults < 1) {
String json = http.getPage(url, vars, true);
JSONObject response = new JSONObject(json);
JSONArray events = response.optJSONArray("events");
if (events == null || events.length() == 0) {
return new ArrayList();
}
List msgs = Message.getMessages(events);
msgs = dateFilter(msgs);
return msgs;
}
// Fetch all pages until we run out
// -- or Twitter complains in which case you'll get an exception
String nextCursor = null;
List msgs = new ArrayList();
while (msgs.size() <= maxResults) {
String json = http.getPage(url, vars, true);
JSONObject response = new JSONObject(json);
JSONArray events = response.optJSONArray("events");
nextCursor = response.optString("next_cursor");
List page = Message.getMessages(events);
List pageDateFiltered = dateFilter(page);
if (pageDateFiltered.isEmpty()) {
// Results are sorted newest first - we've got everything we came for so stop
break;
}
msgs.addAll(pageDateFiltered);
if (nextCursor == null || nextCursor.isEmpty()) {
// no more messages to retrieve in the last 30 days
break;
}
vars.put("cursor", nextCursor);
}
return msgs;
}
/**
* @deprecated Use getHttpClient().getRateLimits()
* What is the current rate limit status? Do we need to throttle back our
* usage? This is the cached info from the last call of that type.
*
* Note: The RateLimit object is created using cached info from a previous
* Twitter call. So this method is quick (it doesn't require a fresh call to
* Twitter), but the RateLimit object isn't available until after you make a
* call of the right type to Twitter.
*
* Status: Heading towards stable, but still a bit experimental.
*
* @param reqType
* Different methods have separate rate limits.
* @return the last rate limit advice received, or null if unknown.
* @see #getRateLimitStatus()
*/
public RateLimit getRateLimit(KRequestType reqType) {
return http.getRateLimit(reqType);
}
/**
* @deprecated Not in v1.1
*
* How many normal rate limit calls do you have left? This calls Twitter,
* which makes it slower than {@link #getRateLimit(KRequestType)} but it's
* up-to-date and safe against threads and other-programs using the same
* allowance.
*
* This may update getRateLimit(KRequestType) for NORMAL requests, but sadly
* it doesn't fetch rate-limit info on other request types.
*
* @return the remaining number of API requests available to the
* authenticating user before the API limit is reached for the
* current hour. If this is zero or negative you should stop
* using Twitter with this login for a bit. Note: Calls to
* rate_limit_status do not count against the rate limit.
* @see #getRateLimit(KRequestType)
*/
public int getRateLimitStatus() {
RateLimit rl = ((URLConnectionHttpClient)http).updateRateLimits().get(KRequestType.NORMAL.rateLimit);
return rl==null? 90 : rl.getRemaining();
}
/**
* Returns the 20 most recent replies/mentions (status updates with
*
* @username) to the authenticating user. Replies are only available to the
* authenticating user; you can not request a list of replies to
* another user whether public or protected.
*
* This is exactly the same as {@link #getMentions()}! Twitter
* changed their API & terminology - we are (currently) keeping
* both methods.
*
* When paging, this method can only go back up to 800 statuses.
*
* Does not include new-style retweets.
* @deprecated Use #getMentions() for preference. This method will be removed June 2013.
*/
public List getReplies() throws TwitterException {
return getMentions();
}
/**
* Show users who (new-style) retweeted the given tweet. Can use count (up
* to 100) and page. This does not include old-style retweeters!
*
* @param tweet
* You can use a "fake" Status created via
* {@link Status#Status(User, String, long, Date)} if you know
* the id number.
*/
public List getRetweeters(Status tweet) {
String url = TWITTER_URL + "/statuses/retweets/" + tweet.id
+ ".json";
Map vars = addStandardishParameters(new HashMap());
String json = http.getPage(url, vars, http.canAuthenticate());
List ss = Status.getStatuses(json);
List users = new ArrayList(ss.size());
for (Status status : ss) {
users.add(status.getUser());
}
return users;
}
/**
* @return Retweets of this tweet. This attempts to cover new-style and
* old-style "manual" retweets. It does so by making retweet call
* and a search call. It will miss edited retweets though.
*/
public List getRetweets(Status tweet) {
String url = TWITTER_URL + "/statuses/retweets/" + tweet.id + ".json";
Map vars = addStandardishParameters(new HashMap());
String json = http.getPage(url, vars, true);
List newStyle = Status.getStatuses(json);
try {
// // Should we also do by search and merge the two lists?
StringBuilder sq = new StringBuilder();
sq.append("\"RT @" + tweet.getUser().getScreenName() + ": ");
if (sq.length() + tweet.text.length() + 1 > MAX_CHARS) {
int i = tweet.text.lastIndexOf(' ', MAX_CHARS - sq.length() - 1);
String words = tweet.text.substring(0, i);
sq.append(words);
} else {
sq.append(tweet.text);
}
sq.append('"');
List oldStyle = search(sq.toString());
// merge them
newStyle.addAll(oldStyle);
Collections.sort(newStyle, InternalUtils.NEWEST_FIRST);
return newStyle;
} catch (TwitterException e) {
// oh well
return newStyle;
}
}
/**
* @deprecated Removed in api v1.1. Simulated with other methods. Will be removed June 2013
*
* @return retweets that you have made using "new-style" retweets rather
* than the RT microfromat. These are your tweets, i.e. they begin
* "RT @whoever: ". You can get the original tweet via
* {@link Status#getOriginal()}
*/
public List getRetweetsByMe() {
List myTweets = getUserTimeline();
List retweets =new ArrayList();
for (Status status : myTweets) {
if (status.getOriginal()!=null && status.getText().startsWith("RT")) {
retweets.add(status);
}
}
return retweets;
}
/**
* @return those of your tweets that have been retweeted. It's a bit of a
* strange one this. You can then query who retweeted you.
*/
public List getRetweetsOfMe() {
String url = TWITTER_URL + "/statuses/retweets_of_me.json";
Map vars = addStandardishParameters(new HashMap());
String json = http.getPage(url, vars, true);
return Status.getStatuses(json);
}
/**
* @return Login name of the authenticating user, or null if not set.
*
* Will call Twitter to find out if null but oauth is set.
* @see #getSelf()
*/
public String getScreenName() {
if (name != null)
return name;
// load if need be
getSelf();
return name;
}
/**
* Equivalent to {@link #getScreenName()} except this won't ever do
* an API call.
* @return screenName or null
* @see #getScreenName()
*/
public String getScreenNameIfKnown() {
return name;
}
/**
* @param searchTerm
* @param rpp
* @return
*/
private Map getSearchParams(String searchTerm, Integer rpp) {
Map vars = InternalUtils.asMap(
"count", rpp,
"q", searchTerm);
if (sinceId != null && sinceId.doubleValue()!=0) {
vars.put("since_id", sinceId.toString());
}
if (untilId != null) {
// It's unclear from the docs whether this will work
// c.f. https://dev.twitter.com/docs/api/1/get/search
vars.put("max_id", untilId.toString());
}
// since date is no longer supported. until is though?!
// if (sinceDate != null) vars.put("since", df.format(sinceDate));
if (untilDate != null) {
vars.put("until", InternalUtils.df.format(untilDate));
}
if (lang != null) {
vars.put("lang", lang);
}
if (geocode != null) {
vars.put("geocode", geocode);
}
if (resultType != null) {
vars.put("result_type", resultType);
}
addStandardishParameters(vars);
return vars;
}
/**
* @return you, or null if this is an anonymous Twitter object.
*
* This will cache the result if it makes an API call.
*/
public User getSelf() {
if (self != null)
return self;
if (!http.canAuthenticate()) {
if (name != null) {
// not sure this case makes sense, but we may as well handle it
self = new User(name);
return self;
}
return null;
}
account().verifyCredentials();
name = self.getScreenName();
return self;
}
/**
* @return The current status of the user. Warning: this is null if (a)
* unset (ie if this user has never tweeted), or (b) their last six
* tweets were all new-style retweets!
* @see #getUserTimeline()
*
* Minor Warning: There can be a very slight delay in Twitter for a status-update
* to take effect (i.e. for the tweet to become visible). Which means if you have
* *just* called updateStatus(), then getStatus() may not match.
*/
public Status getStatus() throws TwitterException {
Map vars = standardishParameters();
vars.put("count", "6");
String json = http.getPage(
TWITTER_URL + "/statuses/user_timeline.json", vars, true);
List statuses = Status.getStatuses(json);
if (statuses.size() == 0)
return null;
return statuses.get(0);
}
/**
* Returns a single status, specified by the id parameter below. The
* status's author will be returned inline.
*
* @param id
* The numerical ID of the status you're trying to retrieve.
*/
public Status getStatus(Number id) throws TwitterException {
boolean auth = InternalUtils.authoriseIn11(this);
Map vars = standardishParameters();
String json = http.getPage(TWITTER_URL + "/statuses/show/" + id
+ ".json", vars, auth);
try {
return new Status(new JSONObject(json), null);
} catch (JSONException e) {
throw new TwitterException.Parsing(json, e);
}
}
/**
* @return The current status of the given user.
*
* Warning: this can be null if the user has been doing enough
* new-style retweets. This is due to flaws in the Twitter API.
*/
public Status getStatus(String username) throws TwitterException {
assert username != null;
// new-style retweets can cause blanks in your timeline
// show(username).status is just as vulnerable
// grab a few tweets to give some robustness
Map vars = standardishParameters();
vars.put("id", username);
vars.put("count", "6");
String json = http.getPage(
TWITTER_URL + "/statuses/user_timeline.json", vars, http.canAuthenticate());
List statuses = Status.getStatuses(json);
if (statuses.size() == 0)
return null;
return statuses.get(0);
}
/**
* Does the grunt work for paged status fetching
*
* @param url
* @param var
* @param authenticate
* @return
*/
List getStatuses(final String url, Map var,
boolean authenticate)
{
// Default: 1 page
if (maxResults < 1) {
List msgs ;
try {
msgs = Status.getStatuses(http.getPage(url, var,
authenticate));
} catch (TwitterException.Parsing pex) {
// Twitter bug, July 2012: malformed responses -- end is chopped off ~1 time in 20
// TODO remove when Twitter fix this!
if (http.isRetryOnError()) {
InternalUtils.sleep(250);
String json = http.getPage(url, var, authenticate);
msgs = Status.getStatuses(json);
} else {
throw pex;
}
}
msgs = dateFilter(msgs);
return msgs;
}
// Fetch all pages until we reach the desired maxResults, or run out
// -- or Twitter complains in which case you'll get an exception
// Use status ids for paging, rather than page number, because this
// allows for "drift" when new tweets are posted during the paging.
BigInteger maxId = untilId;
List msgs = new ArrayList();
while (msgs.size() <= maxResults) {
List nextpage;
try {
String json = http.getPage(url, var, authenticate);
nextpage = Status.getStatuses(json);
} catch (TwitterException.Parsing pex) {
// Twitter bug, July 2012: malformed responses -- end is chopped off ~1 time in 20
// TODO remove when Twitter fix this!
if (http.isRetryOnError()) {
InternalUtils.sleep(250);
String json = http.getPage(url, var, authenticate);
nextpage = Status.getStatuses(json);
} else {
throw pex;
}
}
// This test replaces size<20. It requires an extra call to Twitter.
// But it fixes a bug whereby retweets aren't counted and can thus
// cause the system to quit early.
if (nextpage.size() == 0) {
break;
}
// Next page must start strictly before this one
maxId = InternalUtils.getMinId(maxId, nextpage);
Date maxDate = InternalUtils.getMaxDate(nextpage);
List filtered = dateFilter(nextpage);
msgs.addAll(filtered);
// If we've passed the sinceDate, and we've started to filter messages, to the point
// where we receive none. then we need to stop probing to avoid looping over ever-older messages,
// and the rate-limiting that'll cause.
if (filtered.size() == 0 && sinceDate != null && sinceDate.after(maxDate)){
break;
}
var.put("max_id", maxId.toString());
}
return msgs;
}
public InputStream getDMImage(Message msg) {
List entities = msg.getTweetEntities(KEntityType.media);
if (entities.isEmpty()) return null;
String mediaUrl = entities.get(0).mediaUrl();
try {
HttpURLConnection connection = http.connect(mediaUrl, null, true);
return connection.getInputStream();
} catch (IOException e) {
return null;
}
}
/**
* @return the latest global trending topics on Twitter
*/
public List getTrends() {
return getTrends(1);
}
/**
* @param a
* Yahoo Where-on-Earth ID. c.f.
* http://developer.yahoo.com/geo/geoplanet/
* @return the latest regional trending topics on Twitter
* @see Twitter_Geo#getTrendRegions()
*/
public List getTrends(Number woeid) {
String jsonTrends = http.getPage(TWITTER_URL + "/trends/place.json",
InternalUtils.asMap("id", woeid), true);
try {
JSONArray jarr = new JSONArray(jsonTrends);
JSONObject json1 = jarr.getJSONObject(0);
JSONArray json2 = json1.getJSONArray("trends");
List trends = new ArrayList();
for (int i = 0; i < json2.length(); i++) {
JSONObject ti = json2.getJSONObject(i);
String t = ti.getString("name");
trends.add(t);
}
return trends;
} catch (JSONException e) {
throw new TwitterException.Parsing(jsonTrends, e);
}
}
/**
* @return the untilDate
*/
public Date getUntilDate() {
return untilDate;
}
/**
* @see Twitter_Users#getUser(long)
*/
@Deprecated
public User getUser(long userId) {
return show(userId);
}
/**
* @see Twitter_Users#getUser(String)
*/
@Deprecated
public User getUser(String screenName) {
return show(screenName);
}
/**
* Returns the most recent statuses from the authenticating user. 20 by
* default.
*/
public List getUserTimeline() throws TwitterException {
return getStatuses(TWITTER_URL + "/statuses/user_timeline.json",
standardishParameters(), true);
}
/**
* Equivalent to {@link #getUserTimeline(String)}, but takes a numeric
* user-id instead of a screen-name.
*
* @param userId
* @return tweets by userId
*/
public List getUserTimeline(Long userId) throws TwitterException {
Map vars = InternalUtils.asMap("user_id", userId);
addStandardishParameters(vars);
// Authenticate if we can (for protected streams)
boolean authenticate = http.canAuthenticate();
try {
return getStatuses(TWITTER_URL + "/statuses/user_timeline.json",
vars, authenticate);
} catch (E401 e) {
// Bug in Twitter: this can be a suspended user...
// In which case the call below would generate a SuspendedUser exception
// ...but do we want to conserve our api limit??
// isSuspended(userId);
throw e;
}
}
/**
* Returns the most recent statuses from the given user.
*
* This will return 20 results by default, though
* {@link #setMaxResults(int)} can be used to fetch multiple pages, or
* {@link #setUntilId(Number)} can be used to page backwards.
*
* Note that if you exclude new-style retweets (via
* {@link #setIncludeRTs(boolean)}) then this can return less than 20
* results -- it can even return none if the latest 20 are all retweets.
*
* There is a cap of 3200 tweets - this is the farthest back you can go down
* a user timeline!
*
* This method will authenticate if it can (i.e. if the Twitter object has a
* username and password). Authentication is needed to see the posts of a
* private user.
*
* @param screenName
* Can be null. Specifies the screen name of the user for whom to
* return the user_timeline.
* @throws TwitterException.E401
* if the user has protected their tweets, and you do not have
* access.
* @throws TwitterException.SuspendedUser
* if the user has been suspended
*/
public List getUserTimeline(String screenName)
throws TwitterException {
Map vars = InternalUtils.asMap("screen_name",
screenName);
addStandardishParameters(vars);
// Should we authenticate?
boolean authenticate = http.canAuthenticate();
try {
return getStatuses(TWITTER_URL + "/statuses/user_timeline.json",
vars, authenticate);
} catch (E404 e){
throw new TwitterException.E404("Twitter does not return any information for " + screenName +
". They may have been deleted long ago.");
} catch (E401 e) {
// Bug in Twitter: this can be a suspended user
// - in which case this will generate a SuspendedUser exception
isSuspended(screenName);
throw e;
}
}
/**
* @deprecated Use {@link #setIncludeRTs(boolean)} instead to control
* retweet behaviour.
*
* Returns the most recent statuses posted by the given user.
* Unlike {@link #getUserTimeline(String)}, this includes
* new-style retweets.
*
* This will return 20 by default, though
* {@link #setMaxResults(int)} can be used to fetch multiple
* pages. There is a cap of 3200 tweets - this is the farthest
* back you can go down a user timeline!
*
* This method will authenticate if it can (i.e. if the Twitter
* object has a username and password). Authentication is needed
* to see the posts of a private user.
*
@param screenName
* Can be null. Specifies the screen name of the user for whom to
* return the user_timeline.
*
*/
public List getUserTimelineWithRetweets(String screenName)
throws TwitterException {
Map vars = InternalUtils.asMap("screen_name",
screenName, "include_rts", "1");
addStandardishParameters(vars);
// Should we authenticate?
boolean authenticate = http.canAuthenticate();
try {
return getStatuses(TWITTER_URL + "/statuses/user_timeline.json",
vars, authenticate);
} catch (E401 e) {
isSuspended(screenName);
throw e;
}
}
/**
* @see Twitter_Users#isFollower(String)
*/
@Deprecated
public boolean isFollower(String userB) {
return isFollower(userB, name);
}
/**
* @deprecated
* @see Twitter_Users#isFollower(String, String)
*/
public boolean isFollower(String followerScreenName,
String followedScreenName) {
return users().isFollower(followerScreenName, followedScreenName);
}
/**
* @see Twitter_Users#isFollowing(String)
*/
@Deprecated
public boolean isFollowing(String userB) {
return isFollower(name, userB);
}
/**
* @see Twitter_Users#isFollowing(User)
*/
@Deprecated
public boolean isFollowing(User user) {
return isFollowing(user.screenName);
}
/**
* @deprecated
* Are we rate-limited, based on cached info from previous requests?
* @param type
* @param minCalls
* Standard value = 1. The minimum number of calls which should
* be available.
* @return true if this is currently rate-limited, & should not be used for
* a while. false = OK
*
* @see #getRateLimit(KRequestType) for more info
* @see #getRateLimitStatus() for guaranteed up-to-date info
*/
public boolean isRateLimited(KRequestType reqType, int minCalls) {
RateLimit rl = getRateLimit(reqType);
// assume things are OK
if (rl == null) {
return false;
}
// in credit?
if (rl.getRemaining() >= minCalls)
return false;
// out of date?
if (rl.isOutOfDate())
return false;
// nope - you're over the limit
return true;
}
/**
* Generate an exception if the use is suspended. This is used as a
* work-around for misleading error codes returned by Twitter.
*
* @param screenName
* @throws SuspendedUser
*/
private void isSuspended(String screenName) throws SuspendedUser {
show(screenName);
}
/**
* @deprecated Use {@link TwitLonger}
* Keeping for backwards compatibility until Q2 2013
*
* @return true if {@link #setupTwitlonger(String, String)} has been used to
* provide twitlonger.com details.
* @see #updateLongStatus(String, long)
*/
public boolean isTwitlongerSetup() {
return twitlongerApiKey != null && twitlongerAppName != null;
}
/**
* Are the login details used for authentication valid?
*
* @return true if OK, false if unset or invalid
* @see Twitter_Account#verifyCredentials() which returns user info
*/
public boolean isValidLogin() {
if (!http.canAuthenticate())
return false;
try {
Twitter_Account ta = new Twitter_Account(this);
User u = ta.verifyCredentials();
return true;
} catch (TwitterException.E403 e) {
return false;
} catch (TwitterException.E401 e) {
return false;
} catch (TwitterException e) {
throw e;
}
}
/**
* Wrapper for {@link IHttpClient#post(String, Map, boolean)}.
*/
private String post(String uri, Map vars,
boolean authenticate) throws TwitterException {
String page = http.post(uri, vars, authenticate);
return page;
}
/**
* Wrapper for {@link IHttpClient#postJSON(String, JSONOBject, boolean)}.
*/
private String postJSON(String uri, JSONObject body,
boolean authenticate) throws TwitterException {
String page = http.postJSON(uri, body, authenticate);
return page;
}
/**
* Report a user for being a spammer.
*
* @param screenName
*/
public void reportSpam(String screenName) {
http.getPage(TWITTER_URL + "/version/report_spam.json",
InternalUtils.asMap("screen_name", screenName), true);
}
/**
* Retweet (new-style) a tweet without any edits. You can also retweet by
* starting a status using the RT @username microformat. (this is an
* old-style retweet).
*
* @param tweet
* Note: you cannot retweet your own tweets.
* @return your retweet
*/
public Status retweet(Status tweet) {
try {
Map vars = extendedMode ? InternalUtils.asMap("tweet_mode", "extended") : null;
String result = post(
TWITTER_URL + "/statuses/retweet/" + tweet.getId()
+ ".json", vars, true);
return new Status(new JSONObject(result), null);
// error handling
} catch (E403 e) {
List rts = getRetweetsByMe();
for (Status rt : rts) {
if (tweet.equals(rt.getOriginal()))
throw new TwitterException.Repetition(rt.getText());
}
throw e;
} catch (JSONException e) {
throw new TwitterException.Parsing(null, e);
}
}
/**
* Save the application key and secret so this instance can retrieve a bearer token for app-level methods
* (e.g. Account Activity API: set up webhooks, list subscriptions etc)
* @param key The application key
* @param secret The application secret
*/
public void enableAppAuth(String key, String secret) {
this.appKey = key;
this.appSecret = secret;
}
/**
* Save a previously-obtained bearer token so this instance can use app-level methods
* @param token The bearer token
*/
public void enableAppAuth(String token) {
this.accessToken = token;
}
/**
* Return the bearer token for app-level methods - directly if we have it stored
* Otherwise request one from Twitter using the app key and secret.
* @return
* @throws IllegalStateException If this instance has no bearer token and no key/secret to obtain one
* @throws TwitterException if key/secret are invalid
*/
private String getAccessToken() throws IllegalStateException, TwitterException {
if (accessToken != null && !accessToken.isEmpty()) {
return accessToken; // We've already got one
} else {
// We don't have any way of getting one!
if (appKey == null || appKey.isEmpty() || appSecret == null || appSecret.isEmpty()) {
throw new IllegalStateException("You must enable app-level auth by calling enableAppAuth() with the application key and secret or a previously-obtained access token before you use this method.");
}
// Try to retrieve a token...
Map body = new HashMap();
body.put("grant_type", "client_credentials");
IHttpClient basicClient = new URLConnectionHttpClient(appKey, appSecret);
String basicAuthResponse = basicClient.post("https://api.twitter.com/oauth2/token", body, false);
Object response = new JSONObject(basicAuthResponse).get("access_token");
this.accessToken = response.toString(); // cache it
return accessToken;
}
}
/**
* Register a new Webhooks URL, to which all updates for the specified environment will be sent.
* This Twitter object should be set up with authorisation for the account which owns the application.
* TODO is that correct, or is any user OK?
* @param webhookUrl
* @param envName
* @return
*/
public BigInteger registerWebhook(String webhookUrl, String envName) throws Exception {
String endpointUrl = TWITTER_URL + "/account_activity/all/" + envName + "/webhooks.json";
String rawResponse = http.post(endpointUrl + "?url=" + InternalUtils.urlEncode(webhookUrl), null, true);
JSONObject response = new JSONObject(rawResponse);
return new BigInteger(response.getString("id"));
}
/**
* Unregister a Webhooks URL - updates for the specified environment will no longer be sent to it.
* @param webhookId The numeric ID of the webhook to delete
* @param envName The name of the environment to remove the webhook from
*/
public void unregisterWebhook(BigInteger webhookId, String envName) throws TwitterException {
String endpointUrl = TWITTER_URL + "/account_activity/all/" + envName + "/webhooks/" + webhookId + ".json";
http.delete(endpointUrl, true);
// Successful call returns HTTP 204 NO CONTENT - failure throws exception
}
/**
* Start sending activity updates for the authorised account to the Webhooks URLs registered to the specified environment.
* @param envName
* @return
*/
public void subscribeAccountActivity(String envName) {
http.post(TWITTER_URL + "/account_activity/all/" + envName + "/subscriptions.json", null, true);
// Successful call returns HTTP 204 NO CONTENT - failure throws exception
}
/**
* Stop sending activity updates for the authorised account to the Webhooks URLs registered to the specified environment.
* @param envName
* @return
*/
public void unsubscribeAccountActivity(String envName) throws TwitterException{
http.delete(TWITTER_URL + "/account_activity/all/" + envName + "/subscriptions.json", true);
// Successful call returns HTTP 204 NO CONTENT - failure throw exception
}
/**
* Return the total number of account activity subscriptions on the specified environment.
* @param envName
* @return
*/
public Integer countAccountActivitySubscriptions(String envName) {
try {
String accessToken = getAccessToken();
BearerAuthHttpClient baHttp = new BearerAuthHttpClient();
baHttp.setBearerToken(accessToken);
String rawResult = baHttp.getPage(TWITTER_URL + "/account_activity/all/subscriptions/count.json", null, true);
JSONObject result = new JSONObject(rawResult);
return result.getInt("subscriptions_count");
} catch (Exception e) {
System.out.println(e.getMessage());
}
return null;
}
/**
* Return a list of account activity subscriptions on the specified environment
* @param envName
* @return
* @throws TwitterException
* @throws IllegalStateException If called without first providing either an app key/secret or bearer token
*/
public List listAccountActivitySubscriptions(String envName) throws TwitterException, IllegalStateException {
String accessToken = getAccessToken();
BearerAuthHttpClient baHttp = new BearerAuthHttpClient();
baHttp.setBearerToken(accessToken);
String result = baHttp.getPage(TWITTER_URL + "/account_activity/all/" + envName + "/subscriptions/list.json", null, true);
JSONObject json = new JSONObject(result);
Webhooks.SubscriptionList subs = new Webhooks.SubscriptionList(json);
return subs.subscriptions;
}
/**
* Return a list of registered Webhooks for the application
* @return
* @throws TwitterException
* @throws IllegalStateException If called without first providing either an app key/secret or bearer token
*/
public Webhooks.WebhookList getWebhooks() throws TwitterException, IllegalStateException {
String accessToken = getAccessToken();
BearerAuthHttpClient baHttp = new BearerAuthHttpClient();
baHttp.setBearerToken(accessToken);
String responseRaw = baHttp.getPage(TWITTER_URL + "/account_activity/all/webhooks.json", null, true);
JSONObject response = new JSONObject(responseRaw);
return new Webhooks.WebhookList(response);
}
/**
* Retweet, adding a comment. This is also known as a Quote Tweet.
* See https://support.twitter.com/articles/20169873
* @param tweet
* @param comment This must be 116 characters or less, as the retweet counts like a url (which, technically, is what it is here).
* @return the retweet
*/
public Status retweetWithComment(Status tweet, String comment) {
if (comment==null) return retweet(tweet);
comment = comment.trim();
if (comment.length()==0) return retweet(tweet);
// should we mark it in reply to tweet??
Status s = setStatus(comment+" "+tweet.getUrl());
return s;
}
/**
* Perform a search of Twitter. Convenience wrapper for
* {@link #search(String, ICallback, int)} with no callback and fetching one
* pages worth of results.
*/
public List search(String searchTerm) {
return search(searchTerm, null, 100);
}
/**
* Perform a search of Twitter.
*
* Warning: the User objects returned by a search (as part of the Status
* objects) are dummy-users. The only information that is set is the user's
* screen-name and a profile image url. This reflects the current behaviour
* of the Twitter API. If you need more info, call users().show()
* with the screen names.
*
* This supports {@link #maxResults} and pagination. A language filter can
* be set via {@link #setLanguage(String)} Location can be set via
* {@link #setSearchLocation(double, double, String)}
*
* Other advanced search features can be done via the query string. E.g.
* "from:winterstein" - tweets from user winterstein
* "to:winterstein" - tweets start with @winterstein
* "source:jtwitter" - originating from the application JTwitter - your
* query must also must contain at least one keyword parameter.
* "filter:links" - tweets contain a link
* "apples OR pears" - or ("apples pears" would give you apples and
* pears).
*
* @param searchTerm
* This can include several space-separated keywords, #tags and @username
* (for mentions), and use quotes for \"exact phrase\" searches.
* Limited to 1,000 characters maximum, including operators.
* Queries may additionally be limited by complexity.
* @param callback
* an object whose process() method will be called on each new
* page of results.
* @param rpp
* results per page. 100 is the default
* @return search results - up to maxResults if maxResults is positive, or
* rpp if maxResults is negative/zero. See
* {@link #setMaxResults(int)} to use > 100.
*
* @throws E403 or E406 if the search query can't be handled.
*/
public List search(String searchTerm, ICallback callback, int rpp) {
// TODO refactor to use the metadata returned from Twitter
if (rpp > 100 && maxResults < rpp)
throw new IllegalArgumentException(
"You need to switch on paging to fetch more than 100 search results. First call setMaxResults() to raise the limit above "
+ rpp);
// Too long a query?
if (searchTerm.length() > 1000) {
throw new TwitterException.E406("Search query too long: "+searchTerm);
// Note: queries can still be rejected by twitter on complexity grounds.
}
// searchTerm = search2_bugHack(searchTerm);
Map vars;
if (maxResults < 100 && maxResults > 0) {
// Default: 1 page
vars = getSearchParams(searchTerm, maxResults);
} else {
vars = getSearchParams(searchTerm, rpp);
}
// Fetch all pages until we run out
// -- or Twitter complains in which case you'll get an exception
List allResults = new ArrayList(Math.max(maxResults,
rpp));
String url = TWITTER_URL + "/search/tweets.json";
BigInteger maxId = untilId;
do {
vars.put("max_id", maxId);
List stati;
try {
String json = http.getPage(url, vars, true);
stati = Status.getStatusesFromSearch(this, json);
} catch (TwitterException.Parsing pex) {
// Twitter bug, July 2012: malformed responses -- end is chopped off ~1 time in 20
// TODO remove when Twitter fix this!
if (http.isRetryOnError()) {
InternalUtils.sleep(250);
String json = http.getPage(url, vars, true);
stati = Status.getStatusesFromSearch(this, json);
} else {
throw pex;
}
} catch(TwitterException.E403 ex) {
// Try to send a more helpful error message TODO keep an eye out that this remains valid
if (ex.getMessage()!=null && ex.getMessage().startsWith("code 195:")) {
throw new TwitterException.E406("Search too long/complex: "+ex.getMessage());
}
throw ex;
}
int numResults = stati.size();
maxId = InternalUtils.getMinId(maxId, stati);
stati = dateFilter(stati);
allResults.addAll(stati);
if (callback != null) {
// the callback may tell us to stop, by returning true
if (callback.process(stati)) {
break;
}
}
if ((rpp==100 && numResults<70/* allow for some screening */) || numResults < rpp) { // We've reached the end of the results
break;
}
} while (allResults.size() < maxResults);
return allResults;
}
/* DISABLED, but kept in code, just in case.
*
* This fixes a couple of bugs in Twitter's search API:
*
* 1. Searches using OR and a location return gibberish, unless they also
* include a -term. Strangely that seems to fix things. So we just add one
* if needed.
*
* 2. Searches that start and end with quotes, and use an OR have problems:
* they become AND searches with the OR turned into a keyword. E.g. /"apple"
* OR "pear"/ acts like /"apple" AND or AND "pear"/
*
* It should be tested periodically whether we need this. See
* {@link TwitterTest#testSearchBug()}, {@link TwitterTest#testSearchBug2()}
*
* @param searchTerm
* @return e.g. "apples OR pears" goes to
* "apples OR pears -kfz"
private String search2_bugHack(String searchTerm) {
// if (true) return searchTerm; TODO Looks like this is no longer needed (quick test, 4th Nov 2012)
// zero-length is valid with location
if (searchTerm.length()==0)
return searchTerm;
// bug 1: a OR b near X fails
if (searchTerm.contains(" OR ") && !searchTerm.contains("-")
&& geocode != null)
return searchTerm + " -kfz"; // add a -gibberish term
// bug 2: "a" OR "b" fails
if (searchTerm.contains(" OR ") && searchTerm.charAt(0) == '"'
&& searchTerm.charAt(searchTerm.length() - 1) == '"')
return searchTerm + " -kfz"; // add a -gibberish term
// hopefully fine as-is
return searchTerm;
}
*/
/**
* @see Twitter_Users#searchUsers(String)
*/
@Deprecated
public List searchUsers(String searchTerm) {
return users().searchUsers(searchTerm);
}
/**
* Sends a new direct message (DM) to the specified user from the
* authenticating user. This is a private message!
*
* @param recipientName
* Required. The screen name of the recipient user. This does *not* start with an "@".
* @param text
* Required. The text of your direct message. Keep it under 140
* characters! This should *not* include the "d username" portion
* @return the sent message
* @throws TwitterException.E403
* if the recipient is not following you. (you can \@mention
* anyone but you can only dm people who follow you).
*/
public Message sendMessage(String recipientName, String text) throws TwitterException {
// HACK: Is recipientName a numerical ID?
if (NID.matcher(recipientName).matches()) {
InternalUtils.log("DM", "Numerical recipient ID passed as String");
return sendMessage(new BigInteger(recipientName), text);
}
User recipient = users().getUser(recipientName);
return sendMessage(recipient.id, text);
}
static final Pattern NID = Pattern.compile("\\d{6,20}");
/**
* Sends a new direct message (DM) to the specified user from the
* authenticating user. This is a private message!
*
* @param recipientId
* Required. The numeric ID of the recipient user.
* If this is unavailable or unknown,{@link Twitter#sendMessage(String, String)}
* will retrieve it automatically.
* @param text
* Required. The text of your direct message. Keep it under 140
* characters! This should *not* include the "d username" portion
* @return the sent message
* @throws TwitterException.E403
* if the recipient is not following you. (you can \@mention
* anyone but you can only dm people who follow you).
*/
public Message sendMessage(Number recipientId, String text) {
assert recipientId != null && text != null : recipientId + " " + text;
assert ! text.startsWith("d " + recipientId) : recipientId + " " + text;
if (text.length() > MAX_DM_LENGTH)
throw new IllegalArgumentException("Message is too long.");
JSONObject body = makeMessageObject(recipientId, text);
String result = null;
try {
// post it
result = postJSON(TWITTER_URL + DM_BASE_ENDPOINT + "/new.json", body, true);
JSONObject msgObject = new JSONObject(result);
JSONObject event = msgObject.getJSONObject("event");
Message msg = new Message(event);
// sadly the response doesn't include rate-limit info
return msg;
} catch (JSONException e) {
throw new TwitterException.Parsing(result, e);
} catch(TwitterException.E403 e) {
// repeated DMs get a 403
if (e.getMessage()!=null && e.getMessage().startsWith("code 151:")) {
throw new TwitterException.Repetition("DM "+recipientId+" "+text+" Error:"+e);
}
throw e;
} catch (TwitterException.E404 e) {
// Probably a suspended user. But could be a rename or a delete.
throw new TwitterException.MissingUser(e.getMessage() + " with recipient="
+ recipientId + ", text=" + text);
}
}
private JSONObject makeMessageObject(Number recipientId, String text) {
JSONObject target = new JSONObject();
target.put("recipient_id", recipientId);
JSONObject messageData = new JSONObject();
messageData.put("text", text);
JSONObject messageCreate = new JSONObject();
messageCreate.put("target", target);
messageCreate.put("message_data", messageData);
JSONObject event = new JSONObject();
event.put("message_create", messageCreate);
event.put("type", "message_create");
JSONObject object = new JSONObject();
object.put("event", event);
return object;
}
/**
* Set this to access sites other than Twitter that support the Twitter API.
* E.g. WordPress or Identi.ca. Note that not all methods may work! Also,
* search uses a separate url and is not affected by this method (it will
* continue to point to Twitter).
*
* @param url
* Format: "http://domain-name", e.g. "http://twitter.com" by
* default. Or https
*/
public void setAPIRootUrl(String url) {
assert url.startsWith("http://") || url.startsWith("https://") : url;
assert !url.endsWith("/") : "Please remove the trailing / from " + url;
TWITTER_URL = url;
}
/**
* *Some* methods - the timeline ones for example - allow a count of
* number-of-tweets to return.
*
* @param count
* null for default behaviour. 200 is the current maximum.
* Twitter may reject or ignore high counts.
*/
public void setCount(Integer count) {
this.count = count;
}
/**
*
* @param status The status to favorite.
* Technical note: Only the ID is needed, so you can use a "fake" Status object here e.g. new Status(id).
* @param isFavorite
* @return updated Status, or null if you'd already starred this status.
*/
public Status setFavorite(Status status, boolean isFavorite) {
try {
String uri = isFavorite ? TWITTER_URL + "/favorites/create.json"
: TWITTER_URL + "/favorites/destroy.json";
String json = http.post(uri, InternalUtils.asMap("id", status.id), true);
return new Status(new JSONObject(json), null);
} catch (E403 e) {
// already a favorite?
if (e.getMessage() != null
&& e.getMessage().contains("already favorited")) {
return null;
}
// just a normal 403
throw e;
}
}
/**
* true by default. If true, lists of tweets will include new-style
* retweets. If false, they won't (execpt for the retweet-specific calls).
*
* @param includeRTs
*/
public void setIncludeRTs(boolean includeRTs) {
this.includeRTs = includeRTs;
}
/**
* Note: does NOT work for search() methods (not supported by Twitter).
*
* @param tweetEntities
* Set to true to enable
* {@link Status#getTweetEntities(KEntityType)}, false if you
* don't care. Default is true.
*/
public void setIncludeTweetEntities(boolean tweetEntities) {
this.tweetEntities = tweetEntities;
}
/**
* Set a language filter for search results. Note: This only applies to
* search results.
*
* @param language
* ISO code for language. Can be null for all languages.
*
* Note: there are multiple different ISO codes! Twitter supports
* ISO 639-1. http://en.wikipedia.org/wiki/ISO_639-1
*/
public void setLanguage(String language) {
lang = language;
}
/**
* @param maxResults
* if greater than zero, requests will attempt to fetch as many
* pages as are needed! -1 by default, in which case most methods
* return the first 20 statuses/messages. Zero is not allowed.
*
* If setting a high figure, you should usually also set a
* sinceId or sinceDate to limit your Twitter usage. Otherwise
* you can easily exceed your rate limit.
*/
public void setMaxResults(int maxResults) {
assert maxResults != 0;
this.maxResults = maxResults;
}
/**
* Set the location for your tweets.
*
* Warning: geo-tagging parameters are ignored if geo_enabled for the user
* is false (this is the default setting for all users unless the user has
* enabled geolocation in their settings)!
*
* @param latitudeLongitude
* Can be null (which is the default), in which case your tweets
* will not carry location data.
*
* The valid ranges for latitude is -90.0 to +90.0 (North is
* positive) inclusive. The valid ranges for longitude is -180.0
* to +180.0 (East is positive) inclusive.
*
* @see #setSearchLocation(double, double, String) which is completely
* separate.
*/
public void setMyLocation(double[] latitudeLongitude) {
myLatLong = latitudeLongitude;
if (myLatLong == null)
return;
if (Math.abs(myLatLong[0]) > 90)
throw new IllegalArgumentException(myLatLong[0]
+ " is not within +/- 90");
if (Math.abs(myLatLong[1]) > 180)
throw new IllegalArgumentException(myLatLong[1]
+ " is not within +/- 180");
}
/**
* Restricts {@link #search(String)} to tweets by users located within a
* given radius of the given latitude/longitude.
*
* The location of a tweet is preferably taken from the Geotagging API, but
* will fall back to the Twitter profile.
*
* @param latitude
* @param longitude
* @param radius
* E.g. 3.5mi or 2km. Must be <2500km
*/
public void setSearchLocation(double latitude, double longitude,
String radius) {
assert radius.endsWith("mi") || radius.endsWith("km") : radius;
geocode = ((float)latitude) + "," + ((float)longitude) + "," + radius;
}
/**
* @return latitude,longitude,radius
*/
public String getSearchLocation() {
return geocode;
}
/**
* Optional. Specifies what type of search results you would prefer to
* receive. The current default is "mixed." Valid values:
* {@link #SEARCH_MIXED}: Include both popular and real time results in the
* response.
{@link #SEARCH_RECENT}: return only the most recent results in
* the response
{@link #SEARCH_POPULAR}: return only the most popular
* results in the response.
*
* @param resultType
*/
public void setSearchResultType(String resultType) {
this.resultType = resultType;
}
/**
* Date based filter on statuses and messages. This is done client-side as
* Twitter have - for their own inscrutable reasons - pulled support for
* this feature. Use {@link #setSinceId(Number)} for preference.
* You can use both constraints together.
*
* If using this, you probably also want to increase
* {@link #setMaxResults(int)} - otherwise you get at most 20, and possibly
* less (since the filtering is done client side).
*
* @param sinceDate
* @see #setSinceId(Number)
*/
@Deprecated
public void setSinceDate(Date sinceDate) {
this.sinceDate = sinceDate;
}
/**
* Narrows the returned results to just those statuses created after the
* specified status id. This will be used until it is set to null. The default
* is null.
*
* If using this, you probably also want to use {@link #setUntilId(Number)}.
* Twitter returns the most recent results, so this has little effect unless
* used with setUntilId().
* You may also want to increase {@link #setMaxResults(int)}.
*
* @param statusId Can be null. Only a BigInteger really makes sense (although a double would work to some degree
* -- but beware of rounding errors).
* @see #setSinceDate(Date)
*/
public void setSinceId(Number statusId) {
sinceId = InternalUtils.toBigInteger(statusId);
}
/**
* Set the source application. This will be mentioned on Twitter alongside
* status updates (with a small label saying source: myapp).
*
* In order for this to work, you must first register your app with
* Twitter and get a source name from them! You must also use OAuth to
* connect.
*
* @param sourceApp
* jtwitterlib by default. Set to null for no source.
*/
public void setSource(String sourceApp) {
this.sourceApp = sourceApp;
}
/**
* Sets the authenticating user's status.
*
* Identical to {@link #updateStatus(String)}, but with a Java-style name
* (updateStatus is the Twitter API name for this method).
*
* @param statusText
* The text of your status update. Must not be more than 140
* characters.
* @return The posted status when successful.
*/
public Status setStatus(String statusText) throws TwitterException {
return updateStatus(statusText);
}
/**
* @param untilDate
* the untilDate to set. This is NOT
* properly supported. It operates by post filtering
* results client-side.
* @see #setUntilId(Number) which is better
*/
@Deprecated
public void setUntilDate(Date untilDate) {
this.untilDate = untilDate;
}
/**
* If set, return results older than this.
*
* @param untilId
* aka max_id
*/
public void setUntilId(Number untilId) {
this.untilId = InternalUtils.toBigInteger(untilId);
}
public BigInteger getUntilId() {
return untilId;
}
public BigInteger getSinceId() {
return sinceId;
}
/**
* Compatibility setting: if your app can't work with new-style
* "140 characters + attachment" tweets, turn this off to only
* receive truncated results.
* @param extendedMode
*/
public void setExtendedMode(Boolean extendedMode) {
this.extendedMode = extendedMode;
}
/**
* Compatibility setting: if your app can't generate new-style
* "previously tagged-in users are implicitly included in replies"
* tweets, turn this off to require users be explicitly tagged in every time.
* @param extendedMode
*/
public void setAutoPopulateReplyMetadata(Boolean autoPopulateReplyMetadata) {
this.autoPopulateReplyMetadata = autoPopulateReplyMetadata;
}
/**
* @deprecated User {@link TwitLonger}
* Keeping for backwards compatibility until Q2 2013
*
* Set this to allow the use of twitlonger via
* {@link #updateLongStatus(String, long)}. To get an api-key for your app,
* contact twitlonger as described here: http://www.twitlonger.com/api
*
* This method will be removed June 2013
*
* @param twitlongerAppName
* @param twitlongerApiKey
*/
public void setupTwitlonger(String twitlongerAppName,
String twitlongerApiKey) {
this.twitlongerAppName = twitlongerAppName;
this.twitlongerApiKey = twitlongerApiKey;
}
/**
* @see Twitter_Users#show(Number)
*
* This method will be removed June 2013
*/
@Deprecated
public User show(Number userId) {
return users().show(userId);
}
/**
* @see Twitter_Users#show(String)
*/
@Deprecated
public User show(String screenName) throws TwitterException,
TwitterException.SuspendedUser {
return users().show(screenName);
}
/**
* Split a long message up into shorter chunks suitable for use with
* {@link #setStatus(String)} or {@link #sendMessage(String, String)}.
*
* @param longStatus
* @return longStatus broken into a list of max 140 char strings
*/
public List splitMessage(String longStatus) {
// Is it really long?
if (longStatus.length() <= MAX_CHARS)
return Collections.singletonList(longStatus);
// Multiple tweets for a longer post
List sections = new ArrayList(4);
StringBuilder tweet = new StringBuilder(MAX_CHARS);
String[] words = longStatus.split("\\s+");
for (String w : words) {
// messages have a max length of 140
// plus the last bit of a long tweet tends to be hidden on
// twitter.com, so best to chop 'em short too
if (tweet.length() + w.length() + 1 > MAX_CHARS) {
// Emit
tweet.append("...");
sections.add(tweet.toString());
tweet = new StringBuilder(MAX_CHARS);
tweet.append(w);
} else {
if (tweet.length() != 0) {
tweet.append(" ");
}
tweet.append(w);
}
}
// Final bit
if (tweet.length() != 0) {
sections.add(tweet.toString());
}
return sections;
}
/**
* Map with since_id, page and count, if set. This is called by methods that
* return lists of statuses or messages.
*/
private Map standardishParameters() {
return addStandardishParameters(new HashMap());
}
/**
* @see Twitter_Users#stopFollowing(String)
*
* This method will be removed June 2013
*/
@Deprecated
public User stopFollowing(String username) {
return users().stopFollowing(username);
}
/**
* @see Twitter_Users#stopFollowing(User)
*
* This method will be removed June 2013
*/
@Deprecated
public User stopFollowing(User user) {
return stopFollowing(user.screenName);
}
/**
* Update info on Twitter's configuration -- such as shortened url lengths.
* @return true if we detected a change from the hardcoded defaults.
*/
public boolean updateConfiguration() {
String json = http.getPage(TWITTER_URL + "/help/configuration.json",
null, true);
boolean change = false;
try {
JSONObject jo = new JSONObject(json);
{
int len = jo.getInt("short_url_length");;
if (len != LINK_LENGTH) change = true;
LINK_LENGTH = len;
}
// max_media_per_upload // 1!
{
int len = jo.getInt("characters_reserved_per_media");
if (len != MEDIA_LENGTH) change = true;
MEDIA_LENGTH = len;
}
{
long lmt = jo.getLong("photo_size_limit");
if (lmt != PHOTO_SIZE_LIMIT) change = true;
PHOTO_SIZE_LIMIT = lmt;
}
// NB: video size limit and other video limits are not sent
{
int lmt = jo.getInt("dm_text_character_limit");
if (lmt != MAX_DM_LENGTH) change = true;
MAX_DM_LENGTH = lmt;
}
// photo_sizes
// short_url_length_https
return change;
} catch (JSONException e) {
throw new TwitterException.Parsing(json, e);
}
}
/**
* @deprecated Use {@link TwitLonger}
* Keeping for backwards compatibility until Q2 2013
*
* Use twitlonger.com to post a lengthy tweet. See twitlonger.com for more
* details on their service.
*
* Note: You need to have called {@link #setupTwitlonger(String, String)}
* before calling this.
*
* @param message
* @param inReplyToStatusId
* Can be null if this isn't a reply
* @return A Twitter status using a truncated message with a link to
* twitlonger.com
* @see #setupTwitlonger(String, String)
*/
public Status updateLongStatus(String message, Number inReplyToStatusId) {
TwitLonger tl = new TwitLonger(this, twitlongerApiKey, twitlongerAppName);
return tl.updateLongStatus(message, inReplyToStatusId);
}
/**
* Updates the authenticating user's status.
*
* @param statusText
* The text of your status update. Must not be more than 140 characters.
* @return The posted status when successful.
*/
public Status updateStatus(String statusText) {
return updateStatus(statusText, null);
}
/**
* Compute the effective size of a message, given that Twitter treats things that
* smell like a URL as 23 characters.
* This also checks for DM microformat, e.g. "d winterstein Hello", where the d user part isn't counted.
*
* @param statusText
* The status to check
* @return
* The effective message length in characters
*/
public static int countCharacters(String statusText) {
int shortLength = statusText.length();
// Urls count as 23
Matcher m = Regex.VALID_URL.matcher(statusText);
while(m.find()) {
String grp = m.group();
shortLength += LINK_LENGTH - grp.length();
}
// If a DM, don't count the "d user" microformat
Matcher dmm = InternalUtils.DM.matcher(statusText);
if (dmm.find()) {
shortLength -= dmm.end();
}
return shortLength;
}
/**
* Compatibility wrapper for {@link updateStatus(String, Number, List)}
* @param statusText
* @param inReplyToStatusId
* @return
* @throws TwitterException
*/
public Status updateStatus(String statusText, Number inReplyToStatusId)
throws TwitterException
{
return updateStatus(statusText, inReplyToStatusId, null);
}
/**
* Updates the authenticating user's status and marks it as a reply to the
* tweet with the given ID.
*
* @param statusText
* The text of your status update. Must not be more than 140
* characters (with urls counting as 20 or 21 for https).
*
* @param inReplyToStatusId
* The ID of the tweet that this tweet is in response to. The
* statusText must contain the username (with an "@" prefix) of
* the owner of the tweet being replied to for Twitter to
* agree to mark the tweet as a reply. null to leave this
* unset.
*
* @param excludeReplyIds
* A list of numeric user IDs (not @handles) tagged into the
* previous tweet in the thread who should NOT be tagged into this
* reply.
*
* @return The posted status when successful.
*
* Warning: the microformat for direct messages is supported. BUT:
* the return value from this method will be null, and not the
* direct message. Other microformats (such as follow) may result in
* an exception being thrown.
*
* @throws TwitterException
* if something goes wrong. There is a rare (but not rare
* enough) bug whereby Twitter occasionally returns a success
* code but the wrong tweet. If this happens, the update may or
* may not have worked - wait a bit & check.
*/
public Status updateStatus(String statusText, Number inReplyToStatusId, List excludeReplyIds) {
Map vars = updateStatus2_vars(statusText, inReplyToStatusId, excludeReplyIds);
String result = http.post(TWITTER_URL + "/statuses/update.json", vars,
true);
try {
Status s = new Status(new JSONObject(result), null);
// s = updateStatus2_safetyCheck(statusText, s);
return s;
} catch (JSONException e) {
throw new TwitterException.Parsing(result, e);
}
}
/**
* Check statusText length & prep the parameters
* @param statusText
* @param inReplyToStatusId
* @return The vars to send
*/
private Map updateStatus2_vars(String statusText, Number inReplyToStatusId, List excludeReplyIds)
{
// check for length
if (statusText.length() > MAX_CHARS
&& TWITTER_URL.contains("twitter") // Hack: allow long posts to WordPress
&& CHECK_TWEET_LENGTH)
{
int shortLength = countCharacters(statusText);
if (shortLength > MAX_CHARS) {
// bogus - send a helpful error
if (statusText.startsWith("RT")) {
throw new IllegalArgumentException(
"Status text must be "+MAX_CHARS+" characters or less -- use Twitter.retweet() to do new-style retweets which can be a bit longer: "
+ statusText.length() + " " + statusText);
}
throw new IllegalArgumentException(
"Status text must be "+MAX_CHARS+" characters or less: "
+ statusText.length() + " " + statusText);
}
}
Map vars = InternalUtils.asMap("status", statusText);
if (tweetEntities) vars.put("include_entities", "1");
// add in long/lat if set
if (myLatLong != null) {
vars.put("lat", Double.toString(myLatLong[0]));
vars.put("long", Double.toString(myLatLong[1]));
}
if (placeId != null) {
vars.put("place_id", Long.toString(placeId));
}
if (sourceApp != null) {
vars.put("source", sourceApp);
}
if (inReplyToStatusId != null) {
vars.put("in_reply_to_status_id", inReplyToStatusId.toString());
updateStatus3_vars2_autopop(vars, excludeReplyIds);
}
// If we're making a long post, we want to get the full text back!
if (extendedMode) {
vars.put("tweet_mode", "extended");
}
return vars;
}
void updateStatus3_vars2_autopop(Map vars, List excludeReplyIds) {
if ( ! this.autoPopulateReplyMetadata) return;
vars.put("auto_populate_reply_metadata", "true");
if (excludeReplyIds != null && ! excludeReplyIds.isEmpty()) {
StringBuilder sb = new StringBuilder();
// NB: safety check exclude IDs
for(Object exc : excludeReplyIds) {
if (exc instanceof String) { // paranoia
exc = new BigInteger((String)exc);
}
Number exn = (Number) exc;
if (exn.longValue() < 1) {
InternalUtils.log("jtwitter.error", "(skip) Invalid exclude_reply_user_ids ID: "+exc);
continue;
}
sb.append(exn);
sb.append(",");
}
InternalUtils.pop(sb, 1);
vars.put("exclude_reply_user_ids", sb.toString());
}
}
// /**
// * Test that the updateState worked -- throw TwitterException.Unexplained
// * if it didn't.
// * By default, this only filters DMs.
// * Serious checking is switched on via the {@link #WORRIED_ABOUT_TWITTER} flag.
// * @param statusText What we meant to send
// * @param s What came back
// * @return s, or null for DMs
// * @throws TwitterException#Unexplained
// */
// private Status updateStatus2_safetyCheck(String statusText, Status s) {
// // is it a direct message? - which doesn't return the true status
// String st = statusText.toLowerCase();
// if (st.startsWith("dm ") || st.startsWith("d ")) {
// return null;
// }
// // The checks are dialled down, so let's make this standard
//// if ( ! WORRIED_ABOUT_TWITTER) {
//// return s;
//// }
// // Weird bug: Twitter occasionally rejects tweets?!
// // Sightings...
// // 21/05/12 (spotter: Alex Nuttgens)
// // 27/03/12 (spotter: Alex Nuttgens)
// // + other earlier sightings
//
// // Bug #6748: Unicode mangling *sometimes*
//
// // Sanity check...
// String targetText = statusText.trim();
// String returnedStatusText = s.text.trim();
// // strip the urls to remove the effects of the t.co shortener
// // (obviously this weakens the safety test, but failure would be
// // a corner case of a corner case).
// // TODO Twitter also shorten some not-quite-urls, such as "www.google.com", which stripUrls() won't catch.
// targetText = InternalUtils.stripUrls(targetText);
// returnedStatusText = InternalUtils.stripUrls(returnedStatusText);
// if (returnedStatusText.equals(targetText)) {
// // All OK
// return s;
// }
// InternalUtils.log("jtwitter", "Text mismatch: "+targetText+" != "+returnedStatusText+" tweet:"+s.getId());
// return s;
//
// // More extreme measures... off for now
//// try {
//// Thread.sleep(500);
//// } catch (InterruptedException e) {
//// // igore the interruption
//// }
//// Status s2 = getStatus();
//// if (s2 != null) {
//// returnedStatusText = InternalUtils.stripUrls(s2.text.trim());
//// if (targetText.equals(returnedStatusText)) {
//// return s2;
//// }
//// }
//// throw new TwitterException.Unexplained(
//// "Unexplained failure for tweet: expected \"" + statusText
//// + "\" but got " + s2);
// }
/**
* Convenience for using
* {@link #uploadVideo(File)}
* + {@link #updateStatusWithUploadedMedia(String, BigInteger, List)}
* @param text
* @param video
* @return the video-post
*/
public Status updateStatusWithVideo(String text, File video) {
String mediaId = uploadVideo(video);
Status s = updateStatusWithUploadedMedia(text, null, Arrays.asList(mediaId));
return s;
}
/**
* Updates the user's status with a single image.
*
* @param statusText
* @param inReplyToStatusId Can be null.
* @param excludedIds Untag these users (numeric user IDs) from reply
* @param mediaFile
* @return The posted status when successful.
*
* @see #PHOTO_SIZE_LIMIT
*/
// c.f. https://dev.twitter.com/docs/api/1/post/statuses/update_with_media
// c.f. https://dev.twitter.com/discussions/1059
public Status updateStatusWithMedia(String statusText, BigInteger inReplyToStatusId, List excludedIds, File mediaFile) {
if (mediaFile==null || ! mediaFile.isFile()) {
throw new IllegalArgumentException("Invalid file: "+mediaFile);
}
Map vars = updateStatus2_vars(statusText, inReplyToStatusId, excludedIds);
vars.put("media[]", mediaFile);
// TODO possibly_sensitive
// TODO display_coordinates
String result = null;
try {
// Breaking change from v1.0, which went to upload.twitter.com
String url = TWITTER_URL+"/statuses/update_with_media.json";
result = ((OAuthSignpostClient)http).postMultipartForm(url, vars);
Status s = new Status(new JSONObject(result), null);
// sanity check (c.f. unicode bug #6748)
// updateStatus2_safetyCheck(statusText, s);
return s;
} catch (E403 e) {
// test for repetition (which gets a 403)
Status s = getStatus();
if (s != null && s.getText().equals(statusText))
throw new TwitterException.Repetition(s.getText());
throw e;
} catch (JSONException e) {
throw new TwitterException.Parsing(result, e);
}
}
/**
* Compatibility wrapper for {@link updateStatusWithMedia(String, BigInteger, List, File) {
* @param statusText
* @param inReplyToStatusId
* @param mediaFile
* @return
*/
public Status updateStatusWithMedia(String statusText, BigInteger inReplyToStatusId, File mediaFile) {
return updateStatusWithMedia(statusText, inReplyToStatusId, null, mediaFile);
}
/**
* Updates the user's status with multiple images.
* This does NOT work for video.
*
* @param statusText
* @param inReplyToStatusId Can be null.
* @param excludedIds Untag these users (numeric user IDs) from reply
* @param mediaFiles
* @return The posted status when successful.
*
* @see #PHOTO_SIZE_LIMIT
*/
// c.f. https://dev.twitter.com/docs/api/1/post/statuses/update_with_media
// c.f. https://dev.twitter.com/discussions/1059
public Status updateStatusWithMedia(String statusText, BigInteger inReplyToStatusId, List excludedIds, List mediaFiles) {
// Upload each file, and get a media_id for it
// List.toString() outputs "[item1, ..., itemN]" but Twitter wants "item1,...,itemN" so we do it the hard way
List fileIds = new ArrayList();
for (File file: mediaFiles) {
if (file == null || ! file.isFile()) {
throw new IllegalArgumentException("Invalid file: " + file);
}
Map vars = new HashMap();
vars.put("media", file);
String url = TWITTER_UPLOAD_URL + MEDIA_UPLOAD_ENDPOINT;
String result = ((OAuthSignpostClient)http).postMultipartForm(url, vars);
JSONObject response = new JSONObject(result);
String id = response.optString("media_id_string");
if (id != null) fileIds.add(id);
}
return updateStatusWithUploadedMedia(statusText, inReplyToStatusId, excludedIds, fileIds);
}
/**
* Compatibility wrapper for {@link updateStatusWithMedia(String, BigInteger, List, List}
* @param statusText
* @param inReplyToStatusId
* @param mediaFiles
* @return
*/
public Status updateStatusWithMedia(String statusText, BigInteger inReplyToStatusId, List mediaFiles) {
return updateStatusWithMedia(statusText, inReplyToStatusId, null, mediaFiles);
}
/**
* See https://dev.twitter.com/rest/media/uploading-media#chunkedupload
* @param video
* @return
*/
public String uploadVideo(File video) {
// Video mime-types:
String ftype = video.getName().substring(video.getName().lastIndexOf('.'));
String mimetype = (String) InternalUtils.asMap(
".mov", "video/quicktime",
".avi", "video/x-msvideo",
".wmv", "video/x-ms-wmv",
".m4v", "video/mp4",
".mp4", "video/mp4"
).get(ftype);
if (mimetype==null) mimetype = "video/"+ftype;
boolean async =
// false; // might also be needed for longer duration ||
video.length() > 15000000L;
return uploadVideo(video, mimetype, async);
}
/**
* See https://dev.twitter.com/rest/media/uploading-media#chunkedupload
* @param video
* @return
*/
public String uploadVideo(File video, String mimeType, boolean async) {
if ( ! video.isFile()) throw new RuntimeException(new FileNotFoundException(video.getAbsolutePath()));
// init
long tb = video.length();
if (tb > VIDEO_SIZE_LIMIT) {
throw new TwitterException.UploadTooBig("File "+video+" is "+(tb/(1024*1024))+"mb which exceeds the maximum video upload of "+(VIDEO_SIZE_LIMIT/(1024*1024)));
}
Map vars = InternalUtils.asMap(
"command", "INIT",
"media_type", mimeType,
"total_bytes", tb,
"media_category", "tweet_video"
);
String initresp = http.post(TWITTER_UPLOAD_URL + MEDIA_UPLOAD_ENDPOINT, vars, true);
JSONObject response = new JSONObject(initresp);
String id = response.optString("media_id_string");
final String url = TWITTER_UPLOAD_URL + MEDIA_UPLOAD_ENDPOINT;
// append (the core data upload)
// https://dev.twitter.com/rest/reference/post/media/upload-append
if (tb < MAX_CHUNK_SIZE) {
// append using 1 big chunk This sets a max of 5mb!
Map avars = InternalUtils.asMap(
"command", "APPEND", "media_id", id, "segment_index", 0);
avars.put("media", video);
String appendResult = ((OAuthSignpostClient)http).postMultipartForm(url, avars);
} else {
uploadVideo2_chunks(video, async, id);
}
// finalize
Map fvars = InternalUtils.asMap("command", "FINALIZE", "media_id", id);
String fresp = http.post(TWITTER_UPLOAD_URL + MEDIA_UPLOAD_ENDPOINT, fvars, true);
JSONObject fresponse = new JSONObject(fresp);
String fid = fresponse.optString("media_id_string");
// status
try { // this throws an "invalid mediaId" error?!
while(true) {
Thread.sleep(50);
String statusResp = http.getPage(url, InternalUtils.asMap(
"command", "STATUS",
"media_id", id),
true);
JSONObject status = new JSONObject(statusResp);
JSONObject procInfo = status.getJSONObject("processing_info");
String state = procInfo.getString("state");
int secs = procInfo.optInt("check_after_secs");
System.out.println(procInfo);
if ( ! "in_progress".equals(state)) break;
// pause at least 1/2 second
Thread.sleep(Math.max(2*secs, 1) * 500);
}
} catch(Exception ex) {
ex.printStackTrace();
// oh well
}
return fid;
}
private void uploadVideo2_chunks(File video, boolean async, String id) {
// append in chunks
InputStream stream = null;
ExecutorService threadPool = async? Executors.newFixedThreadPool(10) : null;
final AtomicReference err = new AtomicReference();
try {
final int CHUNK_SIZE = (int) MAX_CHUNK_SIZE;
assert MAX_CHUNK_SIZE < Integer.MAX_VALUE;
stream = new FileInputStream(video);
byte[] bytes = new byte[CHUNK_SIZE];
for(int segmentIndex = 0; segmentIndex<100000; segmentIndex++) {
// check for errors from async requests
if (err.get()!=null) break;
// read in a chunk
int offset = 0;
while(stream.available() > 0 && offset < CHUNK_SIZE) {
int bytesRead = stream.read(bytes, offset, CHUNK_SIZE);
offset += bytesRead;
}
// done?
if (offset==0) break;
// encode it
StringBuilder buf = Base64Encoder.encode(bytes, 0, offset, null);
final Map avars = InternalUtils.asMap(
"command", "APPEND",
"media_id", id,
"segment_index", segmentIndex);
System.out.println("segment "+segmentIndex);
avars.put("media_data", buf.toString());
final String url = TWITTER_UPLOAD_URL + MEDIA_UPLOAD_ENDPOINT;
if (async) {
threadPool.submit(new Runnable() {
@Override
public void run() {
try {
String appendResult = ((OAuthSignpostClient)http).postMultipartForm(url, avars);
} catch(Exception ex) {
err.set(ex);
}
}
});
} else {
String appendResult = ((OAuthSignpostClient)http).postMultipartForm(url, avars);
}
}
// all data sending
if (threadPool!=null) {
threadPool.shutdown();
threadPool.awaitTermination(2, TimeUnit.MINUTES);
}
if (err.get()!=null) throw err.get();
// success :)
} catch(TwitterException ex) {
throw ex;
} catch(Exception ex) {
// ?? retry on some errors??
throw new TwitterException(ex);
} finally {
InternalUtils.close(stream);
if (threadPool!=null) threadPool.shutdownNow();
}
}
/**
* Updates the user's status with one video, or multiple images.
* @param statusText
* @param inReplyToStatusId Can be null
* @param excludedIds Untag these users (numeric user IDs) from reply
* @param mediaFileIds As returned by {@link #uploadVideo(File, String)}
*/
public Status updateStatusWithUploadedMedia(String statusText, BigInteger inReplyToStatusId, List excludedIds, List mediaFileIds) {
Map vars = updateStatus2_vars(statusText, inReplyToStatusId, excludedIds);
if(mediaFileIds!=null && ! mediaFileIds.isEmpty()) {
String mediaIds = InternalUtils.join(mediaFileIds.toArray(new String[0]));
vars.put("media_ids", mediaIds);
}
String result = null;
try {
// Breaking change from v1.0, which went to upload.twitter.com
String url = TWITTER_URL+"/statuses/update.json";
result = http.post(url, vars,
true);
Status s = new Status(new JSONObject(result), null);
return s;
} catch (E403 e) {
// test for repetition (which gets a 403)
Status s = getStatus();
if (s != null && s.getText().equals(statusText))
throw new TwitterException.Repetition(s.getText());
throw e;
} catch (JSONException e) {
throw new TwitterException.Parsing(result, e);
}
}
/**
* Compatibility wrapper for {@link updateStatusWithUploadedMedia(String, BigInteger, List, List}
* @param statusText
* @param inReplyToStatusId
* @param mediaFileIds
* @return
*/
public Status updateStatusWithUploadedMedia(String statusText, BigInteger inReplyToStatusId, List mediaFileIds) {
return updateStatusWithUploadedMedia(statusText, inReplyToStatusId, null, mediaFileIds);
}
/**
* User and social-network related API methods.
*
* Note: this is a new object with an unset cursor.
*/
public Twitter_Users users() {
return new Twitter_Users(this);
}
/**
*
* @param place Can be null (switches off geo-filtering)
*/
public void setSearchLocation(IPlace place) {
if (place==null) {
geocode = null;
return;
}
Location x = place.getCentroid();
if (x==null) {
throw new IllegalArgumentException("Geo-search needs lat/long coordinates - none in "+place);
}
BoundingBox bbox = place.getBoundingBox();
if (bbox==null) {
// default to 10km radius = a 20km diameter
setSearchLocation(x.latitude, x.longitude, "10km");
return;
}
Location ne = bbox.getNorthEast();
Location sw = bbox.getSouthWest();
assert ne != null && sw != null : bbox;
// cast down to m to reduce the sig-figures
float diameterInMetres = (int) ne.distance(sw).getValue();
float radiusInKm = Math.max(diameterInMetres/2000, 5000);
// Warning: cap radius at 2500 (limit imposed by Twitter)
radiusInKm = Math.min(radiusInKm, 2499);
String radiusInGeoCodeKm = radiusInKm + "km";
setSearchLocation(x.latitude, x.longitude, radiusInGeoCodeKm);
}
/**
* Convenience for using getHttpClient().getRateLimits()
* @param apiResourceName
* @param defaultIfUnset If the answer is not known, return this value.
* ie. true for conservative behaviour (beware of never getting data), false for optimistic behaviour.
* @return true if rate-limited
*/
public boolean isRateLimited(String apiResourceName, boolean defaultIfUnset) {
Map ratelimits = getHttpClient().getRateLimits();
if (ratelimits==null) return defaultIfUnset;
RateLimit rl = ratelimits.get(apiResourceName);
if (rl==null) return defaultIfUnset;
if (rl.isOutOfDate()) return defaultIfUnset;
return rl.getRemaining() <= 0;
}
}