From d03f2bca4a7d33b1347db487aa588a5d163e5615 Mon Sep 17 00:00:00 2001 From: Maurice Laveaux Date: Fri, 25 Apr 2014 12:55:46 +0200 Subject: Merged and resolved conflicts. * No functionality of Command is currently used. --- run.sh | 31 +++++++ src/data/Profile.java | 16 ++-- src/main/Main.java | 152 ++++++++++++++++++++++++++++++-- src/mining/AbstractRequester.java | 5 +- src/mining/OAuthRequester.java | 83 ++++++++++++++++- src/mining/TwitterApi.java | 64 +++++++++++++- src/support/OAuthAccessTokenSecret.java | 28 ++++++ stored_tokens.txt | 2 + 8 files changed, 358 insertions(+), 23 deletions(-) create mode 100755 run.sh create mode 100644 src/support/OAuthAccessTokenSecret.java create mode 100644 stored_tokens.txt diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..3a92d1f --- /dev/null +++ b/run.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# You should build the jar file with `ant jar`, then run this script + +if [ -z "$CA" ]; then + CA=/tmp/cap/.keystore +fi + +# Proxy parameters from +# http://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html#Customization +proxy_options() { + # Do not add proxy options if there is no CA or no_proxy env is set + if [ -z "$CA" ] || [ -n "$no_proxy" ]; then + return + fi + + echo -Dhttps.proxyHost=localhost + echo -Dhttps.proxyPort=8008 + echo -Djavax.net.ssl.trustStore=$CA +} + +# Exit on errors +set -e + +# Change dir to project +cd "$(dirname "$(readlink -f "$0")")" + +jar=dist/TwitterDataAnalytics.jar +# Build jar if missing +[ -e "$jar" ] || ant jar + +java $(proxy_options) -jar "$jar" "$@" diff --git a/src/data/Profile.java b/src/data/Profile.java index e4ba602..4e1f413 100644 --- a/src/data/Profile.java +++ b/src/data/Profile.java @@ -1,16 +1,16 @@ - package data; /** - * This class contains the data that is stored of a user. + * This class contains all data that is stored of a user by twitter. */ public class Profile { - - private boolean m_exists; - - private String m_name; - + + // The displayed name of the user. + private String m_displayName; + + // The real name of the user (when set) private String m_realName; - + + // When the profile is created. private String m_creationDate; } diff --git a/src/main/Main.java b/src/main/Main.java index d1b2540..917aaeb 100644 --- a/src/main/Main.java +++ b/src/main/Main.java @@ -1,17 +1,158 @@ package main; import java.io.IOException; +import java.util.Arrays; import java.util.Scanner; import mining.TwitterApi; +import org.json.JSONException; +import org.json.JSONObject; /** - * + * Class for manually testing the Twitter API. */ public class Main { + /** + * Command and parameters without options. + */ + private Command command; + private String[] params; + /** + * Whether to use the Bearer method or OAuth-signed requests. + */ + private boolean useBearer = true; + private TwitterApi api_cached; + + public Main(String[] args) throws IOException { + // parse options and command and return the parameters. + parseGlobalOptions(args); + } + + private TwitterApi getApi() throws IOException { + if (api_cached == null) { + if (useBearer) { + api_cached = TwitterApi.getAppOnly(); + } else { + api_cached = TwitterApi.getOAuth(new ConsolePinSupplier()); + } + } + return api_cached; + } + + private String getParam(int index, String name) { + if (index >= params.length) { + System.err.println("Missing parameter: " + name); + System.exit(1); + } + return params[index]; + } + + private void parseGlobalOptions(String[] args) { + int firstParam = -1; + /* parse global options */ + for (int i = 0; i < args.length; i++) { + if ("--oauth".equals(args[i])) { + useBearer = false; + } else if (args[i].startsWith("-")) { + throw new IllegalArgumentException("Invalid option: " + args[i]); + } else { + /* not an option, must be a command */ + command = Command.fromString(args[i]); + firstParam = i + 1; + break; + } + } + if (firstParam == -1) { + throw new IllegalArgumentException("Missing command, use \"help\""); + } + params = Arrays.copyOfRange(args, firstParam, args.length); + } + public static void main(String[] args) throws IOException { - // create the api state to run request streams or REST. - TwitterApi api = TwitterApi.getAppOnly(); + try { + Main main = new Main(args); + main.execute(); + } catch (IllegalArgumentException ex) { + System.err.println(ex.getMessage()); + System.exit(1); + } + } + + enum Command { + + user, + help; + + public static Command fromString(String command) { + for (Command cmd : values()) { + if (cmd.name().equals(command)) { + return cmd; + } + } + throw new IllegalArgumentException("Unrecognized command "); + } + }; + + private final static String[] HELP = { + "Global options:", + " --oauth Use OAuth (PIN) instead of Bearer tokens", + "", + "Available commands:" + }; + + public void execute() throws IOException { + TwitterApi.Builder req = null; + /* build a request for commands */ + switch (command) { + case user: + req = getApi().build("users/show"); + req.param("screen_name", getParam(0, "screen name")); + break; + case help: + for (String line : HELP) { + System.out.println(line); + } + for (Command cmd : Command.values()) { + System.out.println(" " + cmd.name()); + } + break; + default: + throw new AssertionError(command.name()); + } + if (req != null) { + System.err.println("Executing: " + req.toString()); + JSONObject result = req.request(); + try { + System.out.println(result.toString(4)); + } catch (JSONException ex) { + /* cannot happen */ + System.err.println("Warning: got JSON exception: " + ex); + System.out.println(result); + } + } + } + + private static class ConsolePinSupplier implements TwitterApi.PinSupplier { + + private final Scanner scanner; + + public ConsolePinSupplier() { + scanner = new Scanner(System.in); + } + + @Override + public String requestPin(String url) throws IOException { + System.out.println(url); + System.err.println("Please open the above URL and enter PIN:"); + return scanner.nextLine(); + } + } +} + +/** + * OLD main + * public static void main(String[] args) throws IOException { + * TwitterApi api = TwitterApi.getAppOnly(); // create the object that queues and executes the commands. CommandQueue queue = new CommandQueue(); @@ -29,7 +170,6 @@ public class Main { queue.executeAll(); // parse new input. - parser.parse(input.nextLine()); + parser.parse(scanner.nextLine()); } - } -} + */ \ No newline at end of file diff --git a/src/mining/AbstractRequester.java b/src/mining/AbstractRequester.java index d8c9673..6622b81 100644 --- a/src/mining/AbstractRequester.java +++ b/src/mining/AbstractRequester.java @@ -19,9 +19,6 @@ import org.json.JSONObject; */ public abstract class AbstractRequester implements Requester { - private static final Logger LOGGER - = Logger.getLogger(AbstractRequester.class.getName()); - private static final String API_URL = "https://api.twitter.com/"; @Override @@ -43,7 +40,7 @@ public abstract class AbstractRequester implements Requester { if (resp.has("errors")) { try { String errors = resp.get("errors").toString(); - getLogger().fine("Request failed: " + errors); + getLogger().info("Request failed: " + errors); } catch (JSONException ex) { } } diff --git a/src/mining/OAuthRequester.java b/src/mining/OAuthRequester.java index f5b6a10..f740916 100644 --- a/src/mining/OAuthRequester.java +++ b/src/mining/OAuthRequester.java @@ -2,10 +2,15 @@ package mining; import java.io.IOException; import java.net.URLConnection; +import oauth.signpost.OAuth; import oauth.signpost.OAuthConsumer; import oauth.signpost.basic.DefaultOAuthConsumer; +import oauth.signpost.basic.DefaultOAuthProvider; import oauth.signpost.exception.OAuthException; import org.json.JSONObject; +import support.ConsumerKeySecret; +import support.OAuthAccessTokenSecret; +import utils.Configuration; /** * An API requester that uses OAuth to sign its requests. @@ -19,10 +24,70 @@ public class OAuthRequester extends AbstractRequester { */ private final OAuthConsumer consumer; - public OAuthRequester(String consumerKey, String consumerSecret) { + /** + * Instance that can retrieve an access token for the consumer. + */ + private final DefaultOAuthProvider provider; + + /** + * Instantiates a requester using OAuth. The caller must initialize the + * access token before requests can be sent. + * + * @param cks The consumer secrets provided by Twitter. + */ + public OAuthRequester(ConsumerKeySecret cks) { // create a new application-specific OAuth consumer - consumer = new DefaultOAuthConsumer(consumerKey, consumerSecret); - // TODO: access tokens? + consumer = new DefaultOAuthConsumer(cks.getKey(), cks.getSecret()); + // Note: Access tokens still require PIN + provider = new DefaultOAuthProvider(Configuration.REQUEST_TOKEN_URL, + Configuration.ACCESS_TOKEN_URL, Configuration.AUTHORIZE_URL); + } + + /** + * Set the access token to sign apps with. This access token can be + * retrieved from dev.twitter.com (see + * https://dev.twitter.com/docs/auth/tokens-devtwittercom) or via a PIN + * (https://dev.twitter.com/docs/auth/pin-based-authorization). + * + * @param secrets Access token and token secret. + */ + public void setAccessToken(OAuthAccessTokenSecret secrets) { + String token = secrets.getToken(); + String secret = secrets.getSecret(); + consumer.setTokenWithSecret(token, secret); + } + + /** + * Retrieves an URL which allows an authenticated user to retrieve a PIN for + * this application. + * + * @return An URL. + * @throws IOException if an error occurred while retrieving the URL. + */ + public String getAuthURL() throws IOException { + String authUrl; + try { + authUrl = provider.retrieveRequestToken(consumer, OAuth.OUT_OF_BAND); + } catch (OAuthException ex) { + throw new IOException(ex); + } + return authUrl; + } + + /** + * Gets access tokens from a PIN (out-of-band method). The PIN can be + * retrieved by visiting the URL from {@code getAuthURL()}. See + * https://dev.twitter.com/docs/auth/pin-based-authorization + * + * @param pin The PIN as found on the page. + * @throws IOException if the PIN cannot be used to retrieve access tokens. + */ + public void supplyPINForTokens(String pin) throws IOException { + try { + provider.retrieveAccessToken(consumer, pin); + } catch (OAuthException ex) { + throw new IOException(ex); + } } @Override @@ -34,8 +99,20 @@ public class OAuthRequester extends AbstractRequester { } } + public OAuthAccessTokenSecret getSecrets() { + String token = consumer.getToken(); + String secret = consumer.getTokenSecret(); + if (token == null || secret == null) { + return null; + } + return new OAuthAccessTokenSecret(token, secret); + } + @Override public boolean isValid() throws IOException { + if (consumer.getToken() == null) { + return false; + } // NOTE: this actually contributes to the ratelimit (12/minute) // TODO: find alternative that does not hit the ratelimit JSONObject obj = getJSONRelax("1/application/rate_limit_status"); diff --git a/src/mining/TwitterApi.java b/src/mining/TwitterApi.java index e45f7b7..7ea4e57 100644 --- a/src/mining/TwitterApi.java +++ b/src/mining/TwitterApi.java @@ -10,6 +10,7 @@ import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.message.BasicNameValuePair; import org.json.JSONObject; import support.ConsumerKeySecret; +import support.OAuthAccessTokenSecret; import utils.Configuration; /** @@ -18,6 +19,8 @@ import utils.Configuration; public class TwitterApi { private static final String CFG_BEARER_TOKEN = "bearer-token"; + private static final String CFG_OAUTH_TOKEN = "oauth-token"; + private static final String CFG_OAUTH_SECRET = "oauth-secret"; private final Requester requester; public TwitterApi(Requester requester) { @@ -25,8 +28,7 @@ public class TwitterApi { } /** - * Establishes an instance using application-only authentication using a - * file as cache. + * Establishes an instance using application-only authentication. * * @return An API context usable for requests using app-only auth. * @throws IOException @@ -53,6 +55,64 @@ public class TwitterApi { return new TwitterApi(breq); } + /** + * Establishes an instance using a user context (OAuth signing). + * + * @param ps A supplier of the PIN for a given URL. May be null if not + * supported. + * @return An API context usable for requests using OAuth. + * @throws IOException if no usable context can be instantiated. + */ + public static TwitterApi getOAuth(PinSupplier ps) throws IOException { + Configuration cfg = Configuration.getConfig(); + OAuthAccessTokenSecret secrets = null; + ConsumerKeySecret cks = getConsumerKeySecret(); + OAuthRequester oreq = new OAuthRequester(cks); + + /* check if the stored access tokens are still valid */ + { + String token, secret; + token = cfg.getProperty(CFG_OAUTH_TOKEN); + secret = cfg.getProperty(CFG_OAUTH_SECRET); + if (token != null && secret != null) { + secrets = new OAuthAccessTokenSecret(token, secret); + oreq.setAccessToken(secrets); + if (!oreq.isValid()) { + Logger.getLogger(TwitterApi.class.getName()) + .info("OAuth access tokens invalid"); + secrets = null; + } + } + } + /* if no valid secrets are available, request a new access token */ + if (secrets == null) { + if (ps == null) { + throw new IOException("Unable to retrieve an access token"); + } + String authUrl = oreq.getAuthURL(); + oreq.supplyPINForTokens(ps.requestPin(authUrl)); + secrets = oreq.getSecrets(); + assert secrets != null : "PIN accepted, but no access tokens?"; + cfg.setProperty(CFG_OAUTH_TOKEN, secrets.getToken()); + cfg.setProperty(CFG_OAUTH_SECRET, secrets.getSecret()); + cfg.save(); + } + return new TwitterApi(oreq); + } + + public interface PinSupplier { + + /** + * Given a URL, the pin supplier must synchronously request a PIN. + * + * @param url The URL to be presented to the user. + * @return A non-null string that can be exchanged for access tokens. + * @throws IOException if the PIN could not be retrieved. + */ + public String requestPin(String url) throws IOException; + + } + private static ConsumerKeySecret getConsumerKeySecret() { // static consumer keys retrieved from dev.twitter.com return new ConsumerKeySecret(Configuration.CONSUMER_KEY, diff --git a/src/support/OAuthAccessTokenSecret.java b/src/support/OAuthAccessTokenSecret.java new file mode 100644 index 0000000..795b036 --- /dev/null +++ b/src/support/OAuthAccessTokenSecret.java @@ -0,0 +1,28 @@ +package support; + +/** + * Contains an OAuth token and secret pair. These tokens can be retrieved from + * dev.twitter.com (https://dev.twitter.com/docs/auth/tokens-devtwittercom) or + * by querying the user for a specific PIN. See {@link mining.OAuthRequester} + * for the latter method. + * + * @author Peter Wu + */ +public class OAuthAccessTokenSecret { + + private final String token; + private final String secret; + + public OAuthAccessTokenSecret(String token, String secret) { + this.token = token; + this.secret = secret; + } + + public String getToken() { + return token; + } + + public String getSecret() { + return secret; + } +} diff --git a/stored_tokens.txt b/stored_tokens.txt new file mode 100644 index 0000000..75e6757 --- /dev/null +++ b/stored_tokens.txt @@ -0,0 +1,2 @@ +745405160-agukrMmb7uwujKCKALi8MFlDYSwTuNrsbLqNJryw +4HgVg7nOPTKlqneOKVzErAEXF587GB3vfOrFodVVVSNgU \ No newline at end of file -- cgit v1.2.1