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