package com.nostra13.socialsharing.twitter.extpack.winterwell.jtwitter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; 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.IHttpClient; import com.nostra13.socialsharing.twitter.extpack.winterwell.jtwitter.TwitterException.E403; import com.nostra13.socialsharing.twitter.extpack.winterwell.jtwitter.TwitterException.SuspendedUser; /** * API calls relating to users and relationships (the social network). Use * {@link Twitter#users()} to get one of these objects. * <p> * Conceptually, this is an extension of {@link Twitter}. The methods are here * because Twitter was getting crowded. * * @author Daniel */ public class Twitter_Users { private final IHttpClient http; private final Twitter jtwit; Twitter_Users(Twitter jtwit) { this.jtwit = jtwit; http = jtwit.getHttpClient(); } /** * blocks/create: Blocks screenName from following the authenticating user. * In addition the blocked user will not show in the authenticating users * mentions or timeline (unless retweeted by another user). If a follow or * friend relationship exists it is destroyed. * * @param screenName * @return info on the (now blocked) user * @see #unblock(String) */ public User block(String screenName) { HashMap vars = new HashMap(); vars.put("screen_name", screenName); // Returns if the authenticating user is blocking a target user. // Will return the blocked user's object if a block exists, and error // with // a HTTP 404 response code otherwise. String json = http.post(jtwit.TWITTER_URL + "/blocks/create.json", vars, true); return InternalUtils.user(json); } /** * Common backend for {@link #bulkShow(List)} and * {@link #bulkShowById(List)}. Works in batches of 100. * <p> * This will throw exceptions from the 1st page of results, but swallow them * from subsequent pages (which are likely to be rate limit errors). * <p> * Suspended bot accounts seem to just get ignored. * * @param stringOrNumber * @param screenNamesOrIds */ List<User> bulkShow2(String apiMethod, Class stringOrNumber, Collection screenNamesOrIds) { int batchSize = 100; ArrayList<User> users = new ArrayList<User>(screenNamesOrIds.size()); List _screenNamesOrIds = screenNamesOrIds instanceof List ? (List) screenNamesOrIds : new ArrayList(screenNamesOrIds); for (int i = 0; i < _screenNamesOrIds.size(); i += batchSize) { int last = i + batchSize; String names = InternalUtils.join(_screenNamesOrIds, i, last); String var = stringOrNumber == String.class ? "screen_name" : "user_id"; Map<String, String> vars = InternalUtils.asMap(var, names); try { String json = http.getPage(jtwit.TWITTER_URL + apiMethod, vars, http.canAuthenticate()); List<User> usersi = User.getUsers(json); users.addAll(usersi); } catch (TwitterException e) { // Stop here. // Don't normally throw an exception so we don't waste the // results we have. if (users.size() == 0) throw e; break; } } return users; } /** * Start following a user. * * @param username * Required. The ID or screen name of the user to befriend. * @return The befriended user, or null if (a) they were already being * followed, or (b) they protect their tweets & you already * requested to follow them. * @throws TwitterException * if the user does not exist or has been suspended. * @see #stopFollowing(String) */ public User follow(String username) throws TwitterException { if (username == null) throw new NullPointerException(); if (username.equals(jtwit.getScreenName())) throw new IllegalArgumentException("follow yourself makes no sense"); String page = null; try { Map<String, String> vars = InternalUtils.asMap("screen_name", username); page = http.post(jtwit.TWITTER_URL + "/friendships/create.json", vars, true); // is this needed? doesn't seem to fix things // http.getPage(jtwit.TWITTER_URL+"/friends", null, true); return new User(new JSONObject(page), null); } catch (SuspendedUser e) { throw e; } catch (TwitterException.Repetition e) { return null; } catch (E403 e) { // check if we've tried to follow someone we're already following try { if (isFollowing(username)) return null; } catch (TwitterException e2) { // no extra info then } throw e; } catch (JSONException e) { throw new TwitterException.Parsing(page, e); } } /** * Convenience for {@link #follow(String)} * * @param user * @return fresh user object, or null if (a) they were already being * followed, or (b) they protect their tweets & you already * requested to follow them. */ public User follow(User user) { return follow(user.screenName); } /** * @return an array of numeric user ids the authenticating user is blocking. * Use {@link #showById(Collection)} if you want to convert thse * into User objects. */ public List<Number> getBlockedIds() { String json = http.getPage(jtwit.TWITTER_URL + "/blocks/blocking/ids.json", null, true); try { JSONArray arr = new JSONArray(json); List<Number> ids = new ArrayList(arr.length()); for (int i = 0, n = arr.length(); i < n; i++) { ids.add(arr.getLong(i)); } return ids; } catch (JSONException e) { throw new TwitterException.Parsing(json, e); } } /** * 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 = jtwit.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<Number> getFollowerIDs() throws TwitterException { return getUserIDs(jtwit.TWITTER_URL + "/followers/ids.json", null); } /** * 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<Number> getFollowerIDs(String screenName) throws TwitterException { return getUserIDs(jtwit.TWITTER_URL + "/followers/ids.json", screenName); } /** * Returns the authenticating user's (latest) followers, each with current * status inline. Occasionally contains duplicates. * * @deprecated Twitter advise using {@link #getFollowerIDs()} and * {@link #show(Number)} */ @Deprecated public List<User> getFollowers() throws TwitterException { return getUsers(jtwit.TWITTER_URL + "/statuses/followers.json", null); } /** * * Returns the (latest 100) given user's followers, each with current status * inline. Occasionally contains duplicates. * * @param username * The screen name of the user for whom to request a list of * friends. * @throws TwitterException */ public List<User> getFollowers(String username) throws TwitterException { return getUsers(jtwit.TWITTER_URL + "/statuses/followers.json", username); } /** * Returns the IDs of the authenticating user's friends. (people who the * user follows). * * @throws TwitterException */ public List<Number> getFriendIDs() throws TwitterException { return getUserIDs(jtwit.TWITTER_URL + "/friends/ids.json", null); } /** * Returns the IDs of the specified user's friends. Occasionally contains * duplicates. * * @param The * screen name of the user whose friends are to be fetched. * @throws TwitterException */ public List<Number> getFriendIDs(String screenName) throws TwitterException { return getUserIDs(jtwit.TWITTER_URL + "/friends/ids.json", screenName); } /** * Returns the authenticating user's (latest 100) friends, each with current * status inline. NB - friends are people who *you* follow. Occasionally * contains duplicates. * <p> * Note that there seems to be a small delay from Twitter in updates to this * list. * * @throws TwitterException * @see #getFriendIDs() * @see #isFollowing(String) * @deprecated Twitter advise you to use {@link #getFriendIDs()} with * {@link Twitter_Users#showById(List)} instead. */ @Deprecated public List<User> getFriends() throws TwitterException { return getUsers(jtwit.TWITTER_URL + "/statuses/friends.json", null); } /** * * Returns the (latest 100) given user's friends, each with current status * inline. Occasionally contains duplicates. * * @param username * The 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(jtwit.TWITTER_URL + "/statuses/friends.json", username); } /** * Bulk-fetch relationship info by screen-name. * * @param screenNames * Can be empty * @return User objects which are mostly blank, but do have * {@link User#isFollowingYou()} and {@link User#isFollowedByYou()} * set (plus name, screenname and id). * @see #getRelationshipInfoById(List) */ public List<User> getRelationshipInfo(List<String> screenNames) { if (screenNames.size() == 0) return Collections.EMPTY_LIST; List<User> users = bulkShow2("/friendships/lookup.json", String.class, screenNames); return users; } /** * Bulk-fetch relationship info by user-id. * * @param userIDs * Can be empty * @return User objects which are mostly blank, but which have * {@link User#isFollowingYou()} and {@link User#isFollowedByYou()} * set (plus name, screenname and id). * @see #getRelationshipInfo(List) */ public List<User> getRelationshipInfoById(List<? extends Number> userIDs) { if (userIDs.size() == 0) return Collections.EMPTY_LIST; List<User> users = bulkShow2("/friendships/lookup.json", Number.class, userIDs); return users; } /** * Synonym for {@link #show(long)}. show is the Twitter API name, getUser * feels more Java-like. * * @param userId * The user-id of a user. * @return the user info * @see #getUser(String) */ public User getUser(long userId) { return show(userId); } /** * Synonym for {@link #show(String)}. show is the Twitter API name, getUser * feels more Java-like. * * @param screenName * The screen name of a user. * @return the user info */ public User getUser(String screenName) { return show(screenName); } /** * * @param url * API method to call * @param screenName * @return twitter-id numbers for friends/followers of screenName Is * affected by {@link #maxResults} */ private List<Number> getUserIDs(String url, String screenName) { Long cursor = -1L; List<Number> ids = new ArrayList<Number>(); Map<String, String> vars = InternalUtils.asMap("screen_name", screenName); while (cursor != 0 && !jtwit.enoughResults(ids)) { vars.put("cursor", String.valueOf(cursor)); String json = http.getPage(url, vars, http.canAuthenticate()); try { // it seems Twitter will occasionally return a raw array JSONArray jarr; if (json.charAt(0) == '[') { jarr = new JSONArray(json); cursor = 0L; } else { JSONObject jobj = new JSONObject(json); jarr = (JSONArray) jobj.get("ids"); cursor = new Long(jobj.getString("next_cursor")); } for (int i = 0; i < jarr.length(); i++) { ids.add(jarr.getLong(i)); } } catch (JSONException e) { throw new TwitterException.Parsing(json, e); } } return ids; } /** * Low-level method for fetching e.g. your friends * * @param url * @param screenName * e.g. your screen-name * @return */ private List<User> getUsers(String url, String screenName) { Map<String, String> vars = InternalUtils.asMap("screen_name", screenName); List<User> users = new ArrayList<User>(); Long cursor = -1L; while (cursor != 0 && !jtwit.enoughResults(users)) { vars.put("cursor", cursor.toString()); JSONObject jobj; try { jobj = new JSONObject(http.getPage(url, vars, http.canAuthenticate())); users.addAll(User.getUsers(jobj.getString("users"))); cursor = new Long(jobj.getString("next_cursor")); } catch (JSONException e) { throw new TwitterException.Parsing(null, e); } } return users; } public boolean isBlocked(Long userId) { try { HashMap vars = new HashMap(); vars.put("user_id", Long.toString(userId)); // Returns if the authenticating user is blocking a target user. // Will return the blocked user's object if a block exists, and // error with // a HTTP 404 response code otherwise. String json = http.getPage(jtwit.TWITTER_URL + "/blocks/exists.json", vars, true); return true; } catch (TwitterException.E404 e) { return false; } } public boolean isBlocked(String screenName) { try { HashMap vars = new HashMap(); vars.put("screen_name", screenName); // Returns if the authenticating user is blocking a target user. // Will return the blocked user's object if a block exists, and // error with // a HTTP 404 response code otherwise. String json = http.getPage(jtwit.TWITTER_URL + "/blocks/exists.json", vars, true); return true; } catch (TwitterException.E404 e) { return false; } } /** * 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, jtwit.getScreenName()); } /** * @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! Also throws E403 if one of the users has been * suspended (we use the {@link SuspendedUser} exception * sub-class for this). * @throws TwitterException.E404 * if one of the users does not exist */ public boolean isFollower(String followerScreenName, String followedScreenName) { assert followerScreenName != null && followedScreenName != null; try { Map vars = InternalUtils.asMap("user_a", followerScreenName, "user_b", followedScreenName); String page = http.getPage(jtwit.TWITTER_URL + "/friendships/exists.json", vars, http.canAuthenticate()); return Boolean.valueOf(page); } catch (TwitterException.E403 e) { if (e instanceof SuspendedUser) throw e; // Should this be a suspended user exception instead? // Let's ask Twitter // TODO check rate limits - only do if we have spare capacity String whoFirst = followedScreenName.equals(jtwit.getScreenName()) ? followerScreenName : followedScreenName; try { // this could throw a SuspendedUser exception show(whoFirst); String whoSecond = whoFirst.equals(followedScreenName) ? followerScreenName : followedScreenName; if (whoSecond.equals(jtwit.getScreenName())) throw e; show(whoSecond); } catch (TwitterException.RateLimit e2) { // ignore } // both shows worked? throw e; } catch (TwitterException e) { // FIXME investigating a weird new bug if (e.getMessage() != null && e.getMessage().contains( "Two user ids or screen_names must be supplied")) throw new TwitterException("WTF? inputs: follower=" + followerScreenName + ", followed=" + followedScreenName + ", call-by=" + jtwit.getScreenName() + "; " + e.getMessage()); throw e; } } /** * 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) { return isFollower(jtwit.getScreenName(), userB); } /** * 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 screenName * Stop getting notifications from this user, who must already be * one of your friends. * @return the specified user */ public User leaveNotifications(String screenName) { Map<String, String> vars = InternalUtils.asMap("screen_name", screenName); String page = http.getPage(jtwit.TWITTER_URL + "/notifications/leave.json", vars, true); try { return new User(new JSONObject(page), null); } catch (JSONException e) { throw new TwitterException.Parsing(page, 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) { Map<String, String> vars = InternalUtils.asMap("screen_name", username); String page = http.getPage(jtwit.TWITTER_URL + "/notifications/follow.json", vars, true); try { return new User(new JSONObject(page), null); } catch (JSONException e) { throw new TwitterException.Parsing(page, e); } } public User reportSpammer(String screenName) { HashMap vars = new HashMap(); vars.put("screen_name", screenName); // Returns if the authenticating user is blocking a target user. // Will return the blocked user's object if a block exists, and error // with // a HTTP 404 response code otherwise. String json = http.post(jtwit.TWITTER_URL + "/report_spam.json", vars, true); return InternalUtils.user(json); } /** * Warning: there is a bug within twitter.com which means that * location-based searches are treated as OR. E.g. "John near:Scotland" will * happily return "Andrew from Aberdeen" :( * <p> * Unlike tweet search, this method does not support any operators. Only the * first 1000 matches are available. * <p> * Does not do paging-to-max-results. But does support using * {@link #setPageNumber(Integer)}, and {@link #setMaxResults(int)} for less * than the standard 20. * * @param searchTerm * @return */ public List<User> searchUsers(String searchTerm) { assert searchTerm != null; Map<String, String> vars = InternalUtils.asMap("q", searchTerm); if (jtwit.pageNumber != null) { vars.put("page", jtwit.pageNumber.toString()); } if (jtwit.count != null && jtwit.count < 20) { vars.put("per_page", String.valueOf(jtwit.count)); } // yes, it requires authentication String json = http.getPage(jtwit.TWITTER_URL + "/users/search.json", vars, true); List<User> users = User.getUsers(json); return users; } /** * Lookup user info. This is done in batches of 100. Users can look up at * most 1000 users in an hour. * * @param screenNames * Can be empty (in which case we avoid wasting an API call) * @return user objects for screenNames. Warning 1: This may be less than * the full set if Twitter returns an error part-way through (e.g. * you hit your rate limit). Warning 2: the ordering may be * different from the screenNames parameter * @see #showById(List) */ public List<User> show(List<String> screenNames) { if (screenNames.size() == 0) return Collections.EMPTY_LIST; return bulkShow2("/users/lookup.json", String.class, screenNames); } /** * Returns information of a given user, specified by user-id. * * @param userId * The user-id of a user. * @throws exception * if the user does not exist - or has been terminated (as * happens to spam bots). */ public User show(Number userId) { Map<String, String> vars = InternalUtils.asMap("user_id", userId.toString()); String json = http.getPage(jtwit.TWITTER_URL + "/users/show.json", vars, http.canAuthenticate()); try { User user = new User(new JSONObject(json), null); return user; } catch (JSONException e) { throw new TwitterException.Parsing(json, e); } } /** * Returns information of a given user, specified by screen name. * * @param screenName * The screen name of a user. * @throws exception * if the user does not exist * @throws SuspendedUser * if the user has been terminated (as happens to spam bots). * @see #show(long) */ public User show(String screenName) throws TwitterException, TwitterException.SuspendedUser { Map vars = InternalUtils.asMap("screen_name", screenName); //Test Code Debugger at work - expected closures until 2012 String json = ""; try{ json = http.getPage(jtwit.TWITTER_URL + "/users/show.json", vars, http.canAuthenticate()); } catch (Exception e){ //we get here? throw new TwitterException.E404("User " + screenName + " does not seem to exist, their user account may have been removed from the service"); } //Debuggers no longer at work if (json.length() == 0) throw new TwitterException.E404(screenName + " does not seem to exist"); try { User user = new User(new JSONObject(json), null); return user; } catch (JSONException e) { throw new TwitterException.Parsing(json, e); } } /** * Lookup user info. Same as {@link #show(List)}, but works with Twitter * user-ID numbers. Done in batches of 100, limited to 1000 an hour. * * @param userIds * . Can be empty (in which case we avoid making a wasted API * call). */ public List<User> showById(Collection<? extends Number> userIds) { if (userIds.size() == 0) return Collections.EMPTY_LIST; return bulkShow2("/users/lookup.json", Number.class, userIds); } /** * Destroy: Discontinues friendship with the user specified in the ID * parameter as the authenticating user. * * @param username * The 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) { String page; try { Map<String, String> vars = InternalUtils.asMap("screen_name", username); page = jtwit.http.post(jtwit.TWITTER_URL + "/friendships/destroy.json", vars, true); // ?? is this needed to make Twitter update its cache? doesn't seem // to fix things // http.getPage(jtwit.TWITTER_URL+"/friends", null, true); } catch (TwitterException e) { // were they a friend anyway? if (e.getMessage() != null && e.getMessage().contains("not friends")) return null; // Something else went wrong throw e; } // outside the try-catch block in case there is a json exception try { User user = new User(new JSONObject(page), null); return user; } catch (JSONException e) { throw new TwitterException.Parsing(page, e); } } /** * Convenience for {@link #stopFollowing(String)} * * @param user * @return */ public User stopFollowing(User user) { return stopFollowing(user.screenName); } /** * blocks/destroy: Un-blocks screenName for the authenticating user. Returns * the un-blocked user when successful. If relationships existed before the * block was instated, they will not be restored. * * @param screenName * @return the now un-blocked User * @see #block(String) */ public User unblock(String screenName) { HashMap vars = new HashMap(); vars.put("screen_name", screenName); // Returns if the authenticating user is blocking a target user. // Will return the blocked user's object if a block exists, and error // with // a HTTP 404 response code otherwise. String json = http.post(jtwit.TWITTER_URL + "/blocks/destroy.json", vars, true); return InternalUtils.user(json); } /** * Does a user with the specified name or id exist? * * @param screenName * 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 screenName) { try { // ?? possibly we should bypass the API: // request their twitter.com page & check for a 404 show(screenName); } catch (TwitterException.SuspendedUser e) { return false; } catch (TwitterException.E404 e) { return false; } return true; } }