package org.schtief.twitter; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; 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 org.schtief.util.json.JSONArray; import org.schtief.util.json.JSONException; import org.schtief.util.json.JSONObject; /** * Java wrapper for the Twitter API version 1.2.2 * <p> * Example usage: * <code><pre> // Make a Twitter object Twitter twitter = new Twitter("my-name","my-password"); // Print Winterstein's status System.out.println(twitter.getStatus("winterstein")); // Set my status twitter.updateStatus("Messing about in Java"); </pre></code> * <p> * See {@link http://www.winterwell.com/software/jtwitter.php} for more * information about this wrapper. See * {@link http://apiwiki.twitter.com/Twitter-API-Documentation} 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. I would like to improve error-handling, and welcome suggestions on * cases where more informative exceptions would be helpful. * </ul> * * <h4>Copyright and License</h4> * This code is copyright (c) Winterwell Associates 2008/2009 and (c) ThinkTank * 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 * * modified by Stefan Lischke */ public class Twitter { /** * Interface for an http client - e.g. allows for OAuth to be used instead. * The default version is {@link URLConnectionHttpClient}. * <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 can authenticate to the server. */ boolean canAuthenticate(); /** * Send an HTTP GET request and return the response body. Note that this * will change all line breaks into system line breaks! * * @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; /** * 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; } /** * This gives common access to features that are common to both * {@link Message}s and {@link Status}es. * * @author daniel * */ public static interface ITweet { Date getCreatedAt(); /** * @return The Twitter id for this post. This is used by some API * methods. */ long getId(); /** The actual status text. This is also returned by {@link #toString()} */ String getText(); /** The User who made the tweet */ User getUser(); } /** * A Twitter direct message. Fields are null if unset. * <p> * Historical note: this class used to cover \@you mentions as well, * but these are better described by Statuses. */ public static final class Message implements ITweet { /** * * @param json * @return * @throws TwitterException */ static List<Message> getMessages(String json) throws TwitterException { if (json.trim().equals("")) return Collections.emptyList(); try { List<Message> msgs = new ArrayList<Message>(); JSONArray arr = new JSONArray(json); for (int i = 0; i < arr.length(); i++) { JSONObject obj = arr.getJSONObject(i); Message u = new Message(obj); msgs.add(u); } return msgs; } catch (JSONException e) { throw new TwitterException(e); } } private final Date createdAt; private final long id; private final User recipient; private final User sender; private final String text; /** * @param obj * @throws JSONException * @throws TwitterException */ Message(JSONObject obj) throws JSONException, TwitterException { id = obj.getLong("id"); text = obj.getString("text"); String c = jsonGet("created_at", obj); createdAt = new Date(c); sender = new User(obj.getJSONObject("sender"), null); // recipient - for messages you sent if (obj.has("recipient")) { recipient = new User(obj.getJSONObject("recipient"), null); } else { recipient = null; } } public Date getCreatedAt() { return createdAt; } public long getId() { return id; } /** * @return the recipient (for messages sent by the authenticating user) */ public User getRecipient() { return recipient; } public User getSender() { return sender; } public String getText() { return text; } /** * This is equivalent to {@link #getSender()} */ public User getUser() { return getSender(); } @Override public String toString() { return text; } } /** * A Twitter status post. .toString() returns the status text. * <p> * Notes: This is a finalised data object. It has no methods but exposes its * fields. If you want to change your status, use * {@link Twitter#updateStatus(String)} and * {@link Twitter#destroyStatus(Status)}. */ public static final class Status implements ITweet { /** * Convert from a json array of objects into a list of tweets. * @param json can be empty, must not be null * @throws TwitterException */ static List<Status> getStatuses(String json) throws TwitterException { if (json.trim().equals("")) return Collections.emptyList(); try { List<Status> tweets = new ArrayList<Status>(); JSONArray arr = new JSONArray(json); for (int i = 0; i < arr.length(); i++) { JSONObject obj = arr.getJSONObject(i); Status tweet = new Status(obj, null); tweets.add(tweet); } return tweets; } catch (JSONException e) { throw new TwitterException(e); } } /** * Search results use a slightly different protocol! In particular * w.r.t. user ids and info. * * @param searchResults * @return search results as Status objects - but with dummy users! * The dummy users have a screenname and a profile image url, but * no other information. This reflects the current behaviour of the Twitter API. */ static List<Status> getStatusesFromSearch(Twitter tw, JSONObject searchResults) { try { List<Status> users = new ArrayList<Status>(); JSONArray arr = searchResults.getJSONArray("results"); for (int i = 0; i < arr.length(); i++) { JSONObject obj = arr.getJSONObject(i); String userScreenName = obj.getString("from_user"); String profileImgUrl = obj.getString("profile_image_url"); User user = new User(userScreenName); user.profileImageUrl = URI(profileImgUrl); Status s = new Status(obj, user); users.add(s); } return users; } catch (JSONException e) { throw new TwitterException(e); } } public final Date createdAt; public final long id; /** The actual status text. */ public final String text; //schtief adds geo public final double lat; public final double lon; public final User user; /** * E.g. "web" vs. "im" */ public final String source; /** * regex for @you mentions */ static final Pattern AT_YOU_SIR = Pattern.compile("@(\\w+)"); /** * @param object * @param user * Set when parsing the json returned for a User * @throws TwitterException */ Status(JSONObject object, User user) throws TwitterException { try { id = object.getLong("id"); text = jsonGet("text", object); String c = jsonGet("created_at", object); createdAt = new Date(c); source = jsonGet("source", object); //schtief adds geotweets JSONObject geo = object.optJSONObject("geo"); if (geo != null && !JSONObject.NULL.equals(geo)) { JSONArray coordinates = geo.getJSONArray("coordinates"); lat = coordinates.getDouble(0); lon = coordinates.getDouble(1); }else{ lat=-1; lon=-1; } if (user != null) { this.user = user; } else { JSONObject jsonUser = object.optJSONObject("user"); this.user = new User(jsonUser, this); } } catch (JSONException e) { throw new TwitterException(e); } } public Date getCreatedAt() { return createdAt; } /** * @return The Twitter id for this post. This is used by some API * methods. */ public long getId() { return id; } /** * @return list of \@mentioned people (there is no guarantee that * these mentions are for correct Twitter screen-names). May be empty, * never null. Screen-names are always lowercased. */ public List<String> getMentions() { Matcher m = AT_YOU_SIR.matcher(text); List<String> list = new ArrayList<String>(2); while(m.find()) { // skip email addresses (and other poorly formatted things) if (m.start()!=0 && text.charAt(m.start()-1) != ' ') continue; String mention = m.group(1); // enforce lower case list.add(mention.toLowerCase()); } return list; } /** The actual status text. This is also returned by {@link #toString()} */ public String getText() { return text; } public User getUser() { return user; } public double getLat() { return lat; } public double getLon() { return lon; } /** * @return The text of this status. E.g. "Kicking fommil's arse at * Civilisation." */ @Override public String toString() { return text; } } /** * A Twitter user. Fields are null if unset. * * @author daniel */ public static final class User { static List<User> getUsers(String json) throws TwitterException { if (json.trim().equals("")) return Collections.emptyList(); try { List<User> users = new ArrayList<User>(); JSONArray arr = new JSONArray(json); for (int i = 0; i < arr.length(); i++) { JSONObject obj = arr.getJSONObject(i); User u = new User(obj, null); users.add(u); } return users; } catch (JSONException e) { throw new TwitterException(e); } } public final String description; public final long id; public final String location; /** The display name, e.g. "Daniel Winterstein" */ public final String name; /** we allow this to be edited as a convenience for the User * objects generated by search */ public URI profileImageUrl; public final boolean protectedUser; /** * The login name, e.g. "winterstein" This is the only thing used by * equals() and hashcode(). This is always lower-case, as Twitter * screen-names are case insensitive. */ public final String screenName; public final Status status; public final URI website; /** * Number of seconds between a user's registered time zone and Greenwich * Mean Time (GMT) - aka Coordinated Universal Time or UTC. Can be * positive or negative. */ public final int timezoneOffSet; public final String timezone; public long followersCount; public final String profileBackgroundColor; public final String profileLinkColor; public final String profileTextColor; public final String profileSidebarFillColor; public final String profileSidebarBorderColor; public final long friendsCount; public final String createdAt; public final long favoritesCount; public final URI profileBackgroundImageUrl; public final boolean profileBackgroundTile; public final long statusesCount; public final boolean notifications; public final boolean verified; public final boolean following; User(JSONObject obj, Status status) throws TwitterException { try { id = obj.getLong("id"); name = jsonGet("name", obj); screenName = jsonGet("screen_name", obj).toLowerCase(); location = jsonGet("location", obj); description = jsonGet("description", obj); String img = jsonGet("profile_image_url", obj); profileImageUrl = img == null ? null : URI(img); String url = jsonGet("url", obj); website = url == null ? null : URI(url); protectedUser = obj.getBoolean("protected"); followersCount = obj.getLong("followers_count"); profileBackgroundColor = obj.getString("profile_background_color"); profileLinkColor = obj.getString("profile_link_color"); profileTextColor = obj.getString("profile_text_color"); profileSidebarFillColor = obj.getString("profile_sidebar_fill_color"); profileSidebarBorderColor = obj.getString("profile_sidebar_border_color"); friendsCount = obj.getLong("friends_count"); createdAt = obj.getString("created_at"); favoritesCount = obj.getLong("favourites_count"); String utcOffSet = jsonGet("utc_offset", obj); timezoneOffSet = utcOffSet == null ? 0 : Integer.parseInt(utcOffSet); timezone = jsonGet("time_zone", obj); img = jsonGet("profile_background_image_url", obj); profileBackgroundImageUrl = img == null ? null : URI(img); profileBackgroundTile = obj.getBoolean("profile_background_tile"); statusesCount = obj.getLong("statuses_count"); notifications = obj.optBoolean("notifications"); verified = obj.getBoolean("verified"); following = obj.optBoolean("following"); // status if (status == null) { JSONObject s = obj.optJSONObject("status"); this.status = s == null ? null : new Status(s, this); } else { this.status = status; } } catch (JSONException e) { throw new TwitterException(e); } } /** * Create a dummy User object. All fields are set to null. This will be * equals() to an actual User object, so it can be used to query * collections. E.g. <code><pre> * // Test whether jtwit is a friend * twitter.getFriends().contains(new User("jtwit")); * </pre></code> * * @param screenName This will be converted to lower-case as * Twitter screen-names are case insensitive */ public User(String screenName) { id = -1; name = null; this.screenName = screenName.toLowerCase(); status = null; location = null; description = null; profileImageUrl = null; website = null; protectedUser = false; followersCount = 0; profileBackgroundColor = null; profileLinkColor = null; profileTextColor = null; profileSidebarFillColor = null; profileSidebarBorderColor = null; friendsCount = 0; createdAt = null; favoritesCount = 0; timezoneOffSet = -1; timezone = null; profileBackgroundImageUrl = null; profileBackgroundTile = false; statusesCount = 0; notifications = false; verified = false; following = false; } public boolean equals(Object other) { if (this == other) return true; if (!(other instanceof User)) return false; User ou = (User) other; if (screenName.equals(ou.screenName)) return true; return false; } public String getCreatedAt() { return createdAt; } public String getDescription() { return description; } /** number of statuses a user has marked as favorite */ public long getFavoritesCount() { return favoritesCount; } public long getFollowersCount() { return followersCount; } public long getFriendsCount() { return friendsCount; } public long getId() { return id; } public String getLocation() { return location; } /** The display name, e.g. "Daniel Winterstein" */ public String getName() { return name; } public String getProfileBackgroundColor() { return profileBackgroundColor; } public URI getProfileBackgroundImageUrl() { return profileBackgroundImageUrl; } public URI getProfileImageUrl() { return profileImageUrl; } public String getProfileLinkColor() { return profileLinkColor; } public String getProfileSidebarBorderColor() { return profileSidebarBorderColor; } public String getProfileSidebarFillColor() { return profileSidebarFillColor; } public String getProfileTextColor() { return profileTextColor; } public boolean getProtectedUser() { return protectedUser; } /** The login name, e.g. "winterstein" */ public String getScreenName() { return screenName; } public Status getStatus() { return status; } public long getStatusesCount() { return statusesCount; } /** * String version of the timezone */ public String getTimezone() { return timezone; } /** * Number of seconds between a user's registered time zone and Greenwich * Mean Time (GMT) - aka Coordinated Universal Time or UTC. Can be * positive or negative. */ public int getTimezoneOffSet() { return timezoneOffSet; } public URI getWebsite() { return website; } public int hashCode() { return screenName.hashCode(); } /** * @return true if this is a dummy User object, in which case almost * all of it's fields will be null - with the exception of screenName. * Dummy User objects are equals() to full User objects. */ public boolean isDummyObject() { return name==null; } public boolean isFollowing() { return following; } public boolean isNotifications() { return notifications; } public boolean isProfileBackgroundTile() { return profileBackgroundTile; } public boolean isProtectedUser() { return protectedUser; } /** * @return true if the account has been verified by Twitter to * really be who it claims to be. */ public boolean isVerified() { return verified; } /** * Returns the User's screenName (i.e. their Twitter login) */ @Override public String toString() { return screenName; } } public final static String version = "1.3.0"; /** * Create a map from a list of key, value pairs. An easy way to make small * maps, basically the equivalent of {@link Arrays#asList(Object...)}. */ @SuppressWarnings("unchecked") private static <K, V> Map<K, V> asMap(Object... keyValuePairs) { assert keyValuePairs.length % 2 == 0; Map m = new HashMap(keyValuePairs.length / 2); for (int i = 0; i < keyValuePairs.length; i += 2) { m.put(keyValuePairs[i], keyValuePairs[i + 1]); } return m; } /** * Convenience method for making Dates. Because Date is a tricksy bugger of * a class. * * @param year * @param month * @param day * @return date object */ // public static Date getDate(int year, String month, int day) { // try { // Field field = GregorianCalendar.class.getField(month.toUpperCase()); // int m = field.getInt(null); // Calendar date = new GregorianCalendar(year, m, day); // return date.getTime(); // } catch (Exception x) { // throw new IllegalArgumentException(x.getMessage()); // } // } /** * 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; } /** Helper method to deal with JSON-in-Java weirdness */ protected static String jsonGet(String key, JSONObject jsonObj) { Object val = jsonObj.opt(key); if (val == null) return null; if (JSONObject.NULL.equals(val)) return null; return val.toString(); } /** * * @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]); // 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 or JavaDoc for details on how to use."); // } /** * Convert to a URI, or return null if this is badly formatted */ private static URI URI(String uri) { try { return new URI(uri); } catch (URISyntaxException e) { return null; // Bad syntax } } private String sourceApp = "jtwitterlib"; private Integer pageNumber; private Long sinceId; private Date sinceDate; /** * Provides support for fetching many pages */ private int maxResults; private final IHttpClient http; /** * Can be null even if we have authentication when using OAuth */ private final String name; /** * 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 */ public Twitter(String name, IHttpClient client) { this.name = name; http = client; } /** * Java wrapper for the Twitter API. * * @param screenName * The name of the Twitter user. Only used by some methods. Can * be null if you avoid methods requiring authentication. * @param password * The password of the Twitter user. Can be null if you avoid * methods requiring authentication. */ public Twitter(String screenName, String password) { this(screenName, new URLConnectionHttpClient(screenName, password)); } // private Format format = Format.xml; /** * Add in since and page, 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 (pageNumber != null) { vars.put("page", pageNumber.toString()); // this is used once only pageNumber = null; } return vars; } /** * Create a map from a list of key/value pairs. * @param keyValuePairs * @return */ private Map<String, String> aMap(String... keyValuePairs) { HashMap<String, String> map = new HashMap<String, String>(); for (int i = 0; i < keyValuePairs.length; i+=2) { map.put(keyValuePairs[i],keyValuePairs[i+1]); } return map; } /** * Equivalent to {@link #follow(String)}. C.f. * http://apiwiki.twitter.com/Migrating-to-followers-terminology * * @param username * Required. The ID or 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); } /** * If sinceDate is set, filter keeping only those messages that came after * since date * * @param list * @return filtered list */ private <T extends ITweet> List<T> dateFilter(List<T> list) { if (sinceDate == null) return list; ArrayList<T> filtered = new ArrayList<T>(list.size()); for (T message : list) { if (message.getCreatedAt() == null) filtered.add(message); else if (sinceDate.before(message.getCreatedAt())) filtered.add(message); } return filtered; } /** * Destroys the status specified by the required ID parameter. The * authenticating user must be the author of the specified status. * */ public void destroyStatus(long id) throws TwitterException { String page = post("http://twitter.com/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; } /** * Destroys the given status. Equivalent to {@link #destroyStatus(int)}. The * authenticating user must be the author of the status post. */ public void destroyStatus(Status status) throws TwitterException { destroyStatus(status.getId()); } void flush() { // This seems to prompt twitter to update in some cases! http.getPage("http://twitter.com/" + name, null, true); } /** * Start following a user. * * @param username * Required. The ID or screen name of the user to befriend. * @return The befriended user, or null if they were already being followed. @throws TwitterException if the user does not exist or has been suspended. */ public User follow(String username) throws TwitterException { if (username == null) throw new NullPointerException(); String page; try { page = post("http://twitter.com/friendships/create/" + username + ".json", null, true); // is this needed? doesn't seem to fix things // http.getPage("http://twitter.com/friends", null, true); } catch(TwitterException.E403 e) { // check if we've tried to follow someone we're already // following if (isFollowing(username)) { return null; } throw e; } try { return new User(new JSONObject(page), null); } catch (JSONException e) { throw new TwitterException(e); } } /** * Convenience for {@link #follow(String)} * @param user */ public void follow(User user) { follow(user.screenName); } /** * 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("http://twitter.com/direct_messages.json", standardishParameters()); } /** * Returns a list of the direct messages sent *by* the authenticating user. */ public List<Message> getDirectMessagesSent() { return getMessages("http://twitter.com/direct_messages/sent.json", standardishParameters()); } /** * Returns a list of the users currently featured on the site with their * current statuses inline. * <p> * Note: This is no longer part of the Twitter API. Support is provided via * other methods. */ public List<User> getFeatured() throws TwitterException { List<User> users = new ArrayList<User>(); List<Status> featured = getPublicTimeline(); for (Status status : featured) { User user = status.getUser(); users.add(user); } return users; } /** * Returns the IDs of the authenticating user's followers. * * @throws TwitterException */ public List<Long> getFollowerIDs() throws TwitterException { return getUserIDs("http://twitter.com/followers/ids.json"); } /** * Returns the IDs of the specified user's followers. * * @param The * screen name of the user whose followers are to be fetched. * @throws TwitterException */ public List<Long> getFollowerIDs(String screenName) throws TwitterException { return getUserIDs("http://twitter.com/followers/ids/" + screenName + ".json"); } /** * Returns the authenticating user's (latest) followers, each with current * status inline. */ public List<User> getFollowers() throws TwitterException { return getUsers("http://twitter.com/statuses/followers.json"); } /** * Returns the IDs of the authenticating user's friends. (people who * the user follows). * * @throws TwitterException */ public List<Long> getFriendIDs() throws TwitterException { return getUserIDs("http://twitter.com/friends/ids.json"); } /** * Returns the IDs of the specified user's friends. * * @param The * screen name of the user whose friends are to be fetched. * @throws TwitterException */ public List<Long> getFriendIDs(String screenName) throws TwitterException { return getUserIDs("http://twitter.com/friends/ids/" + screenName + ".json"); } /** * Returns the authenticating user's (latest 100) friends, each with current * status inline. NB - friends are people who *you* follow. * <p> * Note that there seems to be a small delay from Twitter in updates to this list. * @throws TwitterException * @see #getFriendIDs() * @see #isFollowing(String) */ public List<User> getFriends() throws TwitterException { return getUsers("http://twitter.com/statuses/friends.json"); } /** * * Returns the (latest 100) given user's friends, each with current status * inline. * * @param username * The ID or screen name of the user for whom to request a list * of friends. * @throws TwitterException */ public List<User> getFriends(String username) throws TwitterException { return getUsers("http://twitter.com/statuses/friends/" + username + ".json"); } /** * Returns the 20 most recent statuses posted in the last 24 hours from the * authenticating user and that user's friends. */ public List<Status> getFriendsTimeline() throws TwitterException { return getStatuses("http://twitter.com/statuses/friends_timeline.json", standardishParameters()); } /** * Returns the 20 most recent statuses posted in the last 24 hours from the * user (given by id) and that user's friends. * * @param id * Specifies the ID or screen name of the user for whom to return * the friends_timeline. * */ public List<Status> getFriendsTimeline(String id) throws TwitterException { Map<String, String> map = asMap("id", id); addStandardishParameters(map); return getStatuses("http://twitter.com/statuses/friends_timeline.json", map); } /** * * @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; } /** * @return Name of the authenticating user, or null if not set. */ public String getName() { return name; } /** * 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("http://twitter.com/statuses/public_timeline.json", standardishParameters()); } /** * @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 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. */ public int getRateLimitStatus() { String json = http .getPage("http://twitter.com/account/rate_limit_status.json", null, true); try { JSONObject obj = new JSONObject(json); int hits = obj.getInt("remaining_hits"); return hits; } catch (JSONException e) { throw new TwitterException(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> * The Twitter API now refers to replies as <i>mentions</i>. We * have kept the old terminology here. */ public List<Status> getReplies() throws TwitterException { return getStatuses("http://twitter.com/statuses/replies.json", standardishParameters()); } /** * @return The current status of the user. null if unset (ie if they have * never tweeted) */ public Status getStatus() throws TwitterException { Map<String, String> vars = asMap("count", 1); String json = http.getPage( "http://twitter.com/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(long id) throws TwitterException { String json = http.getPage("http://twitter.com/statuses/show/" + id + ".json", null, true); try { return new Status(new JSONObject(json), null); } catch (JSONException e) { throw new TwitterException(e); } } /** * @return The current status of the given user, as a normal String. */ public Status getStatus(String username) throws TwitterException { assert username != null; Map<String, String> vars = asMap("id", username, "count", 1); String json = http.getPage( "http://twitter.com/statuses/user_timeline.json", vars, false); List<Status> statuses = Status.getStatuses(json); if (statuses.size() == 0) return null; return statuses.get(0); } private List<Status> getStatuses(String url, Map<String, String> var) { // Default: 1 page if (maxResults < 1) { List<Status> msgs = Status .getStatuses(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<Status> msgs = new ArrayList<Status>(); while (msgs.size() <= maxResults) { List<Status> nextpage = Status.getStatuses(http.getPage(url, var, true)); nextpage = dateFilter(nextpage); msgs.addAll(nextpage); if (nextpage.size() < 20) break; pageNumber++; var.put("page", Integer.toString(pageNumber)); } return msgs; } private List<Long> getUserIDs(String url) { String json = http.getPage(url, null, true); List<Long> ids = new ArrayList<Long>(); try { JSONArray jarr = new JSONArray(json); for (int i = 0; i < jarr.length(); i++) { ids.add(jarr.getLong(i)); } } catch (JSONException e) { throw new TwitterException("Could not parse id list" + e); } return ids; } private List<User> getUsers(String url) { return User.getUsers(http.getPage(url, null, true)); } /** * Returns the 20 most recent statuses posted in the last 24 hours from the * authenticating user. */ public List<Status> getUserTimeline() throws TwitterException { return getStatuses("http://twitter.com/statuses/user_timeline.json", standardishParameters()); } /** * Returns the most recent statuses posted in the last 24 hours from the * given user. * <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 id * Can be null. Specifies the ID or screen name of the user for * whom to return the user_timeline. * @param since * Can be null. Narrows the returned results to just those * statuses created after the specified date. */ public List<Status> getUserTimeline(String id) throws TwitterException { Map<String, String> vars = asMap("id", id); addStandardishParameters(vars); // Should we authenticate? boolean authenticate = http.canAuthenticate(); String json = http.getPage( "http://twitter.com/statuses/user_timeline.json", vars, authenticate); return Status.getStatuses(json); } /** * Is the authenticating user <i>followed by</i> userB? * * @param userB * The screen name of a Twitter user. * @return Whether or not the user is followed by userB. */ public boolean isFollower(String userB) { return isFollower(userB, name); } /** * @return true if followerScreenName <i>is</i> following followedScreenName * * @throws TwitterException.E403 if one of the users has protected their * updates and you don't have access. This can be counter-intuitive * (and annoying) at times! */ public boolean isFollower(String followerScreenName, String followedScreenName) { assert followerScreenName != null && followedScreenName != null; String page = http.getPage( "http://twitter.com/friendships/exists.json", aMap( "user_a", followerScreenName, "user_b", followedScreenName), true); return Boolean.valueOf(page); } /** * Does the authenticating user <i>follow</i> userB? * * @param userB * The screen name of a Twitter user. * @return Whether or not the user follows userB. */ public boolean isFollowing(String userB) { if (isFollower(name, userB)) return true; // hopefully temporary workarounds for hopefully temporary issues with Twitter's follower API // Note: These do not appear to help! Left in 'cos what harm can they do? List<User> friends = getFriends(); User b = new User(userB); if (friends.contains(b)) return true; long bid = show(userB).getId(); List<Long> fids = getFriendIDs(); if (fids.contains(bid)) return true; return false; } /** * Convenience for {@link #isFollowing(String)} * @param user */ public boolean isFollowing(User user) { return isFollowing(user.screenName); } /** * Switches off notifications for updates from the specified user <i>who * must already be a friend</i>. * * @param username * Stop getting notifications from this user, who must already be * one of your friends. * @return the specified user */ public User leaveNotifications(String username) { String page = http.getPage("http://twitter.com/notifications/leave/" + username + ".json", null, true); try { return new User(new JSONObject(page), null); } catch (JSONException e) { throw new TwitterException(e); } } /** * Enables notifications for updates from the specified user <i>who must * already be a friend</i>. * * @param username * Get notifications from this user, who must already be one of * your friends. * @return the specified user */ public User notify(String username) { String page = http.getPage("http://twitter.com/notifications/follow/" + username + ".json", null, true); try { return new User(new JSONObject(page), null); } catch (JSONException e) { throw new TwitterException(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; } /** * 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. * * TODO support for {@link #maxResults} * * @param searchTerm * @return search results (upto 100 per page) */ public List<Status> search(String searchTerm) { // number of tweets per page, max 100 int rpp = 100; Map<String, String> vars = aMap("rpp", "" + rpp, "q", searchTerm); addStandardishParameters(vars); String json = http.getPage("http://search.twitter.com/search.json", vars, true); try { JSONObject jo = new JSONObject(json); List<Status> stati = Status.getStatusesFromSearch(this, jo); return dateFilter(stati); } catch (Exception e) { throw new TwitterException(e); } } /** * Twitter GeoSearch * geocode: Optional. Returns tweets by users located within a given radius * of the given latitude/longitude. * The location is preferentially taking from the Geotagging API, * but will fall back to their Twitter profile. * The parameter value is specified by "latitide,longitude,radius", * where radius units must be specified as either "mi" (miles) or "km" (kilometers). * Note that you cannot use the near operator via the API to geocode arbitrary locations; * however you can use this geocode parameter to search near geocodes directly. * Example: http://search.twitter.com/search.atom?geocode=40.757929%2C-73.985506%2C25km */ public List<Status> search(double lat, double lon, int kmradius, int maxtweets) { // number of tweets per page, max 100 int rpp = maxtweets; //build geocode String geocode=Double.toString(lat)+","+Double.toString(lon)+","+kmradius+"km"; Map<String, String> vars = aMap("rpp", "" + rpp, "geocode", geocode); addStandardishParameters(vars); String json = http.getPage("http://search.twitter.com/search.json", vars, true); try { JSONObject jo = new JSONObject(json); List<Status> stati = Status.getStatusesFromSearch(this, jo); return dateFilter(stati); } catch (Exception e) { throw new TwitterException(e); } } // public List<Status> geoStream(double lat1, double lon1, double lat2, double lon2) { // // number of tweets per page, max 100 //// int rpp = 1; // //build geocode // String locations=Double.toString(lat1)+","+Double.toString(lon1)+","+Double.toString(lat2)+","+Double.toString(lon2); // Map<String, String> vars = aMap("locations", locations); // addStandardishParameters(vars); // String json = http.getPage("http://stream.twitter.com/1/statuses/filter.json", // vars, true); // try { // JSONObject jo = new JSONObject(json); // List<Status> stati = Status.getStatusesFromSearch(this, jo); // return dateFilter(stati); // } catch (Exception e) { // throw new TwitterException(e); // } // } /** * Sends a new direct message to the specified user from the authenticating * user. * * @param recipient * Required. The ID or screen name of the recipient user. * @param text * Required. The text of your direct message. Keep it under 140 * characters! * @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; if (text.length() > 140) throw new IllegalArgumentException("Message is too long."); Map<String, String> vars = asMap("user", recipient, "text", text); String result = post("http://twitter.com/direct_messages/new.json", vars, true); try { return new Message(new JSONObject(result)); } catch (JSONException e) { throw new TwitterException(e); } } /** * @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. * <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) { this.maxResults = maxResults; } /** * @param pageNumber * null (the default) returns the first page. Pages are indexed * from 1. This is used once only */ public void setPageNumber(Integer pageNumber) { this.pageNumber = pageNumber; } /** * 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. * <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 */ 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(Long 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! Otherwise the source will appear * as "web".</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, double lat, double lon) throws TwitterException { return updateStatus(statusText,lat,lon); } /** * Returns information of a given user, specified by ID or screen name. * * @param id * The ID or screen name of a user. * @throws exception if the user does not exist - or has been terminated * (as happens to spam bots). */ public User show(String id) throws TwitterException { String json = http.getPage("http://twitter.com/users/show/" + id + ".json", null, http.canAuthenticate()); User user; try { user = new User(new JSONObject(json), null); } catch (JSONException e) { throw new TwitterException(e); } return user; } /** * 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; } private Map<String, String> standardishParameters() { return addStandardishParameters(new HashMap<String, String>()); } /** * Destroy: Discontinues friendship with the user specified in the ID * parameter as the authenticating user. * * @param username * The ID or screen name of the user with whom to discontinue * friendship. * @return the un-friended user (if they were a friend), or null if the * method fails because the specified user was not a friend. */ public User stopFollowing(String username) { assert getName() != null; try { String page = post("http://twitter.com/friendships/destroy/" + username + ".json", null, true); // ?? is this needed to make Twitter update its cache? doesn't seem to fix things // http.getPage("http://twitter.com/friends", null, true); User user; try { user = new User(new JSONObject(page), null); } catch (JSONException e) { throw new TwitterException(e); } return user; } catch (TwitterException e) { // were they a friend anyway? if (!isFollower(getName(), username)) { return null; } // Something else went wrong throw e; } } /** * Convenience for {@link #stopFollowing(String)} * @param user */ public void stopFollowing(User user) { stopFollowing(user.screenName); } /** * 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, double lat, double lon) throws TwitterException { if (statusText.length() > 160) throw new IllegalArgumentException( "Status text must be 160 characters or less: " + statusText.length()); Map<String, String> vars = asMap("status", statusText,"lat",Double.toString(lat),"long",Double.toString(lon)); if (sourceApp != null) vars.put("source", sourceApp); String result = post("http://twitter.com/statuses/update.json", vars, true); try { return new Status(new JSONObject(result), null); } catch (JSONException e) { throw new TwitterException(e); } } /** * Does a user with the specified name or id exist? * @param id The screen name or user id of the suspected user. * @return False if the user doesn't exist or has been suspended, true otherwise. */ public boolean userExists(String id) { try { String json = http.getPage("http://twitter.com/users/show/" + id + ".json", null, true); } catch (TwitterException.E404 e) { return false; } return true; } }