Este proyecto NO se puede clonar.
# Acceso a interfaces REST. Descarga masiva de Tweets
### Aplicaciones y librerías a utilizar
* Eclipse **Para esta práctica podemos usar el eclipse ya instalado en la máquina**
* [Unirest](http://unirest.io/java.html): librería de peticiones HTTP (más adelante veremos como descargarla)
* [Commons](https://commons.apache.org/): librerías de funciones auxiliares (más adelante veremos como descargarla)
* [Google-Gson](https://github.com/google/gson): librería de manipulación de JSON (más adelante veremos como descargarla)
Será necesaria una cuenta de twitter
## Crear una aplicación en Twitter
Si no tenías twitter, ha llegado el momento de tenerlo, hazte una cuenta. Si ya tienes una, haz login en twitter.
Tuvieras o no, consulta #uc3m_cd que es la cuenta de la asignatura aunque se usa poco ;)
Vamos a descargar tweets. Para ello, debes dar de alta una aplicación. Ve a la [gestión de apps de twitter](https://developer.twitter.com/) y crea una aplicación si no lo has hecho ya.
El el dashboard verás tus proyectos y tus aplicaciones. Pulsando en la aplicación que has creado, podrás ver los tokens de seguridad (simbolo de la llave) debes copiar y pegar todos ellos (consumer keys, bearer token, authentication tokens) en algún lugar para usarlos después.
## Descarga de tweets con la ayuda de desarrollador de Twitter
Ahora vamos a usar un API de verdad. Está documentado correctamente, de hecho, el de twitter, está muy bien comentado y tiene utilidades que veremos. Ve a la página:
https://dev.twitter.com/rest/public
Observa como todo lo que se puede hacer con twitter tiene su correspondiente entrada en el API.
Vamos a echar un vistazo a la función para obtener tweets que es `GET statuses/user_timeline`
Para ello, ve a la página https://developer.twitter.com/en/docs/twitter-api/v1/tweets/timelines/api-reference/get-statuses-user_timeline (nosotros usaremos el API v1.1.)
Twitter cuida mucho su interfaz, y ayuda a conservarlo y entenderlo. Pero dado su éxito, ha prohibido el acceso anónimo. Por lo que es necesario usar OAuth (usando los tokens que hemos generado) para acceder a cada función del API rest. Para usar OAuth, es necesario calcular una firma y añadirla a la cabecera Authorization de HTTP.
Con la información que proporciona, puedes ver cómo, mediante peticiones REST, puedes acceder a los contenidos de la siguiente manera:
```
GET 1.1/statuses/user_timeline.json?count=2&screen_name=twitterapi HTTP/1.1
Host: api.twitter.com
Authorization: ...
```
Esto nos daría un JSON de este tipo (en breve lo haremos nosotros):
```json
[{"retweeted_status":{"contributors":null,"text":"We have disabled SSLv3 protocol support in response to the vulnerability published today. You may need to update your browser to use Twitter","geo":null,"retweeted":false,"in_reply_to_screen_name":null,"truncated":false,"lang":"en","entities":{"symbols":[],"urls":[],"hashtags":[],"user_mentions":[]},"in_reply_to_status_id_str":null,"id":522190947782643712,"source":"Twitter Web Client<\/a>","in_reply_to_user_id_str":null,"favorited":false,"in_reply_to_status_id":null,"retweet_count":695,"created_at":"Wed Oct 15 01:03:18 +0000 2014","in_reply_to_user_id":null,"favorite_count":226,"id_str":"522190947782643712","place":null,"user":{"location":"Twitter HQ","default_profile":false,"profile_background_tile":true,"statuses_count":40,"lang":"en","profile_link_color":"009999","id":1137751093,"following":false,"protected":false,"favourites_count":0,"profile_text_color":"333333","description":"The Product Security Team at Twitter.","verified":true,"contributors_enabled":false,"profile_sidebar_border_color":"EEEEEE","name":"Twitter Security","profile_background_color":"131516","created_at":"Thu Jan 31 19:37:42 +0000 2013","is_translation_enabled":false,"default_profile_image":false,"followers_count":13852,"profile_image_url_https":"https://pbs.twimg.com/profile_images/3321149170/6bf0ef1ae272b203acfdee4d7e61df49_normal.png","geo_enabled":false,"profile_background_image_url":"http://abs.twimg.com/images/themes/theme14/bg.gif","profile_background_image_url_https":"https://abs.twimg.com/images/themes/theme14/bg.gif","follow_request_sent":false,"entities":{"description":{"urls":[]},"url":{"urls":[{"expanded_url":"https://twitter.com/about/security","indices":[0,23],"display_url":"twitter.com/about/security","url":"https://t.co/E9FCUUoXKF"}]}},"url":"https://t.co/E9FCUUoXKF","utc_offset":-25200,"time_zone":"Pacific Time (US & Canada)","notifications":false,"profile_use_background_image":true,"friends_count":0,"profile_sidebar_fill_color":"EFEFEF","screen_name":"twittersecurity","id_str":"1137751093","profile_image_url":"http://pbs.twimg.com/profile_images/3321149170/6bf0ef1ae272b203acfdee4d7e61df49_normal.png","listed_count":188,"is_translator":false},"coordinates":null},"contributors":null,"text":"RT @twittersecurity: We have disabled SSLv3 protocol support in response to the vulnerability published today. You may need to update your \u2026","geo":null,"retweeted":false,"in_reply_to_screen_name":null,"truncated":false,"lang":"en","entities":{"symbols":[],"urls":[],"hashtags":[],"user_mentions":[{"id":1137751093,"name":"Twitter Security","indices":[3,19],"screen_name":"twittersecurity","id_str":"1137751093"}]},"in_reply_to_status_id_str":null,"id":522339641815748608,"source":"Twitter for Mac<\/a>","in_reply_to_user_id_str":null,"favorited":false,"in_reply_to_status_id":null,"retweet_count":695,"created_at":"Wed Oct 15 10:54:09 +0000 2014","in_reply_to_user_id":null,"favorite_count":0,"id_str":"522339641815748608","place":null,"user":{"location":"San Francisco, CA","default_profile":false,"profile_background_tile":true,"statuses_count":3520,"lang":"en","profile_link_color":"0084B4","profile_banner_url":"https://pbs.twimg.com/profile_banners/6253282/1347394302","id":6253282,"following":false,"protected":false,"favourites_count":26,"profile_text_color":"333333","description":"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.","verified":true,"contributors_enabled":false,"profile_sidebar_border_color":"C0DEED","name":"Twitter API","profile_background_color":"C0DEED","created_at":"Wed May 23 06:01:13 +0000 2007","is_translation_enabled":false,"default_profile_image":false,"followers_count":2418318,"profile_image_url_https":"https://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_normal.png","geo_enabled":true,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3d4vj.png","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3d4vj.png","follow_request_sent":false,"entities":{"description":{"urls":[]},"url":{"urls":[{"expanded_url":"http://dev.twitter.com","indices":[0,22],"display_url":"dev.twitter.com","url":"http://t.co/78pYTvWfJd"}]}},"url":"http://t.co/78pYTvWfJd","utc_offset":-25200,"time_zone":"Pacific Time (US & Canada)","notifications":false,"profile_use_background_image":true,"friends_count":48,"profile_sidebar_fill_color":"DDEEF6","screen_name":"twitterapi","id_str":"6253282","profile_image_url":"http://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_normal.png","listed_count":12846,"is_translator":false},"coordinates":null},{"contributors":null,"text":"The OAuth / xAuth issue affecting REST endpoints has been resolved. Please visit our OAuth forums for support https://t.co/XiKCrlfCNX","geo":null,"retweeted":false,"in_reply_to_screen_name":"twitterapi","possibly_sensitive":false,"truncated":false,"lang":"en","entities":{"symbols":[],"urls":[{"expanded_url":"https://twittercommunity.com/category/oauth","indices":[110,133],"display_url":"twittercommunity.com/category/oauth","url":"https://t.co/XiKCrlfCNX"}],"hashtags":[],"user_mentions":[]},"in_reply_to_status_id_str":"515255192787230720","id":515288036242763776,"source":"Twitter Web Client<\/a>","in_reply_to_user_id_str":"6253282","favorited":false,"in_reply_to_status_id":515255192787230720,"retweet_count":79,"created_at":"Thu Sep 25 23:53:36 +0000 2014","in_reply_to_user_id":6253282,"favorite_count":55,"id_str":"515288036242763776","place":null,"user":{"location":"San Francisco, CA","default_profile":false,"profile_background_tile":true,"statuses_count":3520,"lang":"en","profile_link_color":"0084B4","profile_banner_url":"https://pbs.twimg.com/profile_banners/6253282/1347394302","id":6253282,"following":false,"protected":false,"favourites_count":26,"profile_text_color":"333333","description":"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.","verified":true,"contributors_enabled":false,"profile_sidebar_border_color":"C0DEED","name":"Twitter API","profile_background_color":"C0DEED","created_at":"Wed May 23 06:01:13 +0000 2007","is_translation_enabled":false,"default_profile_image":false,"followers_count":2418318,"profile_image_url_https":"https://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_normal.png","geo_enabled":true,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3d4vj.png","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/656927849/miyt9dpjz77sc0w3d4vj.png","follow_request_sent":false,"entities":{"description":{"urls":[]},"url":{"urls":[{"expanded_url":"http://dev.twitter.com","indices":[0,22],"display_url":"dev.twitter.com","url":"http://t.co/78pYTvWfJd"}]}},"url":"http://t.co/78pYTvWfJd","utc_offset":-25200,"time_zone":"Pacific Time (US & Canada)","notifications":false,"profile_use_background_image":true,"friends_count":48,"profile_sidebar_fill_color":"DDEEF6","screen_name":"twitterapi","id_str":"6253282","profile_image_url":"http://pbs.twimg.com/profile_images/2284174872/7df3h38zabcvjylnyfe3_normal.png","listed_count":12846,"is_translator":false},"coordinates":null}]
```
## Descarga de tweets programática
### Oauth bearer token
¿Qué es ese "bearer token" que he copiado en un notepar? Oauth permite delegar el uso de tu cuenta de twitter (o de Facebook o de google...) a una aplicación, de forma que pueda hacer uso de ella **en tu nombre**. Para hacer eso, es necesario incluir una firma generada mediante OAuth en todas las peticiones HTTP que haga tu aplicación al API.
Lee detenidamente las siguientes instrucciones para usar OAUTH y permitir que el usuario delegue a la aplicación el uso (read/write) de tu cuenta de twitter aunque nosotros usaremos la última de todas (application only), que es para lo que se usa el "bearer token":
* Información de la aplicación: https://apps.twitter.com/
* Overview: https://dev.twitter.com/oauth/overview
* Autenticación por PIN: https://dev.twitter.com/oauth/pin-based
* Autenticación 3-Legged Oauth: https://dev.twitter.com/oauth/3-legged
* Application only https://dev.twitter.com/oauth/application-only
Bearer Token es una buena forma de recabar datos de twitter dado que sólo queremos acceso read only a la cuenta para leer tweets. El esquema es el siguiente:
### Crear proyecto y descargar dependencias
**Para esta práctica no hace falta un Eclipse especial, podemos usar el eclipse ya instalado en la máquina**
Crea un proyecto en Java, por ejemplo `CdistREST`. Transformalo en `Maven` (botón derecho sobre el `proyecto > Configure > Convert to Maven Project`).
Añade las dependencias entre los tags `` ``:
```xml
com.konghq
unirest-java
3.12.0
com.google.code.gson
gson
2.8.6
```
### Obtención del Bearer Token (no necesario)
**Antes, era necesario obtener el bearer token, ahora te lo dan generado (solo tienes que copiarlo)**. Esta es la explicación (**no tenéis que hacerlo**) de cómo se obtendría el bearer token a partir de los secretos de la aplicación:
1. codificar en forma de URL ([URLEncode](https://en.wikipedia.org/wiki/Percent-encoding)) tanto el consumer key como el consumer secret.
2. unir ambos en una string separándolos por ":"
3. codificar el resultado con base64
4. hacer una petición a "https://api.twitter.com/oauth2/token" añadiendo una cabecera con el formato Authorization: Basic {Credenciales codificadas}
Para conseguirlo, era necesario usar código (**desde el 2019 no es necesario, se puede obtener el bearer token directamente de twitter**):
```java
package cdistRest;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
import org.apache.commons.codec.binary.Base64;
/* es necesario incluir esta dependencia en el fichermo pom
commons-codec
commons-codec
1.10
*/
import com.google.gson.Gson;
import kong.unirest.GetRequest;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import kong.unirest.UnirestException;
public class TwitterCrawler {
private static final String oauth_consumer_key = "XXXXXXXXx";
private static final String oauth_consumer_secret = "XXXXXXXXXXXXXXXXXXXXXXXXXX";
public class BearerToken
{
private String access_token;
private String token_type;
}
public static String deserializeJson(String input_json)
{
Gson gson = new Gson();
BearerToken new_bearerToken = gson.fromJson(input_json, BearerToken.class);
return new_bearerToken.access_token;
}
public static void main(String args[]) throws UnsupportedEncodingException {
try {
/* get bearer token according to https://dev.twitter.com/oauth/application-only */
String URLEncoderConsumerKey = URLEncoder.encode(oauth_consumer_key, "UTF-8");
String URLEncoderConsumerSecret = URLEncoder.encode(oauth_consumer_secret, "UTF-8");
String AuthorizationHeader = URLEncoderConsumerKey+":"+URLEncoderConsumerSecret;
String AuthorizationHeaderB64 = Base64.encodeBase64String(AuthorizationHeader.getBytes("UTF8"));
System.out.println(AuthorizationHeaderB64);
RequestBodyEntity postReq = Unirest.post("https://api.twitter.com/oauth2/token")
.header("User-Agent","TwitterApp")
.header("host", "api.twitter.com")
.header("Authorization", "Basic " + AuthorizationHeaderB64)
.header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
.body("grant_type=client_credentials");
HttpResponse res = postReq.asString();
System.out.println("Bearer Token : " + TwitterCrawler.deserializeJson(res.getBody()));
/* we now have a brearer token so can make requests to Twitter */
} catch (UnirestException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
```
Si usas Java 11, tu fichero `module-info.java` deberá contener (recuerda que el nombre del módulo es el del package, por lo que si lo has cambiado, éste fichero variará):
```java
module CDistTwitter {
requires com.google.gson;
requires unirest.java;
exports cdistRest;
}
```
Sustituyendo `oauth_consumer_key` y `oauth_consumer_secret` por los valores de tu aplicación se obtenía el bearer token.
### Gestión de tweets con Json
Los tweets devueltos por Twitter (en general cualquier información que devuelva el API) vienen codificados con Json. En la práctica 1 de rest, usamos Gson para almacenar datos en json dentro de clases java.
Recuerda que, si usas Java 11, tu fichero `module-info.java` deberá contener (recuerda que el nombre del módulo es el del package, por lo que si lo has cambiado, éste fichero variará):
```java
module CDistTwitter {
requires com.google.gson;
requires unirest.java;
exports cdistRest;
}
```
En esta ocasión, creamos dos clases para guardar, por un lado el Tweet y por otro lado el usuario que lo creó, aunque el primero apunta al segundo. Las clases son las siguientes (usad el mismo paquete para todos, en este caso, por ejemplo **cdistRest**), `Tweet` para almacenar el tweet propiamente dicho, y `User` para guardar los datos del usuario que envía el tweet:
**Clase `Tweet.java`:**
```java
package cdistRest;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
public class Tweet {
public static Tweet deserializeJson(String input_json) {
Gson gson = new Gson();
Tweet new_Tweet = gson.fromJson(input_json, Tweet.class);
return new_Tweet;
}
public static List deserializeJsonArray(String input_json) {
Gson gson = new Gson();
Type collectionType = new TypeToken>() {}.getType();
Collection new_tweet_collection = gson.fromJson(input_json,
collectionType);
ArrayList new_Tweet_list = new ArrayList(
new_tweet_collection);
return new_Tweet_list;
}
@SerializedName("geo")
public String geo;
@SerializedName("in_reply_to_status_id")
public String in_reply_to_status_id;
@SerializedName("truncated")
public String truncated;
@SerializedName("created_at")
public String created_at;
@SerializedName("retweet_count")
public String retweet_count;
@SerializedName("in_reply_to_user_id")
public String in_reply_to_user_id;
@SerializedName("id_str")
public String id_str;
@SerializedName("place")
public transient String place;
@SerializedName("favorited")
public boolean favorited;
@SerializedName("source")
public String source;
@SerializedName("in_reply_to_screen_name")
public String in_reply_to_screen_name;
@SerializedName("in_reply_to_status_id_str")
public String in_reply_to_status_id_str;
@SerializedName("id")
public long id;
@SerializedName("contributors")
public String contributors;
@SerializedName("coordinates")
public String coordinates;
@SerializedName("retweeted")
public boolean retweeted;
@SerializedName("text")
public String text;
@SerializedName("profile_image_url")
public String profile_image_url;
public User user;
}
```
**Clase `User.java`:**
```java
package cdistRest;
import com.google.gson.annotations.SerializedName;
public class User {
@SerializedName("friends_count")
public int friends_count;
@SerializedName("profile_background_color")
public String profile_background_color;
@SerializedName("profile_background_image_url")
public String profile_background_image_url;
@SerializedName("created_at")
public String created_at;
@SerializedName("description")
public String description;
@SerializedName("favourites_count")
public int favourites_count;
@SerializedName("lang")
public String lang;
@SerializedName("notifications")
public boolean notifications;
@SerializedName("id_str")
public String id_str;
@SerializedName("default_profile_image")
public boolean default_profile_image;
@SerializedName("profile_text_color")
public String profile_text_color;
@SerializedName("default_profile")
public boolean default_profile;
@SerializedName("show_all_inline_media")
public boolean show_all_inline_media;
@SerializedName("contributors_enabled")
public boolean contributors_enabled;
@SerializedName("geo_enabled")
public boolean geo_enabled;
@SerializedName("screen_name")
public String screen_name;
@SerializedName("profile_sidebar_fill_color")
public String profile_sidebar_fill_color;
@SerializedName("profile_image_url")
public String profile_image_url;
@SerializedName("profile_background_tile")
public boolean profile_background_tile;
@SerializedName("follow_request_sent")
public boolean follow_request_sent;
@SerializedName("url")
public String url;
@SerializedName("statuses_count")
public int statuses_count;
@SerializedName("following")
public boolean following;
@SerializedName("time_zone")
public String time_zone;
@SerializedName("profile_link_color")
public String profile_link_color;
@SerializedName("protected")
public boolean protectedd;
@SerializedName("verified")
public boolean verified;
@SerializedName("profile_sidebar_border_color")
public String profile_sidebar_border_color;
@SerializedName("followers_count")
public int followers_count;
@SerializedName("location")
public String location;
@SerializedName("name")
public String name;
@SerializedName("is_translator")
public boolean is_translator;
@SerializedName("id")
public long id;
@SerializedName("listed_count")
public int listed_count;
@SerializedName("profile_use_background_image")
public boolean profile_use_background_image;
@SerializedName("utc_offset")
public int utc_offset;
}
```
### Anade una consulta al API de Twetter
Vamos a probar con la función del API [get user timeline](https://developer.twitter.com/en/docs/twitter-api/v1/tweets/timelines/api-reference/get-statuses-user_timeline) que permite obtener los tweets más recientes de un usuario.
Inspecciona la página del API. Revisa los parámetros de la llamada a esa función del API Rest, verás que puedes usar `user_id` que es el ID de twetter (poco conocido, un número) o bien el `screen_name' que es el nombre de la cuenta de twitter que se muestra por internet, por ejemplo `@realmadrid`.
Además se pueden especificar el número de Tweets a obtener.
El bearer token se obtiene tras crear una aplicación y generar las claves:
Para obtener 2 tweets de la cuenta `@realmadrid` usa el siguiente código:
```java
package cdistRest;
import java.io.IOException;
import java.util.List;
import kong.unirest.GetRequest;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import kong.unirest.UnirestException;
public class TwitterCrawler {
private static final String bearer_token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
public static void main(String args[]) throws IOException {
try {
/* https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=realmadrid&count=2 */
HttpResponse json_str_Response = null;
GetRequest getReq = null;
getReq = Unirest.get("https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name={screen_name}&count={count}")
.routeParam("screen_name","realmadrid")
.routeParam("count","2")
.header("Authorization", "Bearer " + bearer_token);
System.out.println("Request to: " + getReq.getUrl());
System.out.println("Authorization header Bearer " + bearer_token);
json_str_Response = getReq.asString();
List tweet_list = Tweet.deserializeJsonArray(json_str_Response.getBody());
for(int i=0; i gettweets() {
/*
* si los tweets caben en una sóla petición, o bien se piden más de
* MAX_TWEET_COUNT_PER_REQUEST en principio la primera petición hay que hacerla
* para obtener el max_id (máximo id de la secuencia de tweets)
*/
List tweet_list_total = new ArrayList();
pending = tweetcount;
long max_id = Long.MAX_VALUE;
do {
long request_twetcount = 0;
long tweets_obtained = 0;
if (pending > MAX_TWEET_COUNT_PER_REQUEST) {
request_twetcount = MAX_TWEET_COUNT_PER_REQUEST;
} else {
request_twetcount = pending;
}
HttpResponse json_str_Response = null;
GetRequest getReq = null;
getReq = Unirest.get(
"https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name={screen_name}&count={count}&max_id={max_id}")
.routeParam("screen_name", screen_name).routeParam("count", "" + request_twetcount)
.routeParam("max_id", ""+(max_id-1))
.header("Authorization", "Bearer " + bearer_token);
System.out.println("Request " + request_twetcount + " tweets to: " + getReq.getUrl());
json_str_Response = getReq.asString();
List tweet_list_request = Tweet.deserializeJsonArray(json_str_Response.getBody());
tweets_obtained = tweet_list_request.size();
System.out.println("received " + tweets_obtained + " tweets");
/* actualizamos tweetcount con los recibidos */
pending -= tweets_obtained;
/* actualizamos la lista total de tweets */
tweet_list_total.addAll(tweet_list_request);
/*
* PARADA
* si hemos hemos recibido en total de tweets, paramos
*/
if(pending <= 0) break;
/*
* PARADA
* si hemos hemos recibido menos de request_twetcount es que no hay más.
* Hay que finalizar
*/
if(tweets_obtained < request_twetcount) break;
for (Tweet tw : tweet_list_request) {
if (tw.id < max_id)
max_id = tw.id;
}
} while (true);
return tweet_list_total;
}
public static void main(String args[]) throws IOException {
TwitterCrawler tc = new TwitterCrawler(400, "realmadrid");
List my_tweets = tc.gettweets();
for(int i=0; i