summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaurice Laveaux <m.laveaux@student.tue.nl>2014-04-25 12:55:46 +0200
committerMaurice Laveaux <m.laveaux@student.tue.nl>2014-04-25 12:55:46 +0200
commitd03f2bca4a7d33b1347db487aa588a5d163e5615 (patch)
tree58e4df0c9f025b69898ad84a998bb8814b209677
parentcc55914d61978b46ec8e3673c4b37fdc3b392e48 (diff)
downloadTwitterDataAnalytics-d03f2bca4a7d33b1347db487aa588a5d163e5615.tar.gz
Merged and resolved conflicts.
* No functionality of Command is currently used.
-rwxr-xr-xrun.sh31
-rw-r--r--src/data/Profile.java16
-rw-r--r--src/main/Main.java152
-rw-r--r--src/mining/AbstractRequester.java5
-rw-r--r--src/mining/OAuthRequester.java83
-rw-r--r--src/mining/TwitterApi.java64
-rw-r--r--src/support/OAuthAccessTokenSecret.java28
-rw-r--r--stored_tokens.txt2
8 files changed, 358 insertions, 23 deletions
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