package com.nostra13.socialsharing.twitter.extpack.winterwell.jtwitter;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.nostra13.socialsharing.twitter.extpack.winterwell.json.JSONArray;
import com.nostra13.socialsharing.twitter.extpack.winterwell.json.JSONException;
import com.nostra13.socialsharing.twitter.extpack.winterwell.json.JSONObject;
import com.nostra13.socialsharing.twitter.extpack.winterwell.jtwitter.Twitter.KEntityType;
import com.nostra13.socialsharing.twitter.extpack.winterwell.jtwitter.TwitterException.E401;
import com.nostra13.socialsharing.twitter.extpack.winterwell.jtwitter.TwitterException.E403;
import com.nostra13.socialsharing.twitter.extpack.winterwell.jtwitter.TwitterException.SuspendedUser;
/**
* Java wrapper for the Twitter API version {@value #version}
* <p>
* Example usage:<br>
* First, you should get the user to authorise access via OAuth. There are a
* couple of ways of doing this -- we show one below -- see
* {@link OAuthSignpostClient} for more details.
* <p>
* Note that you don't need to do this for some operations - e.g. you can look
* up public posts without logging in (use the {@link #Twitter()} constructor.
* You can also - for now! - use username and password to login, but Twitter
* plan to switch this off soon.
*
* <code><pre>
// First, OAuth to login: Make an oauth client
OAuthSignpostClient oauthClient = new OAuthSignpostClient(JTWITTER_OAUTH_KEY, JTWITTER_OAUTH_SECRET, "oob");
// open the authorisation page in the user's browser
oauthClient.authorizeDesktop(); // Note: this only works on desktop PCs
// or direct the user to the webpage given jby oauthClient.authorizeUrl()
// get the pin from the user since we're using "oob" instead of a callback servlet
String v = oauthClient.askUser("Please enter the verification PIN from Twitter");
oauthClient.setAuthorizationCode(v);
// You can store the authorisation token details for future use
Object accessToken = client.getAccessToken();
</pre></code>
*
* Now we can access Twitter: <code><pre>
// Make a Twitter object
Twitter twitter = new Twitter("my-name", oauthClient);
// Print Winterstein's status
System.out.println(twitter.getStatus("winterstein"));
// Set my status
twitter.updateStatus("Messing about in Java");
</pre></code>
*
* <p>
* If you can handle callbacks, then the OAuth login can be streamlined. You
* need a webserver and a servlet (eg. use Jetty or Tomcat) to handle callbacks.
* Replace "oob" with your callback url. Direct the user to
* client.authorizeUrl(). Twitter will then call your callback with the request
* token and verifier (authorisation code).
* </p>
* <p>
* See {@link http://www.winterwell.com/software/jtwitter.php} for more
* information about this wrapper. See {@link http://dev.twitter.com/doc} for
* more information about the Twitter API.
* <p>
* Notes:
* <ul>
* <li>This wrapper takes care of all url-encoding/decoding.
* <li>This wrapper will throw a runtime exception (TwitterException) if a
* methods fails, e.g. it cannot connect to Twitter.com or you make a bad
* request.
* <li>Note that Twitter treats old-style retweets (those made by sending a
* normal tweet beginning "RT @whoever") differently from new-style retweets
* (those made using the retweet API). The differences are documented in various
* methods.
* <li>Most methods are available via this class (Twitter), except for list
* support (in {@link TwitterList} - though {@link #getLists()} is here) and
* some profile/account settings (in {@link Twitter_Account}).
* <li>This class is NOT thread safe. If you're using multiple threads, it is
* best to create separate Twitter objects (which is fine).
* </ul>
*
* <h4>Copyright and License</h4>
* This code is copyright (c) Winterwell Associates 2008/2009 and (c) winterwell
* Mathematics Ltd, 2007 except where otherwise stated. It is released as
* open-source under the LGPL license. See <a
* href="http://www.gnu.org/licenses/lgpl.html"
* >http://www.gnu.org/licenses/lgpl.html</a> for license details. This code
* comes with no warranty or support.
*
* <h4>Change List</h4>
* The change list is kept online at: {@link http
* ://www.winterwell.com/software/changelist.txt}
*
* @author Daniel Winterstein
*/
public class Twitter implements Serializable {
/**
* Use to register per-page callbacks for long-running searches. To stop the
* search, return true.
*
*/
public interface ICallback {
public boolean process(List<Status> statuses);
}
/** TODO
*
*/
// If available, returns an array of replies and mentions related to the
// specified Tweet. There is no guarantee there will be any replies or
// mentions in the response. This method is only available to users who
// have access to #newtwitter.
@Deprecated // TODO
public List<ITweet> getRelated(ITweet tweet) {
// TODO
String url = TWITTER_URL+"/related_results/show/"+tweet.getId()+".json";
throw new RuntimeException("TODO");
}
/**
* How is the Twitter API today?
* See {@link https://dev.twitter.com/status} for more information.
* @return map of {method: %uptime in the last 24 hours}.
* An empty map indicates this method itself failed!
*
* @throws Exception This method is not officially supported! As such,
* it could break at some future point.
*/
public static Map<String,Double> getAPIStatus() throws Exception{
HashMap<String,Double> map = new HashMap();
// c.f. https://dev.twitter.com/status & https://status.io.watchmouse.com/7617
// https://api.io.watchmouse.com/synth/current/39657/folder/7617/?fields=info;cur;24h.uptime;24h.status;last.date;daily.avg;daily.uptime;daily.status;daily.period
String json = null;
try {
URLConnectionHttpClient client = new URLConnectionHttpClient();
json = client.getPage("https://api.io.watchmouse.com/synth/current/39657/folder/7617/?fields=info;cur;24h.uptime", null, false);
JSONObject jobj = new JSONObject(json);
JSONArray jarr = jobj.getJSONArray("result");
for(int i=0; i<jarr.length(); i++) {
JSONObject jo = jarr.getJSONObject(i);
String name = jo.getJSONObject("info").getString("name");
JSONObject h24 = jo.getJSONObject("24h");
double value = h24.getDouble("uptime");
map.put(name, value);
}
return map;
} catch (JSONException e) {
throw new TwitterException.Parsing(json, e);
} catch (Exception e) {
return map;
}
}
/**
* Interface for an http client - e.g. allows for OAuth to be used instead.
* The standard version is {@link OAuthSignpostClient}.
* <p>
* If creating your own version, please provide support for throwing the
* right subclass of TwitterException - see
* {@link URLConnectionHttpClient#processError(java.net.HttpURLConnection)}
* for example code.
*
* @author Daniel Winterstein
*/
public static interface IHttpClient {
/**
* Whether this client is setup to do authentication when contacting the
* Twitter server. Note: This is a fast method that does not call the
* server, so it does not check whether the access token or password is
* valid. See {Twitter#isValidLogin()} or
* {@link Twitter_Account#verifyCredentials()} if you need to check a
* login.
* */
boolean canAuthenticate();
/**
* Lower-level GET method.
*
* @param url
* @param vars
* @param authenticate
* @return
* @throws IOException
*/
HttpURLConnection connect(String url, Map<String, String> vars,
boolean authenticate) throws IOException;
/**
* @return a copy of this client. The copy can share structure, but it
* MUST be safe for passing to a new thread to be used in
* parallel with the original.
*/
IHttpClient copy();
/**
* Fetch a header from the last http request. This is inherently NOT
* thread safe. Headers from error messages should (probably) be cached.
*
* @param headerName
* @return header value, or null if unset
*/
String getHeader(String headerName);
/**
* Send an HTTP GET request and return the response body. Note that this
* will change all line breaks into system line breaks!
*
* @param uri
* The uri to fetch
* @param vars
* get arguments to add to the uri
* @param authenticate
* If true, use authentication. The authentication method
* used depends on the implementation (basic-auth, OAuth). It
* is an error to use true if no authentication details have
* been set.
*
* @throws TwitterException
* for a variety of reasons
* @throws TwitterException.E404
* for resource-does-not-exist errors
*/
String getPage(String uri, Map<String, String> vars,
boolean authenticate) throws TwitterException;
/**
* @see Twitter#getRateLimit(KRequestType) This is where the Twitter
* method is implemented.
*/
RateLimit getRateLimit(KRequestType reqType);
/**
* Send an HTTP POST request and return the response body.
*
* @param uri
* The uri to post to.
* @param vars
* The form variables to send. These are URL encoded before
* sending.
* @param authenticate
* If true, send user authentication
* @return The response from the server.
*
* @throws TwitterException
* for a variety of reasons
* @throws TwitterException.E404
* for resource-does-not-exist errors
*/
String post(String uri, Map<String, String> vars, boolean authenticate)
throws TwitterException;
/**
* Lower-level POST method.
*
* @param uri
* @param vars
* @return a freshly opened authorised connection
* @throws TwitterException
*/
HttpURLConnection post2_connect(String uri, Map<String, String> vars)
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);
}
/**
* 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. This may be a Long or a BigInteger.
*/
Number 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.
* <p>
* 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()}.
* <p>
* 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<String> 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.
* <p>
* 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<TweetEntity> 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
}
/**
* The different types of API request. These can have different rate limits.
*/
public static enum KRequestType {
NORMAL(""), SEARCH("Feature"),
/** this is X-Feature Class "namesearch" in the response headers */
SEARCH_USERS("Feature"), SHOW_USER(""), UPLOAD_MEDIA("Media");
/**
* 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 List<TweetEntity> 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<TweetEntity> list = new ArrayList<TweetEntity>(
arr.length());
for (int i = 0; i < arr.length(); i++) {
JSONObject obj = arr.getJSONObject(i);
TweetEntity te = new TweetEntity(tweet, rawText, type, obj);
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;
/**
*
* @param tweet
* @param rawText Needed to undo the indexing errors created by entity encoding
* @param type
* @param obj
* @throws JSONException
*/
TweetEntity(ITweet tweet, String rawText, KEntityType type, JSONObject obj)
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;
default:
display = 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 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
start = _start;
end = _end;
return;
}
String entityText = rawText.substring(_start, _end);
int i = text.indexOf(entityText);
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;
}
/**
* @return For a url: the expanded version For a user-mention: the
* user's name
*/
public String displayVersion() {
return display == null ? toString() : display;
}
/**
* 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.
* <p>
* Screen-names are case insensitive as far as Twitter is concerned. However
* you might want to preserve the case people use for display purposes.
* <p>
* false by default.
*/
public static boolean CASE_SENSITIVE_SCREENNAMES;
static final Pattern contentTag = Pattern.compile(
"<content>(.+?)<\\/content>", Pattern.DOTALL);
static final Pattern idTag = Pattern.compile("<id>(.+?)<\\/id>",
Pattern.DOTALL);
/**
* The length of a url after t.co shortening. Currently 20 characters.
* <p>
* Use updateConfiguration() if you want to get the latest settings from
* Twitter.
*/
public static int LINK_LENGTH = 20;
public static long PHOTO_SIZE_LIMIT;
public static final String SEARCH_MIXED = "mixed";
public static final String SEARCH_POPULAR = "popular";
public static final String SEARCH_RECENT = "recent";
private static final long serialVersionUID = 1L;
/**
* Search has to go through a separate url (Twitter's decision, June 2010).
*/
private static final String TWITTER_SEARCH_URL = "http://search.twitter.com";
/**
* JTwitter version
*/
public final static String version = "2.4";
/**
* 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<User> 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<Long> 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;
boolean includeRTs = true;
private String lang;
private BigInteger maxId;
/**
* Provides support for fetching many pages
*/
private int maxResults = -1;
private double[] myLatLong;
/**
* Twitter login name. Can be null even if we have authentication when using
* OAuth.
*/
private String name;
/**
* Gets used once then reset to null by
* {@link #addStandardishParameters(Map)}. Gets updated in the while loops
* of methods doing a get-all-pages.
*/
Integer pageNumber;
private String resultType;
/**
* The user. Can be null. Can be a "fake-user" (screenname-only) object.
*/
User self;
private Date sinceDate;
private Number sinceId;
private String sourceApp = "jtwitterlib";
boolean tweetEntities = true;
private String twitlongerApiKey;
private String twitlongerAppName;
/**
* Change this to access sites other than Twitter that support the Twitter
* API. <br>
* Note: Does not include the final "/"
*/
String TWITTER_URL = "https://api.twitter.com/1.1";
private Date untilDate;
private Number untilId;
/**
* Create a Twitter client without specifying a user. This is an easy way to
* access public posts. But you can't post of course.
*/
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
* @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.
*
* @param jtwit
*/
public Twitter(Twitter jtwit) {
this(jtwit.getScreenName(), jtwit.http.copy());
}
/**
* API methods relating to your account.
*/
public Twitter_Account account() {
return new Twitter_Account(this);
}
/**
* Add in since_id, page and count, if set. This is called by methods that
* return lists of statuses or messages.
*
* @param vars
* @return vars
*/
private Map<String, String> addStandardishParameters(
Map<String, String> vars) {
if (sinceId != null) {
vars.put("since_id", sinceId.toString());
}
if (untilId != null) {
vars.put("max_id", untilId.toString());
}
if (pageNumber != null) {
vars.put("page", pageNumber.toString());
// this is used once only
pageNumber = null;
}
if (count != null) {
vars.put("count", count.toString());
}
if (tweetEntities) {
vars.put("include_entities", "1");
}
if (includeRTs) {
vars.put("include_rts", "1");
}
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<User> bulkShow(List<String> screenNames) {
return users().show(screenNames);
}
/**
* @deprecated Use {@link #showById(List)} instead
*/
public List<User> 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 <T extends ITweet> List<T> dateFilter(List<T> list) {
if (sinceDate == null && untilDate == null)
return list;
ArrayList<T> filtered = new ArrayList<T>(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) {
String page = post(TWITTER_URL + "/direct_messages/destroy/" + dm.id
+ ".json", null, true);
assert page != null;
}
/**
* 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) {
String page = post(TWITTER_URL + "/direct_messages/destroy/" + id
+ ".json", null, 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 false if maxResults is set to -1 (ie, unlimited) or if list
* contains less than maxResults results.
*/
boolean enoughResults(List list) {
return (maxResults != -1 && list.size() >= maxResults);
}
void flush() {
// This seems to prompt twitter to update in some cases!
http.getPage("http://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.
*/
public Twitter_Geo geo() {
return new Twitter_Geo(this);
}
/**
* Returns a list of the direct messages sent to the authenticating user.
* <p>
* Note: the Twitter API makes this available in rss if that's of interest.
*/
public List<Message> getDirectMessages() {
return getMessages(TWITTER_URL + "/direct_messages.json",
standardishParameters());
}
/**
* Returns a list of the direct messages sent *by* the authenticating user.
*/
public List<Message> getDirectMessagesSent() {
return getMessages(TWITTER_URL + "/direct_messages/sent.json",
standardishParameters());
}
/**
* The most recent 20 favourite tweets. (Note: This can use page - and page
* only - to fetch older favourites).
*/
public List<Status> getFavorites() {
return getStatuses(TWITTER_URL + "/favorites.json",
standardishParameters(), true);
}
/**
* The most recent 20 favourite tweets for the given user. (Note: This can
* use page - and page only - to fetch older favourites).
*
* @param screenName
* login-name.
*/
public List<Status> getFavorites(String screenName) {
Map<String, String> vars = InternalUtils.asMap("screen_name",
screenName);
return getStatuses(TWITTER_URL + "/favorites.json",
addStandardishParameters(vars), http.canAuthenticate());
}
/**
* @see Twitter_Users#getFollowerIDs()
*/
@Deprecated
public List<Number> getFollowerIDs() throws TwitterException {
return users().getFollowerIDs();
}
/**
* @see Twitter_Users#getFollowerIDs(String)
*/
@Deprecated
public List<Number> getFollowerIDs(String screenName)
throws TwitterException {
return users().getFollowerIDs(screenName);
}
/**
* @see Twitter_Users#getFollowers()
*/
@Deprecated
public List<User> getFollowers() throws TwitterException {
return users().getFollowers();
}
/**
* @see Twitter_Users#getFollowers(String)
*/
@Deprecated
public List<User> getFollowers(String username) throws TwitterException {
return users().getFollowers(username);
}
/**
* @see Twitter_Users#getFriendIDs()
*/
@Deprecated
public List<Number> getFriendIDs() throws TwitterException {
return users().getFriendIDs();
}
/**
* @see Twitter_Users#getFriendIDs(String)
*/
@Deprecated
public List<Number> getFriendIDs(String screenName) throws TwitterException {
return users().getFriendIDs(screenName);
}
/**
* @see Twitter_Users#getFriends()
*/
@Deprecated
public List<User> getFriends() throws TwitterException {
return users().getFriends();
}
/**
* @see Twitter_Users#getFriendss(String)
*/
@Deprecated
public List<User> 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<Status> 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<Status> 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<TwitterList> getLists() {
return getLists(name);
}
/**
*
Returns <i>all</i> 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<TwitterList> getListsAll(User user) {
assert user!=null || http.canAuthenticate() : "No authenticating user";
try {
String url = TWITTER_URL + "/lists/all.json";
Map<String, String> 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<TwitterList> lists = new ArrayList<TwitterList>();
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<TwitterList> getLists(String screenName) {
assert screenName != null;
try {
String url = TWITTER_URL + "/" + screenName + "/lists.json";
String listsJson = http.getPage(url, null, true);
JSONObject wrapper = new JSONObject(listsJson);
JSONArray jarr = (JSONArray) wrapper.get("lists");
List<TwitterList> lists = new ArrayList<TwitterList>();
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<TwitterList> getListsContaining(String screenName,
boolean filterToOwned) {
assert screenName != null;
try {
String url = TWITTER_URL + "/lists/memberships.json";
Map<String, String> 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<TwitterList> lists = new ArrayList<TwitterList>();
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<TwitterList> getListsContainingMe() {
return getListsContaining(name, false);
}
/**
* @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) {
// regex for http://tl.gd/ID
int i = truncatedStatus.text.indexOf("http://tl.gd/");
if (i == -1)
return truncatedStatus.text;
String id = truncatedStatus.text.substring(i + 13).trim();
String response = http.getPage("http://www.twitlonger.com/api_read/"
+ id, null, false);
Matcher m = contentTag.matcher(response);
boolean ok = m.find();
if (!ok)
throw new TwitterException.TwitLongerException(
"TwitLonger call failed", response);
String longMsg = m.group(1).trim();
return longMsg;
}
/**
* Provides support for fetching many pages. -1 indicates "give me as much
* as Twitter will let me have."
*/
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 can not request a list of replies to
* another user whether public or protected.
* <p>
* This is exactly the same as {@link #getReplies()}
* <p>
* When paging, this method can only go back up to 800 statuses.
* <p>
* Does not include new-style retweets.
*/
public List<Status> getMentions() {
return getStatuses(TWITTER_URL + "/statuses/mentions.json",
standardishParameters(), true);
}
/**
*
* @param url
* @param var
* @param isPublic
* Value to set for Message.isPublic
* @return
*/
private List<Message> getMessages(String url, Map<String, String> var) {
// Default: 1 page
if (maxResults < 1) {
List<Message> msgs = Message.getMessages(http.getPage(url, var,
true));
msgs = dateFilter(msgs);
return msgs;
}
// Fetch all pages until we run out
// -- or Twitter complains in which case you'll get an exception
pageNumber = 1;
List<Message> msgs = new ArrayList<Message>();
while (msgs.size() <= maxResults) {
String p = http.getPage(url, var, true);
List<Message> nextpage = Message.getMessages(p);
nextpage = dateFilter(nextpage);
msgs.addAll(nextpage);
if (nextpage.size() < 20) {
break;
}
pageNumber++;
var.put("page", Integer.toString(pageNumber));
}
return msgs;
}
/**
* Returns the 20 most recent statuses from non-protected users who have set
* a custom user icon. Does not require authentication.
* <p>
* Note: Twitter cache-and-refresh this every 60 seconds, so there is little
* point calling it more frequently than that.
*/
public List<Status> getPublicTimeline() throws TwitterException {
return getStatuses(TWITTER_URL + "/statuses/public_timeline.json",
standardishParameters(), false);
}
/**
* 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.
* <p>
* 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.
* <p>
* Status: Headin 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);
}
/**
* 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.
* <p>
* 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. <i>If this is zero or negative you should stop
* using Twitter with this login for a bit.</i> Note: Calls to
* rate_limit_status do not count against the rate limit.
* @see #getRateLimit(KRequestType)
*/
public int getRateLimitStatus() {
String json = http.getPage(TWITTER_URL
+ "/account/rate_limit_status.json", null,
http.canAuthenticate());
try {
JSONObject obj = new JSONObject(json);
int hits = obj.getInt("remaining_hits");
// Update the RateLimit objects
// http.updateRateLimits(KRequestType.NORMAL); no header info sent!
if (http instanceof URLConnectionHttpClient) {
URLConnectionHttpClient _http = (URLConnectionHttpClient) http;
RateLimit rateLimit = new RateLimit(
obj.getString("hourly_limit"), Integer.toString(hits),
obj.getString("reset_time"));
_http.rateLimits.put(KRequestType.NORMAL, rateLimit);
}
return hits;
} catch (JSONException e) {
throw new TwitterException.Parsing(json, e);
}
}
/**
* 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.
* <p>
* This is exactly the same as {@link #getMentions()}! Twitter
* changed their API & terminology - we are (currently) keeping
* both methods.
* <p>
* When paging, this method can only go back up to 800 statuses.
* <p>
* Does not include new-style retweets.
* @deprecated Use #getMentions() for preference
*/
public List<Status> 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<User> getRetweeters(Status tweet) {
String url = TWITTER_URL + "/statuses/" + tweet.id
+ "/retweeted_by.json";
Map<String, String> vars = addStandardishParameters(new HashMap<String, String>());
String json = http.getPage(url, vars, http.canAuthenticate());
List<User> users = User.getUsers(json);
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<Status> getRetweets(Status tweet) {
String url = TWITTER_URL + "/statuses/retweets/" + tweet.id + ".json";
Map<String, String> vars = addStandardishParameters(new HashMap<String, String>());
String json = http.getPage(url, vars, true);
List<Status> 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 > 140) {
int i = tweet.text.lastIndexOf(' ', 140 - sq.length() - 1);
String words = tweet.text.substring(0, i);
sq.append(words);
} else {
sq.append(tweet.text);
}
sq.append('"');
List<Status> oldStyle = search(sq.toString());
// merge them
newStyle.addAll(oldStyle);
Collections.sort(newStyle, InternalUtils.NEWEST_FIRST);
return newStyle;
} catch (TwitterException e) {
// oh well
return newStyle;
}
}
/**
* @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<Status> getRetweetsByMe() {
String url = TWITTER_URL + "/statuses/retweeted_by_me.json";
Map<String, String> vars = addStandardishParameters(new HashMap<String, String>());
String json = http.getPage(url, vars, true);
return Status.getStatuses(json);
}
/**
* @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<Status> getRetweetsOfMe() {
String url = TWITTER_URL + "/statuses/retweets_of_me.json";
Map<String, String> vars = addStandardishParameters(new HashMap<String, String>());
String json = http.getPage(url, vars, true);
return Status.getStatuses(json);
}
/**
* @return Login name of the authenticating user, or null if not set.
* <p>
* 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;
}
/**
* @param searchTerm
* @param rpp
* @return
*/
private Map<String, String> getSearchParams(String searchTerm, int rpp) {
Map<String, String> vars = InternalUtils.asMap("rpp",
Integer.toString(rpp), "q", searchTerm);
if (sinceId != null) {
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.
* <p>
* 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!
*/
public Status getStatus() throws TwitterException {
Map<String, String> vars = InternalUtils.asMap("count", 6);
String json = http.getPage(
TWITTER_URL + "/statuses/user_timeline.json", vars, true);
List<Status> 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 {
Map vars = tweetEntities ? InternalUtils.asMap("include_entities", "1")
: null;
String json = http.getPage(TWITTER_URL + "/statuses/show/" + id
+ ".json", vars, http.canAuthenticate());
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.
* <p>
* 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<String, String> vars = InternalUtils.asMap("id", username, "count",
6);
String json = http.getPage(
TWITTER_URL + "/statuses/user_timeline.json", vars, false);
List<Status> 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
*/
private List<Status> getStatuses(final String url, Map<String, String> var,
boolean authenticate) {
// Default: 1 page
if (maxResults < 1) {
List<Status> msgs = Status.getStatuses(http.getPage(url, var,
authenticate));
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.
maxId = null;
// pageNumber = 1;
List<Status> msgs = new ArrayList<Status>();
while (msgs.size() <= maxResults) {
String json = http.getPage(url, var, authenticate);
List<Status> nextpage = Status.getStatuses(json);
// 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 = nextpage.get(nextpage.size() - 1).id
.subtract(BigInteger.ONE);
// System.out.println(maxId + " -> " + nextpage.get(0).id);
msgs.addAll(dateFilter(nextpage));
// pageNumber++;
var.put("max_id", maxId.toString());
}
return msgs;
}
/**
* @return the latest global trending topics on Twitter
*/
public List<String> 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<String> getTrends(Number woeid) {
String jsonTrends = http.getPage(TWITTER_URL + "/trends/" + woeid
+ ".json", null, false);
try {
JSONArray jarr = new JSONArray(jsonTrends);
JSONObject json1 = jarr.getJSONObject(0);
JSONArray json2 = json1.getJSONArray("trends");
List<String> trends = new ArrayList<String>();
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<Status> 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<Status> getUserTimeline(Long userId) throws TwitterException {
Map<String, String> 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.
* <p>
* This will return 20 results by default, though
* {@link #setMaxResults(int)} can be used to fetch multiple pages.
*
* 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.
* <p>
* There is a cap of 3200 tweets - this is the farthest back you can go down
* a user timeline!
* <p>
* 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<Status> getUserTimeline(String screenName)
throws TwitterException {
Map<String, String> 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 (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.
* <p>
* 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!
* <p>
* 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<Status> getUserTimelineWithRetweets(String screenName)
throws TwitterException {
Map<String, String> 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);
}
/**
* @see Twitter_Users#isFollower(String, String)
*/
@Deprecated
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);
}
/**
* @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) {
// Check NORMAL first
if (reqType != KRequestType.NORMAL) {
boolean isLimited = isRateLimited(KRequestType.NORMAL, minCalls);
if (isLimited)
return true;
}
RateLimit rl = getRateLimit(reqType);
// assume things are OK (except for NORMAL which we quickly check by
// calling Twitter)
if (rl == null) {
if (reqType == KRequestType.NORMAL) {
int rls = getRateLimitStatus();
return rls >= minCalls;
}
return false;
}
// in credit?
if (rl.getRemaining() >= minCalls)
return false;
// out of date?
if (rl.getReset().getTime() < System.currentTimeMillis())
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);
}
/**
* @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<String, String> vars,
boolean authenticate) throws TwitterException {
String page = http.post(uri, vars, 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 {
String result = post(
TWITTER_URL + "/statuses/retweet/" + tweet.getId()
+ ".json", null, true);
return new Status(new JSONObject(result), null);
// error handling
} catch (E403 e) {
List<Status> 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);
}
}
/**
* 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<Status> search(String searchTerm) {
return search(searchTerm, null, 100);
}
/**
* Perform a search of Twitter.
* <p>
* 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 {@link #show(String)}
* with the screen name.
* <p>
* 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.<br>
* "from:winterstein" - tweets from user winterstein<br>
* "to:winterstein" - tweets start with @winterstein<br>
* "source:jtwitter" - originating from the application JTwitter - your
* query must also must contain at least one keyword parameter. <br>
* "filter:links" - tweets contain a link<br>
* "apples OR pears" - or ("apples pears" would give you apples <i>and</i>
* pears).
*
* @param searchTerm
* This can include several space-separated keywords, #tags and @username
* (for mentions), and use quotes for \"exact phrase\" searches.
* @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.
*/
public List<Status> search(String searchTerm, ICallback callback, int rpp) {
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);
searchTerm = search2_bugHack(searchTerm);
Map<String, String> 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<Status> allResults = new ArrayList<Status>(Math.max(maxResults,
rpp));
String url = TWITTER_SEARCH_URL + "/search.json";
int localPageNumber = 1; // pageNumber is nulled by getSearchParams
do {
pageNumber = localPageNumber;
vars.put("page", Integer.toString(pageNumber));
String json = http.getPage(url, vars, false);
List<Status> stati = Status.getStatusesFromSearch(this, json);
int numResults = stati.size();
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 (numResults < rpp) { // We've reached the end of the results
break;
}
// paranoia
localPageNumber++;
} while (allResults.size() < maxResults);
// null for the next method
pageNumber = null;
return allResults;
}
/**
* 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.<br>
*
* 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"/
* <p>
* It should be tested periodically whether we need this. See
* {@link TwitterTest#testSearchBug()}, {@link TwitterTest#testSearchBug2()}
*
* @param searchTerm
* @return e.g. "apples OR pears" (near Edinburgh) goes to
* "apples OR pears -kfz" (near Edinburgh)
*/
private String search2_bugHack(String searchTerm) {
// 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<User> 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 recipient
* Required. The screen name of the recipient user.
* @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 recipient, String text)
throws TwitterException {
assert recipient != null && text != null : recipient + " " + text;
assert !text.startsWith("d " + recipient) : recipient + " " + text;
if (text.length() > 140)
throw new IllegalArgumentException("Message is too long.");
Map<String, String> vars = InternalUtils.asMap("user", recipient,
"text", text);
if (tweetEntities) {
vars.put("include_entities", "1");
}
String result = null;
try {
// post it
result = post(TWITTER_URL + "/direct_messages/new.json", vars, true);
// sadly the response doesn't include rate-limit info
return new Message(new JSONObject(result));
} catch (JSONException e) {
throw new TwitterException.Parsing(result, e);
} catch (TwitterException.E404 e) {
// suspended user?? TODO investigate
throw new TwitterException.E404(e.getMessage() + " with recipient="
+ recipient + ", text=" + text);
}
}
/**
* 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.
*/
public void setAPIRootUrl(String url) {
assert url.startsWith("http://") || url.startsWith("https://");
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;
}
public void setFavorite(Status status, boolean isFavorite) {
try {
String uri = isFavorite ? TWITTER_URL + "/favorites/create/"
+ status.id + ".json" : TWITTER_URL + "/favorites/destroy/"
+ status.id + ".json";
http.post(uri, null, true);
} catch (E403 e) {
// already a favorite?
if (e.getMessage() != null
&& e.getMessage().contains("already favorited"))
throw new TwitterException.Repetition(
"You have already favorited this status.");
// 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.
* <p>
* 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.
* <p>
* 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.<br>
*
* 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.
* <p>
* 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");
}
/**
* @param pageNumber
* null (the default) returns the first page. Pages are indexed
* from 1. This is used once only! Then it is reset to null
*/
public void setPageNumber(Integer pageNumber) {
this.pageNumber = pageNumber;
}
/**
* Restricts {@link #search(String)} to tweets by users located within a
* given radius of the given latitude/longitude.
* <p>
* 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 = latitude + "," + longitude + "," + radius;
}
/**
* Optional. Specifies what type of search results you would prefer to
* receive. The current default is "mixed." Valid values:<br>
* {@link #SEARCH_MIXED}: Include both popular and real time results in the
* response.<br> {@link #SEARCH_RECENT}: return only the most recent results in
* the response<br> {@link #SEARCH_POPULAR}: return only the most popular
* results in the response.<br>
*
* @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.
* <p>
* 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. Default
* is null.
* <p>
* If using this, you probably also want to increase
* {@link #setMaxResults(int)} (otherwise you just get the most recent 20).
*
* @param statusId
*/
public void setSinceId(Number statusId) {
sinceId = statusId;
}
/**
* Set the source application. This will be mentioned on Twitter alongside
* status updates (with a small label saying source: myapp).
*
* <i>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.</i>
*
* @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.
* <p>
* 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 160
* characters and should not be more than 140 characters to
* ensure optimal display.
* @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 = untilId;
}
/**
* 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
*
* @param twitlongerAppName
* @param twitlongerApiKey
*/
public void setupTwitlonger(String twitlongerAppName,
String twitlongerApiKey) {
this.twitlongerAppName = twitlongerAppName;
this.twitlongerApiKey = twitlongerApiKey;
}
/**
* @see Twitter_Users#show(Number)
*/
@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<String> splitMessage(String longStatus) {
// Is it really long?
if (longStatus.length() <= 140)
return Collections.singletonList(longStatus);
// Multiple tweets for a longer post
List<String> sections = new ArrayList<String>(4);
StringBuilder tweet = new StringBuilder(140);
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 > 140) {
// Emit
tweet.append("...");
sections.add(tweet.toString());
tweet = new StringBuilder(140);
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<String, String> standardishParameters() {
return addStandardishParameters(new HashMap<String, String>());
}
// /**
// * The length of an https url after t.co shortening.
// * This is just 1 more than {@link #LINK_LENGTH}
// * <p>
// * Use updateConfiguration() if you want to get the latest settings from
// Twitter.
// */
// public static int LINK_LENGTH_HTTPS = LINK_LENGTH+1;
/**
* @see Twitter_Users#stopFollowing(String)
*/
@Deprecated
public User stopFollowing(String username) {
return users().stopFollowing(username);
}
/**
* @see Twitter_Users#stopFollowing(User)
*/
@Deprecated
public User stopFollowing(User user) {
return stopFollowing(user.screenName);
}
/**
* Update info on Twitter's configuration -- such as shortened url lengths.
*/
public void updateConfiguration() {
String json = http.getPage(TWITTER_URL + "/help/configuration.json",
null, false);
try {
JSONObject jo = new JSONObject(json);
LINK_LENGTH = jo.getInt("short_url_length");
// LINK_LENGTH_HTTPS = jo.getInt("short_url_length_https");
// LINK_LENGTH + 1
// characters_reserved_per_media -- this is just LINK_LENGTH
// max_media_per_upload // 1!
PHOTO_SIZE_LIMIT = jo.getLong("photo_size_limit");
// photo_sizes
// short_url_length_https
} catch (JSONException e) {
throw new TwitterException.Parsing(json, e);
}
}
/**
* Use twitlonger.com to post a lengthy tweet. See twitlonger.com for more
* details on their service.
* <p>
* 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) {
if (twitlongerApiKey == null || twitlongerAppName == null)
throw new IllegalStateException(
"Twitlonger api details have not been set! Call #setupTwitlonger() first.");
if (message.length() < 141)
throw new IllegalArgumentException("Message too short ("
+ inReplyToStatusId
+ " chars). Just post a normal Twitter status. ");
String url = "http://www.twitlonger.com/api_post";
Map<String, String> vars = InternalUtils.asMap("application",
twitlongerAppName, "api_key", twitlongerApiKey, "username",
name, "message", message);
if (inReplyToStatusId != null && inReplyToStatusId.doubleValue() != 0) {
vars.put("in_reply", inReplyToStatusId.toString());
}
// ?? set direct_message 0/1 as appropriate if allowing long DMs
String response = http.post(url, vars, false);
Matcher m = contentTag.matcher(response);
boolean ok = m.find();
if (!ok)
throw new TwitterException.TwitLongerException(
"TwitLonger call failed", response);
String shortMsg = m.group(1).trim();
// Post to Twitter
Status s = updateStatus(shortMsg, inReplyToStatusId);
m = idTag.matcher(response);
ok = m.find();
if (!ok)
// weird - but oh well
return s;
String id = m.group(1);
// Once a message has been successfully posted to Twitlonger and
// Twitter, it would be really useful to send back the Twitter ID for
// the message. This will allow users to manage their Twitlonger posts
// and delete not only the Twitlonger post, but also the Twitter post
// associated with it. It will also makes replies much more effective.
try {
url = "http://www.twitlonger.com/api_set_id";
vars.remove("message");
vars.remove("in_reply");
vars.remove("username");
vars.put("message_id", "" + id);
vars.put("twitter_id", "" + s.getId());
http.post(url, vars, false);
} catch (Exception e) {
// oh well
}
// done
return s;
}
/**
* Updates the authenticating user's status.
*
* @param statusText
* The text of your status update. Must not be more than 160
* characters and should not be more than 140 characters to
* ensure optimal display.
* @return The posted status when successful.
*/
public Status updateStatus(String statusText) {
return updateStatus(statusText, 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 160
* characters and should not be more than 140 characters to
* ensure optimal display.
*
*
* @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 for Twitter to
* agree to mark the tweet as a reply. <i>null</i> to leave this
* unset.
*
* @return The posted status when successful.
* <p>
* 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)
throws TwitterException
{
// check for length
if (statusText.length() > 160) {
int shortLength = statusText.length();
Matcher m = InternalUtils.URL_REGEX.matcher(statusText);
while(m.find()) {
shortLength += LINK_LENGTH - m.group().length();
}
if (shortLength > 140) {
// bogus - send a helpful error
if (statusText.startsWith("RT")) {
throw new IllegalArgumentException(
"Status text must be 140 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 140 characters or less: "
+ statusText.length() + " " + statusText);
}
}
Map<String, String> 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 (sourceApp != null) {
vars.put("source", sourceApp);
}
if (inReplyToStatusId != null) {
// TODO remove this legacy check
double v = inReplyToStatusId.doubleValue();
assert v != 0 && v != -1;
vars.put("in_reply_to_status_id", inReplyToStatusId.toString());
}
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);
}
}
private Status updateStatus2_safetyCheck(String statusText, Status s) {
// Weird bug: Twitter occasionally rejects tweets?!
// TODO does this still happen or have they fixed it? Hard to know
// with an intermittent bug!
// 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))
return 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;
}
// Assume Twitter have fixed this bug -- TODO check this periodically
if (true) return s;
// try waiting and rechecking - maybe it did work after all
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);
}
// TODO
// c.f. https://dev.twitter.com/discussions/1059
Status updateStatusWithMedia(String statusText, Number inReplyToStatusId,
File media) {
// should we trim statusText??
// TODO support URL shortening
if (statusText.length() > 160)
throw new IllegalArgumentException(
"Status text must be 160 characters or less: "
+ statusText.length() + " " + statusText);
Map<String, String> vars = InternalUtils.asMap("status", statusText);
// add in long/lat if set
if (myLatLong != null) {
vars.put("lat", Double.toString(myLatLong[0]));
vars.put("long", Double.toString(myLatLong[1]));
}
if (sourceApp != null) {
vars.put("source", sourceApp);
}
if (inReplyToStatusId != null) {
// TODO remove this legacy check
double v = inReplyToStatusId.doubleValue();
assert v != 0 && v != -1;
vars.put("in_reply_to_status_id", inReplyToStatusId.toString());
}
// media[]
// possibly_sensitive
// place_id
// display_coordinates
String result = null;
try {
result = http
.post( // WithMedia
// TWITTER_URL +
"http://upload.twitter.com/1/statuses/update_with_media.json",
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);
}
}
/**
* User and social-network related API methods.
*/
public Twitter_Users users() {
return new Twitter_Users(this);
}
}