PageRenderTime 80ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/src-extra/winterwell/jtwitter/OAuthScribeClient.java

http://github.com/winterstein/JTwitter
Java | 471 lines | 252 code | 37 blank | 182 comment | 41 complexity | ec7b3cf77c5c25089bdd79657c9e0d66 MD5 | raw file
  1. package winterwell.jtwitter;
  2. import java.io.IOException;
  3. import java.lang.reflect.Method;
  4. import java.net.HttpURLConnection;
  5. import java.net.URI;
  6. import java.net.URISyntaxException;
  7. import java.net.URLEncoder;
  8. import java.util.Collections;
  9. import java.util.Map;
  10. import java.util.Map.Entry;
  11. import org.scribe.builder.ServiceBuilder;
  12. import org.scribe.builder.api.TwitterApi;
  13. import org.scribe.model.OAuthRequest;
  14. import org.scribe.model.Response;
  15. import org.scribe.model.Token;
  16. import org.scribe.model.Verb;
  17. import org.scribe.model.Verifier;
  18. import org.scribe.oauth.OAuthService;
  19. import winterwell.json.JSONException;
  20. import winterwell.json.JSONObject;
  21. import winterwell.jtwitter.Twitter.IHttpClient;
  22. import winterwell.jtwitter.Twitter.KRequestType;
  23. /**
  24. * It is recommended that you use {@link OAuthSignpostClient} instead. OAuth
  25. * based login using Scribe (http://github.com/fernandezpablo85/scribe). <i>You
  26. * need version 1.x of Scribe!</i>
  27. * <p>
  28. * Example Usage (desktop based):
  29. *
  30. * <pre>
  31. * <code>
  32. * OAuthScribeClient client = new OAuthScribeClient(JTWITTER_OAUTH_KEY, JTWITTER_OAUTH_SECRET, "oob");
  33. * Twitter jtwit = new Twitter("yourtwittername", client);
  34. * // open the authorisation page in the user's browser
  35. * client.authorizeDesktop();
  36. * // get the pin
  37. * String v = client.askUser("Please enter the verification PIN from Twitter");
  38. * client.setAuthorizationCode(v);
  39. * // use the API!
  40. * jtwit.setStatus("Messing about in Java");
  41. * </code>
  42. * </pre>
  43. *
  44. * @author daniel
  45. *
  46. * <p>
  47. * There are alternative OAuth libraries you can use:
  48. * @see OAuthSignpostClient This is the "officially supported" JTwitter OAuth
  49. * client.
  50. * @see OAuthHttpClient
  51. */
  52. public class OAuthScribeClient implements IHttpClient {
  53. @Override
  54. public boolean isRetryOnError() {
  55. return false;
  56. }
  57. /**
  58. * This consumer key (and secret) allows you to get up and running fast.
  59. * However you are strongly advised to register your own app at
  60. * http://dev.twitter.com Then use your own key and secret. This will be
  61. * less confusing for users, and it protects you incase the JTwitter key
  62. * gets changed.
  63. */
  64. public static final String JTWITTER_OAUTH_KEY = "Cz8ZLgitPR2jrQVaD6ncw";
  65. /**
  66. * For use with {@link #JTWITTER_OAUTH_KEY}
  67. */
  68. public static final String JTWITTER_OAUTH_SECRET = "9FFYaWJSvQ6Yi5tctN30eN6DnXWmdw0QgJMl7V6KGI";
  69. /**
  70. * <p>
  71. * <i>Convenience method for desktop apps only - does not work in
  72. * Android</i>
  73. * </p>
  74. *
  75. * Opens a popup dialog asking the user to enter the verification code. (you
  76. * would then call {@link #setAuthorizationCode(String)}). This is only
  77. * relevant when using out-of-band instead of a callback-url. This is a
  78. * convenience method -- you will probably want to build your own UI around
  79. * this.
  80. * <p>
  81. * <i>This method requires Swing. It will not work on all devices.</i>
  82. *
  83. * @param question
  84. * e.g. "Please enter the authorisation code from Twitter"
  85. * @return
  86. */
  87. public static String askUser(String question) {
  88. // This cumbersome approach avoids importing Swing classes
  89. // It will create a runtime exception on Android
  90. // -- but will allow the rest of the class to be used.
  91. // JOptionPane.showInputDialog(question);
  92. try {
  93. Class<?> JOptionPaneClass = Class
  94. .forName("javax.swing.JOptionPane");
  95. Method showInputDialog = JOptionPaneClass.getMethod(
  96. "showInputDialog", Object.class);
  97. return (String) showInputDialog.invoke(null, question);
  98. } catch (Exception e) {
  99. throw new RuntimeException(e);
  100. }
  101. }
  102. @SuppressWarnings("deprecation")
  103. private static String encode(Object x) {
  104. return URLEncoder.encode(String.valueOf(x));
  105. }
  106. private Token accessToken;
  107. private String callbackUrl;
  108. private String consumerKey;
  109. private String consumerSecret;
  110. // private final Map<KRequestType, RateLimit> rateLimits = new EnumMap<KRequestType, RateLimit>(KRequestType.class);
  111. private Token requestToken;
  112. private boolean retryingFlag;
  113. private boolean retryOnError;
  114. private OAuthService scribe;
  115. // TODO use this!
  116. private int timeout;
  117. /**
  118. *
  119. * @param consumerKey
  120. * @param consumerSecret
  121. * @param callbackUrl
  122. * Servlet that will get the verifier sent to it, or "oob" for
  123. * out-of-band (user copies and pastes the pin /** Opens a popup
  124. * dialog asking the user to enter the verification code. (you
  125. * would then call {@link #setAuthorizationCode(String)}). This
  126. * is only relevant when using out-of-band instead of a
  127. * callback-url. This is a convenience method -- you will
  128. * probably want to build your own UI around this.
  129. *
  130. * @param question
  131. * e.g. "Please enter the authorisation code from Twitter"
  132. * @return
  133. */
  134. public OAuthScribeClient(String consumerKey, String consumerSecret,
  135. String callbackUrl) {
  136. assert consumerKey != null && consumerSecret != null
  137. && callbackUrl != null;
  138. this.consumerKey = consumerKey;
  139. this.consumerSecret = consumerSecret;
  140. this.callbackUrl = callbackUrl;
  141. init();
  142. }
  143. /**
  144. * Use this if you already have an accessToken for the user. You can then go
  145. * straight to using the API without having to authorise again.
  146. *
  147. * @param consumerKey
  148. * @param consumerSecret
  149. * @param accessToken
  150. */
  151. public OAuthScribeClient(String consumerKey, String consumerSecret,
  152. Token accessToken) {
  153. this.consumerKey = consumerKey;
  154. this.consumerSecret = consumerSecret;
  155. this.accessToken = accessToken;
  156. init();
  157. }
  158. /**
  159. * Redirect the user's browser to Twitter's authorise page. You will need to
  160. * collect the verifier pin - either from the callback servlet, or from the
  161. * user (out-of-band).
  162. * <p>
  163. * <i>This method requires Swing. It will not work on all devices.</i>
  164. *
  165. * @see #authorizeUrl()
  166. */
  167. public void authorizeDesktop() {
  168. URI uri = authorizeUrl();
  169. try {
  170. // This cumbersome approach avoids importing Swing classes
  171. // It will create a runtime exception on Android
  172. // -- but will allow the rest of the class to be used.
  173. // Desktop d = Desktop.getDesktop();
  174. Class<?> desktopClass = Class.forName("java.awt.Desktop");
  175. Method getDesktop = desktopClass.getMethod("getDesktop");
  176. Object d = getDesktop.invoke(null);
  177. // d.browse(uri);
  178. Method browse = desktopClass.getMethod("browse", URI.class);
  179. browse.invoke(d, uri);
  180. } catch (Exception e) {
  181. throw new RuntimeException(e);
  182. }
  183. }
  184. /**
  185. * @return url to direct the user to for authorisation.
  186. */
  187. public URI authorizeUrl() {
  188. try {
  189. requestToken = scribe.getRequestToken();
  190. String url = "https://api.twitter.com/oauth/authorize?oauth_token="
  191. + requestToken.getToken()
  192. // +"&oauth_callback="+callbackUrl
  193. ;
  194. return new URI(url);
  195. } catch (URISyntaxException e) {
  196. throw new TwitterException(e);
  197. }
  198. }
  199. @Override
  200. public boolean canAuthenticate() {
  201. return accessToken != null;
  202. }
  203. @Override
  204. public HttpURLConnection connect(String url, Map<String, String> vars,
  205. boolean b) throws IOException {
  206. throw new UnsupportedOperationException();
  207. }
  208. @Override
  209. public IHttpClient copy() {
  210. OAuthScribeClient c = new OAuthScribeClient(consumerKey,
  211. consumerSecret, accessToken);
  212. c.callbackUrl = callbackUrl;
  213. c.setTimeout(timeout);
  214. c.setRetryOnError(retryOnError);
  215. return c;
  216. }
  217. /**
  218. * @return the access token, if set.
  219. */
  220. public Token getAccessToken() {
  221. return accessToken;
  222. }
  223. /**
  224. * TODO not implemented yet. Please see {@link URLConnectionHttpClient} for
  225. * example code.
  226. */
  227. @Override
  228. public String getHeader(String headerName) throws RuntimeException {
  229. throw new RuntimeException("TODO: not implemented yet");
  230. }
  231. @Override
  232. public String getPage(String uri, Map<String, String> vars,
  233. boolean authenticate) throws TwitterException {
  234. try {
  235. assert canAuthenticate();
  236. if (vars != null && vars.size() != 0) {
  237. uri += "?";
  238. for (Entry<String, String> e : vars.entrySet()) {
  239. if (e.getValue() == null) {
  240. continue;
  241. }
  242. uri += encode(e.getKey()) + "=" + encode(e.getValue())
  243. + "&";
  244. }
  245. }
  246. OAuthRequest request = new OAuthRequest(Verb.GET, uri);
  247. // request.setTimeout(timeout);
  248. scribe.signRequest(accessToken, request);
  249. Response response = request.send();
  250. processError(response);
  251. return response.getBody();
  252. // retry on error?
  253. } catch (TwitterException.E50X e) {
  254. if (!retryOnError || retryingFlag)
  255. throw e;
  256. try {
  257. retryingFlag = true;
  258. Thread.sleep(1000);
  259. return getPage(uri, vars, authenticate);
  260. } catch (InterruptedException ex) {
  261. throw new TwitterException(ex);
  262. } finally {
  263. retryingFlag = false;
  264. }
  265. }
  266. }
  267. @Override
  268. public RateLimit getRateLimit(KRequestType reqType) {
  269. return null; //rateLimits.get(reqType);
  270. }
  271. /**
  272. * @deprecated // TODO update for v1.1
  273. */
  274. public Map<String, RateLimit> getRateLimits() {
  275. return Collections.EMPTY_MAP; //rateLimits;
  276. }
  277. /**
  278. * @return the request token, if one has been created via
  279. * {@link #authorizeUrl()}.
  280. */
  281. public Token getRequestToken() {
  282. return requestToken;
  283. }
  284. private void init() {
  285. /*
  286. Properties props = new Properties();
  287. // hard coded for efficiency & why not?
  288. // props.load(YahooEqualizer.class.getResourceAsStream("twitter.properties"));
  289. props.put("request.token.url", "http://twitter.com/oauth/request_token");
  290. props.put("access.token.verb", "POST");
  291. props.put("request.token.verb", "POST");
  292. props.put("access.token.url", "http://twitter.com/oauth/access_token");
  293. props.put("consumer.key", consumerKey);
  294. props.put("consumer.secret", consumerSecret);
  295. if (callbackUrl != null) {
  296. props.put("callback.url", callbackUrl);
  297. }
  298. */
  299. ServiceBuilder serviceBuilder = new ServiceBuilder().apiKey(consumerKey).apiSecret(consumerSecret).provider(TwitterApi.class);
  300. if (callbackUrl != null) {
  301. serviceBuilder.callback(callbackUrl);
  302. }
  303. scribe = serviceBuilder.build();
  304. }
  305. @Override
  306. public String post(String uri, Map<String, String> vars,
  307. boolean authenticate) throws TwitterException {
  308. try {
  309. assert canAuthenticate();
  310. OAuthRequest request = new OAuthRequest(Verb.POST, uri);
  311. if (vars != null && vars.size() != 0) {
  312. for (Entry<String, String> e : vars.entrySet()) {
  313. if (e.getValue() == null) {
  314. continue;
  315. }
  316. request.addBodyParameter(e.getKey(), e.getValue());
  317. }
  318. }
  319. // request.setTimeout(timeout);
  320. scribe.signRequest(accessToken, request);
  321. Response response = request.send();
  322. processError(response);
  323. return response.getBody();
  324. // retry on error?
  325. } catch (TwitterException.E50X e) {
  326. if (!retryOnError || retryingFlag)
  327. throw e;
  328. try {
  329. retryingFlag = true;
  330. Thread.sleep(1000);
  331. return getPage(uri, vars, authenticate);
  332. } catch (InterruptedException ex) {
  333. throw new TwitterException(ex);
  334. } finally {
  335. retryingFlag = false;
  336. }
  337. }
  338. }
  339. @Override
  340. public HttpURLConnection post2_connect(String uri, Map<String, String> vars)
  341. throws TwitterException {
  342. throw new UnsupportedOperationException();
  343. }
  344. /**
  345. * Throw an exception if the connection failed
  346. *
  347. * @param response
  348. */
  349. void processError(Response response) {
  350. int code = response.getCode();
  351. if (code == 200)
  352. return;
  353. Map<String, String> headers = response.getHeaders();
  354. String error = headers.get(null);
  355. if (code == 401)
  356. throw new TwitterException.E401(error);
  357. if (code == 403)
  358. throw new TwitterException.E403(error);
  359. if (code == 404)
  360. throw new TwitterException.E404(error);
  361. if (code >= 500 && code < 600)
  362. throw new TwitterException.E50X(error);
  363. boolean rateLimitExceeded = error.contains("Rate limit exceeded");
  364. if (rateLimitExceeded)
  365. throw new TwitterException.RateLimit(error);
  366. // Rate limiter can sometimes cause a 400 Bad Request
  367. if (code == 400) {
  368. String json = getPage(
  369. "http://twitter.com/account/rate_limit_status.json", null,
  370. true);
  371. try {
  372. JSONObject obj = new JSONObject(json);
  373. int hits = obj.getInt("remaining_hits");
  374. if (hits < 1)
  375. throw new TwitterException.RateLimit(error);
  376. } catch (JSONException e) {
  377. // oh well
  378. }
  379. }
  380. // just report it as a vanilla exception
  381. throw new TwitterException(code + " " + error);
  382. }
  383. /**
  384. * Set the authorisation code (aka the verifier). This is only relevant when
  385. * using out-of-band instead of a callback-url.
  386. *
  387. * @param verifier
  388. * a pin code which Twitter gives the user
  389. * @throws RuntimeException
  390. * Scribe throws an exception if the verifier is invalid
  391. */
  392. public void setAuthorizationCode(String verifier) throws RuntimeException {
  393. accessToken = scribe.getAccessToken(requestToken, new Verifier(verifier));
  394. }
  395. /**
  396. * False by default. Setting this to true switches on a robustness
  397. * workaround: when presented with a 50X server error, the system will wait
  398. * 1 second and make a second attempt. This is NOT thread safe.
  399. */
  400. @Override
  401. public void setRetryOnError(boolean retryOnError) {
  402. this.retryOnError = retryOnError;
  403. }
  404. /**
  405. * This does not do anything at present!
  406. */
  407. @Deprecated
  408. @Override
  409. public void setTimeout(int millisecs) {
  410. this.timeout = millisecs;
  411. }
  412. // TODO can we call this whenever a RateLimit exception is thrown?
  413. public void updateRateLimits(KRequestType reqType) {
  414. if (true) return; // TODO update for v1.1
  415. // String limit = null, remaining = null, reset = null;
  416. // switch (reqType) {
  417. // case NORMAL:
  418. // case SHOW_USER:
  419. // limit = getHeader("X-RateLimit-Limit");
  420. // remaining = getHeader("X-RateLimit-Remaining");
  421. // reset = getHeader("X-RateLimit-Reset");
  422. // break;
  423. // case SEARCH:
  424. // case SEARCH_USERS:
  425. // limit = getHeader("X-FeatureRateLimit-Limit");
  426. // remaining = getHeader("X-FeatureRateLimit-Remaining");
  427. // reset = getHeader("X-FeatureRateLimit-Reset");
  428. // break;
  429. // }
  430. // if (limit != null) {
  431. // rateLimits.put(reqType, new RateLimit(limit, remaining, reset));
  432. // }
  433. }
  434. }