Lab 3: Introduction to Twitter API v21

Introduction

Today we are going to jump in to the topics of Twitter, the Twitter API and how to analyze Twitter data. After going through the basics, the rest of the lab will be yours to start playing around with collecting and analyzing your own Twitter data!

A note on preliminaries: to follow along with the below on your own, you will require

  1. An active Twitter account
  2. A Developer account with basic access (this now costs $100.000 USD/month, but should have been provided by NYU Abu Dhabi)
    • Part of the below will require Academic Research access which is somewhat harder to attain as it is restricted to graduate students and faculty. Should you require the full set of Tweets from somebody for your final projects, however, the code contained below will help you construct the calls which I can send on your behalf. Note that this is time-dependent as my access has not yet been cut off (fingers crossed!).

What is Twitter?

Twitter is a microblogging and social networking service founded in 2006. Today, the platform boasts the following statistics

  • 237+ million daily active users
  • 500+ million Tweets sent per day
  • 23% of US adults use Twitter
  • 69% of users get news on the site
  • Something like 95% of Members of U.S. Congress have a Twitter Account
  • Some thing like 77% of U.N. recognized governments have a presence on Twitter

Twitter is based on microblogging: users can send messages of up to 280 characters called Tweets. User names (screen names or handles) start with an “@” sign. Each individual can choose to follow other users, which will make their Tweets appear on that individual’s timeline. Some other features of note:

  • hashtags: words or phrases prefixed with the # symbol what are used to group tweets by topic.
  • @-replies: Tweets that begin with the @ symbol followed by a user name (public messages)
  • retweets: Re-publication of another user’s content with an indication of its original authos
  • mentions: Action of including the screen name of another user in a tweet
  • trending: Popular hashtags or phrases

Why study Twitter?

There are a number of reasons why Twitter can be useful for academic study, most clearly that Twitter can have offline effects.

  1. Twitter metrics can predict real-world outcomes
    • Box-office revenue, spread of flu, happiness and general mood, epicenter of eartquakes, winner of reality TV shows, etc.
  2. Studies show that different Twitter metrics were correlated with election results in many countries
    • BUT: “the predictive power of Twitter regarding elections has been greatly exaggerated” (see Gayo-Avello, 2013)
  3. Social media solves collective action problems, facilitate information diffusion, and thus can foster the spread of protest
    • Arab Spring: “The revolutions were tweeted?”
    • Necessary or sufficient cause? Lack of rigorous empirical work
  4. Twitter and word-of-mouth marketing

Many studies on the effect of social media published thus far have used Twitter data for a variety of reasons.

  1. Many influential actors (journalists, politicians, celebrities, etc) use Twitter. Spillover effects into reality.
  2. Many political campaigns try to generate users’ engagement
  3. Twitter is public in nature: the company facilitates access to Tweets with their API, and generates a dynamic of competition and public expression on their platform
  4. Academic research has been able to establish a firm connection between online and offline behavior

The Twitter API

The Twitter Application Programming Interface (API) allows for four basic methods for collecting Tweets.

  1. Filter Stream: Tweets filtered by keywords
    • Example: Tweets mentioning “Obama” or “Biden”
  2. Geo Stream: Tweets filtered by location
    • Example: Tweets sent from the Arabian peninsula
  3. Sample Stream: Provides a random sample of 1% of Tweets
  4. Timeline: Tweets sent by a given user
    • Example: Tweets sent by @nytimes or @JoeBiden

Except for the last option, the one we will be using primarily in this course, Tweets can only be downloaded in real time (as they are being published).

Using the Twitter API v2 in R

To access the Twitter API v2, you must have an authenticated developer account and a Bearer Token. I personally like to save these within a file of the following structure for easy access:

wd <- "D:/Twitter"
setwd(wd)

twitter_info <- read.csv("twitter_info2.csv",
                         stringsAsFactors = F)

names(twitter_info)
## [1] "api_key"             "api_key_secret"      "bearer_token"       
## [4] "access_token"        "access_token_secret"

I.e. a .csv file with three columns: one including my API Key, another including my API Key Secret, and the third including my Bearer Token. It is only this last bit of information, which you should get from your Essential Access Account, which we need to start using the API!

Unfortunately, since Twitter updated their API to v2, the number of working R packages have decreased substantially. Only two appear to exist in working form, one of which requires Academic Research Access (which you likely don’t have) and the other is a package on GitHub which doesn’t work particularly well (in my experience).

But fear not! With the skills we have been developing for using the R programming language we can easily write ourselves a few functions for making the collection of Tweets quite straightforward after learning a bit about how the API works!

Getting Started

We are going to start exactly where I started when learning about accessing the API: with the Twitter created tutorial on Getting started with R and v2 of the Twitter API.

You should go ahead and read that document, as well as the below referenced Twitter API v2 documentation, on your own time to get the most out of the platform. For our purposes it is worthwhile to study the anatomy of a API call and how to generalize what we learn into useful functions.

To start, we need a few packages loaded into our environment:

library(httr)
library(jsonlite)
library(dplyr)
library(plyr)

The first of these packages is for using R to make calls to the internet. The second package is useful for converting data which comes in JSON format (essentially nested lists) into data.frames which are easily used within R. The last packages are, as we already know, quite useful for data wrangling tasks.

Step 1 for any API call is to declare our bearer token, the thing which Twitter uses to verify who is making an API call and what permissions they have. From this, we will create a “header” which transmits this information alongside our request.

bearer_token <- twitter_info$bearer_token
headers <- c(`Authorization` = sprintf('Bearer %s', bearer_token))

Step 2 for any API call is to determine which endpoint we want to pass a query to and what sort of information we want to request. Here are a number of useful links to relevant API documentation.

  1. All Endpoints: Provides paths and documentation for every endpoint you can access via the API v2.
  2. Get Tweets: The endpoint for getting Tweets from a particular user ID.
  3. Fields and Expansions: The API v2 data objects include only a small number of default fields when making a request. To get more information you will need to include the appropriate fields and expansions.
  4. Fields: Fields allow you to select the data that you want from each of the objects in your endpoint response. These include
    • Tweet Fields
    • User Fields
    • Media Fields
    • Poll Fields
    • Place Fields
  5. Expansions: Expansions expand objects referenced in the payload. These include
    • Referenced Tweets
    • The user ID of who is being replied to
    • Media attachments
    • Poll attachments
    • Location metadata
    • Data on the user mentioned in the Tweet
    • Data on the author of a referenced Tweet
  6. Building Queries: How to build up a request to return a set of historical Tweets which match the query.

Once we have determined what we want, we then have to form a query. The structure of a query is a simple list indicating all of parameters we want to pass along to the API. For example, suppose that we wanted to get basic profile information for Joe Biden’s personal Twitter account:

params <- list(user.fields = "public_metrics,description",
               expansions = "pinned_tweet_id")

handle <- 'JoeBiden'

We can then set up the query by identifying the correct endpoint and sending the request to the API:

url_handle <- sprintf('https://api.twitter.com/2/users/by?usernames=%s', handle)

response <- httr::GET(url = url_handle,
                      httr::add_headers(.headers = headers),
                      query = params)
obj <- httr::content(response, as = "text")

Responses from the API take the form of a JSON file:

prettify(obj)
## {
##     "data": [
##         {
##             "description": "Husband to @DrBiden, proud father and grandfather. Ready to build back better for all Americans. Official account is @POTUS.",
##             "name": "Joe Biden",
##             "pinned_tweet_id": "1650801827728986112",
##             "public_metrics": {
##                 "followers_count": 37219172,
##                 "following_count": 47,
##                 "tweet_count": 9215,
##                 "listed_count": 41827
##             },
##             "id": "939091",
##             "username": "JoeBiden"
##         }
##     ],
##     "includes": {
##         "tweets": [
##             {
##                 "edit_history_tweet_ids": [
##                     "1650801827728986112"
##                 ],
##                 "id": "1650801827728986112",
##                 "text": "Every generation has a moment where they have had to stand up for democracy. To stand up for their fundamental freedoms. I believe this is ours.\n\nThat’s why I’m running for reelection as President of the United States. Join us. Let’s finish the job. https://t.co/V9Mzpw8Sqy https://t.co/Y4NXR6B8ly"
##             }
##         ]
##     }
## }
## 

To get this into a format that can be easily manipulated within R, we need to flatten the JSON into a data.frame:

json_data <- fromJSON(obj, flatten = TRUE) %>% 
  as.data.frame()
as_tibble(json_data)
## # A tibble: 1 × 12
##   data.description              data.name data.pinned_twe… data.id data.username
##   <chr>                         <chr>     <chr>            <chr>   <chr>        
## 1 Husband to @DrBiden, proud f… Joe Biden 165080182772898… 939091  JoeBiden     
## # … with 7 more variables: data.public_metrics.followers_count <int>,
## #   data.public_metrics.following_count <int>,
## #   data.public_metrics.tweet_count <int>,
## #   data.public_metrics.listed_count <int>,
## #   includes.tweets.edit_history_tweet_ids <list>, includes.tweets.id <chr>,
## #   includes.tweets.text <chr>

Great! How might we modify the above to collect Tweets filtering by keywords and location? First, we need to select the appropriate API and modify the query accordingly. Since you only have “Essential” access, we are limited to looking through “recent” Tweets published over the past seven days.

Let’s suppose that we want to collect Tweets from persons in the UK who Tweeted about either Biden or Trump and have geolocation data on.

endpoint <- 'https://api.twitter.com/2/tweets/search/recent'

params <- list(query = "(Biden OR Trump) (place_country:GB has:geo)",
               tweet.fields = "author_id,created_at,public_metrics,geo",
               max_results = 100)

response <- httr::GET(url = endpoint,
                      httr::add_headers(.headers = headers),
                      query = params) %>% 
            httr::content(., as = "text") %>% 
            fromJSON(., flatten = TRUE) %>% 
            as.data.frame()

as_tibble(response)
## # A tibble: 100 × 17
##    data.author_id      data.id        data.text data.created_at data.edit_histo…
##    <chr>               <chr>          <chr>     <chr>           <list>          
##  1 1487008168693702659 1659729304685… "@RonnyJ… 2023-05-20T01:… <chr [1]>       
##  2 1487008168693702659 1659723856607… "@MAGAIn… 2023-05-20T00:… <chr [1]>       
##  3 1487008168693702659 1659718443136… "@its_th… 2023-05-20T00:… <chr [1]>       
##  4 1487008168693702659 1659713948029… "@lauren… 2023-05-20T00:… <chr [1]>       
##  5 1468892948309987338 1659712146412… "#RUSSIA… 2023-05-20T00:… <chr [1]>       
##  6 1487008168693702659 1659708156132… "@TeamTr… 2023-05-19T23:… <chr [1]>       
##  7 1487008168693702659 1659707295591… "@Gunthe… 2023-05-19T23:… <chr [1]>       
##  8 1266307739938144257 1659706676637… "Trump h… 2023-05-19T23:… <chr [1]>       
##  9 1487008168693702659 1659706261284… "@Gunthe… 2023-05-19T23:… <chr [1]>       
## 10 1464504657263353860 1659706020648… "https:/… 2023-05-19T23:… <chr [1]>       
## # … with 90 more rows, and 12 more variables: data.geo.place_id <chr>,
## #   data.geo.coordinates.type <chr>, data.geo.coordinates.coordinates <list>,
## #   data.public_metrics.retweet_count <int>,
## #   data.public_metrics.reply_count <int>,
## #   data.public_metrics.like_count <int>,
## #   data.public_metrics.quote_count <int>,
## #   data.public_metrics.impression_count <int>, meta.newest_id <chr>, …

If we wanted to find the location of these tweets, we could then do something like…

get_geo <- function(place_id){
  
  link <- paste0("https://api.twitter.com/1.1/geo/id/",
                  place_id,
                  ".json")
  all <- httr::GET(url = link,
                   httr::add_headers(.headers = headers)) %>% 
               httr::content(., as = "text") %>% 
               fromJSON(., flatten = TRUE)
  
  out <- data.frame(id = all$id,
                    name = all$name,
                    full_name = all$full_name,
                    country = all$country,
                    country_code = all$country_code,
                    place_type = all$place_type)
  
  out
  
}

locations <- list()

for(i in 1:length(unique(response$data.geo.place_id))){
  
  place <- unique(response$data.geo.place_id)[i]
  
  out <- get_geo(place)
  
  locations[[i]] <- out
  
}
  
locs <- do.call("rbind",locations)

locs
##                  id                 name                    full_name
## 1  6f31c24707aca514             Holywell              Holywell, Wales
## 2  791e00bcadc4615f              Glasgow            Glasgow, Scotland
## 3  53b67b1d1cc81a51           Birmingham          Birmingham, England
## 4  703ee413ec69365a             Newhaven            Newhaven, England
## 5  2fa9f57ed641748a             Menstrie           Menstrie, Scotland
## 6  511655fc081bb251           Portsmouth          Portsmouth, England
## 7  65b23b0045f450f6 Kingston upon Thames Kingston upon Thames, London
## 8  01c7ed8caf11e8b2           Kensington           Kensington, London
## 9  25d3e991f5637f5a           South West          South West, England
## 10 28679b23ed15b380              Belfast    Belfast, Northern Ireland
## 11 4cb7ff8db49dfaa0              Ashford             Ashford, England
## 12 038247c1b5bb34c9            Greenwich            Greenwich, London
## 13 0af014accd6f6e99             Scotland     Scotland, United Kingdom
## 14 315b740b108481f6           Manchester          Manchester, England
## 15 06cb7db38dd5f950            Worcester           Worcester, England
## 16 3d8aada45e3c5866             Beverley            Beverley, England
## 17 3bc1b6cfd27ef7f6                 East                East, England
## 18 2a3f152d1ac5044a              Hackney              Hackney, London
## 19 1039a706613a52c5         Bricket Wood           Bricket Wood, East
## 20 5f54245c51670911       Milford on Sea      Milford on Sea, England
## 21 457b4814b4240d87               London              London, England
## 22 01d711761615d783             Ormskirk            Ormskirk, England
## 23 6a779d5cb7e570e8           Chelmsford             Chelmsford, East
## 24 1f36c2b60fbc98ac              Swansea               Swansea, Wales
## 25 2fbecaad4fd6b148    Higher Penwortham   Higher Penwortham, England
## 26 7ae9e2f2ff7a87cd            Edinburgh          Edinburgh, Scotland
## 27 31fffbe34de66921              Salford             Salford, England
## 28 4b6c0ea1297b258a              Leyland             Leyland, England
## 29 56c45474148ca4da           Paddington           Paddington, London
## 30 544762ebf7fda780            Islington            Islington, London
## 31 7ef79c5ab17d518c               Barnet               Barnet, London
## 32 3bb508d078dcdf12          King's Lynn         King's Lynn, England
## 33 4249bb41acf40cfb                Byker               Byker, England
## 34 67bc7263f7b9047b           North East          North East, England
## 35 06168d1feda43857           South East          South East, England
## 36 46f2260bd86a6f0c               Redcar              Redcar, England
## 37 46e17f9d2f1027e9         Gustard Wood           Gustard Wood, East
## 38 42d0cf7d49d27c95           Hillingdon           Hillingdon, London
## 39 62a2a7f86cd9a5b4               Dundee             Dundee, Scotland
## 40 074898ace1d8ef8a              Horsham             Horsham, England
## 41 1da00c8852cc9da2              Newport               Newport, Wales
## 42 593b55aac2dd394d     Wimborne Minster    Wimborne Minster, England
## 43 3395783754204776              Kirkham             Kirkham, England
## 44 06f9f5a068aa411f                Corby               Corby, England
## 45 6e99018e409ed45f           West Ashby          West Ashby, England
## 46 1c37515518593fe3             Richmond             Richmond, London
## 47 3455d7166dccdac5         West Molesey     West Molesey, South East
## 48 01cf9c6409a7f7a0           Kilmarnock         Kilmarnock, Scotland
## 49 58468d6e28fde202            Fleetwood           Fleetwood, England
## 50 1e8d77a6081c8ce6             Pwllheli              Pwllheli, Wales
## 51 07e9c7d1954fff64            Sheffield           Sheffield, England
## 52 46d6cb31bd607799            Middleton           Middleton, England
## 53 2dc53e413a6cbcd8        Middlesbrough       Middlesbrough, England
## 54 758461cd66db17e0               Sutton               Sutton, London
## 55 48eef71af51c9838           Stapleford          Stapleford, England
## 56 52b3e5e1ab04b40e            Hambleton           Hambleton, England
## 57 759dfe79a02eb78a            Blackburn           Blackburn, England
## 58 38d05a66be6d4ee1           Colchester          Colchester, England
## 59 45549e3e82b86df1           Braunstone          Braunstone, England
##           country country_code place_type
## 1  United Kingdom           GB       city
## 2  United Kingdom           GB       city
## 3  United Kingdom           GB       city
## 4  United Kingdom           GB       city
## 5  United Kingdom           GB       city
## 6  United Kingdom           GB       city
## 7  United Kingdom           GB       city
## 8  United Kingdom           GB       city
## 9  United Kingdom           GB      admin
## 10 United Kingdom           GB       city
## 11 United Kingdom           GB       city
## 12 United Kingdom           GB       city
## 13 United Kingdom           GB      admin
## 14 United Kingdom           GB       city
## 15 United Kingdom           GB       city
## 16 United Kingdom           GB       city
## 17 United Kingdom           GB      admin
## 18 United Kingdom           GB       city
## 19 United Kingdom           GB       city
## 20 United Kingdom           GB       city
## 21 United Kingdom           GB       city
## 22 United Kingdom           GB       city
## 23 United Kingdom           GB       city
## 24 United Kingdom           GB       city
## 25 United Kingdom           GB       city
## 26 United Kingdom           GB       city
## 27 United Kingdom           GB       city
## 28 United Kingdom           GB       city
## 29 United Kingdom           GB       city
## 30 United Kingdom           GB       city
## 31 United Kingdom           GB       city
## 32 United Kingdom           GB       city
## 33 United Kingdom           GB       city
## 34 United Kingdom           GB      admin
## 35 United Kingdom           GB      admin
## 36 United Kingdom           GB       city
## 37 United Kingdom           GB       city
## 38 United Kingdom           GB       city
## 39 United Kingdom           GB       city
## 40 United Kingdom           GB       city
## 41 United Kingdom           GB       city
## 42 United Kingdom           GB       city
## 43 United Kingdom           GB       city
## 44 United Kingdom           GB       city
## 45 United Kingdom           GB       city
## 46 United Kingdom           GB       city
## 47 United Kingdom           GB       city
## 48 United Kingdom           GB       city
## 49 United Kingdom           GB       city
## 50 United Kingdom           GB       city
## 51 United Kingdom           GB       city
## 52 United Kingdom           GB       city
## 53 United Kingdom           GB       city
## 54 United Kingdom           GB       city
## 55 United Kingdom           GB       city
## 56 United Kingdom           GB       city
## 57 United Kingdom           GB       city
## 58 United Kingdom           GB       city
## 59 United Kingdom           GB       city

Although this endpoint is rate-limited to 75 requests per 15-minute window and does not accept batch requests.

Anyway, that’s cool! What if we want to grab a bunch of Tweets by a particular user instead? With “essential” access, you can grab up to the last 3200 Tweets from a user. Applying the principles from above, and reading about pagination, we can develop the following function:

last_n_tweets <- function(bearer_token = "", user_id = "", n = 100,
                          tweet_fields = c("attachments",
                                           "created_at",
                                           "entities",
                                           "in_reply_to_user_id",
                                           "public_metrics",
                                           "referenced_tweets",
                                           "source")){
  
  headers <- c(`Authorization` = sprintf('Bearer %s', bearer_token))
  
  # Convert User ID into Numerical ID
  
  sprintf('https://api.twitter.com/2/users/by?usernames=%s', user_id) %>% 
    httr::GET(url = .,
              httr::add_headers(.headers = headers),
              query = list()) %>% 
    httr::content(.,as="text") %>% 
    fromJSON(.,flatten = T) %>% 
    as.data.frame() -> tmp
  
  num_id <- tmp$data.id

  # For that user, grab most recent n tweets, in batches of 100
  
  if(n <= 100){
    requests <- n
  }else{
    requests <- rep(100,floor(n/100))
    if(n %% 100 != 0){
      requests <- c(requests, n %% 100)
    }
  }
  
  next_token <- NA
  
  all <- list()
  
  # Initialize, grab first results  
  paste0('https://api.twitter.com/2/users/',num_id,'/tweets') %>% 
    httr::GET(url = .,
              httr::add_headers(.headers = headers),
              query = list(`max_results` = requests[1],
                           tweet.fields = paste(tweet_fields,collapse=","))) %>% 
    httr::content(.,as="text") %>% 
    fromJSON(.,flatten = T) %>% 
    as.data.frame() -> out
    
  all[[1]] <- out
  
  
  # For more than 100, need to use pagination tokens.
  if(length(requests) >= 2){
    next_token[2] <- unique(as.character(all[[1]]$meta.next_token))
  
    for(i in 2:length(requests)){
      paste0('https://api.twitter.com/2/users/',num_id,'/tweets') %>% 
        httr::GET(url = .,
                  httr::add_headers(.headers = headers),
                  query = list(`max_results` = requests[i],
                               tweet.fields = paste(tweet_fields,collapse=","),
                               pagination_token = next_token[i])) %>% 
        httr::content(.,as="text") %>% 
        fromJSON(.,flatten = T) %>% 
        as.data.frame() -> out
    
      all[[i]] <- out
      next_token[i + 1] <- unique(as.character(all[[i]]$meta.next_token))
    }
  }
  
  do.call("rbind.fill",all)
  
}

Let’s test it out!

out <- last_n_tweets(twitter_info$bearer_token,"JoeBiden",3200)

as_tibble(out)
## # A tibble: 3,200 × 21
##    data.id           data.created_at data.edit_histo… data.text data.referenced…
##    <chr>             <chr>           <list>           <chr>     <list>          
##  1 1659319725359345… 2023-05-18T22:… <chr [1]>        "I'm pro… <NULL>          
##  2 1659209989939154… 2023-05-18T14:… <chr [1]>        "RT @POT… <df [1 × 2]>    
##  3 1659008430835658… 2023-05-18T01:… <chr [1]>        "Made a … <NULL>          
##  4 1658985812770398… 2023-05-18T00:… <chr [1]>        "To this… <NULL>          
##  5 1658959709041221… 2023-05-17T22:… <chr [1]>        "Nancy P… <NULL>          
##  6 1658933179405705… 2023-05-17T20:… <chr [1]>        "We had … <NULL>          
##  7 1658921350390530… 2023-05-17T19:… <chr [1]>        "That’s … <NULL>          
##  8 1658471867022467… 2023-05-16T13:… <chr [1]>        "My plan… <NULL>          
##  9 1658196835868049… 2023-05-15T19:… <chr [1]>        "Student… <NULL>          
## 10 1658189261529571… 2023-05-15T19:… <chr [1]>        "We pass… <NULL>          
## # … with 3,190 more rows, and 16 more variables:
## #   data.in_reply_to_user_id <chr>, data.entities.urls <list>,
## #   data.entities.annotations <list>, data.entities.mentions <list>,
## #   data.attachments.media_keys <list>,
## #   data.public_metrics.retweet_count <int>,
## #   data.public_metrics.reply_count <int>,
## #   data.public_metrics.like_count <int>, …

Excellent! Now, if you had an academic researcher account, you might want to do a similar thing but to collect up to all of the Tweets by a user! The below function does just that (you won’t be able to use it, but the code is illustrative):

get_all_tweets <- function(bearer_token = "", user_id = "",
                           tweet_fields = c("attachments",
                                            "created_at",
                                            "entities",
                                            "in_reply_to_user_id",
                                            "public_metrics",
                                            "referenced_tweets",
                                            "source")){
  # Initialize, Grab first results
  
  all <- list()
  
  paste0('https://api.twitter.com/2/tweets/search/all') %>% 
    httr::GET(url = .,
              httr::add_headers(.headers = headers),
              query = list(query = paste0("from:",user_id),
                           start_time = "2006-03-22T00:00:00Z",
                           max_results = 500,
                           tweet.fields = paste(tweet_fields,collapse=",")
              )) %>% 
    httr::content(.,as="text") %>% 
    fromJSON(.,flatten = T) %>% 
    as.data.frame() -> out
  
  all[[1]] <- out
  
  # For more than 500 Tweets, need to loop
  
  nxt <- unique(as.character(all[[1]]$meta.next_token))
  if(identical(nxt,character(0))){
    return(out)
  }else{
    stop <- F
    i <- 2
  }
  
  while(stop == F){
    Sys.sleep(2)
    
    paste0('https://api.twitter.com/2/tweets/search/all') %>% 
      httr::GET(url = .,
                httr::add_headers(.headers = headers),
                query = list(query = paste0("from:",user_id),
                             start_time = "2006-03-22T00:00:00Z",
                             max_results = 500,
                             next_token = nxt,
                             tweet.fields = paste(tweet_fields,collapse=",")
                )) %>% 
      httr::content(.,as="text") %>% 
      fromJSON(.,flatten = T) %>% 
      as.data.frame() -> out
    
    all[[i]] <- out
    
    nxt <- unique(as.character(all[[i]]$meta.next_token))
    stop <- identical(nxt,character(0))
    i <- i + 1
    
  }
  
  do.call("rbind.fill",all)
  
}

Let’s take it for a spin!

tmp <- get_all_tweets(twitter_info$bearer_token, "POTUS")
plot(table(as.Date(tmp$data.created_at)))

Exercises

Before we get ahead of ourselves, we want to make sure that you have fundamentals in order. Do the following:

Write a script which…

  1. Select a Twitter account of your choice. Determine the following, perhaps by modifying the functions given above and referencing the appropriate documentation:
    • When was their account created?
    • What are their public metrics?
    • Are they verified?
  2. Now collect the last 1000 Tweets by that user, including the following information:
    • When the Tweet was created. After collecting, make a plot of their Tweets over time.
    • Attachments; what type (if any) media did they share?
    • Entities; did they use any hashtags?
    • What source have they generally used to Tweet?
  3. Now collect 1000 Tweets with the hashtag #BlackLivesMatter OR #MeToo from the past 7 days.
    • What sort of media do they have attached?
    • What are their public metrics?

Save and submit your working R script to the Exercise/Quiz Submission Link by the end of the day (ideally, end of lab session!).


  1. This lab is partially based off of materials provided by Sean Kates, Pablo Barbera, and Drew Dimmery.↩︎

LS0tDQp0aXRsZTogIiINCmF1dGhvcjogIkNocmlzdG9waGVyIFNjaHdhcnoiDQpwYWdlczoNCiAgZXh0cmE6IHRydWUNCm91dHB1dDogDQogIGh0bWxfZG9jdW1lbnQ6DQogICAgdG9jOiB0cnVlDQogICAgDQogICAgdG9jX2RlcHRoOiAzDQogICAgdG9jX2Zsb2F0OiB0cnVlDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KLS0tDQoNCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQ0KYGBgDQoNCiMgTGFiIDM6IEludHJvZHVjdGlvbiB0byBUd2l0dGVyIEFQSSB2Ml5bVGhpcyBsYWIgaXMgcGFydGlhbGx5IGJhc2VkIG9mZiBvZiBtYXRlcmlhbHMgcHJvdmlkZWQgYnkgU2VhbiBLYXRlcywgUGFibG8gQmFyYmVyYSwgYW5kIERyZXcgRGltbWVyeS5dDQoNCiMjIEludHJvZHVjdGlvbg0KDQpUb2RheSB3ZSBhcmUgZ29pbmcgdG8ganVtcCBpbiB0byB0aGUgdG9waWNzIG9mIFR3aXR0ZXIsIHRoZSBUd2l0dGVyIEFQSSBhbmQgaG93IHRvIGFuYWx5emUgVHdpdHRlciBkYXRhLiAgQWZ0ZXIgZ29pbmcgdGhyb3VnaCB0aGUgYmFzaWNzLCB0aGUgcmVzdCBvZiB0aGUgbGFiIHdpbGwgYmUgeW91cnMgdG8gc3RhcnQgcGxheWluZyBhcm91bmQgd2l0aCBjb2xsZWN0aW5nIGFuZCBhbmFseXppbmcgeW91ciBvd24gVHdpdHRlciBkYXRhIQ0KDQpBIG5vdGUgb24gcHJlbGltaW5hcmllczogdG8gZm9sbG93IGFsb25nIHdpdGggdGhlIGJlbG93IG9uIHlvdXIgb3duLCB5b3Ugd2lsbCByZXF1aXJlDQoNCjEuIEFuIGFjdGl2ZSBbVHdpdHRlciBhY2NvdW50XShodHRwczovL2hlbHAudHdpdHRlci5jb20vZW4vdXNpbmctdHdpdHRlci9jcmVhdGUtdHdpdHRlci1hY2NvdW50KQ0KMi4gQSBbRGV2ZWxvcGVyIGFjY291bnRdKGh0dHBzOi8vZGV2ZWxvcGVyLnR3aXR0ZXIuY29tL2VuL2RvY3MvdHdpdHRlci1hcGkvZ2V0dGluZy1zdGFydGVkL2dldHRpbmctYWNjZXNzLXRvLXRoZS10d2l0dGVyLWFwaSkgd2l0aCBiYXNpYyBhY2Nlc3MgKHRoaXMgbm93IGNvc3RzICQxMDAuMDAwIFVTRC9tb250aCwgYnV0IHNob3VsZCBoYXZlIGJlZW4gcHJvdmlkZWQgYnkgTllVIEFidSBEaGFiaSkNCiAgICArIFBhcnQgb2YgdGhlIGJlbG93IHdpbGwgcmVxdWlyZSBbQWNhZGVtaWMgUmVzZWFyY2ggYWNjZXNzXShodHRwczovL2RldmVsb3Blci50d2l0dGVyLmNvbS9lbi9wcm9kdWN0cy90d2l0dGVyLWFwaS9hY2FkZW1pYy1yZXNlYXJjaCkgd2hpY2ggaXMgc29tZXdoYXQgaGFyZGVyIHRvIGF0dGFpbiBhcyBpdCBpcyByZXN0cmljdGVkIHRvIGdyYWR1YXRlIHN0dWRlbnRzIGFuZCBmYWN1bHR5LiAgU2hvdWxkIHlvdSByZXF1aXJlIHRoZSBmdWxsIHNldCBvZiBUd2VldHMgZnJvbSBzb21lYm9keSBmb3IgeW91ciBmaW5hbCBwcm9qZWN0cywgaG93ZXZlciwgdGhlIGNvZGUgY29udGFpbmVkIGJlbG93IHdpbGwgaGVscCB5b3UgY29uc3RydWN0IHRoZSBjYWxscyB3aGljaCBJIGNhbiBzZW5kIG9uIHlvdXIgYmVoYWxmLiAgTm90ZSB0aGF0IHRoaXMgaXMgdGltZS1kZXBlbmRlbnQgYXMgbXkgYWNjZXNzIGhhcyBub3QgeWV0IGJlZW4gY3V0IG9mZiAoZmluZ2VycyBjcm9zc2VkISkuDQogICAgDQojIyBXaGF0IGlzIFR3aXR0ZXI/DQoNClR3aXR0ZXIgaXMgYSBtaWNyb2Jsb2dnaW5nIGFuZCBzb2NpYWwgbmV0d29ya2luZyBzZXJ2aWNlIGZvdW5kZWQgaW4gMjAwNi4gIFRvZGF5LCB0aGUgcGxhdGZvcm0gYm9hc3RzIHRoZSBmb2xsb3dpbmcgW3N0YXRpc3RpY3NdKGh0dHBzOi8vd3d3Lm9tbmljb3JlYWdlbmN5LmNvbS90d2l0dGVyLXN0YXRpc3RpY3MvKQ0KDQorIDIzNysgbWlsbGlvbiBkYWlseSBhY3RpdmUgdXNlcnMNCisgNTAwKyBtaWxsaW9uIFR3ZWV0cyBzZW50IHBlciBkYXkNCisgMjMlIG9mIFVTIGFkdWx0cyB1c2UgVHdpdHRlcg0KKyA2OSUgb2YgdXNlcnMgZ2V0IG5ld3Mgb24gdGhlIHNpdGUNCisgU29tZXRoaW5nIGxpa2UgOTUlIG9mIE1lbWJlcnMgb2YgVS5TLiBDb25ncmVzcyBoYXZlIGEgW1R3aXR0ZXIgQWNjb3VudF0oaHR0cHM6Ly90cmlhZ2VjYW5jZXIub3JnL2NvbmdyZXNzaW9uYWwtc29jaWFsLW1lZGlhKQ0KKyBTb21lIHRoaW5nIGxpa2UgNzclIG9mIFUuTi4gcmVjb2duaXplZCBnb3Zlcm5tZW50cyBoYXZlIGEgcHJlc2VuY2Ugb24gVHdpdHRlcg0KDQpUd2l0dGVyIGlzIGJhc2VkIG9uIG1pY3JvYmxvZ2dpbmc6IHVzZXJzIGNhbiBzZW5kIG1lc3NhZ2VzIG9mIHVwIHRvIDI4MCBjaGFyYWN0ZXJzIGNhbGxlZCAqKlR3ZWV0cyoqLiAgVXNlciBuYW1lcyAoc2NyZWVuIG5hbWVzIG9yIGhhbmRsZXMpIHN0YXJ0IHdpdGggYW4gIkAiIHNpZ24uICBFYWNoIGluZGl2aWR1YWwgY2FuIGNob29zZSB0byAqKmZvbGxvdyoqIG90aGVyIHVzZXJzLCB3aGljaCB3aWxsIG1ha2UgdGhlaXIgVHdlZXRzIGFwcGVhciBvbiB0aGF0IGluZGl2aWR1YWwncyAqKnRpbWVsaW5lKiouICBTb21lIG90aGVyIGZlYXR1cmVzIG9mIG5vdGU6DQoNCisgaGFzaHRhZ3M6ICB3b3JkcyBvciBwaHJhc2VzIHByZWZpeGVkIHdpdGggdGhlICMgc3ltYm9sIHdoYXQgYXJlIHVzZWQgdG8gZ3JvdXAgdHdlZXRzIGJ5IHRvcGljLg0KKyBALXJlcGxpZXM6IFR3ZWV0cyB0aGF0IGJlZ2luIHdpdGggdGhlIEAgc3ltYm9sIGZvbGxvd2VkIGJ5IGEgdXNlciBuYW1lIChwdWJsaWMgbWVzc2FnZXMpDQorIHJldHdlZXRzOiAgUmUtcHVibGljYXRpb24gb2YgYW5vdGhlciB1c2VyJ3MgY29udGVudCB3aXRoIGFuIGluZGljYXRpb24gb2YgaXRzIG9yaWdpbmFsIGF1dGhvcw0KKyBtZW50aW9uczogIEFjdGlvbiBvZiBpbmNsdWRpbmcgdGhlIHNjcmVlbiBuYW1lIG9mIGFub3RoZXIgdXNlciBpbiBhIHR3ZWV0DQorIHRyZW5kaW5nOiAgUG9wdWxhciBoYXNodGFncyBvciBwaHJhc2VzDQoNCiMjIFdoeSBzdHVkeSBUd2l0dGVyPw0KDQpUaGVyZSBhcmUgYSBudW1iZXIgb2YgcmVhc29ucyB3aHkgVHdpdHRlciBjYW4gYmUgdXNlZnVsIGZvciBhY2FkZW1pYyBzdHVkeSwgbW9zdCBjbGVhcmx5IHRoYXQgVHdpdHRlciBjYW4gaGF2ZSBvZmZsaW5lIGVmZmVjdHMuDQoNCjEuIFR3aXR0ZXIgbWV0cmljcyBjYW4gcHJlZGljdCByZWFsLXdvcmxkIG91dGNvbWVzDQogICAgKyBCb3gtb2ZmaWNlIHJldmVudWUsIHNwcmVhZCBvZiBmbHUsIGhhcHBpbmVzcyBhbmQgZ2VuZXJhbCBtb29kLCBlcGljZW50ZXIgb2YgZWFydHF1YWtlcywgd2lubmVyIG9mIHJlYWxpdHkgVFYgc2hvd3MsIGV0Yy4NCjIuIFN0dWRpZXMgc2hvdyB0aGF0IGRpZmZlcmVudCBUd2l0dGVyIG1ldHJpY3Mgd2VyZSBjb3JyZWxhdGVkIHdpdGggZWxlY3Rpb24gcmVzdWx0cyBpbiBtYW55IGNvdW50cmllcw0KICAgICsgQlVUOiAidGhlIHByZWRpY3RpdmUgcG93ZXIgb2YgVHdpdHRlciByZWdhcmRpbmcgZWxlY3Rpb25zIGhhcyBiZWVuIGdyZWF0bHkgZXhhZ2dlcmF0ZWQiIChzZWUgW0dheW8tQXZlbGxvLCAyMDEzXShodHRwczovL2pvdXJuYWxzLnNhZ2VwdWIuY29tL2RvaS9mdWxsLzEwLjExNzcvMDg5NDQzOTMxMzQ5Mzk3OSkpDQozLiBTb2NpYWwgbWVkaWEgc29sdmVzIGNvbGxlY3RpdmUgYWN0aW9uIHByb2JsZW1zLCBmYWNpbGl0YXRlIGluZm9ybWF0aW9uIGRpZmZ1c2lvbiwgYW5kIHRodXMgY2FuIGZvc3RlciB0aGUgc3ByZWFkIG9mIHByb3Rlc3QNCiAgICArIEFyYWIgU3ByaW5nOiAiVGhlIHJldm9sdXRpb25zIHdlcmUgdHdlZXRlZD8iDQogICAgKyBOZWNlc3Nhcnkgb3Igc3VmZmljaWVudCBjYXVzZT8gTGFjayBvZiByaWdvcm91cyBlbXBpcmljYWwgd29yaw0KNC4gVHdpdHRlciBhbmQgd29yZC1vZi1tb3V0aCBtYXJrZXRpbmcNCg0KTWFueSBzdHVkaWVzIG9uIHRoZSBlZmZlY3Qgb2Ygc29jaWFsIG1lZGlhIHB1Ymxpc2hlZCB0aHVzIGZhciBoYXZlIHVzZWQgVHdpdHRlciBkYXRhIGZvciBhIHZhcmlldHkgb2YgcmVhc29ucy4NCg0KMS4gTWFueSBpbmZsdWVudGlhbCBhY3RvcnMgKGpvdXJuYWxpc3RzLCBwb2xpdGljaWFucywgY2VsZWJyaXRpZXMsIGV0YykgdXNlIFR3aXR0ZXIuICBTcGlsbG92ZXIgZWZmZWN0cyBpbnRvIHJlYWxpdHkuDQoyLiBNYW55IHBvbGl0aWNhbCBjYW1wYWlnbnMgdHJ5IHRvIGdlbmVyYXRlIHVzZXJzJyBlbmdhZ2VtZW50DQozLiBUd2l0dGVyIGlzIHB1YmxpYyBpbiBuYXR1cmU6IHRoZSBjb21wYW55IGZhY2lsaXRhdGVzIGFjY2VzcyB0byBUd2VldHMgd2l0aCB0aGVpciBBUEksIGFuZCBnZW5lcmF0ZXMgYSBkeW5hbWljIG9mIGNvbXBldGl0aW9uIGFuZCBwdWJsaWMgZXhwcmVzc2lvbiBvbiB0aGVpciBwbGF0Zm9ybQ0KNC4gQWNhZGVtaWMgcmVzZWFyY2ggaGFzIGJlZW4gYWJsZSB0byBlc3RhYmxpc2ggYSBmaXJtIGNvbm5lY3Rpb24gYmV0d2VlbiBvbmxpbmUgYW5kIG9mZmxpbmUgYmVoYXZpb3INCg0KIyMgVGhlIFR3aXR0ZXIgQVBJDQoNClRoZSBUd2l0dGVyIEFwcGxpY2F0aW9uIFByb2dyYW1taW5nIEludGVyZmFjZSAoQVBJKSBhbGxvd3MgZm9yIGZvdXIgYmFzaWMgbWV0aG9kcyBmb3IgY29sbGVjdGluZyBUd2VldHMuDQoNCjEuICoqRmlsdGVyIFN0cmVhbSoqOiBUd2VldHMgZmlsdGVyZWQgYnkga2V5d29yZHMNCiAgICArIEV4YW1wbGU6IFR3ZWV0cyBtZW50aW9uaW5nICJPYmFtYSIgb3IgIkJpZGVuIg0KMi4gKipHZW8gU3RyZWFtKio6IFR3ZWV0cyBmaWx0ZXJlZCBieSBsb2NhdGlvbg0KICAgICsgRXhhbXBsZTogVHdlZXRzIHNlbnQgZnJvbSB0aGUgQXJhYmlhbiBwZW5pbnN1bGENCjMuICoqU2FtcGxlIFN0cmVhbSoqOiBQcm92aWRlcyBhIHJhbmRvbSBzYW1wbGUgb2YgMSUgb2YgVHdlZXRzDQo0LiAqKlRpbWVsaW5lKio6IFR3ZWV0cyBzZW50IGJ5IGEgZ2l2ZW4gdXNlcg0KICAgICsgRXhhbXBsZTogVHdlZXRzIHNlbnQgYnkgQG55dGltZXMgb3IgQEpvZUJpZGVuDQogICAgDQpFeGNlcHQgZm9yIHRoZSBsYXN0IG9wdGlvbiwgdGhlIG9uZSB3ZSB3aWxsIGJlIHVzaW5nIHByaW1hcmlseSBpbiB0aGlzIGNvdXJzZSwgVHdlZXRzIGNhbiBvbmx5IGJlIGRvd25sb2FkZWQgaW4gcmVhbCB0aW1lIChhcyB0aGV5IGFyZSBiZWluZyBwdWJsaXNoZWQpLg0KDQojIyBVc2luZyB0aGUgVHdpdHRlciBBUEkgdjIgaW4gUg0KDQpUbyBhY2Nlc3MgdGhlIFR3aXR0ZXIgQVBJIHYyLCB5b3UgbXVzdCBoYXZlIGFuIGF1dGhlbnRpY2F0ZWQgZGV2ZWxvcGVyIGFjY291bnQgYW5kIGEgW0JlYXJlciBUb2tlbl0oaHR0cHM6Ly9kZXZlbG9wZXIudHdpdHRlci5jb20vZW4vZG9jcy9hdXRoZW50aWNhdGlvbi9vYXV0aC0yLTAvYmVhcmVyLXRva2VucykuICBJIHBlcnNvbmFsbHkgbGlrZSB0byBzYXZlIHRoZXNlIHdpdGhpbiBhIGZpbGUgb2YgdGhlIGZvbGxvd2luZyBzdHJ1Y3R1cmUgZm9yIGVhc3kgYWNjZXNzOg0KDQpgYGB7cn0NCndkIDwtICJEOi9Ud2l0dGVyIg0Kc2V0d2Qod2QpDQoNCnR3aXR0ZXJfaW5mbyA8LSByZWFkLmNzdigidHdpdHRlcl9pbmZvMi5jc3YiLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHN0cmluZ3NBc0ZhY3RvcnMgPSBGKQ0KDQpuYW1lcyh0d2l0dGVyX2luZm8pDQoNCmBgYA0KDQpJLmUuIGEgLmNzdiBmaWxlIHdpdGggdGhyZWUgY29sdW1uczogb25lIGluY2x1ZGluZyBteSBBUEkgS2V5LCBhbm90aGVyIGluY2x1ZGluZyBteSBBUEkgS2V5IFNlY3JldCwgYW5kIHRoZSB0aGlyZCBpbmNsdWRpbmcgbXkgQmVhcmVyIFRva2VuLiAgSXQgaXMgb25seSB0aGlzIGxhc3QgYml0IG9mIGluZm9ybWF0aW9uLCB3aGljaCB5b3Ugc2hvdWxkIGdldCBmcm9tIHlvdXIgRXNzZW50aWFsIEFjY2VzcyBBY2NvdW50LCB3aGljaCB3ZSBuZWVkIHRvIHN0YXJ0IHVzaW5nIHRoZSBBUEkhDQoNClVuZm9ydHVuYXRlbHksIHNpbmNlIFR3aXR0ZXIgdXBkYXRlZCB0aGVpciBBUEkgdG8gdjIsIHRoZSBudW1iZXIgb2Ygd29ya2luZyBSIHBhY2thZ2VzIGhhdmUgZGVjcmVhc2VkIHN1YnN0YW50aWFsbHkuICBbT25seSB0d29dKGh0dHBzOi8vZGV2ZWxvcGVyLnR3aXR0ZXIuY29tL2VuL2RvY3MvdHdpdHRlci1hcGkvdG9vbHMtYW5kLWxpYnJhcmllcy92MikgYXBwZWFyIHRvIGV4aXN0IGluIHdvcmtpbmcgZm9ybSwgb25lIG9mIHdoaWNoIHJlcXVpcmVzIEFjYWRlbWljIFJlc2VhcmNoIEFjY2VzcyAod2hpY2ggeW91IGxpa2VseSBkb24ndCBoYXZlKSBhbmQgdGhlIG90aGVyIGlzIGEgcGFja2FnZSBvbiBHaXRIdWIgd2hpY2ggZG9lc24ndCB3b3JrIHBhcnRpY3VsYXJseSB3ZWxsIChpbiBteSBleHBlcmllbmNlKS4NCg0KQnV0IGZlYXIgbm90ISAgV2l0aCB0aGUgc2tpbGxzIHdlIGhhdmUgYmVlbiBkZXZlbG9waW5nIGZvciB1c2luZyB0aGUgUiBwcm9ncmFtbWluZyBsYW5ndWFnZSB3ZSBjYW4gZWFzaWx5IHdyaXRlIG91cnNlbHZlcyBhIGZldyBmdW5jdGlvbnMgZm9yIG1ha2luZyB0aGUgY29sbGVjdGlvbiBvZiBUd2VldHMgcXVpdGUgc3RyYWlnaHRmb3J3YXJkIGFmdGVyIGxlYXJuaW5nIGEgYml0IGFib3V0IGhvdyB0aGUgQVBJIHdvcmtzIQ0KDQojIyMgR2V0dGluZyBTdGFydGVkDQoNCldlIGFyZSBnb2luZyB0byBzdGFydCBleGFjdGx5IHdoZXJlIEkgc3RhcnRlZCB3aGVuIGxlYXJuaW5nIGFib3V0IGFjY2Vzc2luZyB0aGUgQVBJOiB3aXRoIHRoZSBUd2l0dGVyIGNyZWF0ZWQgdHV0b3JpYWwgb24gW0dldHRpbmcgc3RhcnRlZCB3aXRoIFIgYW5kIHYyIG9mIHRoZSBUd2l0dGVyIEFQSV0oaHR0cHM6Ly9kZXZlbG9wZXIudHdpdHRlci5jb20vZW4vZG9jcy90dXRvcmlhbHMvZ2V0dGluZy1zdGFydGVkLXdpdGgtci1hbmQtdjItb2YtdGhlLXR3aXR0ZXItYXBpKS4NCg0KWW91IHNob3VsZCBnbyBhaGVhZCBhbmQgcmVhZCB0aGF0IGRvY3VtZW50LCBhcyB3ZWxsIGFzIHRoZSBiZWxvdyByZWZlcmVuY2VkIFR3aXR0ZXIgQVBJIHYyIGRvY3VtZW50YXRpb24sIG9uIHlvdXIgb3duIHRpbWUgdG8gZ2V0IHRoZSBtb3N0IG91dCBvZiB0aGUgcGxhdGZvcm0uICBGb3Igb3VyIHB1cnBvc2VzIGl0IGlzIHdvcnRod2hpbGUgdG8gc3R1ZHkgdGhlIGFuYXRvbXkgb2YgYSBBUEkgY2FsbCBhbmQgaG93IHRvIGdlbmVyYWxpemUgd2hhdCB3ZSBsZWFybiBpbnRvIHVzZWZ1bCBmdW5jdGlvbnMuDQoNClRvIHN0YXJ0LCB3ZSBuZWVkIGEgZmV3IHBhY2thZ2VzIGxvYWRlZCBpbnRvIG91ciBlbnZpcm9ubWVudDoNCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoaHR0cikNCmxpYnJhcnkoanNvbmxpdGUpDQpsaWJyYXJ5KGRwbHlyKQ0KbGlicmFyeShwbHlyKQ0KYGBgDQoNClRoZSBmaXJzdCBvZiB0aGVzZSBwYWNrYWdlcyBpcyBmb3IgdXNpbmcgUiB0byBtYWtlIGNhbGxzIHRvIHRoZSBpbnRlcm5ldC4gIFRoZSBzZWNvbmQgcGFja2FnZSBpcyB1c2VmdWwgZm9yIGNvbnZlcnRpbmcgZGF0YSB3aGljaCBjb21lcyBpbiBKU09OIGZvcm1hdCAoZXNzZW50aWFsbHkgbmVzdGVkIGxpc3RzKSBpbnRvIGRhdGEuZnJhbWVzIHdoaWNoIGFyZSBlYXNpbHkgdXNlZCB3aXRoaW4gUi4gIFRoZSBsYXN0IHBhY2thZ2VzIGFyZSwgYXMgd2UgYWxyZWFkeSBrbm93LCBxdWl0ZSB1c2VmdWwgZm9yIGRhdGEgd3JhbmdsaW5nIHRhc2tzLg0KDQoqKlN0ZXAgMSoqIGZvciBhbnkgQVBJIGNhbGwgaXMgdG8gZGVjbGFyZSBvdXIgYmVhcmVyIHRva2VuLCB0aGUgdGhpbmcgd2hpY2ggVHdpdHRlciB1c2VzIHRvIHZlcmlmeSB3aG8gaXMgbWFraW5nIGFuIEFQSSBjYWxsIGFuZCB3aGF0IHBlcm1pc3Npb25zIHRoZXkgaGF2ZS4gIEZyb20gdGhpcywgd2Ugd2lsbCBjcmVhdGUgYSAiaGVhZGVyIiB3aGljaCB0cmFuc21pdHMgdGhpcyBpbmZvcm1hdGlvbiBhbG9uZ3NpZGUgb3VyIHJlcXVlc3QuDQoNCmBgYHtyfQ0KYmVhcmVyX3Rva2VuIDwtIHR3aXR0ZXJfaW5mbyRiZWFyZXJfdG9rZW4NCmhlYWRlcnMgPC0gYyhgQXV0aG9yaXphdGlvbmAgPSBzcHJpbnRmKCdCZWFyZXIgJXMnLCBiZWFyZXJfdG9rZW4pKQ0KYGBgDQoNCioqU3RlcCAyKiogZm9yIGFueSBBUEkgY2FsbCBpcyB0byBkZXRlcm1pbmUgd2hpY2ggZW5kcG9pbnQgd2Ugd2FudCB0byBwYXNzIGEgcXVlcnkgdG8gYW5kIHdoYXQgc29ydCBvZiBpbmZvcm1hdGlvbiB3ZSB3YW50IHRvIHJlcXVlc3QuICBIZXJlIGFyZSBhIG51bWJlciBvZiB1c2VmdWwgbGlua3MgdG8gcmVsZXZhbnQgQVBJIGRvY3VtZW50YXRpb24uDQoNCjEuIFtBbGwgRW5kcG9pbnRzXShodHRwczovL2RldmVsb3Blci50d2l0dGVyLmNvbS9lbi9kb2NzL2FwaS1yZWZlcmVuY2UtaW5kZXgpOiBQcm92aWRlcyBwYXRocyBhbmQgZG9jdW1lbnRhdGlvbiBmb3IgZXZlcnkgZW5kcG9pbnQgeW91IGNhbiBhY2Nlc3MgdmlhIHRoZSBBUEkgdjIuDQoyLiBbR2V0IFR3ZWV0c10oaHR0cHM6Ly9kZXZlbG9wZXIudHdpdHRlci5jb20vZW4vZG9jcy90d2l0dGVyLWFwaS90d2VldHMvdGltZWxpbmVzL2FwaS1yZWZlcmVuY2UvZ2V0LXVzZXJzLWlkLXR3ZWV0cyk6IFRoZSBlbmRwb2ludCBmb3IgZ2V0dGluZyBUd2VldHMgZnJvbSBhIHBhcnRpY3VsYXIgdXNlciBJRC4NCjMuIFtGaWVsZHMgYW5kIEV4cGFuc2lvbnNdKGh0dHBzOi8vZGV2ZWxvcGVyLnR3aXR0ZXIuY29tL2VuL2RvY3MvdHdpdHRlci1hcGkvZGF0YS1kaWN0aW9uYXJ5L3VzaW5nLWZpZWxkcy1hbmQtZXhwYW5zaW9ucyk6IFRoZSBBUEkgdjIgZGF0YSBvYmplY3RzIGluY2x1ZGUgb25seSBhIHNtYWxsIG51bWJlciBvZiBkZWZhdWx0IGZpZWxkcyB3aGVuIG1ha2luZyBhIHJlcXVlc3QuICBUbyBnZXQgbW9yZSBpbmZvcm1hdGlvbiB5b3Ugd2lsbCBuZWVkIHRvIGluY2x1ZGUgdGhlIGFwcHJvcHJpYXRlIGZpZWxkcyBhbmQgZXhwYW5zaW9ucy4NCjQuIFtGaWVsZHNdKGh0dHBzOi8vZGV2ZWxvcGVyLnR3aXR0ZXIuY29tL2VuL2RvY3MvdHdpdHRlci1hcGkvZmllbGRzKTogRmllbGRzIGFsbG93IHlvdSB0byBzZWxlY3QgdGhlIGRhdGEgdGhhdCB5b3Ugd2FudCBmcm9tIGVhY2ggb2YgdGhlIG9iamVjdHMgaW4geW91ciBlbmRwb2ludCByZXNwb25zZS4gIFRoZXNlIGluY2x1ZGUNCiAgICArIFR3ZWV0IEZpZWxkcw0KICAgICsgVXNlciBGaWVsZHMNCiAgICArIE1lZGlhIEZpZWxkcw0KICAgICsgUG9sbCBGaWVsZHMNCiAgICArIFBsYWNlIEZpZWxkcw0KNS4gW0V4cGFuc2lvbnNdKGh0dHBzOi8vZGV2ZWxvcGVyLnR3aXR0ZXIuY29tL2VuL2RvY3MvdHdpdHRlci1hcGkvZXhwYW5zaW9ucyk6IEV4cGFuc2lvbnMgZXhwYW5kIG9iamVjdHMgcmVmZXJlbmNlZCBpbiB0aGUgcGF5bG9hZC4gIFRoZXNlIGluY2x1ZGUNCiAgICArIFJlZmVyZW5jZWQgVHdlZXRzDQogICAgKyBUaGUgdXNlciBJRCBvZiB3aG8gaXMgYmVpbmcgcmVwbGllZCB0bw0KICAgICsgTWVkaWEgYXR0YWNobWVudHMNCiAgICArIFBvbGwgYXR0YWNobWVudHMNCiAgICArIExvY2F0aW9uIG1ldGFkYXRhDQogICAgKyBEYXRhIG9uIHRoZSB1c2VyIG1lbnRpb25lZCBpbiB0aGUgVHdlZXQNCiAgICArIERhdGEgb24gdGhlIGF1dGhvciBvZiBhIHJlZmVyZW5jZWQgVHdlZXQNCjYuIFtCdWlsZGluZyBRdWVyaWVzXShodHRwczovL2RldmVsb3Blci50d2l0dGVyLmNvbS9lbi9kb2NzL3R3aXR0ZXItYXBpL3R3ZWV0cy9zZWFyY2gvaW50ZWdyYXRlL2J1aWxkLWEtcXVlcnkpOiBIb3cgdG8gYnVpbGQgdXAgYSByZXF1ZXN0IHRvIHJldHVybiBhIHNldCBvZiBoaXN0b3JpY2FsIFR3ZWV0cyB3aGljaCBtYXRjaCB0aGUgcXVlcnkuDQoNCk9uY2Ugd2UgaGF2ZSBkZXRlcm1pbmVkIHdoYXQgd2Ugd2FudCwgd2UgdGhlbiBoYXZlIHRvIGZvcm0gYSBxdWVyeS4gIFRoZSBzdHJ1Y3R1cmUgb2YgYSBxdWVyeSBpcyBhIHNpbXBsZSBsaXN0IGluZGljYXRpbmcgYWxsIG9mIHBhcmFtZXRlcnMgd2Ugd2FudCB0byBwYXNzIGFsb25nIHRvIHRoZSBBUEkuICBGb3IgZXhhbXBsZSwgc3VwcG9zZSB0aGF0IHdlIHdhbnRlZCB0byBnZXQgYmFzaWMgcHJvZmlsZSBpbmZvcm1hdGlvbiBmb3IgSm9lIEJpZGVuJ3MgcGVyc29uYWwgVHdpdHRlciBhY2NvdW50Og0KDQpgYGB7cn0NCnBhcmFtcyA8LSBsaXN0KHVzZXIuZmllbGRzID0gInB1YmxpY19tZXRyaWNzLGRlc2NyaXB0aW9uIiwNCiAgICAgICAgICAgICAgIGV4cGFuc2lvbnMgPSAicGlubmVkX3R3ZWV0X2lkIikNCg0KaGFuZGxlIDwtICdKb2VCaWRlbicNCmBgYA0KDQpXZSBjYW4gdGhlbiBzZXQgdXAgdGhlIHF1ZXJ5IGJ5IGlkZW50aWZ5aW5nIHRoZSBjb3JyZWN0IGVuZHBvaW50IGFuZCBzZW5kaW5nIHRoZSByZXF1ZXN0IHRvIHRoZSBBUEk6DQoNCmBgYHtyfQ0KdXJsX2hhbmRsZSA8LSBzcHJpbnRmKCdodHRwczovL2FwaS50d2l0dGVyLmNvbS8yL3VzZXJzL2J5P3VzZXJuYW1lcz0lcycsIGhhbmRsZSkNCg0KcmVzcG9uc2UgPC0gaHR0cjo6R0VUKHVybCA9IHVybF9oYW5kbGUsDQogICAgICAgICAgICAgICAgICAgICAgaHR0cjo6YWRkX2hlYWRlcnMoLmhlYWRlcnMgPSBoZWFkZXJzKSwNCiAgICAgICAgICAgICAgICAgICAgICBxdWVyeSA9IHBhcmFtcykNCm9iaiA8LSBodHRyOjpjb250ZW50KHJlc3BvbnNlLCBhcyA9ICJ0ZXh0IikNCmBgYA0KDQpSZXNwb25zZXMgZnJvbSB0aGUgQVBJIHRha2UgdGhlIGZvcm0gb2YgYSBKU09OIGZpbGU6DQoNCmBgYHtyfQ0KcHJldHRpZnkob2JqKQ0KYGBgDQoNClRvIGdldCB0aGlzIGludG8gYSBmb3JtYXQgdGhhdCBjYW4gYmUgZWFzaWx5IG1hbmlwdWxhdGVkIHdpdGhpbiBSLCB3ZSBuZWVkIHRvIGZsYXR0ZW4gdGhlIEpTT04gaW50byBhIGRhdGEuZnJhbWU6DQoNCmBgYHtyfQ0KanNvbl9kYXRhIDwtIGZyb21KU09OKG9iaiwgZmxhdHRlbiA9IFRSVUUpICU+JSANCiAgYXMuZGF0YS5mcmFtZSgpDQphc190aWJibGUoanNvbl9kYXRhKQ0KYGBgDQoNCkdyZWF0ISAgSG93IG1pZ2h0IHdlIG1vZGlmeSB0aGUgYWJvdmUgdG8gY29sbGVjdCBUd2VldHMgZmlsdGVyaW5nIGJ5IGtleXdvcmRzIGFuZCBsb2NhdGlvbj8gIEZpcnN0LCB3ZSBuZWVkIHRvIHNlbGVjdCB0aGUgYXBwcm9wcmlhdGUgQVBJIGFuZCBtb2RpZnkgdGhlIHF1ZXJ5IGFjY29yZGluZ2x5LiAgU2luY2UgeW91IG9ubHkgaGF2ZSAiRXNzZW50aWFsIiBhY2Nlc3MsIHdlIGFyZSBsaW1pdGVkIHRvIGxvb2tpbmcgdGhyb3VnaCAicmVjZW50IiBUd2VldHMgcHVibGlzaGVkIG92ZXIgdGhlIHBhc3Qgc2V2ZW4gZGF5cy4gIA0KDQpMZXQncyBzdXBwb3NlIHRoYXQgd2Ugd2FudCB0byBjb2xsZWN0IFR3ZWV0cyBmcm9tIHBlcnNvbnMgaW4gdGhlIFVLIHdobyBUd2VldGVkIGFib3V0IGVpdGhlciBCaWRlbiBvciBUcnVtcCBhbmQgaGF2ZSBnZW9sb2NhdGlvbiBkYXRhIG9uLg0KDQpgYGB7cn0NCmVuZHBvaW50IDwtICdodHRwczovL2FwaS50d2l0dGVyLmNvbS8yL3R3ZWV0cy9zZWFyY2gvcmVjZW50Jw0KDQpwYXJhbXMgPC0gbGlzdChxdWVyeSA9ICIoQmlkZW4gT1IgVHJ1bXApIChwbGFjZV9jb3VudHJ5OkdCIGhhczpnZW8pIiwNCiAgICAgICAgICAgICAgIHR3ZWV0LmZpZWxkcyA9ICJhdXRob3JfaWQsY3JlYXRlZF9hdCxwdWJsaWNfbWV0cmljcyxnZW8iLA0KICAgICAgICAgICAgICAgbWF4X3Jlc3VsdHMgPSAxMDApDQoNCnJlc3BvbnNlIDwtIGh0dHI6OkdFVCh1cmwgPSBlbmRwb2ludCwNCiAgICAgICAgICAgICAgICAgICAgICBodHRyOjphZGRfaGVhZGVycyguaGVhZGVycyA9IGhlYWRlcnMpLA0KICAgICAgICAgICAgICAgICAgICAgIHF1ZXJ5ID0gcGFyYW1zKSAlPiUgDQogICAgICAgICAgICBodHRyOjpjb250ZW50KC4sIGFzID0gInRleHQiKSAlPiUgDQogICAgICAgICAgICBmcm9tSlNPTiguLCBmbGF0dGVuID0gVFJVRSkgJT4lIA0KICAgICAgICAgICAgYXMuZGF0YS5mcmFtZSgpDQoNCmFzX3RpYmJsZShyZXNwb25zZSkNCmBgYA0KSWYgd2Ugd2FudGVkIHRvIGZpbmQgdGhlIGxvY2F0aW9uIG9mIHRoZXNlIHR3ZWV0cywgd2UgY291bGQgdGhlbiBkbyBzb21ldGhpbmcgbGlrZS4uLg0KDQpgYGB7cn0NCg0KZ2V0X2dlbyA8LSBmdW5jdGlvbihwbGFjZV9pZCl7DQogIA0KICBsaW5rIDwtIHBhc3RlMCgiaHR0cHM6Ly9hcGkudHdpdHRlci5jb20vMS4xL2dlby9pZC8iLA0KICAgICAgICAgICAgICAgICAgcGxhY2VfaWQsDQogICAgICAgICAgICAgICAgICAiLmpzb24iKQ0KICBhbGwgPC0gaHR0cjo6R0VUKHVybCA9IGxpbmssDQogICAgICAgICAgICAgICAgICAgaHR0cjo6YWRkX2hlYWRlcnMoLmhlYWRlcnMgPSBoZWFkZXJzKSkgJT4lIA0KICAgICAgICAgICAgICAgaHR0cjo6Y29udGVudCguLCBhcyA9ICJ0ZXh0IikgJT4lIA0KICAgICAgICAgICAgICAgZnJvbUpTT04oLiwgZmxhdHRlbiA9IFRSVUUpDQogIA0KICBvdXQgPC0gZGF0YS5mcmFtZShpZCA9IGFsbCRpZCwNCiAgICAgICAgICAgICAgICAgICAgbmFtZSA9IGFsbCRuYW1lLA0KICAgICAgICAgICAgICAgICAgICBmdWxsX25hbWUgPSBhbGwkZnVsbF9uYW1lLA0KICAgICAgICAgICAgICAgICAgICBjb3VudHJ5ID0gYWxsJGNvdW50cnksDQogICAgICAgICAgICAgICAgICAgIGNvdW50cnlfY29kZSA9IGFsbCRjb3VudHJ5X2NvZGUsDQogICAgICAgICAgICAgICAgICAgIHBsYWNlX3R5cGUgPSBhbGwkcGxhY2VfdHlwZSkNCiAgDQogIG91dA0KICANCn0NCg0KbG9jYXRpb25zIDwtIGxpc3QoKQ0KDQpmb3IoaSBpbiAxOmxlbmd0aCh1bmlxdWUocmVzcG9uc2UkZGF0YS5nZW8ucGxhY2VfaWQpKSl7DQogIA0KICBwbGFjZSA8LSB1bmlxdWUocmVzcG9uc2UkZGF0YS5nZW8ucGxhY2VfaWQpW2ldDQogIA0KICBvdXQgPC0gZ2V0X2dlbyhwbGFjZSkNCiAgDQogIGxvY2F0aW9uc1tbaV1dIDwtIG91dA0KICANCn0NCiAgDQpsb2NzIDwtIGRvLmNhbGwoInJiaW5kIixsb2NhdGlvbnMpDQoNCmxvY3MNCg0KYGBgDQpBbHRob3VnaCB0aGlzIGVuZHBvaW50IGlzIHJhdGUtbGltaXRlZCB0byA3NSByZXF1ZXN0cyBwZXIgMTUtbWludXRlIHdpbmRvdyBhbmQgZG9lcyBub3QgYWNjZXB0IGJhdGNoIHJlcXVlc3RzLg0KDQpBbnl3YXksIHRoYXQncyBjb29sISAgV2hhdCBpZiB3ZSB3YW50IHRvIGdyYWIgYSBidW5jaCBvZiBUd2VldHMgYnkgYSBwYXJ0aWN1bGFyIHVzZXIgaW5zdGVhZD8gIFdpdGggImVzc2VudGlhbCIgYWNjZXNzLCB5b3UgY2FuIGdyYWIgdXAgdG8gdGhlIGxhc3QgMzIwMCBUd2VldHMgZnJvbSBhIHVzZXIuICBBcHBseWluZyB0aGUgcHJpbmNpcGxlcyBmcm9tIGFib3ZlLCBhbmQgcmVhZGluZyBhYm91dCBbcGFnaW5hdGlvbl0oaHR0cHM6Ly9kZXZlbG9wZXIudHdpdHRlci5jb20vZW4vZG9jcy90d2l0dGVyLWFwaS9wYWdpbmF0aW9uKSwgd2UgY2FuIGRldmVsb3AgdGhlIGZvbGxvd2luZyBmdW5jdGlvbjoNCg0KYGBge3J9DQpsYXN0X25fdHdlZXRzIDwtIGZ1bmN0aW9uKGJlYXJlcl90b2tlbiA9ICIiLCB1c2VyX2lkID0gIiIsIG4gPSAxMDAsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHR3ZWV0X2ZpZWxkcyA9IGMoImF0dGFjaG1lbnRzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiY3JlYXRlZF9hdCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImVudGl0aWVzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiaW5fcmVwbHlfdG9fdXNlcl9pZCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInB1YmxpY19tZXRyaWNzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicmVmZXJlbmNlZF90d2VldHMiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzb3VyY2UiKSl7DQogIA0KICBoZWFkZXJzIDwtIGMoYEF1dGhvcml6YXRpb25gID0gc3ByaW50ZignQmVhcmVyICVzJywgYmVhcmVyX3Rva2VuKSkNCiAgDQogICMgQ29udmVydCBVc2VyIElEIGludG8gTnVtZXJpY2FsIElEDQogIA0KICBzcHJpbnRmKCdodHRwczovL2FwaS50d2l0dGVyLmNvbS8yL3VzZXJzL2J5P3VzZXJuYW1lcz0lcycsIHVzZXJfaWQpICU+JSANCiAgICBodHRyOjpHRVQodXJsID0gLiwNCiAgICAgICAgICAgICAgaHR0cjo6YWRkX2hlYWRlcnMoLmhlYWRlcnMgPSBoZWFkZXJzKSwNCiAgICAgICAgICAgICAgcXVlcnkgPSBsaXN0KCkpICU+JSANCiAgICBodHRyOjpjb250ZW50KC4sYXM9InRleHQiKSAlPiUgDQogICAgZnJvbUpTT04oLixmbGF0dGVuID0gVCkgJT4lIA0KICAgIGFzLmRhdGEuZnJhbWUoKSAtPiB0bXANCiAgDQogIG51bV9pZCA8LSB0bXAkZGF0YS5pZA0KDQogICMgRm9yIHRoYXQgdXNlciwgZ3JhYiBtb3N0IHJlY2VudCBuIHR3ZWV0cywgaW4gYmF0Y2hlcyBvZiAxMDANCiAgDQogIGlmKG4gPD0gMTAwKXsNCiAgICByZXF1ZXN0cyA8LSBuDQogIH1lbHNlew0KICAgIHJlcXVlc3RzIDwtIHJlcCgxMDAsZmxvb3Iobi8xMDApKQ0KICAgIGlmKG4gJSUgMTAwICE9IDApew0KICAgICAgcmVxdWVzdHMgPC0gYyhyZXF1ZXN0cywgbiAlJSAxMDApDQogICAgfQ0KICB9DQogIA0KICBuZXh0X3Rva2VuIDwtIE5BDQogIA0KICBhbGwgPC0gbGlzdCgpDQogIA0KICAjIEluaXRpYWxpemUsIGdyYWIgZmlyc3QgcmVzdWx0cyAgDQogIHBhc3RlMCgnaHR0cHM6Ly9hcGkudHdpdHRlci5jb20vMi91c2Vycy8nLG51bV9pZCwnL3R3ZWV0cycpICU+JSANCiAgICBodHRyOjpHRVQodXJsID0gLiwNCiAgICAgICAgICAgICAgaHR0cjo6YWRkX2hlYWRlcnMoLmhlYWRlcnMgPSBoZWFkZXJzKSwNCiAgICAgICAgICAgICAgcXVlcnkgPSBsaXN0KGBtYXhfcmVzdWx0c2AgPSByZXF1ZXN0c1sxXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHR3ZWV0LmZpZWxkcyA9IHBhc3RlKHR3ZWV0X2ZpZWxkcyxjb2xsYXBzZT0iLCIpKSkgJT4lIA0KICAgIGh0dHI6OmNvbnRlbnQoLixhcz0idGV4dCIpICU+JSANCiAgICBmcm9tSlNPTiguLGZsYXR0ZW4gPSBUKSAlPiUgDQogICAgYXMuZGF0YS5mcmFtZSgpIC0+IG91dA0KICAgIA0KICBhbGxbWzFdXSA8LSBvdXQNCiAgDQogIA0KICAjIEZvciBtb3JlIHRoYW4gMTAwLCBuZWVkIHRvIHVzZSBwYWdpbmF0aW9uIHRva2Vucy4NCiAgaWYobGVuZ3RoKHJlcXVlc3RzKSA+PSAyKXsNCiAgICBuZXh0X3Rva2VuWzJdIDwtIHVuaXF1ZShhcy5jaGFyYWN0ZXIoYWxsW1sxXV0kbWV0YS5uZXh0X3Rva2VuKSkNCiAgDQogICAgZm9yKGkgaW4gMjpsZW5ndGgocmVxdWVzdHMpKXsNCiAgICAgIHBhc3RlMCgnaHR0cHM6Ly9hcGkudHdpdHRlci5jb20vMi91c2Vycy8nLG51bV9pZCwnL3R3ZWV0cycpICU+JSANCiAgICAgICAgaHR0cjo6R0VUKHVybCA9IC4sDQogICAgICAgICAgICAgICAgICBodHRyOjphZGRfaGVhZGVycyguaGVhZGVycyA9IGhlYWRlcnMpLA0KICAgICAgICAgICAgICAgICAgcXVlcnkgPSBsaXN0KGBtYXhfcmVzdWx0c2AgPSByZXF1ZXN0c1tpXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0d2VldC5maWVsZHMgPSBwYXN0ZSh0d2VldF9maWVsZHMsY29sbGFwc2U9IiwiKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYWdpbmF0aW9uX3Rva2VuID0gbmV4dF90b2tlbltpXSkpICU+JSANCiAgICAgICAgaHR0cjo6Y29udGVudCguLGFzPSJ0ZXh0IikgJT4lIA0KICAgICAgICBmcm9tSlNPTiguLGZsYXR0ZW4gPSBUKSAlPiUgDQogICAgICAgIGFzLmRhdGEuZnJhbWUoKSAtPiBvdXQNCiAgICANCiAgICAgIGFsbFtbaV1dIDwtIG91dA0KICAgICAgbmV4dF90b2tlbltpICsgMV0gPC0gdW5pcXVlKGFzLmNoYXJhY3RlcihhbGxbW2ldXSRtZXRhLm5leHRfdG9rZW4pKQ0KICAgIH0NCiAgfQ0KICANCiAgZG8uY2FsbCgicmJpbmQuZmlsbCIsYWxsKQ0KICANCn0NCmBgYA0KDQpMZXQncyB0ZXN0IGl0IG91dCENCg0KYGBge3J9DQpvdXQgPC0gbGFzdF9uX3R3ZWV0cyh0d2l0dGVyX2luZm8kYmVhcmVyX3Rva2VuLCJKb2VCaWRlbiIsMzIwMCkNCg0KYXNfdGliYmxlKG91dCkNCmBgYA0KDQpFeGNlbGxlbnQhICBOb3csIGlmIHlvdSBoYWQgYW4gYWNhZGVtaWMgcmVzZWFyY2hlciBhY2NvdW50LCB5b3UgbWlnaHQgd2FudCB0byBkbyBhIHNpbWlsYXIgdGhpbmcgYnV0IHRvIGNvbGxlY3QgdXAgdG8gYWxsIG9mIHRoZSBUd2VldHMgYnkgYSB1c2VyISAgVGhlIGJlbG93IGZ1bmN0aW9uIGRvZXMganVzdCB0aGF0ICh5b3Ugd29uJ3QgYmUgYWJsZSB0byB1c2UgaXQsIGJ1dCB0aGUgY29kZSBpcyBpbGx1c3RyYXRpdmUpOg0KDQpgYGB7cn0NCmdldF9hbGxfdHdlZXRzIDwtIGZ1bmN0aW9uKGJlYXJlcl90b2tlbiA9ICIiLCB1c2VyX2lkID0gIiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICB0d2VldF9maWVsZHMgPSBjKCJhdHRhY2htZW50cyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJjcmVhdGVkX2F0IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImVudGl0aWVzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImluX3JlcGx5X3RvX3VzZXJfaWQiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicHVibGljX21ldHJpY3MiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicmVmZXJlbmNlZF90d2VldHMiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAic291cmNlIikpew0KICAjIEluaXRpYWxpemUsIEdyYWIgZmlyc3QgcmVzdWx0cw0KICANCiAgYWxsIDwtIGxpc3QoKQ0KICANCiAgcGFzdGUwKCdodHRwczovL2FwaS50d2l0dGVyLmNvbS8yL3R3ZWV0cy9zZWFyY2gvYWxsJykgJT4lIA0KICAgIGh0dHI6OkdFVCh1cmwgPSAuLA0KICAgICAgICAgICAgICBodHRyOjphZGRfaGVhZGVycyguaGVhZGVycyA9IGhlYWRlcnMpLA0KICAgICAgICAgICAgICBxdWVyeSA9IGxpc3QocXVlcnkgPSBwYXN0ZTAoImZyb206Iix1c2VyX2lkKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0YXJ0X3RpbWUgPSAiMjAwNi0wMy0yMlQwMDowMDowMFoiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgbWF4X3Jlc3VsdHMgPSA1MDAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICB0d2VldC5maWVsZHMgPSBwYXN0ZSh0d2VldF9maWVsZHMsY29sbGFwc2U9IiwiKQ0KICAgICAgICAgICAgICApKSAlPiUgDQogICAgaHR0cjo6Y29udGVudCguLGFzPSJ0ZXh0IikgJT4lIA0KICAgIGZyb21KU09OKC4sZmxhdHRlbiA9IFQpICU+JSANCiAgICBhcy5kYXRhLmZyYW1lKCkgLT4gb3V0DQogIA0KICBhbGxbWzFdXSA8LSBvdXQNCiAgDQogICMgRm9yIG1vcmUgdGhhbiA1MDAgVHdlZXRzLCBuZWVkIHRvIGxvb3ANCiAgDQogIG54dCA8LSB1bmlxdWUoYXMuY2hhcmFjdGVyKGFsbFtbMV1dJG1ldGEubmV4dF90b2tlbikpDQogIGlmKGlkZW50aWNhbChueHQsY2hhcmFjdGVyKDApKSl7DQogICAgcmV0dXJuKG91dCkNCiAgfWVsc2V7DQogICAgc3RvcCA8LSBGDQogICAgaSA8LSAyDQogIH0NCiAgDQogIHdoaWxlKHN0b3AgPT0gRil7DQogICAgU3lzLnNsZWVwKDIpDQogICAgDQogICAgcGFzdGUwKCdodHRwczovL2FwaS50d2l0dGVyLmNvbS8yL3R3ZWV0cy9zZWFyY2gvYWxsJykgJT4lIA0KICAgICAgaHR0cjo6R0VUKHVybCA9IC4sDQogICAgICAgICAgICAgICAgaHR0cjo6YWRkX2hlYWRlcnMoLmhlYWRlcnMgPSBoZWFkZXJzKSwNCiAgICAgICAgICAgICAgICBxdWVyeSA9IGxpc3QocXVlcnkgPSBwYXN0ZTAoImZyb206Iix1c2VyX2lkKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RhcnRfdGltZSA9ICIyMDA2LTAzLTIyVDAwOjAwOjAwWiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1heF9yZXN1bHRzID0gNTAwLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuZXh0X3Rva2VuID0gbnh0LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0d2VldC5maWVsZHMgPSBwYXN0ZSh0d2VldF9maWVsZHMsY29sbGFwc2U9IiwiKQ0KICAgICAgICAgICAgICAgICkpICU+JSANCiAgICAgIGh0dHI6OmNvbnRlbnQoLixhcz0idGV4dCIpICU+JSANCiAgICAgIGZyb21KU09OKC4sZmxhdHRlbiA9IFQpICU+JSANCiAgICAgIGFzLmRhdGEuZnJhbWUoKSAtPiBvdXQNCiAgICANCiAgICBhbGxbW2ldXSA8LSBvdXQNCiAgICANCiAgICBueHQgPC0gdW5pcXVlKGFzLmNoYXJhY3RlcihhbGxbW2ldXSRtZXRhLm5leHRfdG9rZW4pKQ0KICAgIHN0b3AgPC0gaWRlbnRpY2FsKG54dCxjaGFyYWN0ZXIoMCkpDQogICAgaSA8LSBpICsgMQ0KICAgIA0KICB9DQogIA0KICBkby5jYWxsKCJyYmluZC5maWxsIixhbGwpDQogIA0KfQ0KYGBgDQoNCkxldCdzIHRha2UgaXQgZm9yIGEgc3BpbiENCg0KYGBge3J9DQp0bXAgPC0gZ2V0X2FsbF90d2VldHModHdpdHRlcl9pbmZvJGJlYXJlcl90b2tlbiwgIlBPVFVTIikNCnBsb3QodGFibGUoYXMuRGF0ZSh0bXAkZGF0YS5jcmVhdGVkX2F0KSkpDQpgYGANCg0KDQojIyBFeGVyY2lzZXMNCg0KQmVmb3JlIHdlIGdldCBhaGVhZCBvZiBvdXJzZWx2ZXMsIHdlIHdhbnQgdG8gbWFrZSBzdXJlIHRoYXQgeW91IGhhdmUgZnVuZGFtZW50YWxzIGluIG9yZGVyLiAgRG8gdGhlIGZvbGxvd2luZzoNCg0KV3JpdGUgYSBzY3JpcHQgd2hpY2guLi4NCg0KMS4gU2VsZWN0IGEgVHdpdHRlciBhY2NvdW50IG9mIHlvdXIgY2hvaWNlLiAgRGV0ZXJtaW5lIHRoZSBmb2xsb3dpbmcsIHBlcmhhcHMgYnkgbW9kaWZ5aW5nIHRoZSBmdW5jdGlvbnMgZ2l2ZW4gYWJvdmUgYW5kIHJlZmVyZW5jaW5nIHRoZSBhcHByb3ByaWF0ZSBkb2N1bWVudGF0aW9uOg0KICAgICsgV2hlbiB3YXMgdGhlaXIgYWNjb3VudCBjcmVhdGVkPw0KICAgICsgV2hhdCBhcmUgdGhlaXIgcHVibGljIG1ldHJpY3M/DQogICAgKyBBcmUgdGhleSB2ZXJpZmllZD8NCjIuIE5vdyBjb2xsZWN0IHRoZSBsYXN0IDEwMDAgVHdlZXRzIGJ5IHRoYXQgdXNlciwgaW5jbHVkaW5nIHRoZSBmb2xsb3dpbmcgaW5mb3JtYXRpb246DQogICAgKyBXaGVuIHRoZSBUd2VldCB3YXMgY3JlYXRlZC4gIEFmdGVyIGNvbGxlY3RpbmcsIG1ha2UgYSBwbG90IG9mIHRoZWlyIFR3ZWV0cyBvdmVyIHRpbWUuDQogICAgKyBBdHRhY2htZW50czsgd2hhdCB0eXBlIChpZiBhbnkpIG1lZGlhIGRpZCB0aGV5IHNoYXJlPw0KICAgICsgRW50aXRpZXM7IGRpZCB0aGV5IHVzZSBhbnkgaGFzaHRhZ3M/DQogICAgKyBXaGF0IHNvdXJjZSBoYXZlIHRoZXkgZ2VuZXJhbGx5IHVzZWQgdG8gVHdlZXQ/DQozLiBOb3cgY29sbGVjdCAxMDAwIFR3ZWV0cyB3aXRoIHRoZSBoYXNodGFnICNCbGFja0xpdmVzTWF0dGVyIE9SICNNZVRvbyBmcm9tIHRoZSBwYXN0IDcgZGF5cy4NCiAgICArIFdoYXQgc29ydCBvZiBtZWRpYSBkbyB0aGV5IGhhdmUgYXR0YWNoZWQ/DQogICAgKyBXaGF0IGFyZSB0aGVpciBwdWJsaWMgbWV0cmljcz8NCg0KU2F2ZSBhbmQgc3VibWl0IHlvdXIgd29ya2luZyBSIHNjcmlwdCB0byB0aGUgW0V4ZXJjaXNlL1F1aXogU3VibWlzc2lvbiBMaW5rXShodHRwczovL2Zvcm1zLmdsZS96VHprNllLajdxdmVMUkJNNikgYnkgdGhlIGVuZCBvZiB0aGUgZGF5IChpZGVhbGx5LCBlbmQgb2YgbGFiIHNlc3Npb24hKS4NCg==