Lab 6: Advanced Twitter

Introduction

Today we are going to conclude our lab sessions (except for a full day of open lab) by talking about two more advanced topics for analyzing Twitter data (and text data more generally). Sentiment analysis and topic models.

Sentiment Analysis

Sentiment analysis (also sometimes called opinion mining or emotion AI) is a natural language processing technique to systematically quantify affective states represented by text.

To get started, let’s grab some Tweets using a function from Lab 3. In particular, we are going to compare the most recent 1000 Tweets from AOC’s personal account, AOC’s official account, and MTG’s official account. You won’t be able to run this code on your own without Developer access, but here is how the data was collected:

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

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

last_n_tweets(bearer_token = twitter_info$bearer_token,
              user_id = "AOC",
              n = 1000,
              tweet_fields = c("created_at",
                               "public_metrics",
                               "source")) -> AOC

last_n_tweets(bearer_token = twitter_info$bearer_token,
              user_id = "RepAOC",
              n = 1000,
              tweet_fields = c("created_at",
                               "public_metrics",
                               "source")) -> RepAOC

last_n_tweets(bearer_token = twitter_info$bearer_token,
              user_id = "RepMTG",
              n = 1000,
              tweet_fields = c("created_at",
                               "public_metrics",
                               "source")) -> RepMTG

saveRDS(AOC,"AOC.RDS")
saveRDS(RepAOC, "RepAOC.RDS")
saveRDS(RepMTG, "RepMTG.RDS")

Since I have already collected these for you, we can just read them in and load the required packages for today:

library(dplyr)
library(plyr)
library(stringdist)

AOC <- readRDS(url("https://www.dropbox.com/s/ueamyw0vg85lglp/AOC.RDS?dl=1"))
RepAOC <- readRDS(url("https://www.dropbox.com/s/fq1ug2v2ern9yzz/RepAOC.RDS?dl=1"))
RepMTG <- readRDS(url("https://www.dropbox.com/s/atkz3ib6dm9vhwy/RepMTG.RDS?dl=1"))

Let’s start with AOC’s personal account and take a quick look at what we grabbed:

head(AOC$data.text,10)
##  [1] "RT @thedailybeast: EXCLUSIVE: Twitter is failing to remove 99 percent of hate speech posted by Twitter Blue users, new research has found,…"                                                                                                                                              
##  [2] "RT @Acyn: McCarthy: We might have a child that has no job, no dependents but sitting on the couch, we’re going to encourage that person to…"                                                                                                                                              
##  [3] "FYI there’s a fake account on here impersonating me and going viral. The Twitter CEO has engaged it, boosting visibility.\n\nIt is releasing false policy statements and gaining spread.\n\nI am assessing with my team how to move forward. In the meantime, be careful of what you see."
##  [4] "RT @oneunderscore__: This quote that Elon Musk retweeted is not actually from Voltaire, it's from neo-Nazi and white supremacist Kevin Stro…"                                                                                                                                             
##  [5] "RT @JudiciaryDems: Let's be clear:\n \n-Harlan Crow does not have the authority to claim separation of powers to withhold information\n-We are…"                                                                                                                                          
##  [6] "Thank you to everyone who came to our Memorial Day weekend town hall! We had such a great crowd. 🤗\n\nIt was awesome answering all your questions, discussing the debt limit negotiations, and more.\n\nSee you next month! https://t.co/XdAe13wzSQ"                                     
##  [7] "RT @IndivisibleTeam: If you missed our phonebank with @AOC: \"Contrary to what Kevin McCarthy says, the debt limit is not about raising our…"                                                                                                                                             
##  [8] "RT @JoeBiden: Defaulting on the debt—including trillions incurred under Donald Trump—could mean seniors missing Social Security checks and…"                                                                                                                                              
##  [9] "RT @NoLieWithBTC: If you have student loan debt, you should know this:\n\nEvery single House Republican just voted to overturn President Bide…"                                                                                                                                           
## [10] "RT @jackiekcalmes: Little-noted this wk: While McCarthy was all budget-cutting bravado in debt-limit talks with Biden, the House Appropriat…"

It is fairly clear that some of these Tweets have a “positive” connotation whereas others have a “negative” connotation. How might we systematically identify, across a large corpus, what sort of sentiment each of these texts conveys?

The basic idea underlying sentiment analysis is the following. For each of the texts in our corpus we want to…

  1. Clean the text, usually resulting in word stems
  2. Determine an appropriate dictionary
    • Dictionaries usually contain “stems” and “scores” across “attitudes”
  3. For each cleaned text, convert “stems” into “scores” according to a “rule,” perhaps normalizing
    • Normalization here means that the text “abhor abhor abhor” would be considered just as negative as the text “abhor” despite being three times as long.
    • Alternative “rules” can be as simple as summing up the score of individual word stems
    • The method matters, so you should learn and pay attention to it!

The SentimentAnalysis Package

A number of approaches to sentiment analysis are contained in the appropriately named SentimentAnalysis package and is quite straightforward to run after a bit of cleaning to get rid of those pesky emojis.

library(SentimentAnalysis)

AOC$data.text <- gsub('[^\x01-\x7F]', '', AOC$data.text)

sentiments <- analyzeSentiment(AOC$data.text)
summary(sentiments)
##    WordCount      SentimentGI        NegativityGI      PositivityGI    
##  Min.   : 0.00   Min.   :-0.52000   Min.   :0.00000   Min.   :0.00000  
##  1st Qu.:10.00   1st Qu.:-0.03846   1st Qu.:0.00000   1st Qu.:0.07143  
##  Median :13.00   Median : 0.00000   Median :0.09091   Median :0.14907  
##  Mean   :14.16   Mean   : 0.04405   Mean   :0.11146   Mean   :0.15552  
##  3rd Qu.:17.00   3rd Qu.: 0.14286   3rd Qu.:0.16667   3rd Qu.:0.23077  
##  Max.   :35.00   Max.   : 1.00000   Max.   :0.60000   Max.   :1.00000  
##                  NA's   :1          NA's   :1         NA's   :1        
##   SentimentHE         NegativityHE       PositivityHE     SentimentLM      
##  Min.   :-0.250000   Min.   :0.000000   Min.   :0.0000   Min.   :-0.50000  
##  1st Qu.: 0.000000   1st Qu.:0.000000   1st Qu.:0.0000   1st Qu.:-0.08333  
##  Median : 0.000000   Median :0.000000   Median :0.0000   Median : 0.00000  
##  Mean   : 0.005803   Mean   :0.004601   Mean   :0.0104   Mean   :-0.04076  
##  3rd Qu.: 0.000000   3rd Qu.:0.000000   3rd Qu.:0.0000   3rd Qu.: 0.00000  
##  Max.   : 0.200000   Max.   :0.250000   Max.   :0.2000   Max.   : 0.33333  
##  NA's   :1           NA's   :1          NA's   :1        NA's   :1         
##   NegativityLM      PositivityLM     RatioUncertaintyLM SentimentQDAP     
##  Min.   :0.00000   Min.   :0.00000   Min.   :0.000000   Min.   :-0.60000  
##  1st Qu.:0.00000   1st Qu.:0.00000   1st Qu.:0.000000   1st Qu.: 0.00000  
##  Median :0.03571   Median :0.00000   Median :0.000000   Median : 0.00000  
##  Mean   :0.05781   Mean   :0.01705   Mean   :0.008136   Mean   : 0.04504  
##  3rd Qu.:0.08696   3rd Qu.:0.00000   3rd Qu.:0.000000   3rd Qu.: 0.13793  
##  Max.   :0.50000   Max.   :0.33333   Max.   :0.333333   Max.   : 0.66667  
##  NA's   :1         NA's   :1         NA's   :1          NA's   :1         
##  NegativityQDAP    PositivityQDAP  
##  Min.   :0.00000   Min.   :0.0000  
##  1st Qu.:0.00000   1st Qu.:0.0000  
##  Median :0.06897   Median :0.1111  
##  Mean   :0.07779   Mean   :0.1228  
##  3rd Qu.:0.12500   3rd Qu.:0.1818  
##  Max.   :0.60000   Max.   :0.6667  
##  NA's   :1         NA's   :1

Dictionary methods are pretty quick! Here we can see that, by default, the SentimentAnaysis package gives us the results from 13 different approaches. Here is a quick breakdown:

  1. Dictionaries
    • HE: Positive and negative words according to Henry’s finance-specific dictionary
    • GI: Positive and negative words according to psychological Harvard-IV dictionary.
    • LM: Positive, negative, and uncertainty words according o Loughran-McDonald finance-specific dictionary.
    • QDAP: Hu and Llu (2004) dictionary for opinion features in customer reviews
  2. Rules
    • Negativity: Ratio of words labled negative to total words in document
    • Positivity: Ratio of words labled positive to total words in document
    • Ratio: Ratio of words in dictionary to total words in document
    • Sentiment: Difference between positive and negative counts divided by total words

Here are some glimpses as the dictionaries being used:

GI <- loadDictionaryGI()
data.frame(pos = GI$positiveWords[1:10],
           neg = GI$negativeWords[1:10])
##        pos     neg
## 1     abid abandon
## 2     abil    abat
## 3      abl   abdic
## 4   abound   abhor
## 5   absolv  abject
## 6   absorb  abnorm
## 7  absorpt abolish
## 8    abund  abomin
## 9    acced   abras
## 10 accentu  abrupt
HE <- loadDictionaryHE()
data.frame(pos = HE$positiveWords[1:10],
           neg = HE$negativeWords[1:10])
##           pos        neg
## 1        abov      below
## 2  accomplish   challeng
## 3      achiev     declin
## 4        beat    decreas
## 5        best    depress
## 6      better   deterior
## 7     certain  difficult
## 8   certainti difficulti
## 9     definit disappoint
## 10      deliv       down
LM <- loadDictionaryLM()
data.frame(pos = LM$positiveWords[1:10],
           neg = LM$negativeWords[1:10])
##           pos     neg
## 1         abl abandon
## 2       abund   abdic
## 3     acclaim   aberr
## 4  accomplish    abet
## 5      achiev  abnorm
## 6       adequ abolish
## 7      advanc   abrog
## 8    advantag  abrupt
## 9     allianc  absenc
## 10      assur absente
QDAP <- loadDictionaryQDAP()
data.frame(pos = QDAP$positiveWords[1:10],
           neg = QDAP$negativeWords[1:10])
##           pos         neg
## 1      a plus      abnorm
## 2      abound     abolish
## 3       abund      abomin
## 4      access       abort
## 5     acclaim       abrad
## 6      acclam       abras
## 7     accolad      abrupt
## 8    accommod     abscond
## 9     accomod      absenc
## 10 accomplish absent mind

Let’s take a look at how the GI and QDAP metric look in comparison to each other:

library(ggplot2)

ggplot(sentiments,aes(x=SentimentGI,y=SentimentQDAP)) +
  geom_point()

They are strongly linearly related to each other, but the dictionary used matters for the score! Worth note is that there is a positive relationship between positivity and negativity scores as well:

ggplot(sentiments,aes(x=NegativityGI,y=PositivityGI)) +
  geom_point()
## Warning: Removed 1 rows containing missing values (`geom_point()`).

Let’s take a quick look at a few of the most positive Tweets!

AOC$QDAPpos <- sentiments$PositivityQDAP
AOC %>% 
  arrange(desc(QDAPpos)) %>% 
  dplyr::select(data.text) %>% 
  as_tibble()
## # A tibble: 995 × 1
##    data.text                                                                    
##    <chr>                                                                        
##  1 "@msolurin Thank you for your work! "                                        
##  2 "Wow. A truly incredible breakthrough moment.\n\nThank you @NASA and to ever…
##  3 "RT @jimmy_wales: What Wikipedia did: we stood strong for our principles and…
##  4 "Wow. https://t.co/1iDO2rX8U0"                                               
##  5 "That was fast https://t.co/ROLUIFYsIV"                                      
##  6 "@Fuddmuggler Understandable"                                                
##  7 "@mattduss Thanks!"                                                          
##  8 "@NabilahIslam Congratulations! "                                            
##  9 "RT @KathyHochul: Honored to have Deco's support "                           
## 10 "Thank you to  https://t.co/5LURrPOG6U"                                      
## # … with 985 more rows
AOC$QDAPneg <- sentiments$NegativityQDAP
AOC %>% 
  arrange(desc(QDAPneg)) %>% 
  dplyr::select(data.text) %>% 
  as_tibble()
## # A tibble: 995 × 1
##    data.text                                                                    
##    <chr>                                                                        
##  1 "I have yet to hear a real explanation from any official hesitating to conde…
##  2 "Lets see if he throttles this as negativity too "                           
##  3 "@PabloReports No worries "                                                  
##  4 "Forced pregnancy is a crime against humanity."                              
##  5 "RT @Acyn: Jones: To expel voices of opposition and dissent is a signal of a…
##  6 "RT @justicedems: \"Jordan Neely was killed by public policy. He was killed …
##  7 "If a petty HOA complaint were a person https://t.co/bJVorhQh8Y"             
##  8 "RT @BMWEDIBT: The only chance we had at obtaining sick leave was to pass bo…
##  9 "RT @IndivisibleTeam: If you missed our phonebank with @AOC: \"Contrary to w…
## 10 "RT @radleybalko: Fearmongering works:\n\nOK violent crime rate: 458 per 100…
## # … with 985 more rows

And the top performers using the GI dictionary:

AOC$GIpos <- sentiments$PositivityGI
AOC %>% 
  arrange(desc(GIpos)) %>% 
  dplyr::select(data.text) %>% 
  as_tibble()
## # A tibble: 995 × 1
##    data.text                                                                    
##    <chr>                                                                        
##  1 "BUY*"                                                                       
##  2 "@harikondabolu  I completely and totally understand. This is hilarious"     
##  3 "No matter where you are in the United States, we will build community with …
##  4 "RT @RepEscobar: Today, Congresswoman Escobar was arrested in front of the S…
##  5 "RT @EricLevitz: The sole reason why the GOP is able to demand one-way conce…
##  6 "RT @JaneMayerNYer: Lawmakers have just added a provision to the National De…
##  7 "@Fuddmuggler Understandable"                                                
##  8 "@mattduss Thanks!"                                                          
##  9 "@NabilahIslam Congratulations! "                                            
## 10 "RT @KathyHochul: Honored to have Deco's support "                           
## # … with 985 more rows

And negative:

AOC$GIneg <- sentiments$NegativityGI
AOC %>% 
  arrange(desc(GIneg)) %>% 
  dplyr::select(data.text) %>% 
  as_tibble()
## # A tibble: 995 × 1
##    data.text                                                                    
##    <chr>                                                                        
##  1 "I have yet to hear a real explanation from any official hesitating to conde…
##  2 "Lets see if he throttles this as negativity too "                           
##  3 "@PabloReports No worries "                                                  
##  4 "Forced pregnancy is a crime against humanity."                              
##  5 "RT @equalityAlec: According to FBI crime estimates released today, \"violen…
##  6 "Republicans keep blaming mass shootings on mental health, but then defend t…
##  7 "RT @justicedems: \"Jordan Neely was killed by public policy. He was killed …
##  8 "RT @adamconover: This is why were striking. The studios are trying to turn …
##  9 "RT @housing4allNY: Fighting  to pass #GoodCause eviction is essential. We h…
## 10 "RT @NYTWA: NYC Uber drivers are ON STRIKE!!! Our strike goes until 11:59 pm…
## # … with 985 more rows

While some Tweets appear counter-intuitively placed, the division into positive and negative seems about right! More generally, the sentiment score should give a more balanced impression. Here are the Tweets with the highest GI sentiment:

AOC$GIsentiment <- sentiments$SentimentGI
AOC %>% 
  arrange(desc(GIsentiment)) %>% 
  dplyr::select(data.text) %>% 
  as_tibble()
## # A tibble: 995 × 1
##    data.text                                                                    
##    <chr>                                                                        
##  1 "BUY*"                                                                       
##  2 "@harikondabolu  I completely and totally understand. This is hilarious"     
##  3 "RT @EricLevitz: The sole reason why the GOP is able to demand one-way conce…
##  4 "@Fuddmuggler Understandable"                                                
##  5 "@mattduss Thanks!"                                                          
##  6 "@NabilahIslam Congratulations! "                                            
##  7 "RT @KathyHochul: Honored to have Deco's support "                           
##  8 "Thank you to  https://t.co/5LURrPOG6U"                                      
##  9 "@originalspin Oh thank you! I never knew this and will correct it moving fo…
## 10 "RT @Eve6: Its just funny that as the quality of everything from airlines to…
## # … with 985 more rows

And the lowest:

AOC %>% 
  arrange(GIsentiment) %>% 
  dplyr::select(data.text) %>% 
  as_tibble()
## # A tibble: 995 × 1
##    data.text                                                                    
##    <chr>                                                                        
##  1 "I have yet to hear a real explanation from any official hesitating to conde…
##  2 "Lets see if he throttles this as negativity too "                           
##  3 "@PabloReports No worries "                                                  
##  4 "RT @justicedems: \"Jordan Neely was killed by public policy. He was killed …
##  5 "RT @adamconover: This is why were striking. The studios are trying to turn …
##  6 "RT @NYTWA: NYC Uber drivers are ON STRIKE!!! Our strike goes until 11:59 pm…
##  7 "If a petty HOA complaint were a person https://t.co/bJVorhQh8Y"             
##  8 "RT @akela_lacy: Almost 1/4 of this money was wasted trying to defeat Summer…
##  9 "RT @equalityAlec: According to FBI crime estimates released today, \"violen…
## 10 "RT @IndivisibleTeam: If you missed our phonebank with @AOC: \"Contrary to w…
## # … with 985 more rows

A more sophisticated analysis might include custom dictionaries or custom rules for aggregating the scores to fit the use case and corpus you are working with. In general, however, even the default methods do a good job at sorting out the clearly positive from the clearly negative.

The tidy Approach

To get a better understanding of how sentiment analysis works, and give you the ability to easily create your own custom dictionaries, let’s take a look at the tidy approach to sentiment analysis. Let’s load in the required packages:

library(tm)
library(dplyr)
library(tidytext)
library(textdata)
library(tidyr)

Step 1 of the process is to convert our text into a corpus and then a document-term matrix. For convenience, we will use the canned cleaning abilities of the latter be used. But, as noted in a previous lab, buyer beware when using pre-canned cleaning procedures for text analysis!

AOC <- AOC[which(nchar(AOC$data.text) > 15),]
AOC$data.text <- gsub("amp","",AOC$data.text)

corp <- VCorpus(VectorSource(AOC$data.text))
dt <- DocumentTermMatrix(corp, control = list(removePunctuation = T,
                                              tolower = T,
                                              removeNumbers = T,
                                              stopwords = T,
                                              stemming = T))
tidy_dt <- tidy(dt)
tidy_dt
## # A tibble: 13,417 × 3
##    document term     count
##    <chr>    <chr>    <dbl>
##  1 1        blue         1
##  2 1        exclus       1
##  3 1        fail         1
##  4 1        found        1
##  5 1        hate         1
##  6 1        new          1
##  7 1        percent      1
##  8 1        post         1
##  9 1        remov        1
## 10 1        research     1
## # … with 13,407 more rows

Let’s load in the bing dictionary for use:

bing_dict <- get_sentiments("bing")
bing_dict
## # A tibble: 6,786 × 2
##    word        sentiment
##    <chr>       <chr>    
##  1 2-faces     negative 
##  2 abnormal    negative 
##  3 abolish     negative 
##  4 abominable  negative 
##  5 abominably  negative 
##  6 abominate   negative 
##  7 abomination negative 
##  8 abort       negative 
##  9 aborted     negative 
## 10 aborts      negative 
## # … with 6,776 more rows

We can then merge these two datasets together:

tidy_dt %>% 
  inner_join(bing_dict,by=c(term="word")) -> tidy_sentiment

as_tibble(tidy_sentiment)
## # A tibble: 1,334 × 4
##    document term  count sentiment
##    <chr>    <chr> <dbl> <chr>    
##  1 1        fail      1 negative 
##  2 1        hate      1 negative 
##  3 3        boost     1 positive 
##  4 3        fake      1 negative 
##  5 3        gain      1 positive 
##  6 5        clear     1 positive 
##  7 6        debt      1 negative 
##  8 6        great     1 positive 
##  9 6        limit     1 negative 
## 10 6        thank     1 positive 
## # … with 1,324 more rows

To aggregate these sentiments into a document-score, we can do the following:

tidy_sentiment %>% 
  dplyr::count(document, sentiment, wt = count) %>% 
  spread(sentiment, n, fill = 0) %>% 
  mutate(sentiment = positive - negative) %>% 
  arrange(sentiment) -> document_scores

as_tibble(document_scores)
## # A tibble: 643 × 4
##    document negative positive sentiment
##    <chr>       <dbl>    <dbl>     <dbl>
##  1 128            11        0       -11
##  2 221             6        0        -6
##  3 51              5        0        -5
##  4 687             5        0        -5
##  5 794             6        1        -5
##  6 895             5        0        -5
##  7 115             4        0        -4
##  8 135             5        1        -4
##  9 222             5        1        -4
## 10 251             5        1        -4
## # … with 633 more rows

Let’s take a look at the most negative tweets from this method:

AOC$document <- rownames(AOC)
AOC <- merge(AOC,document_scores,by="document",all.x=T)

AOC %>% 
  arrange(sentiment) %>% 
  dplyr::select(data.text) %>% 
  as_tibble()
## # A tibble: 985 × 1
##    data.text                                                                    
##    <chr>                                                                        
##  1 "I represent Rikers. I cannot tell you how many times Ive heard from both CO…
##  2 "RT @RyanElward:  UPS RANK AND FILE \n\nThe @Teamsters are introducing a nat…
##  3 "@JCColtin @CityAndStateNY Is this the new who youd want to have a beer with…
##  4 "For full context, one of Greenes companions in that video was part of the v…
##  5 "I remember how folks stepped up to help Texans when you left them cold and …
##  6 "80%+ of rapes go unreported to police. Should those be treated as false too…
##  7 "Our country criminalizes poverty and homelessness while making it impossibl…
##  8 "RT @chrislhayes: What on *earth* is this statement?"                        
##  9 "RT @JohnTeufelNYC: NYPD officers who work for five years will now make appr…
## 10 "RT @ManhattanDA: https://t.co/wZ4X6SVDLy"                                   
## # … with 975 more rows

And the most positive:

AOC %>% 
  arrange(desc(sentiment)) %>% 
  dplyr::select(data.text) %>% 
  as_tibble()
## # A tibble: 985 × 1
##    data.text                                                                    
##    <chr>                                                                        
##  1 "What is it with people randomly blaming the mere existence of others for th…
##  2 "RT @MaxKennerly: amazon wanted $3.5 billion in benefits from NY to build th…
##  3 "Lets goooooo  https://t.co/2f1CMrBWCc"                                      
##  4 "The coalition united in this push represents a significant breakthrough.\n\…
##  5 "RT @JakeSherman: https://t.co/26w22gLDb0"                                   
##  6 "You sound insecure. As you should be.\n\nYour attempt to seize bodily auton…
##  7 "Jokes aside, this is setting the stage for major potential harm when a natu…
##  8 "RT @nycDSA: Last week we rallied with @AOC @Gonzalez4NY @JuliaCarmel__ @Zoh…
##  9 "@ebottcher Im so sorry youre going through this. If you or your staff need …
## 10 "RT @RollingStone: Bad Bunnys highly anticipated music video for El Apagn in…
## # … with 975 more rows

Not bad, but some obvious room for improvement! One of issues with simple sentiment analysis is that tweets like the following have a lot of positive words which are negated.

AOC %>% 
  arrange(desc(sentiment)) %>% 
  dplyr::select(data.text) %>% 
  .[1,] %>% 
  as.character() %>% 
  strsplit(.," ") %>% 
  unlist() -> words

words
##  [1] "What"                    "is"                     
##  [3] "it"                      "with"                   
##  [5] "people"                  "randomly"               
##  [7] "blaming"                 "the"                    
##  [9] "mere"                    "existence"              
## [11] "of"                      "others"                 
## [13] "for"                     "their"                  
## [15] "own"                     "descent"                
## [17] "into"                    "embracing"              
## [19] "neo-nazism?"             "Like"                   
## [21] "girl"                    "you"                    
## [23] "did"                     "that"                   
## [25] "all"                     "on"                     
## [27] "your"                    "own.\n\nUnless"         
## [29] "her"                     "suggestion"             
## [31] "here"                    "is"                     
## [33] "she"                     "started"                
## [35] "endorsing"               "great"                  
## [37] "replacement"             "theory"                 
## [39] "because"                 "she"                    
## [41] "couldnt"                 "treat"                  
## [43] "me"                      "like"                   
## [45] "the"                     "help"                   
## [47] "https://t.co/5dC2JsKoNY"
bing_dict[which(bing_dict$word %in% words),]
## # A tibble: 4 × 2
##   word      sentiment
##   <chr>     <chr>    
## 1 endorsing positive 
## 2 great     positive 
## 3 like      positive 
## 4 randomly  negative

So that’s why it was scored positively! Observing this might make you create your own dictionary where, for example, terms like “neo-nazism” etc are treated negatively. More generally, simple dictionary methods such as the above cannot pick up on complex negative connotations such as those held in this tweet.

Machine Learned Dictionaries

An alternative approach might be to use a statistical model trained on labeled data and used to predict onto the rest of the dataset. To illustrate this, I hand coded 100 randomly selected Tweets as being positive (1), negative (-1), or neutral/unclear (0).

set.seed(1234)

inds <- sample(1:nrow(AOC),100)

training <- AOC[inds,c("document","data.text")]
predict <- AOC[-inds,]

training$data.text[1:6]
## [1] "RT @RepAOC: \"I had a member of the Republican caucus threaten my life and the Republican caucus rewarded him with one of the most prestigio"                                                                                                                                                                        
## [2] "Does anyone else miss BBM or is that just me  https://t.co/XqU3GMCRmN"                                                                                                                                                                                                                                               
## [3] "Now were talking! Time for people to see a real, forceful push for it. Use the bully pulpit. We need more. https://t.co/dZ1qhdu8iM"                                                                                                                                                                                  
## [4] "@bilalfarooqui If that angers you, maybe you can try comparing it to an NYC teachers salary. They make $50k less under this deal, have higher ed requirements many must go into debt to finance, and arent eligible for overtime."                                                                                   
## [5] "Lmao at a billionaire earnestly trying to sell people on the idea that free speech is actually a $8/mo subscription plan"                                                                                                                                                                                            
## [6] "We are witnessing a judicial coup in process.\n\nIf the President and Congress do not restrain the Court now, the Court is signaling they will come for the Presidential election next.\n\nAll our leaders - regardless of party - must recognize this Constitutional crisis for what it is. https://t.co/DzoIh4n08D"
# training$document <- as.character(training$document)
# training$data.text <- as.character(training$data.text)
# write.csv(training,"training.csv")

The first of these tweets is negative, the second and third positive, the fourth negative, the fifth and sixth negative, and so on. Here is how I coded the entire set:

coded <- read.csv("https://www.dropbox.com/s/01er5ypnmtc6o43/training_coded.csv?dl=1")
table(coded$score)
## 
## -1  0  1 
## 48 25 27

Let’s look at some positive tweets:

head(coded$data.text[which(coded$score == 1)])
## [1] "Does anyone else miss BBM or is that just me  https://t.co/XqU3GMCRmN"                                                                      
## [2] "Now were talking! Time for people to see a real, forceful push for it. Use the bully pulpit. We need more. https://t.co/dZ1qhdu8iM"         
## [3] "RT @librarycongress: Hearing @lizzo play some of the Library's priceless antique instruments on Monday was such a gift, and we were honored"
## [4] "Shout out to NY Assemblymember @yuhline who has more info here: https://t.co/ZQbA93bUZE"                                                    
## [5] "@Welcome2theBX @KathyHochul Thank you! We are securing similar pedestrian investments for Westchester Square on the Bronx side as well!"    
## [6] "RT @billscher: Progressives with two heartland wins tonight: Wisconsin and Chicago."

Some negative tweets:

head(coded$data.text[which(coded$score == -1)])
## [1] "RT @RepAOC: \"I had a member of the Republican caucus threaten my life and the Republican caucus rewarded him with one of the most prestigio"                                                                                                                                                                        
## [2] "@bilalfarooqui If that angers you, maybe you can try comparing it to an NYC teachers salary. They make $50k less under this deal, have higher ed requirements many must go into debt to finance, and arent eligible for overtime."                                                                                   
## [3] "Lmao at a billionaire earnestly trying to sell people on the idea that free speech is actually a $8/mo subscription plan"                                                                                                                                                                                            
## [4] "We are witnessing a judicial coup in process.\n\nIf the President and Congress do not restrain the Court now, the Court is signaling they will come for the Presidential election next.\n\nAll our leaders - regardless of party - must recognize this Constitutional crisis for what it is. https://t.co/DzoIh4n08D"
## [5] "Reminder: This is who the Republican Party elects + elevates to positions of power. This is how they act in the halls of Congress, and this the exle they set for acolytes to follow.\n\nThese people want media to both sides fascism. Dont fall for it. \nhttps://t.co/9Y0GV2Revw"                                 
## [6] "RT @ryanlcooper: they're never going to forgive this women for being braver than them https://t.co/JsqJDhpruO"

And some tweets I couldn’t classify:

head(coded$data.text[which(coded$score == 0)])
## [1] "RT @ABCNewsLive: .@AOC joins @LinseyDavis following the Republican response to Pres. Bidens #SOTU: \"It's unsurprising that Gov. Huckabee S"                                                                                                                                      
## [2] "When you encounter someone with a misperception, share your story. Your personal experiences and facts can powerfully dismantle common misperceptions and propaganda in the people around you. Its one of the most powerful tools we have - far more powerful than any TV pundit."
## [3] "Its almost midnight. Welcome to political asmr Twitter \n\n NY Early Voting starts next Saturday Oct 29th  \n\n California Early Ballots have hit mailboxes, send yours back this week https://t.co/GyTRJaigt2"                                                                   
## [4] "@Fuddmuggler Understandable"                                                                                                                                                                                                                                                      
## [5] "RT @POTUS: Lets be clear about what changes Republicans in Congress want to make to Medicare and Social Security.\n\nThey want to raise th"                                                                                                                                       
## [6] "RT @katie_honan: Queens safer than Nassau, would you look at that"

What we are going to do is use words as features in a predictive model. We won’t do anything fancy here, like splitting into a train and test set or cross-validating the results, but such would likely improve the model. Let’s add the scores back onto our main data and then clean the tweets into usable format. For easily replicability, we will fist collect all our cleaning tasks into one function, the output of which will be a tidy document term matrix:

clean_text <- function(text_vector){
  
  text_vector %>% 
    gsub('\\n','\\\\n',.) %>% 
    gsub("(www|http:|https:)([^(\\s)]*)","",.) %>% 
    gsub('\\\\n','',.) %>% 
    gsub("[[:punct:] ]+",' ',.) %>% 
    gsub('[[:digit:]]+', '', .) %>% 
    gsub('[^\x01-\x7F]', '', .) %>% 
    tolower() -> text_vector

  stopwords <- c("amp",stopwords("English"))
  sw2 <- stopwords
  sw2 <- gsub("'"," ",sw2)
  stopwords <- unique(stopwords,sw2)

  for(i in stopwords){
    text_vector <- gsub(paste0("\\b",i,"\\b"), "", text_vector)
  }

  corpus <- VCorpus(VectorSource(text_vector))
  dt <- DocumentTermMatrix(corpus)
  tidy_dt <- tidy(dt)

  tidy_dt$term <- stemDocument(tidy_dt$term)

  tidy_dt %>% 
    dplyr::group_by(document,term) %>% 
    dplyr::summarise(count = sum(count, na.rm=T)) -> tidy_dt

  unique_terms <- unique(tidy_dt$term)
  sdm <- stringdistmatrix(unique_terms, unique_terms, method = "jw", useNames = TRUE)

  diag(sdm) <- NA
  inds <- which(apply(sdm,2,min, na.rm=T) <= 0.05)

  min_dist <- apply(sdm,2,min,na.rm=T)
  thresh <- min_dist < 0.05
  sub <- sdm[thresh,thresh]

  old_terms <- colnames(sub)

  out <- list()
  for(i in old_terms){
    out[[i]] <- rownames(sub)[which.min(sub[,i])]
  }

  dict <- data.frame(term1 = old_terms,
                     term2 = unlist(out))

  dict$first_term <- nchar(as.character(dict$term1)) < nchar(as.character(dict$term2))

  dict$replacement <- ifelse(dict$first_term,
                            as.character(dict$term1),
                            as.character(dict$term2))

  dict$term1 <- as.character(dict$term1)
  dict$term2 <- as.character(dict$term2)
  dict$replacement <- as.character(dict$replacement)

  for(i in 1:nrow(tidy_dt)){
  
    term <- tidy_dt[i,"term"]
    
    if(term %in% dict$term1){
    
      tidy_dt$term[i] <- dict$replacement[match(term,dict$term1)]
    
    }
  
  }

  tidy_dt %>% 
    dplyr::group_by(document,term) %>% 
    dplyr::summarise(count = sum(count)) %>% 
    arrange(as.numeric(as.character(document))) -> tidy_dt
  
  tidy_dt
  
}

Let’s clean the text and get it ready to go, focusing only on those observations for which we have a positive or negative sentiment determined. Details put to the side, to fit the model we are going to use what is called ridge regression to estimate the association between the terms in these tweets and the our positive/negative association score.

set.seed(1234)
inds <- sample(1:nrow(AOC),100)

training <- AOC[inds,]
training$score <- coded$score
training %>%
  filter(score == 1 | score == -1) -> training

tidy_dt <- clean_text(training$data.text)

colnames(tidy_dt)[1] <- "Document"

tidy_dt$count <- as.numeric(tidy_dt$count)

tidy_dt %>% 
  pivot_wider(id_cols = Document,
              names_from = term,
              values_from = count,
              values_fill = 0) -> wide

wide$score <- training$score

library(glmnet)

X <- as.matrix(wide[,-c(1,ncol(wide))])
y <- wide$score

lambdas <- 10^seq(2, -3, by = -.1)
cv_ridge <- cv.glmnet(X, y, alpha = 0, lambda = lambdas)
optimal_lambda <- cv_ridge$lambda.min
ridge_reg <- glmnet(X, y, alpha = 0, family = 'gaussian', lambda = optimal_lambda)

Here is the main result – essentially a data-driven dictionary!

custom_dict <- data.frame(term = rownames(ridge_reg$beta),
                          association = ridge_reg$beta[,1])
rownames(custom_dict) <- NULL

head(custom_dict,10)
##          term association
## 1      caucus -0.01895338
## 2        life -0.03790213
## 3      member -0.03148998
## 4         one -0.01719916
## 5   prestigio -0.03789975
## 6      repaoc -0.03723530
## 7  republican -0.02141854
## 8      reward -0.03789196
## 9    threaten -0.03788965
## 10      anyon  0.07088130

Let’s look at the most negative terms:

custom_dict %>% 
  arrange(association) %>% 
  head()
##              term association
## 1 katiemcfaddenni -0.05412858
## 2          nooooo -0.05412755
## 3          govern -0.05320666
## 4             sir -0.05320317
## 5       incorrect -0.04733786
## 6        elonmusk -0.04733737

And now the most positive:

custom_dict %>% 
  arrange(desc(association)) %>% 
  head()
##           term association
## 1      goooooo  0.08279192
## 2       sunday  0.07975051
## 3        happi  0.07974974
## 4  zoandbehold  0.07789266
## 5      capitol  0.07789195
## 6 boldprogress  0.07479448

To make this useful for the rest of the data, we need to first clean it as before.

AOC_clean <- clean_text(AOC$data.text)
## `summarise()` has grouped output by 'document'. You can override using the
## `.groups` argument.
## `summarise()` has grouped output by 'document'. You can override using the
## `.groups` argument.

Now let’s merge our dictionary onto this

AOC_clean <- merge(AOC_clean,custom_dict,by="term",all.x=T)
summary(AOC_clean)
##      term             document             count        association    
##  Length:13247       Length:13247       Min.   :1.000   Min.   :-0.054  
##  Class :character   Class :character   1st Qu.:1.000   1st Qu.:-0.030  
##  Mode  :character   Mode  :character   Median :1.000   Median :-0.017  
##                                        Mean   :1.044   Mean   :-0.005  
##                                        3rd Qu.:1.000   3rd Qu.: 0.013  
##                                        Max.   :5.000   Max.   : 0.083  
##                                                        NA's   :6854

Note that there are a lot of terms for which we have no association! This is because the terms did not appear in our training dataset and so we were unable to learn their association with tweets of a particular sentiment; an unsurprsing state of affairs given our small training size and the limited vocabulary therein. Let’s code these as zero’s for the time being and aggregate the scores to the tweet level.

AOC_clean$association <- ifelse(is.na(AOC_clean$association),0,AOC_clean$association)

AOC_clean %>% 
  group_by(document) %>% 
  dplyr::summarise(custom_sentiment = sum(count * association)) -> custom_scores

AOC$document_fix <- 1:nrow(AOC)

AOC <- merge(AOC,custom_scores,by.x="document_fix",by.y="document",all.x=T)
summary(AOC$custom_sentiment)
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max.     NA's 
## -0.54276 -0.09858 -0.03108 -0.03705  0.02567  0.74462       13

Let’s see how we did! Let’s start by looking at the negative Tweets

AOC %>% 
  arrange(custom_sentiment) %>% 
  select(data.text) %>% 
  head()
##                                                                                                                                                                                                                                                                                                              data.text
## 1                              Tyre Nichols should be alive. Charges alone arent justice. Change is.\n\nAt least 1,176 people were killed by law enforcement last year - a record. Billions in trainings, body cams, and reforms havent stopped it. In fact, its gotten worse. We must grow out of this cycle together
## 2                           https://t.co/TCSPMIaOlb \n\nAfter leading the party to a catastrophic ballot measure loss that wouldve saved Dem House seats, the party chair (Jacobs) compared a Black woman Dem nominee to the KKK. He was protected.\n\nLast nights underperformance is a consequence of that decision.
## 3                             Trying to advance police reporting as the standard for belief denies the overwhelming reality of sexual assault, rape, &; child/domestic abuse &; strengthens a system that protects violators &; silences victims.\n\nIts also one exle why criminalization and justice arent synonyms.
## 4 Just a few months ago I literally had to explain to Republican members of Congress how periods work.\n\nTheir complete and utter incompetence is now killing women and pregnant people across the US.\n\nThere remains no legitimate grounding or basis to force birth in the United States. https://t.co/KWay0Enguy
## 5                            Lastly, many moderate dems + leaders made it very clear that our help was not welcome nor wanted. Despite our many, many offers. Yet found ways to try to help from afar. So for them to blame us for respecting their approach in their districts is laughable. \n\nTake some ownership.
## 6                              It is appalling how so many take advantage of headlines re: crime for an obsolete tough on crime political, media, &; budgetary gain, but when a public murder happens that reinforces existing power structures, those same forces rush to exonerate&;look the other way. We shouldnt.

Now the most positive:

AOC %>% 
  arrange(desc(custom_sentiment)) %>% 
  select(data.text) %>% 
  head()
##                                                                                                                                                                                                                                                                                                            data.text
## 1                      Thank you for calling attention to my pet project of making New York State a global leader in combating climate change and creating tons of good, high-paying jobs for people in the process. Im proud of it! \n\nWhats your pet project? Being a hater for a living? https://t.co/VkpBLQyNdu
## 2 The last time we stood with @Teamsters Local 202, we stared down a national food crisis over resistance to a $1 raise.\n\nBack then, railroad workers stood with us. They turned trains around to not cross a picket line.\n\nWe won then, and we can win now. \nLets get these sick days  https://t.co/YAUFIKRawD
## 3                              Shout out to Massachusetts and the people of Marthas Vineyard for showing the world what the best of America looks like  \n\nIts unsurprising that they also send some of the best to Congress, like @ewarren, @EdMarkey, @AyannaPressley, @RepMcGovern, @RepKClark and so many more!
## 4                                                                                                                                                                            @Welcome2theBX @KathyHochul Thank you! We are securing similar pedestrian investments for Westchester Square on the Bronx side as well!
## 5                                                                                                                                                                      RT @MattGertz: Great news everyone, "America's Crime Crisis" (trademark Fox News) is over!\n\nAll it took was Election Day eliminating the pe
## 6                                                                                                                                                                       RT @therecount: Rep. @AOC (D-NY), speaking to a rowdy crowd, praises the Stand Up To Violence program for reducing Bronx crime:\n\nWe reduce

Pretty cool, but a lot of improvements could be made. For example, by increasing the size of the training set we would get better estimates of the association between words and our subjective positive/negative sentiment assessment of the tweet as a whole. We might use a more sophisticated methodology for predicting the scores, be more selective of the terms we include in the model, etc etc.

Topic Models

Topic models are a natural language processing technique in which a statistical model is used to discover abstract “topics” that occur in a collection of documents. Similar to our exploration of PCA, we would expect documents that speak to a particular topic to use more similar words than those that don’t. A classic example is that we would expect the terms “dog” and “bone” to occur in documents about dogs more than “cat” and “meow,” which are more likely to occur in documents about cats. Unlike PCA, documents will be given probabilities of assignment into a variety of topics which allows us to reflect the notion that a single document might contain multiple topics.

Putting details to the side, let’s take a look at one of the most common varieties of topic model: Latent Dirichlet allocation. Let’s start with a simple example using the AOC data where we fit only three topics to our text. We start by cleaning as usual, if in a somewhat hackish way.

library(topicmodels)

AOC_clean <- clean_text(AOC$data.text)

AOC_clean %>% 
  group_by(document) %>% 
  dplyr::summarise(clean_text = paste(term, collapse = " ")) -> AOC_reduced

corp <- VCorpus(VectorSource(AOC_reduced$clean_text))
dt <- DocumentTermMatrix(corp)

m1 <- LDA(dt, k = 3,
          control = list(seed = 1234))

Fitting the model is the easy part! The rest of the analysis involves exploring and interpreting the model to determine which topics were found and if they are meaningful.

To get started, lets’ take a look at the per-topic-per-word probabilities estimated by the model.

tidy(m1, matrix = "beta")
## # A tibble: 11,610 × 3
##    topic term               beta
##    <int> <chr>             <dbl>
##  1     1 aaronnarraph 0.0000873 
##  2     2 aaronnarraph 0.00000121
##  3     3 aaronnarraph 0.000139  
##  4     1 abandon      0.000238  
##  5     2 abandon      0.000270  
##  6     3 abandon      0.000173  
##  7     1 abbott       0.0000721 
##  8     2 abbott       0.000147  
##  9     3 abbott       0.00000755
## 10     1 abc          0.000119  
## # … with 11,600 more rows

Note that each term has three rows, each corresponding to one of the estimated topics. The beta column is the estimated probability from being generated from that particular topic. We can visualize the top words included in topics by doing the following:

tidy(m1, matrix = "beta") %>% 
  group_by(topic) %>% 
  slice_max(beta,n=10) %>% 
  ungroup() %>% 
  arrange(topic,-beta) %>% 
  mutate(term = reorder_within(term, beta, topic)) %>% 
  ggplot(aes(x=beta, y= term, fill=factor(topic))) + 
    geom_col(show.legend = FALSE) +
    facet_wrap(~topic, scales = "free") +
    scale_y_reordered()

Cool! The first topic latches on to things like herself, power and justice. The second category looks like it deals with republicans and voting. The last seems to refer to people and the house.

One of the most difficult preliminary tasks when fitting topic models is first determining the number of topics to fit. A good first-cut solution to this problem is to estimate a variety of models and then choose that model which provides the best fit to the data. For our purposes, we can use the log-likelihood values saved in the fitted objects to choose a model which has enough, but not too many, topics by calculating AIC values.

First we fit the models, using a bit of parallel processing to speed things up…

library(pbapply)
library(parallel)

cl <- makeCluster(detectCores())
clusterExport(cl,"dt")
clusterEvalQ(cl,library(topicmodels))
## [[1]]
## [1] "topicmodels" "stats"       "graphics"    "grDevices"   "utils"      
## [6] "datasets"    "methods"     "base"       
## 
## [[2]]
## [1] "topicmodels" "stats"       "graphics"    "grDevices"   "utils"      
## [6] "datasets"    "methods"     "base"       
## 
## [[3]]
## [1] "topicmodels" "stats"       "graphics"    "grDevices"   "utils"      
## [6] "datasets"    "methods"     "base"       
## 
## [[4]]
## [1] "topicmodels" "stats"       "graphics"    "grDevices"   "utils"      
## [6] "datasets"    "methods"     "base"       
## 
## [[5]]
## [1] "topicmodels" "stats"       "graphics"    "grDevices"   "utils"      
## [6] "datasets"    "methods"     "base"       
## 
## [[6]]
## [1] "topicmodels" "stats"       "graphics"    "grDevices"   "utils"      
## [6] "datasets"    "methods"     "base"       
## 
## [[7]]
## [1] "topicmodels" "stats"       "graphics"    "grDevices"   "utils"      
## [6] "datasets"    "methods"     "base"       
## 
## [[8]]
## [1] "topicmodels" "stats"       "graphics"    "grDevices"   "utils"      
## [6] "datasets"    "methods"     "base"
mods <- pblapply(1:20, function(x)LDA(dt, k = x + 1,
                                      control = list(seed = 1234)), cl=cl)
stopCluster(cl)

To evaluate which model to use, we will use the perplexity of the model. Discussing the mathematical details of this metric is outside the realm of our discussion, but the metric relates to how well a model predicts the data.

perplex <- sapply(mods,function(x)perplexity(x))

plot(perplex)

We see a HUGE dip around 11 topics, so let’s take a look at that!

The results of the model are the following:

opt_mod <- mods[[10]]

tidy(opt_mod, matrix = "beta") %>% 
  group_by(topic) %>% 
  slice_max(beta,n=10) %>% 
  ungroup() %>% 
  arrange(topic,-beta) %>% 
  mutate(term = reorder_within(term, beta, topic)) %>% 
  ggplot(aes(x=beta, y= term, fill=factor(topic))) + 
    geom_col(show.legend = FALSE) +
    facet_wrap(~topic, scales = "free") +
    scale_y_reordered()

That’s not bad! We could likely improve performance further by doing additional cleaning prior to estimating the topic models, perhaps using a custom dictionary to remove non-political words. We might also use a different criteria for selecting the number of topics, increase the number of Tweets analyzed, etc.

But now that we have a decent topic model, we might want to go ahead and associate Tweets with their relevant topics. This will help us go further to determine which texts are associated with which topics, and will likely help us further determine their meaning. To do this, we need to extract another parameter from the fitted model:

tidy(opt_mod, matrix = "gamma")
## # A tibble: 10,692 × 3
##    document topic   gamma
##    <chr>    <int>   <dbl>
##  1 1            1 0.00233
##  2 2            1 0.00271
##  3 3            1 0.0913 
##  4 4            1 0.00157
##  5 5            1 0.0245 
##  6 6            1 0.00143
##  7 7            1 0.00132
##  8 8            1 0.00137
##  9 9            1 0.00251
## 10 10           1 0.00251
## # … with 10,682 more rows

Each of these values is an estimated proportion of words from that document that are generated from that topic. Many of these appear to be quite narrow in scope, which makes sense given the nature of Tweets, but a few look like they are a mix of topics. Let’s take a look at a few Tweets that rank highly on each topic. Here are the top 5 for topic 2:

tidy(opt_mod, matrix = "gamma") %>% 
  filter(topic == 2) %>% 
  slice_max(gamma, n = 5) %>% 
  dplyr::select(document) %>% 
  as.vector() %>% 
  unlist() %>% 
  as.numeric() %>% 
  AOC_reduced[.,"document"] %>% 
  as.vector() %>% 
  unlist() %>% 
  as.character() %>% 
  as.numeric() %>% 
  AOC[.,"data.text"]
## [1] "to confront the Courts structure (and core gerontocracy problem of lifetime appointments) via public appeal. While he did not succeed, that check came from the ppl &; Congress, NOT scotus.\n\nThe ruling is Roe, but the crisis is democracy. Leaders must share specific plans for both"                                            
## [2] "We cannot allow Supreme Court nominees lying and/or misleading the Senate under oath to go unanswered.\n\nBoth GOP &; Dem Senators stated SCOTUS justices misled them. This cannot be accepted as precedent.\n\nDoing so erodes rule of law, delegitimizes the court, and imperils democracy. https://t.co/yZW6BKnqFG"                 
## [3] "Republicans love to ask what would happen if the right-wing harassed someone in restaurant?? as if they havent been doing that since Day 1.\n\nThese are their own tweets from 2019!\n\nSo the answer of what happens when its a Dem is: nothing. 0 sympathy for hypocritical whiners. https://t.co/p9ECQUUS71 https://t.co/QZwdAr5tuz"
## [4] "RT @SawyerHackett: Since taking office, Kyrsten Sinema has voted twice for measures with filibuster carve-outsto raise the debt ceiling an"                                                                                                                                                                                            
## [5] "RT @nowthisnews: 'I believe lying under oath is an impeachable offense'  Days after SCOTUS overturned Roe v. Wade, AOC said justices must"

What about topic 9?

tidy(opt_mod, matrix = "gamma") %>% 
  filter(topic == 9) %>% 
  slice_max(gamma, n = 5) %>% 
  dplyr::select(document) %>% 
  as.vector() %>% 
  unlist() %>% 
  as.numeric() %>% 
  AOC_reduced[.,"document"] %>% 
  as.vector() %>% 
  unlist() %>% 
  as.character() %>% 
  as.numeric() %>% 
  AOC[.,"data.text"]
## [1] "The President &; Dem leaders can no longer get away with familiar tactics of committees and studies to avoid tackling our crises head-on anymore:\n\n- Restrain judicial review\n- Open clinics on federal lands\n- Court expansion\n- Expand Fed access/awareness of pill abortions\n- etc"                    
## [2] "Jokes aside, this is setting the stage for major potential harm when a natural disaster hits and no one knows what agencies, reporters, or outlets are real.\n\nNot long ago we had major flash floods. We had to mobilize trusted info fast to save lives. Today just made that harder https://t.co/bAG8ayBTa6"
## [3] "- Restrain judicial review\n- Expand the court\n- Clinics on federal lands\n- Expand education and access to Plan C\n- Repeal Hyde\n- Hold floor votes codifying Griswold, Obergefell, Lawrence, Loving, etc\n- Vote on Escobars bill protecting clinics\n\nWe can do it!\nWe can at least TRY"                 
## [4] "Actually thats not quite true! Hyde amdt prevents Fed gov from financing a clinic itself, but does NOT necessarily prevent the Fed gov from leasing land to an independent clinic.\n\nThis is why we shouldnt dismiss ideas out of hand, but try to thoroughly explore + investigate. https://t.co/XldXvwqP8E"  
## [5] "@HacknerTyler @ryangrim Perfectly reasonable. This is the best we could do w/ the hand we were dealt - WH sprung this on us &; we had a window of &lt;24h to secure sick leave when they had the votes locked to pass w/ no changes. Tanking wasnt an option bc of GOP votes, we moved to keep sick leave alive"

Not bad! Some improvements could obviously be made to the processing of the text to get more politically relevant results, but as an exploratory tool for answering “what topics might exist, and what terms are associated with them?” unsupervised methods like topic modeling are hard to beat!

Bonus Code

As promised, here is some code for accessing the Twitter volume stream.

library(curl)

stream_tweets_v2 <- function(bearer_token, max_seconds, max_tweets){
  
  # Set up endpoint and connection
  h <- new_handle()
  handle_setheaders(h,"Authorization" = paste0("Bearer ",bearer_token))
  endpoint <- "https://api.twitter.com/2/tweets/sample/stream"
  con <- curl(endpoint, handle = h)
  
  # Set up termination conditions
  max_time <- Sys.time() + max_seconds
  
  if(max_tweets <= 100){
    requests <- max_tweets
  }else{
    requests <- c(rep(100,floor(max_tweets/100)), max_tweets %% 100)
  }
  
  max_iter <- length(requests)
  now <- Sys.time()
  iter <- 1
  
  # Collect Tweets
  dumps <- list()
  
  open(con)
  while( (max_time > now) & (max_iter >= iter) ){
    
    dumps[[iter]] <- readLines(con,n=requests[iter])
    
    iter <- iter + 1
    now <- Sys.time()
    
  }
  
  # Close connection and return 
  
  close(con)
  
  all <- unlist(dumps)
  out <- stream_in(textConnection(all),flatten = T)
  out
  
}



# Test output (requires bearer token!)

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

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

test <- stream_tweets_v2(twitter_info$bearer_token,10,1000)
as_tibble(test)

Imaging what you would have to go through to process that!

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. With the RepAOC Tweets given above…
    • Conduct a sentiment analysis. Which 10 Tweets are the most positive and negative?
    • What are the optimal number of topics? Interpret them.
  2. With the RepMTG Tweets given above…
    • Conduct a sentiment analysis. Which 10 Tweets are the most positive and negative?
    • What are the optimal number of topics? Interpret them.
  3. Collect data from either Google News or Reddit on at least five distinct search terms of your choice.
    • Conduct a sentiment analysis. Which 10 texts are the most positive and negative?
    • Fit a topic model, as above, to this corpus. Are you able to distinguish the topics?

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

LS0tDQp0aXRsZTogIiINCmF1dGhvcjogIkNocmlzdG9waGVyIFNjaHdhcnoiDQpwYWdlczoNCiAgZXh0cmE6IHRydWUNCm91dHB1dDogDQogIGh0bWxfZG9jdW1lbnQ6DQogICAgdG9jOiB0cnVlDQogICAgDQogICAgdG9jX2RlcHRoOiAzDQogICAgdG9jX2Zsb2F0OiB0cnVlDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KLS0tDQoNCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQ0KYGBgDQoNCiMgTGFiIDY6IEFkdmFuY2VkIFR3aXR0ZXINCg0KIyMgSW50cm9kdWN0aW9uDQoNClRvZGF5IHdlIGFyZSBnb2luZyB0byBjb25jbHVkZSBvdXIgbGFiIHNlc3Npb25zIChleGNlcHQgZm9yIGEgZnVsbCBkYXkgb2Ygb3BlbiBsYWIpIGJ5IHRhbGtpbmcgYWJvdXQgdHdvIG1vcmUgYWR2YW5jZWQgdG9waWNzIGZvciBhbmFseXppbmcgVHdpdHRlciBkYXRhIChhbmQgdGV4dCBkYXRhIG1vcmUgZ2VuZXJhbGx5KS4gIFNlbnRpbWVudCBhbmFseXNpcyBhbmQgdG9waWMgbW9kZWxzLg0KDQojIyBTZW50aW1lbnQgQW5hbHlzaXMNCg0KU2VudGltZW50IGFuYWx5c2lzIChhbHNvIHNvbWV0aW1lcyBjYWxsZWQgb3BpbmlvbiBtaW5pbmcgb3IgZW1vdGlvbiBBSSkgaXMgYSBuYXR1cmFsIGxhbmd1YWdlIHByb2Nlc3NpbmcgdGVjaG5pcXVlIHRvIHN5c3RlbWF0aWNhbGx5IHF1YW50aWZ5IGFmZmVjdGl2ZSBzdGF0ZXMgcmVwcmVzZW50ZWQgYnkgdGV4dC4NCg0KVG8gZ2V0IHN0YXJ0ZWQsIGxldCdzIGdyYWIgc29tZSBUd2VldHMgdXNpbmcgYSBmdW5jdGlvbiBmcm9tIExhYiAzLiAgSW4gcGFydGljdWxhciwgd2UgYXJlIGdvaW5nIHRvIGNvbXBhcmUgdGhlIG1vc3QgcmVjZW50IDEwMDAgVHdlZXRzIGZyb20gQU9DJ3MgcGVyc29uYWwgYWNjb3VudCwgQU9DJ3Mgb2ZmaWNpYWwgYWNjb3VudCwgYW5kIE1URydzIG9mZmljaWFsIGFjY291bnQuICBZb3Ugd29uJ3QgYmUgYWJsZSB0byBydW4gdGhpcyBjb2RlIG9uIHlvdXIgb3duIHdpdGhvdXQgRGV2ZWxvcGVyIGFjY2VzcywgYnV0IGhlcmUgaXMgaG93IHRoZSBkYXRhIHdhcyBjb2xsZWN0ZWQ6DQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0UsIGV2YWwgPSBGQUxTRX0NCndkIDwtICJEOi9Ud2l0dGVyIg0Kc2V0d2Qod2QpDQoNCnR3aXR0ZXJfaW5mbyA8LSByZWFkLmNzdigidHdpdHRlcl9pbmZvMi5jc3YiLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHN0cmluZ3NBc0ZhY3RvcnMgPSBGKQ0KDQpsYXN0X25fdHdlZXRzKGJlYXJlcl90b2tlbiA9IHR3aXR0ZXJfaW5mbyRiZWFyZXJfdG9rZW4sDQogICAgICAgICAgICAgIHVzZXJfaWQgPSAiQU9DIiwNCiAgICAgICAgICAgICAgbiA9IDEwMDAsDQogICAgICAgICAgICAgIHR3ZWV0X2ZpZWxkcyA9IGMoImNyZWF0ZWRfYXQiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwdWJsaWNfbWV0cmljcyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNvdXJjZSIpKSAtPiBBT0MNCg0KbGFzdF9uX3R3ZWV0cyhiZWFyZXJfdG9rZW4gPSB0d2l0dGVyX2luZm8kYmVhcmVyX3Rva2VuLA0KICAgICAgICAgICAgICB1c2VyX2lkID0gIlJlcEFPQyIsDQogICAgICAgICAgICAgIG4gPSAxMDAwLA0KICAgICAgICAgICAgICB0d2VldF9maWVsZHMgPSBjKCJjcmVhdGVkX2F0IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicHVibGljX21ldHJpY3MiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzb3VyY2UiKSkgLT4gUmVwQU9DDQoNCmxhc3Rfbl90d2VldHMoYmVhcmVyX3Rva2VuID0gdHdpdHRlcl9pbmZvJGJlYXJlcl90b2tlbiwNCiAgICAgICAgICAgICAgdXNlcl9pZCA9ICJSZXBNVEciLA0KICAgICAgICAgICAgICBuID0gMTAwMCwNCiAgICAgICAgICAgICAgdHdlZXRfZmllbGRzID0gYygiY3JlYXRlZF9hdCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInB1YmxpY19tZXRyaWNzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAic291cmNlIikpIC0+IFJlcE1URw0KDQpzYXZlUkRTKEFPQywiQU9DLlJEUyIpDQpzYXZlUkRTKFJlcEFPQywgIlJlcEFPQy5SRFMiKQ0Kc2F2ZVJEUyhSZXBNVEcsICJSZXBNVEcuUkRTIikNCg0KYGBgDQoNClNpbmNlIEkgaGF2ZSBhbHJlYWR5IGNvbGxlY3RlZCB0aGVzZSBmb3IgeW91LCB3ZSBjYW4ganVzdCByZWFkIHRoZW0gaW4gYW5kIGxvYWQgdGhlIHJlcXVpcmVkIHBhY2thZ2VzIGZvciB0b2RheToNCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KGRwbHlyKQ0KbGlicmFyeShwbHlyKQ0KbGlicmFyeShzdHJpbmdkaXN0KQ0KDQpBT0MgPC0gcmVhZFJEUyh1cmwoImh0dHBzOi8vd3d3LmRyb3Bib3guY29tL3MvdWVhbXl3MHZnODVsZ2xwL0FPQy5SRFM/ZGw9MSIpKQ0KUmVwQU9DIDwtIHJlYWRSRFModXJsKCJodHRwczovL3d3dy5kcm9wYm94LmNvbS9zL2ZxMXVnMnYyZXJuOXl6ei9SZXBBT0MuUkRTP2RsPTEiKSkNClJlcE1URyA8LSByZWFkUkRTKHVybCgiaHR0cHM6Ly93d3cuZHJvcGJveC5jb20vcy9hdGt6M2liNmRtOXZod3kvUmVwTVRHLlJEUz9kbD0xIikpDQpgYGANCg0KTGV0J3Mgc3RhcnQgd2l0aCBBT0MncyBwZXJzb25hbCBhY2NvdW50IGFuZCB0YWtlIGEgcXVpY2sgbG9vayBhdCB3aGF0IHdlIGdyYWJiZWQ6DQoNCmBgYHtyfQ0KaGVhZChBT0MkZGF0YS50ZXh0LDEwKQ0KYGBgDQoNCkl0IGlzIGZhaXJseSBjbGVhciB0aGF0IHNvbWUgb2YgdGhlc2UgVHdlZXRzIGhhdmUgYSAicG9zaXRpdmUiIGNvbm5vdGF0aW9uIHdoZXJlYXMgb3RoZXJzIGhhdmUgYSAibmVnYXRpdmUiIGNvbm5vdGF0aW9uLiAgSG93IG1pZ2h0IHdlIHN5c3RlbWF0aWNhbGx5IGlkZW50aWZ5LCBhY3Jvc3MgYSBsYXJnZSBjb3JwdXMsIHdoYXQgc29ydCBvZiBzZW50aW1lbnQgZWFjaCBvZiB0aGVzZSB0ZXh0cyBjb252ZXlzPw0KDQpUaGUgYmFzaWMgaWRlYSB1bmRlcmx5aW5nIHNlbnRpbWVudCBhbmFseXNpcyBpcyB0aGUgZm9sbG93aW5nLiAgRm9yIGVhY2ggb2YgdGhlIHRleHRzIGluIG91ciBjb3JwdXMgd2Ugd2FudCB0by4uLg0KDQoxLiBDbGVhbiB0aGUgdGV4dCwgdXN1YWxseSByZXN1bHRpbmcgaW4gd29yZCBzdGVtcw0KMi4gRGV0ZXJtaW5lIGFuIGFwcHJvcHJpYXRlIGRpY3Rpb25hcnkNCiAgICArIERpY3Rpb25hcmllcyB1c3VhbGx5IGNvbnRhaW4gInN0ZW1zIiBhbmQgInNjb3JlcyIgYWNyb3NzICJhdHRpdHVkZXMiDQozLiBGb3IgZWFjaCBjbGVhbmVkIHRleHQsIGNvbnZlcnQgInN0ZW1zIiBpbnRvICJzY29yZXMiIGFjY29yZGluZyB0byBhICJydWxlLCIgcGVyaGFwcyBub3JtYWxpemluZw0KICAgICsgTm9ybWFsaXphdGlvbiBoZXJlIG1lYW5zIHRoYXQgdGhlIHRleHQgImFiaG9yIGFiaG9yIGFiaG9yIiB3b3VsZCBiZSBjb25zaWRlcmVkIGp1c3QgYXMgbmVnYXRpdmUgYXMgdGhlIHRleHQgImFiaG9yIiBkZXNwaXRlIGJlaW5nIHRocmVlIHRpbWVzIGFzIGxvbmcuDQogICAgKyBBbHRlcm5hdGl2ZSAicnVsZXMiIGNhbiBiZSBhcyBzaW1wbGUgYXMgc3VtbWluZyB1cCB0aGUgc2NvcmUgb2YgaW5kaXZpZHVhbCB3b3JkIHN0ZW1zDQogICAgKyBUaGUgbWV0aG9kIG1hdHRlcnMsIHNvIHlvdSBzaG91bGQgbGVhcm4gYW5kIHBheSBhdHRlbnRpb24gdG8gaXQhDQogICAgDQogICAgDQojIyMgVGhlIFNlbnRpbWVudEFuYWx5c2lzIFBhY2thZ2UgICAgDQoNCkEgbnVtYmVyIG9mIGFwcHJvYWNoZXMgdG8gc2VudGltZW50IGFuYWx5c2lzIGFyZSBjb250YWluZWQgaW4gdGhlIGFwcHJvcHJpYXRlbHkgbmFtZWQgU2VudGltZW50QW5hbHlzaXMgcGFja2FnZSBhbmQgaXMgcXVpdGUgc3RyYWlnaHRmb3J3YXJkIHRvIHJ1biBhZnRlciBhIGJpdCBvZiBjbGVhbmluZyB0byBnZXQgcmlkIG9mIHRob3NlIHBlc2t5IGVtb2ppcy4NCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KFNlbnRpbWVudEFuYWx5c2lzKQ0KDQpBT0MkZGF0YS50ZXh0IDwtIGdzdWIoJ1teXHgwMS1ceDdGXScsICcnLCBBT0MkZGF0YS50ZXh0KQ0KDQpzZW50aW1lbnRzIDwtIGFuYWx5emVTZW50aW1lbnQoQU9DJGRhdGEudGV4dCkNCnN1bW1hcnkoc2VudGltZW50cykNCmBgYA0KDQpEaWN0aW9uYXJ5IG1ldGhvZHMgYXJlIHByZXR0eSBxdWljayEgIEhlcmUgd2UgY2FuIHNlZSB0aGF0LCBieSBkZWZhdWx0LCB0aGUgU2VudGltZW50QW5heXNpcyBwYWNrYWdlIGdpdmVzIHVzIHRoZSByZXN1bHRzIGZyb20gMTMgZGlmZmVyZW50IGFwcHJvYWNoZXMuICBIZXJlIGlzIGEgcXVpY2sgYnJlYWtkb3duOg0KDQoxLiBEaWN0aW9uYXJpZXMNCiAgICArIEhFOiBQb3NpdGl2ZSBhbmQgbmVnYXRpdmUgd29yZHMgYWNjb3JkaW5nIHRvIEhlbnJ5J3MgZmluYW5jZS1zcGVjaWZpYyBkaWN0aW9uYXJ5DQogICAgKyBHSTogUG9zaXRpdmUgYW5kIG5lZ2F0aXZlIHdvcmRzIGFjY29yZGluZyB0byBwc3ljaG9sb2dpY2FsIEhhcnZhcmQtSVYgZGljdGlvbmFyeS4NCiAgICArIExNOiBQb3NpdGl2ZSwgbmVnYXRpdmUsIGFuZCB1bmNlcnRhaW50eSB3b3JkcyBhY2NvcmRpbmcgbyBMb3VnaHJhbi1NY0RvbmFsZCBmaW5hbmNlLXNwZWNpZmljIGRpY3Rpb25hcnkuDQogICAgKyBRREFQOiBIdSBhbmQgTGx1ICgyMDA0KSBkaWN0aW9uYXJ5IGZvciBvcGluaW9uIGZlYXR1cmVzIGluIGN1c3RvbWVyIHJldmlld3MNCjIuIFJ1bGVzDQogICAgKyBOZWdhdGl2aXR5OiBSYXRpbyBvZiB3b3JkcyBsYWJsZWQgbmVnYXRpdmUgdG8gdG90YWwgd29yZHMgaW4gZG9jdW1lbnQNCiAgICArIFBvc2l0aXZpdHk6IFJhdGlvIG9mIHdvcmRzIGxhYmxlZCBwb3NpdGl2ZSB0byB0b3RhbCB3b3JkcyBpbiBkb2N1bWVudA0KICAgICsgUmF0aW86IFJhdGlvIG9mIHdvcmRzIGluIGRpY3Rpb25hcnkgdG8gdG90YWwgd29yZHMgaW4gZG9jdW1lbnQNCiAgICArIFNlbnRpbWVudDogRGlmZmVyZW5jZSBiZXR3ZWVuIHBvc2l0aXZlIGFuZCBuZWdhdGl2ZSBjb3VudHMgZGl2aWRlZCBieSB0b3RhbCB3b3Jkcw0KICAgIA0KSGVyZSBhcmUgc29tZSBnbGltcHNlcyBhcyB0aGUgZGljdGlvbmFyaWVzIGJlaW5nIHVzZWQ6DQogICAgDQpgYGB7cn0NCkdJIDwtIGxvYWREaWN0aW9uYXJ5R0koKQ0KZGF0YS5mcmFtZShwb3MgPSBHSSRwb3NpdGl2ZVdvcmRzWzE6MTBdLA0KICAgICAgICAgICBuZWcgPSBHSSRuZWdhdGl2ZVdvcmRzWzE6MTBdKQ0KYGBgDQoNCmBgYHtyfQ0KSEUgPC0gbG9hZERpY3Rpb25hcnlIRSgpDQpkYXRhLmZyYW1lKHBvcyA9IEhFJHBvc2l0aXZlV29yZHNbMToxMF0sDQogICAgICAgICAgIG5lZyA9IEhFJG5lZ2F0aXZlV29yZHNbMToxMF0pDQpgYGANCg0KYGBge3J9DQpMTSA8LSBsb2FkRGljdGlvbmFyeUxNKCkNCmRhdGEuZnJhbWUocG9zID0gTE0kcG9zaXRpdmVXb3Jkc1sxOjEwXSwNCiAgICAgICAgICAgbmVnID0gTE0kbmVnYXRpdmVXb3Jkc1sxOjEwXSkNCmBgYA0KDQpgYGB7cn0NClFEQVAgPC0gbG9hZERpY3Rpb25hcnlRREFQKCkNCmRhdGEuZnJhbWUocG9zID0gUURBUCRwb3NpdGl2ZVdvcmRzWzE6MTBdLA0KICAgICAgICAgICBuZWcgPSBRREFQJG5lZ2F0aXZlV29yZHNbMToxMF0pDQpgYGANCg0KTGV0J3MgdGFrZSBhIGxvb2sgYXQgaG93IHRoZSBHSSBhbmQgUURBUCBtZXRyaWMgbG9vayBpbiBjb21wYXJpc29uIHRvIGVhY2ggb3RoZXI6DQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShnZ3Bsb3QyKQ0KDQpnZ3Bsb3Qoc2VudGltZW50cyxhZXMoeD1TZW50aW1lbnRHSSx5PVNlbnRpbWVudFFEQVApKSArDQogIGdlb21fcG9pbnQoKQ0KYGBgDQoNClRoZXkgYXJlIHN0cm9uZ2x5IGxpbmVhcmx5IHJlbGF0ZWQgdG8gZWFjaCBvdGhlciwgYnV0IHRoZSBkaWN0aW9uYXJ5IHVzZWQgbWF0dGVycyBmb3IgdGhlIHNjb3JlISAgV29ydGggbm90ZSBpcyB0aGF0IHRoZXJlIGlzIGEgcG9zaXRpdmUgcmVsYXRpb25zaGlwIGJldHdlZW4gcG9zaXRpdml0eSBhbmQgbmVnYXRpdml0eSBzY29yZXMgYXMgd2VsbDoNCg0KYGBge3J9DQpnZ3Bsb3Qoc2VudGltZW50cyxhZXMoeD1OZWdhdGl2aXR5R0kseT1Qb3NpdGl2aXR5R0kpKSArDQogIGdlb21fcG9pbnQoKQ0KYGBgDQoNCkxldCdzIHRha2UgYSBxdWljayBsb29rIGF0IGEgZmV3IG9mIHRoZSBtb3N0IHBvc2l0aXZlIFR3ZWV0cyENCg0KYGBge3J9DQpBT0MkUURBUHBvcyA8LSBzZW50aW1lbnRzJFBvc2l0aXZpdHlRREFQDQpBT0MgJT4lIA0KICBhcnJhbmdlKGRlc2MoUURBUHBvcykpICU+JSANCiAgZHBseXI6OnNlbGVjdChkYXRhLnRleHQpICU+JSANCiAgYXNfdGliYmxlKCkNCmBgYA0KDQpgYGB7cn0NCkFPQyRRREFQbmVnIDwtIHNlbnRpbWVudHMkTmVnYXRpdml0eVFEQVANCkFPQyAlPiUgDQogIGFycmFuZ2UoZGVzYyhRREFQbmVnKSkgJT4lIA0KICBkcGx5cjo6c2VsZWN0KGRhdGEudGV4dCkgJT4lIA0KICBhc190aWJibGUoKQ0KYGBgDQoNCkFuZCB0aGUgdG9wIHBlcmZvcm1lcnMgdXNpbmcgdGhlIEdJIGRpY3Rpb25hcnk6DQoNCmBgYHtyfQ0KQU9DJEdJcG9zIDwtIHNlbnRpbWVudHMkUG9zaXRpdml0eUdJDQpBT0MgJT4lIA0KICBhcnJhbmdlKGRlc2MoR0lwb3MpKSAlPiUgDQogIGRwbHlyOjpzZWxlY3QoZGF0YS50ZXh0KSAlPiUgDQogIGFzX3RpYmJsZSgpDQpgYGANCg0KQW5kIG5lZ2F0aXZlOg0KDQpgYGB7cn0NCkFPQyRHSW5lZyA8LSBzZW50aW1lbnRzJE5lZ2F0aXZpdHlHSQ0KQU9DICU+JSANCiAgYXJyYW5nZShkZXNjKEdJbmVnKSkgJT4lIA0KICBkcGx5cjo6c2VsZWN0KGRhdGEudGV4dCkgJT4lIA0KICBhc190aWJibGUoKQ0KYGBgDQoNCldoaWxlIHNvbWUgVHdlZXRzIGFwcGVhciBjb3VudGVyLWludHVpdGl2ZWx5IHBsYWNlZCwgdGhlIGRpdmlzaW9uIGludG8gcG9zaXRpdmUgYW5kIG5lZ2F0aXZlIHNlZW1zIGFib3V0IHJpZ2h0ISAgTW9yZSBnZW5lcmFsbHksIHRoZSBzZW50aW1lbnQgc2NvcmUgc2hvdWxkIGdpdmUgYSBtb3JlIGJhbGFuY2VkIGltcHJlc3Npb24uICBIZXJlIGFyZSB0aGUgVHdlZXRzIHdpdGggdGhlIGhpZ2hlc3QgR0kgc2VudGltZW50Og0KDQpgYGB7cn0NCkFPQyRHSXNlbnRpbWVudCA8LSBzZW50aW1lbnRzJFNlbnRpbWVudEdJDQpBT0MgJT4lIA0KICBhcnJhbmdlKGRlc2MoR0lzZW50aW1lbnQpKSAlPiUgDQogIGRwbHlyOjpzZWxlY3QoZGF0YS50ZXh0KSAlPiUgDQogIGFzX3RpYmJsZSgpDQpgYGANCg0KQW5kIHRoZSBsb3dlc3Q6DQoNCmBgYHtyfQ0KQU9DICU+JSANCiAgYXJyYW5nZShHSXNlbnRpbWVudCkgJT4lIA0KICBkcGx5cjo6c2VsZWN0KGRhdGEudGV4dCkgJT4lIA0KICBhc190aWJibGUoKQ0KYGBgDQoNCkEgbW9yZSBzb3BoaXN0aWNhdGVkIGFuYWx5c2lzIG1pZ2h0IGluY2x1ZGUgY3VzdG9tIGRpY3Rpb25hcmllcyBvciBjdXN0b20gcnVsZXMgZm9yIGFnZ3JlZ2F0aW5nIHRoZSBzY29yZXMgdG8gZml0IHRoZSB1c2UgY2FzZSBhbmQgY29ycHVzIHlvdSBhcmUgd29ya2luZyB3aXRoLiAgSW4gZ2VuZXJhbCwgaG93ZXZlciwgZXZlbiB0aGUgZGVmYXVsdCBtZXRob2RzIGRvIGEgZ29vZCBqb2IgYXQgc29ydGluZyBvdXQgdGhlIGNsZWFybHkgcG9zaXRpdmUgZnJvbSB0aGUgY2xlYXJseSBuZWdhdGl2ZS4NCg0KIyMjIFRoZSB0aWR5IEFwcHJvYWNoDQoNClRvIGdldCBhIGJldHRlciB1bmRlcnN0YW5kaW5nIG9mIGhvdyBzZW50aW1lbnQgYW5hbHlzaXMgd29ya3MsIGFuZCBnaXZlIHlvdSB0aGUgYWJpbGl0eSB0byBlYXNpbHkgY3JlYXRlIHlvdXIgb3duIGN1c3RvbSBkaWN0aW9uYXJpZXMsIGxldCdzIHRha2UgYSBsb29rIGF0IHRoZSB0aWR5IGFwcHJvYWNoIHRvIHNlbnRpbWVudCBhbmFseXNpcy4gIExldCdzIGxvYWQgaW4gdGhlIHJlcXVpcmVkIHBhY2thZ2VzOg0KDQpgYGB7ciB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KbGlicmFyeSh0bSkNCmxpYnJhcnkoZHBseXIpDQpsaWJyYXJ5KHRpZHl0ZXh0KQ0KbGlicmFyeSh0ZXh0ZGF0YSkNCmxpYnJhcnkodGlkeXIpDQpgYGANCg0KU3RlcCAxIG9mIHRoZSBwcm9jZXNzIGlzIHRvIGNvbnZlcnQgb3VyIHRleHQgaW50byBhIGNvcnB1cyBhbmQgdGhlbiBhIGRvY3VtZW50LXRlcm0gbWF0cml4LiAgRm9yIGNvbnZlbmllbmNlLCB3ZSB3aWxsIHVzZSB0aGUgY2FubmVkIGNsZWFuaW5nIGFiaWxpdGllcyBvZiB0aGUgbGF0dGVyIGJlIHVzZWQuICBCdXQsIGFzIG5vdGVkIGluIGEgcHJldmlvdXMgbGFiLCBidXllciBiZXdhcmUgd2hlbiB1c2luZyBwcmUtY2FubmVkIGNsZWFuaW5nIHByb2NlZHVyZXMgZm9yIHRleHQgYW5hbHlzaXMhDQoNCmBgYHtyfQ0KDQpBT0MgPC0gQU9DW3doaWNoKG5jaGFyKEFPQyRkYXRhLnRleHQpID4gMTUpLF0NCkFPQyRkYXRhLnRleHQgPC0gZ3N1YigiYW1wIiwiIixBT0MkZGF0YS50ZXh0KQ0KDQpjb3JwIDwtIFZDb3JwdXMoVmVjdG9yU291cmNlKEFPQyRkYXRhLnRleHQpKQ0KZHQgPC0gRG9jdW1lbnRUZXJtTWF0cml4KGNvcnAsIGNvbnRyb2wgPSBsaXN0KHJlbW92ZVB1bmN0dWF0aW9uID0gVCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0b2xvd2VyID0gVCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZW1vdmVOdW1iZXJzID0gVCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdG9wd29yZHMgPSBULA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZW1taW5nID0gVCkpDQp0aWR5X2R0IDwtIHRpZHkoZHQpDQp0aWR5X2R0DQpgYGANCg0KTGV0J3MgbG9hZCBpbiB0aGUgYmluZyBkaWN0aW9uYXJ5IGZvciB1c2U6DQoNCmBgYHtyfQ0KYmluZ19kaWN0IDwtIGdldF9zZW50aW1lbnRzKCJiaW5nIikNCmJpbmdfZGljdA0KYGBgDQoNCldlIGNhbiB0aGVuIG1lcmdlIHRoZXNlIHR3byBkYXRhc2V0cyB0b2dldGhlcjoNCg0KYGBge3J9DQp0aWR5X2R0ICU+JSANCiAgaW5uZXJfam9pbihiaW5nX2RpY3QsYnk9Yyh0ZXJtPSJ3b3JkIikpIC0+IHRpZHlfc2VudGltZW50DQoNCmFzX3RpYmJsZSh0aWR5X3NlbnRpbWVudCkNCmBgYA0KDQpUbyBhZ2dyZWdhdGUgdGhlc2Ugc2VudGltZW50cyBpbnRvIGEgZG9jdW1lbnQtc2NvcmUsIHdlIGNhbiBkbyB0aGUgZm9sbG93aW5nOg0KDQpgYGB7cn0NCnRpZHlfc2VudGltZW50ICU+JSANCiAgZHBseXI6OmNvdW50KGRvY3VtZW50LCBzZW50aW1lbnQsIHd0ID0gY291bnQpICU+JSANCiAgc3ByZWFkKHNlbnRpbWVudCwgbiwgZmlsbCA9IDApICU+JSANCiAgbXV0YXRlKHNlbnRpbWVudCA9IHBvc2l0aXZlIC0gbmVnYXRpdmUpICU+JSANCiAgYXJyYW5nZShzZW50aW1lbnQpIC0+IGRvY3VtZW50X3Njb3Jlcw0KDQphc190aWJibGUoZG9jdW1lbnRfc2NvcmVzKQ0KYGBgDQoNCkxldCdzIHRha2UgYSBsb29rIGF0IHRoZSBtb3N0IG5lZ2F0aXZlIHR3ZWV0cyBmcm9tIHRoaXMgbWV0aG9kOg0KDQpgYGB7cn0NCkFPQyRkb2N1bWVudCA8LSByb3duYW1lcyhBT0MpDQpBT0MgPC0gbWVyZ2UoQU9DLGRvY3VtZW50X3Njb3JlcyxieT0iZG9jdW1lbnQiLGFsbC54PVQpDQoNCkFPQyAlPiUgDQogIGFycmFuZ2Uoc2VudGltZW50KSAlPiUgDQogIGRwbHlyOjpzZWxlY3QoZGF0YS50ZXh0KSAlPiUgDQogIGFzX3RpYmJsZSgpDQoNCmBgYA0KDQpBbmQgdGhlIG1vc3QgcG9zaXRpdmU6DQoNCmBgYHtyfQ0KQU9DICU+JSANCiAgYXJyYW5nZShkZXNjKHNlbnRpbWVudCkpICU+JSANCiAgZHBseXI6OnNlbGVjdChkYXRhLnRleHQpICU+JSANCiAgYXNfdGliYmxlKCkNCmBgYA0KDQpOb3QgYmFkLCBidXQgc29tZSBvYnZpb3VzIHJvb20gZm9yIGltcHJvdmVtZW50ISAgT25lIG9mIGlzc3VlcyB3aXRoIHNpbXBsZSBzZW50aW1lbnQgYW5hbHlzaXMgaXMgdGhhdCB0d2VldHMgbGlrZSB0aGUgZm9sbG93aW5nIGhhdmUgYSBsb3Qgb2YgcG9zaXRpdmUgd29yZHMgd2hpY2ggYXJlIG5lZ2F0ZWQuDQoNCmBgYHtyfQ0KQU9DICU+JSANCiAgYXJyYW5nZShkZXNjKHNlbnRpbWVudCkpICU+JSANCiAgZHBseXI6OnNlbGVjdChkYXRhLnRleHQpICU+JSANCiAgLlsxLF0gJT4lIA0KICBhcy5jaGFyYWN0ZXIoKSAlPiUgDQogIHN0cnNwbGl0KC4sIiAiKSAlPiUgDQogIHVubGlzdCgpIC0+IHdvcmRzDQoNCndvcmRzDQpgYGANCg0KYGBge3J9DQoNCmJpbmdfZGljdFt3aGljaChiaW5nX2RpY3Qkd29yZCAlaW4lIHdvcmRzKSxdDQoNCmBgYA0KDQpTbyB0aGF0J3Mgd2h5IGl0IHdhcyBzY29yZWQgcG9zaXRpdmVseSEgIE9ic2VydmluZyB0aGlzIG1pZ2h0IG1ha2UgeW91IGNyZWF0ZSB5b3VyIG93biBkaWN0aW9uYXJ5IHdoZXJlLCBmb3IgZXhhbXBsZSwgdGVybXMgbGlrZSAibmVvLW5hemlzbSIgZXRjIGFyZSB0cmVhdGVkIG5lZ2F0aXZlbHkuICBNb3JlIGdlbmVyYWxseSwgc2ltcGxlIGRpY3Rpb25hcnkgbWV0aG9kcyBzdWNoIGFzIHRoZSBhYm92ZSBjYW5ub3QgcGljayB1cCBvbiBjb21wbGV4IG5lZ2F0aXZlIGNvbm5vdGF0aW9ucyBzdWNoIGFzIHRob3NlIGhlbGQgaW4gdGhpcyB0d2VldC4NCg0KIyMjIE1hY2hpbmUgTGVhcm5lZCBEaWN0aW9uYXJpZXMNCg0KQW4gYWx0ZXJuYXRpdmUgYXBwcm9hY2ggbWlnaHQgYmUgdG8gdXNlIGEgc3RhdGlzdGljYWwgbW9kZWwgdHJhaW5lZCBvbiBsYWJlbGVkIGRhdGEgYW5kIHVzZWQgdG8gcHJlZGljdCBvbnRvIHRoZSByZXN0IG9mIHRoZSBkYXRhc2V0LiAgVG8gaWxsdXN0cmF0ZSB0aGlzLCBJIGhhbmQgY29kZWQgMTAwIHJhbmRvbWx5IHNlbGVjdGVkIFR3ZWV0cyBhcyBiZWluZyBwb3NpdGl2ZSAoMSksIG5lZ2F0aXZlICgtMSksIG9yIG5ldXRyYWwvdW5jbGVhciAoMCkuDQoNCmBgYHtyfQ0Kc2V0LnNlZWQoMTIzNCkNCg0KaW5kcyA8LSBzYW1wbGUoMTpucm93KEFPQyksMTAwKQ0KDQp0cmFpbmluZyA8LSBBT0NbaW5kcyxjKCJkb2N1bWVudCIsImRhdGEudGV4dCIpXQ0KcHJlZGljdCA8LSBBT0NbLWluZHMsXQ0KDQp0cmFpbmluZyRkYXRhLnRleHRbMTo2XQ0KDQojIHRyYWluaW5nJGRvY3VtZW50IDwtIGFzLmNoYXJhY3Rlcih0cmFpbmluZyRkb2N1bWVudCkNCiMgdHJhaW5pbmckZGF0YS50ZXh0IDwtIGFzLmNoYXJhY3Rlcih0cmFpbmluZyRkYXRhLnRleHQpDQojIHdyaXRlLmNzdih0cmFpbmluZywidHJhaW5pbmcuY3N2IikNCg0KYGBgDQoNClRoZSBmaXJzdCBvZiB0aGVzZSB0d2VldHMgaXMgbmVnYXRpdmUsIHRoZSBzZWNvbmQgYW5kIHRoaXJkIHBvc2l0aXZlLCB0aGUgZm91cnRoIG5lZ2F0aXZlLCB0aGUgZmlmdGggYW5kIHNpeHRoIG5lZ2F0aXZlLCBhbmQgc28gb24uICBIZXJlIGlzIGhvdyBJIGNvZGVkIHRoZSBlbnRpcmUgc2V0Og0KDQpgYGB7cn0NCmNvZGVkIDwtIHJlYWQuY3N2KCJodHRwczovL3d3dy5kcm9wYm94LmNvbS9zLzAxZXI1eXBubXRjNm80My90cmFpbmluZ19jb2RlZC5jc3Y/ZGw9MSIpDQp0YWJsZShjb2RlZCRzY29yZSkNCmBgYA0KTGV0J3MgbG9vayBhdCBzb21lIHBvc2l0aXZlIHR3ZWV0czoNCg0KYGBge3J9DQpoZWFkKGNvZGVkJGRhdGEudGV4dFt3aGljaChjb2RlZCRzY29yZSA9PSAxKV0pDQpgYGANCg0KU29tZSBuZWdhdGl2ZSB0d2VldHM6DQoNCmBgYHtyfQ0KaGVhZChjb2RlZCRkYXRhLnRleHRbd2hpY2goY29kZWQkc2NvcmUgPT0gLTEpXSkNCmBgYA0KDQpBbmQgc29tZSB0d2VldHMgSSBjb3VsZG4ndCBjbGFzc2lmeToNCg0KYGBge3J9DQpoZWFkKGNvZGVkJGRhdGEudGV4dFt3aGljaChjb2RlZCRzY29yZSA9PSAwKV0pDQpgYGANCg0KV2hhdCB3ZSBhcmUgZ29pbmcgdG8gZG8gaXMgdXNlIHdvcmRzIGFzIGZlYXR1cmVzIGluIGEgcHJlZGljdGl2ZSBtb2RlbC4gIFdlIHdvbid0IGRvIGFueXRoaW5nIGZhbmN5IGhlcmUsIGxpa2Ugc3BsaXR0aW5nIGludG8gYSB0cmFpbiBhbmQgdGVzdCBzZXQgb3IgY3Jvc3MtdmFsaWRhdGluZyB0aGUgcmVzdWx0cywgYnV0IHN1Y2ggd291bGQgbGlrZWx5IGltcHJvdmUgdGhlIG1vZGVsLiAgTGV0J3MgYWRkIHRoZSBzY29yZXMgYmFjayBvbnRvIG91ciBtYWluIGRhdGEgYW5kIHRoZW4gY2xlYW4gdGhlIHR3ZWV0cyBpbnRvIHVzYWJsZSBmb3JtYXQuICBGb3IgZWFzaWx5IHJlcGxpY2FiaWxpdHksIHdlIHdpbGwgZmlzdCBjb2xsZWN0IGFsbCBvdXIgY2xlYW5pbmcgdGFza3MgaW50byBvbmUgZnVuY3Rpb24sIHRoZSBvdXRwdXQgb2Ygd2hpY2ggd2lsbCBiZSBhIHRpZHkgZG9jdW1lbnQgdGVybSBtYXRyaXg6DQoNCmBgYHtyfQ0KDQpjbGVhbl90ZXh0IDwtIGZ1bmN0aW9uKHRleHRfdmVjdG9yKXsNCiAgDQogIHRleHRfdmVjdG9yICU+JSANCiAgICBnc3ViKCdcXG4nLCdcXFxcbicsLikgJT4lIA0KICAgIGdzdWIoIih3d3d8aHR0cDp8aHR0cHM6KShbXihcXHMpXSopIiwiIiwuKSAlPiUgDQogICAgZ3N1YignXFxcXG4nLCcnLC4pICU+JSANCiAgICBnc3ViKCJbWzpwdW5jdDpdIF0rIiwnICcsLikgJT4lIA0KICAgIGdzdWIoJ1tbOmRpZ2l0Ol1dKycsICcnLCAuKSAlPiUgDQogICAgZ3N1YignW15ceDAxLVx4N0ZdJywgJycsIC4pICU+JSANCiAgICB0b2xvd2VyKCkgLT4gdGV4dF92ZWN0b3INCg0KICBzdG9wd29yZHMgPC0gYygiYW1wIixzdG9wd29yZHMoIkVuZ2xpc2giKSkNCiAgc3cyIDwtIHN0b3B3b3Jkcw0KICBzdzIgPC0gZ3N1YigiJyIsIiAiLHN3MikNCiAgc3RvcHdvcmRzIDwtIHVuaXF1ZShzdG9wd29yZHMsc3cyKQ0KDQogIGZvcihpIGluIHN0b3B3b3Jkcyl7DQogICAgdGV4dF92ZWN0b3IgPC0gZ3N1YihwYXN0ZTAoIlxcYiIsaSwiXFxiIiksICIiLCB0ZXh0X3ZlY3RvcikNCiAgfQ0KDQogIGNvcnB1cyA8LSBWQ29ycHVzKFZlY3RvclNvdXJjZSh0ZXh0X3ZlY3RvcikpDQogIGR0IDwtIERvY3VtZW50VGVybU1hdHJpeChjb3JwdXMpDQogIHRpZHlfZHQgPC0gdGlkeShkdCkNCg0KICB0aWR5X2R0JHRlcm0gPC0gc3RlbURvY3VtZW50KHRpZHlfZHQkdGVybSkNCg0KICB0aWR5X2R0ICU+JSANCiAgICBkcGx5cjo6Z3JvdXBfYnkoZG9jdW1lbnQsdGVybSkgJT4lIA0KICAgIGRwbHlyOjpzdW1tYXJpc2UoY291bnQgPSBzdW0oY291bnQsIG5hLnJtPVQpKSAtPiB0aWR5X2R0DQoNCiAgdW5pcXVlX3Rlcm1zIDwtIHVuaXF1ZSh0aWR5X2R0JHRlcm0pDQogIHNkbSA8LSBzdHJpbmdkaXN0bWF0cml4KHVuaXF1ZV90ZXJtcywgdW5pcXVlX3Rlcm1zLCBtZXRob2QgPSAianciLCB1c2VOYW1lcyA9IFRSVUUpDQoNCiAgZGlhZyhzZG0pIDwtIE5BDQogIGluZHMgPC0gd2hpY2goYXBwbHkoc2RtLDIsbWluLCBuYS5ybT1UKSA8PSAwLjA1KQ0KDQogIG1pbl9kaXN0IDwtIGFwcGx5KHNkbSwyLG1pbixuYS5ybT1UKQ0KICB0aHJlc2ggPC0gbWluX2Rpc3QgPCAwLjA1DQogIHN1YiA8LSBzZG1bdGhyZXNoLHRocmVzaF0NCg0KICBvbGRfdGVybXMgPC0gY29sbmFtZXMoc3ViKQ0KDQogIG91dCA8LSBsaXN0KCkNCiAgZm9yKGkgaW4gb2xkX3Rlcm1zKXsNCiAgICBvdXRbW2ldXSA8LSByb3duYW1lcyhzdWIpW3doaWNoLm1pbihzdWJbLGldKV0NCiAgfQ0KDQogIGRpY3QgPC0gZGF0YS5mcmFtZSh0ZXJtMSA9IG9sZF90ZXJtcywNCiAgICAgICAgICAgICAgICAgICAgIHRlcm0yID0gdW5saXN0KG91dCkpDQoNCiAgZGljdCRmaXJzdF90ZXJtIDwtIG5jaGFyKGFzLmNoYXJhY3RlcihkaWN0JHRlcm0xKSkgPCBuY2hhcihhcy5jaGFyYWN0ZXIoZGljdCR0ZXJtMikpDQoNCiAgZGljdCRyZXBsYWNlbWVudCA8LSBpZmVsc2UoZGljdCRmaXJzdF90ZXJtLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFzLmNoYXJhY3RlcihkaWN0JHRlcm0xKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBhcy5jaGFyYWN0ZXIoZGljdCR0ZXJtMikpDQoNCiAgZGljdCR0ZXJtMSA8LSBhcy5jaGFyYWN0ZXIoZGljdCR0ZXJtMSkNCiAgZGljdCR0ZXJtMiA8LSBhcy5jaGFyYWN0ZXIoZGljdCR0ZXJtMikNCiAgZGljdCRyZXBsYWNlbWVudCA8LSBhcy5jaGFyYWN0ZXIoZGljdCRyZXBsYWNlbWVudCkNCg0KICBmb3IoaSBpbiAxOm5yb3codGlkeV9kdCkpew0KICANCiAgICB0ZXJtIDwtIHRpZHlfZHRbaSwidGVybSJdDQogICAgDQogICAgaWYodGVybSAlaW4lIGRpY3QkdGVybTEpew0KICAgIA0KICAgICAgdGlkeV9kdCR0ZXJtW2ldIDwtIGRpY3QkcmVwbGFjZW1lbnRbbWF0Y2godGVybSxkaWN0JHRlcm0xKV0NCiAgICANCiAgICB9DQogIA0KICB9DQoNCiAgdGlkeV9kdCAlPiUgDQogICAgZHBseXI6Omdyb3VwX2J5KGRvY3VtZW50LHRlcm0pICU+JSANCiAgICBkcGx5cjo6c3VtbWFyaXNlKGNvdW50ID0gc3VtKGNvdW50KSkgJT4lIA0KICAgIGFycmFuZ2UoYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIoZG9jdW1lbnQpKSkgLT4gdGlkeV9kdA0KICANCiAgdGlkeV9kdA0KICANCn0NCg0KYGBgDQoNCkxldCdzIGNsZWFuIHRoZSB0ZXh0IGFuZCBnZXQgaXQgcmVhZHkgdG8gZ28sIGZvY3VzaW5nIG9ubHkgb24gdGhvc2Ugb2JzZXJ2YXRpb25zIGZvciB3aGljaCB3ZSBoYXZlIGEgcG9zaXRpdmUgb3IgbmVnYXRpdmUgc2VudGltZW50IGRldGVybWluZWQuICBEZXRhaWxzIHB1dCB0byB0aGUgc2lkZSwgdG8gZml0IHRoZSBtb2RlbCB3ZSBhcmUgZ29pbmcgdG8gdXNlIHdoYXQgaXMgY2FsbGVkIHJpZGdlIHJlZ3Jlc3Npb24gdG8gZXN0aW1hdGUgdGhlIGFzc29jaWF0aW9uIGJldHdlZW4gdGhlIHRlcm1zIGluIHRoZXNlIHR3ZWV0cyBhbmQgdGhlIG91ciBwb3NpdGl2ZS9uZWdhdGl2ZSBhc3NvY2lhdGlvbiBzY29yZS4NCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpzZXQuc2VlZCgxMjM0KQ0KaW5kcyA8LSBzYW1wbGUoMTpucm93KEFPQyksMTAwKQ0KDQp0cmFpbmluZyA8LSBBT0NbaW5kcyxdDQp0cmFpbmluZyRzY29yZSA8LSBjb2RlZCRzY29yZQ0KdHJhaW5pbmcgJT4lDQogIGZpbHRlcihzY29yZSA9PSAxIHwgc2NvcmUgPT0gLTEpIC0+IHRyYWluaW5nDQoNCnRpZHlfZHQgPC0gY2xlYW5fdGV4dCh0cmFpbmluZyRkYXRhLnRleHQpDQoNCmNvbG5hbWVzKHRpZHlfZHQpWzFdIDwtICJEb2N1bWVudCINCg0KdGlkeV9kdCRjb3VudCA8LSBhcy5udW1lcmljKHRpZHlfZHQkY291bnQpDQoNCnRpZHlfZHQgJT4lIA0KICBwaXZvdF93aWRlcihpZF9jb2xzID0gRG9jdW1lbnQsDQogICAgICAgICAgICAgIG5hbWVzX2Zyb20gPSB0ZXJtLA0KICAgICAgICAgICAgICB2YWx1ZXNfZnJvbSA9IGNvdW50LA0KICAgICAgICAgICAgICB2YWx1ZXNfZmlsbCA9IDApIC0+IHdpZGUNCg0Kd2lkZSRzY29yZSA8LSB0cmFpbmluZyRzY29yZQ0KDQpsaWJyYXJ5KGdsbW5ldCkNCg0KWCA8LSBhcy5tYXRyaXgod2lkZVssLWMoMSxuY29sKHdpZGUpKV0pDQp5IDwtIHdpZGUkc2NvcmUNCg0KbGFtYmRhcyA8LSAxMF5zZXEoMiwgLTMsIGJ5ID0gLS4xKQ0KY3ZfcmlkZ2UgPC0gY3YuZ2xtbmV0KFgsIHksIGFscGhhID0gMCwgbGFtYmRhID0gbGFtYmRhcykNCm9wdGltYWxfbGFtYmRhIDwtIGN2X3JpZGdlJGxhbWJkYS5taW4NCnJpZGdlX3JlZyA8LSBnbG1uZXQoWCwgeSwgYWxwaGEgPSAwLCBmYW1pbHkgPSAnZ2F1c3NpYW4nLCBsYW1iZGEgPSBvcHRpbWFsX2xhbWJkYSkNCmBgYA0KDQpIZXJlIGlzIHRoZSBtYWluIHJlc3VsdCAtLSBlc3NlbnRpYWxseSBhIGRhdGEtZHJpdmVuIGRpY3Rpb25hcnkhDQoNCmBgYHtyfQ0KY3VzdG9tX2RpY3QgPC0gZGF0YS5mcmFtZSh0ZXJtID0gcm93bmFtZXMocmlkZ2VfcmVnJGJldGEpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBhc3NvY2lhdGlvbiA9IHJpZGdlX3JlZyRiZXRhWywxXSkNCnJvd25hbWVzKGN1c3RvbV9kaWN0KSA8LSBOVUxMDQoNCmhlYWQoY3VzdG9tX2RpY3QsMTApDQpgYGANCkxldCdzIGxvb2sgYXQgdGhlIG1vc3QgbmVnYXRpdmUgdGVybXM6DQoNCmBgYHtyfQ0KY3VzdG9tX2RpY3QgJT4lIA0KICBhcnJhbmdlKGFzc29jaWF0aW9uKSAlPiUgDQogIGhlYWQoKQ0KYGBgDQoNCkFuZCBub3cgdGhlIG1vc3QgcG9zaXRpdmU6DQoNCmBgYHtyfQ0KY3VzdG9tX2RpY3QgJT4lIA0KICBhcnJhbmdlKGRlc2MoYXNzb2NpYXRpb24pKSAlPiUgDQogIGhlYWQoKQ0KYGBgDQoNClRvIG1ha2UgdGhpcyB1c2VmdWwgZm9yIHRoZSByZXN0IG9mIHRoZSBkYXRhLCB3ZSBuZWVkIHRvIGZpcnN0IGNsZWFuIGl0IGFzIGJlZm9yZS4NCg0KYGBge3J9DQpBT0NfY2xlYW4gPC0gY2xlYW5fdGV4dChBT0MkZGF0YS50ZXh0KQ0KYGBgDQpOb3cgbGV0J3MgbWVyZ2Ugb3VyIGRpY3Rpb25hcnkgb250byB0aGlzDQoNCmBgYHtyfQ0KQU9DX2NsZWFuIDwtIG1lcmdlKEFPQ19jbGVhbixjdXN0b21fZGljdCxieT0idGVybSIsYWxsLng9VCkNCnN1bW1hcnkoQU9DX2NsZWFuKQ0KYGBgDQoNCk5vdGUgdGhhdCB0aGVyZSBhcmUgYSBsb3Qgb2YgdGVybXMgZm9yIHdoaWNoIHdlIGhhdmUgbm8gYXNzb2NpYXRpb24hICBUaGlzIGlzIGJlY2F1c2UgdGhlIHRlcm1zIGRpZCBub3QgYXBwZWFyIGluIG91ciB0cmFpbmluZyBkYXRhc2V0IGFuZCBzbyB3ZSB3ZXJlIHVuYWJsZSB0byBsZWFybiB0aGVpciBhc3NvY2lhdGlvbiB3aXRoIHR3ZWV0cyBvZiBhIHBhcnRpY3VsYXIgc2VudGltZW50OyBhbiB1bnN1cnByc2luZyBzdGF0ZSBvZiBhZmZhaXJzIGdpdmVuIG91ciBzbWFsbCB0cmFpbmluZyBzaXplIGFuZCB0aGUgbGltaXRlZCB2b2NhYnVsYXJ5IHRoZXJlaW4uICBMZXQncyBjb2RlIHRoZXNlIGFzIHplcm8ncyBmb3IgdGhlIHRpbWUgYmVpbmcgYW5kIGFnZ3JlZ2F0ZSB0aGUgc2NvcmVzIHRvIHRoZSB0d2VldCBsZXZlbC4NCg0KYGBge3J9DQpBT0NfY2xlYW4kYXNzb2NpYXRpb24gPC0gaWZlbHNlKGlzLm5hKEFPQ19jbGVhbiRhc3NvY2lhdGlvbiksMCxBT0NfY2xlYW4kYXNzb2NpYXRpb24pDQoNCkFPQ19jbGVhbiAlPiUgDQogIGdyb3VwX2J5KGRvY3VtZW50KSAlPiUgDQogIGRwbHlyOjpzdW1tYXJpc2UoY3VzdG9tX3NlbnRpbWVudCA9IHN1bShjb3VudCAqIGFzc29jaWF0aW9uKSkgLT4gY3VzdG9tX3Njb3Jlcw0KDQpBT0MkZG9jdW1lbnRfZml4IDwtIDE6bnJvdyhBT0MpDQoNCkFPQyA8LSBtZXJnZShBT0MsY3VzdG9tX3Njb3JlcyxieS54PSJkb2N1bWVudF9maXgiLGJ5Lnk9ImRvY3VtZW50IixhbGwueD1UKQ0Kc3VtbWFyeShBT0MkY3VzdG9tX3NlbnRpbWVudCkNCmBgYA0KDQpMZXQncyBzZWUgaG93IHdlIGRpZCEgIExldCdzIHN0YXJ0IGJ5IGxvb2tpbmcgYXQgdGhlIG5lZ2F0aXZlIFR3ZWV0cw0KDQpgYGB7cn0NCkFPQyAlPiUgDQogIGFycmFuZ2UoY3VzdG9tX3NlbnRpbWVudCkgJT4lIA0KICBzZWxlY3QoZGF0YS50ZXh0KSAlPiUgDQogIGhlYWQoKQ0KYGBgDQpOb3cgdGhlIG1vc3QgcG9zaXRpdmU6DQoNCmBgYHtyfQ0KQU9DICU+JSANCiAgYXJyYW5nZShkZXNjKGN1c3RvbV9zZW50aW1lbnQpKSAlPiUgDQogIHNlbGVjdChkYXRhLnRleHQpICU+JSANCiAgaGVhZCgpDQpgYGANClByZXR0eSBjb29sLCBidXQgYSBsb3Qgb2YgaW1wcm92ZW1lbnRzIGNvdWxkIGJlIG1hZGUuICBGb3IgZXhhbXBsZSwgYnkgaW5jcmVhc2luZyB0aGUgc2l6ZSBvZiB0aGUgdHJhaW5pbmcgc2V0IHdlIHdvdWxkIGdldCBiZXR0ZXIgZXN0aW1hdGVzIG9mIHRoZSBhc3NvY2lhdGlvbiBiZXR3ZWVuIHdvcmRzIGFuZCBvdXIgc3ViamVjdGl2ZSBwb3NpdGl2ZS9uZWdhdGl2ZSBzZW50aW1lbnQgYXNzZXNzbWVudCBvZiB0aGUgdHdlZXQgYXMgYSB3aG9sZS4gIFdlIG1pZ2h0IHVzZSBhIG1vcmUgc29waGlzdGljYXRlZCBtZXRob2RvbG9neSBmb3IgcHJlZGljdGluZyB0aGUgc2NvcmVzLCBiZSBtb3JlIHNlbGVjdGl2ZSBvZiB0aGUgdGVybXMgd2UgaW5jbHVkZSBpbiB0aGUgbW9kZWwsIGV0YyBldGMuDQoNCg0KIyMgVG9waWMgTW9kZWxzDQoNClRvcGljIG1vZGVscyBhcmUgYSBuYXR1cmFsIGxhbmd1YWdlIHByb2Nlc3NpbmcgdGVjaG5pcXVlIGluIHdoaWNoIGEgc3RhdGlzdGljYWwgbW9kZWwgaXMgdXNlZCB0byBkaXNjb3ZlciBhYnN0cmFjdCAidG9waWNzIiB0aGF0IG9jY3VyIGluIGEgY29sbGVjdGlvbiBvZiBkb2N1bWVudHMuICBTaW1pbGFyIHRvIG91ciBleHBsb3JhdGlvbiBvZiBQQ0EsIHdlIHdvdWxkIGV4cGVjdCBkb2N1bWVudHMgdGhhdCBzcGVhayB0byBhIHBhcnRpY3VsYXIgdG9waWMgdG8gdXNlIG1vcmUgc2ltaWxhciB3b3JkcyB0aGFuIHRob3NlIHRoYXQgZG9uJ3QuICBBIGNsYXNzaWMgZXhhbXBsZSBpcyB0aGF0IHdlIHdvdWxkIGV4cGVjdCB0aGUgdGVybXMgImRvZyIgYW5kICJib25lIiB0byBvY2N1ciBpbiBkb2N1bWVudHMgYWJvdXQgZG9ncyBtb3JlIHRoYW4gImNhdCIgYW5kICJtZW93LCIgd2hpY2ggYXJlIG1vcmUgbGlrZWx5IHRvIG9jY3VyIGluIGRvY3VtZW50cyBhYm91dCBjYXRzLiAgVW5saWtlIFBDQSwgZG9jdW1lbnRzIHdpbGwgYmUgZ2l2ZW4gcHJvYmFiaWxpdGllcyBvZiBhc3NpZ25tZW50IGludG8gYSB2YXJpZXR5IG9mIHRvcGljcyB3aGljaCBhbGxvd3MgdXMgdG8gcmVmbGVjdCB0aGUgbm90aW9uIHRoYXQgYSBzaW5nbGUgZG9jdW1lbnQgbWlnaHQgY29udGFpbiBtdWx0aXBsZSB0b3BpY3MuDQoNClB1dHRpbmcgZGV0YWlscyB0byB0aGUgc2lkZSwgbGV0J3MgdGFrZSBhIGxvb2sgYXQgb25lIG9mIHRoZSBtb3N0IGNvbW1vbiB2YXJpZXRpZXMgb2YgdG9waWMgbW9kZWw6IExhdGVudCBEaXJpY2hsZXQgYWxsb2NhdGlvbi4gIExldCdzIHN0YXJ0IHdpdGggYSBzaW1wbGUgZXhhbXBsZSB1c2luZyB0aGUgQU9DIGRhdGEgd2hlcmUgd2UgZml0IG9ubHkgdGhyZWUgdG9waWNzIHRvIG91ciB0ZXh0LiAgV2Ugc3RhcnQgYnkgY2xlYW5pbmcgYXMgdXN1YWwsIGlmIGluIGEgc29tZXdoYXQgaGFja2lzaCB3YXkuDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh0b3BpY21vZGVscykNCg0KQU9DX2NsZWFuIDwtIGNsZWFuX3RleHQoQU9DJGRhdGEudGV4dCkNCg0KQU9DX2NsZWFuICU+JSANCiAgZ3JvdXBfYnkoZG9jdW1lbnQpICU+JSANCiAgZHBseXI6OnN1bW1hcmlzZShjbGVhbl90ZXh0ID0gcGFzdGUodGVybSwgY29sbGFwc2UgPSAiICIpKSAtPiBBT0NfcmVkdWNlZA0KDQpjb3JwIDwtIFZDb3JwdXMoVmVjdG9yU291cmNlKEFPQ19yZWR1Y2VkJGNsZWFuX3RleHQpKQ0KZHQgPC0gRG9jdW1lbnRUZXJtTWF0cml4KGNvcnApDQoNCm0xIDwtIExEQShkdCwgayA9IDMsDQogICAgICAgICAgY29udHJvbCA9IGxpc3Qoc2VlZCA9IDEyMzQpKQ0KYGBgDQoNCkZpdHRpbmcgdGhlIG1vZGVsIGlzIHRoZSBlYXN5IHBhcnQhICBUaGUgcmVzdCBvZiB0aGUgYW5hbHlzaXMgaW52b2x2ZXMgZXhwbG9yaW5nIGFuZCBpbnRlcnByZXRpbmcgdGhlIG1vZGVsIHRvIGRldGVybWluZSB3aGljaCB0b3BpY3Mgd2VyZSBmb3VuZCBhbmQgaWYgdGhleSBhcmUgbWVhbmluZ2Z1bC4NCg0KVG8gZ2V0IHN0YXJ0ZWQsIGxldHMnIHRha2UgYSBsb29rIGF0IHRoZSBwZXItdG9waWMtcGVyLXdvcmQgcHJvYmFiaWxpdGllcyBlc3RpbWF0ZWQgYnkgdGhlIG1vZGVsLg0KDQpgYGB7cn0NCnRpZHkobTEsIG1hdHJpeCA9ICJiZXRhIikNCmBgYA0KDQpOb3RlIHRoYXQgZWFjaCB0ZXJtIGhhcyB0aHJlZSByb3dzLCBlYWNoIGNvcnJlc3BvbmRpbmcgdG8gb25lIG9mIHRoZSBlc3RpbWF0ZWQgdG9waWNzLiAgVGhlIGJldGEgY29sdW1uIGlzIHRoZSBlc3RpbWF0ZWQgcHJvYmFiaWxpdHkgZnJvbSBiZWluZyBnZW5lcmF0ZWQgZnJvbSB0aGF0IHBhcnRpY3VsYXIgdG9waWMuICBXZSBjYW4gdmlzdWFsaXplIHRoZSB0b3Agd29yZHMgaW5jbHVkZWQgaW4gdG9waWNzIGJ5IGRvaW5nIHRoZSBmb2xsb3dpbmc6DQoNCmBgYHtyfQ0KdGlkeShtMSwgbWF0cml4ID0gImJldGEiKSAlPiUgDQogIGdyb3VwX2J5KHRvcGljKSAlPiUgDQogIHNsaWNlX21heChiZXRhLG49MTApICU+JSANCiAgdW5ncm91cCgpICU+JSANCiAgYXJyYW5nZSh0b3BpYywtYmV0YSkgJT4lIA0KICBtdXRhdGUodGVybSA9IHJlb3JkZXJfd2l0aGluKHRlcm0sIGJldGEsIHRvcGljKSkgJT4lIA0KICBnZ3Bsb3QoYWVzKHg9YmV0YSwgeT0gdGVybSwgZmlsbD1mYWN0b3IodG9waWMpKSkgKyANCiAgICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEZBTFNFKSArDQogICAgZmFjZXRfd3JhcCh+dG9waWMsIHNjYWxlcyA9ICJmcmVlIikgKw0KICAgIHNjYWxlX3lfcmVvcmRlcmVkKCkNCmBgYA0KDQpDb29sISAgVGhlIGZpcnN0IHRvcGljIGxhdGNoZXMgb24gdG8gdGhpbmdzIGxpa2UgaGVyc2VsZiwgcG93ZXIgYW5kIGp1c3RpY2UuICBUaGUgc2Vjb25kIGNhdGVnb3J5IGxvb2tzIGxpa2UgaXQgZGVhbHMgd2l0aCByZXB1YmxpY2FucyBhbmQgdm90aW5nLiAgVGhlIGxhc3Qgc2VlbXMgdG8gcmVmZXIgdG8gcGVvcGxlIGFuZCB0aGUgaG91c2UuDQoNCk9uZSBvZiB0aGUgbW9zdCBkaWZmaWN1bHQgcHJlbGltaW5hcnkgdGFza3Mgd2hlbiBmaXR0aW5nIHRvcGljIG1vZGVscyBpcyBmaXJzdCBkZXRlcm1pbmluZyB0aGUgbnVtYmVyIG9mIHRvcGljcyB0byBmaXQuICBBIGdvb2QgZmlyc3QtY3V0IHNvbHV0aW9uIHRvIHRoaXMgcHJvYmxlbSBpcyB0byBlc3RpbWF0ZSBhIHZhcmlldHkgb2YgbW9kZWxzIGFuZCB0aGVuIGNob29zZSB0aGF0IG1vZGVsIHdoaWNoIHByb3ZpZGVzIHRoZSBiZXN0IGZpdCB0byB0aGUgZGF0YS4gIEZvciBvdXIgcHVycG9zZXMsIHdlIGNhbiB1c2UgdGhlIGxvZy1saWtlbGlob29kIHZhbHVlcyBzYXZlZCBpbiB0aGUgZml0dGVkIG9iamVjdHMgdG8gY2hvb3NlIGEgbW9kZWwgd2hpY2ggaGFzIGVub3VnaCwgYnV0IG5vdCB0b28gbWFueSwgdG9waWNzIGJ5IGNhbGN1bGF0aW5nIEFJQyB2YWx1ZXMuDQoNCkZpcnN0IHdlIGZpdCB0aGUgbW9kZWxzLCB1c2luZyBhIGJpdCBvZiBwYXJhbGxlbCBwcm9jZXNzaW5nIHRvIHNwZWVkIHRoaW5ncyB1cC4uLg0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkocGJhcHBseSkNCmxpYnJhcnkocGFyYWxsZWwpDQoNCmNsIDwtIG1ha2VDbHVzdGVyKGRldGVjdENvcmVzKCkpDQpjbHVzdGVyRXhwb3J0KGNsLCJkdCIpDQpjbHVzdGVyRXZhbFEoY2wsbGlicmFyeSh0b3BpY21vZGVscykpDQoNCm1vZHMgPC0gcGJsYXBwbHkoMToyMCwgZnVuY3Rpb24oeClMREEoZHQsIGsgPSB4ICsgMSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY29udHJvbCA9IGxpc3Qoc2VlZCA9IDEyMzQpKSwgY2w9Y2wpDQpzdG9wQ2x1c3RlcihjbCkNCmBgYA0KVG8gZXZhbHVhdGUgd2hpY2ggbW9kZWwgdG8gdXNlLCB3ZSB3aWxsIHVzZSB0aGUgcGVycGxleGl0eSBvZiB0aGUgbW9kZWwuICBEaXNjdXNzaW5nIHRoZSBtYXRoZW1hdGljYWwgZGV0YWlscyBvZiB0aGlzIG1ldHJpYyBpcyBvdXRzaWRlIHRoZSByZWFsbSBvZiBvdXIgZGlzY3Vzc2lvbiwgYnV0IHRoZSBtZXRyaWMgcmVsYXRlcyB0byBob3cgd2VsbCBhIG1vZGVsIHByZWRpY3RzIHRoZSBkYXRhLg0KDQpgYGB7cn0NCnBlcnBsZXggPC0gc2FwcGx5KG1vZHMsZnVuY3Rpb24oeClwZXJwbGV4aXR5KHgpKQ0KDQpwbG90KHBlcnBsZXgpDQpgYGANCldlIHNlZSBhIEhVR0UgZGlwIGFyb3VuZCAxMSB0b3BpY3MsIHNvIGxldCdzIHRha2UgYSBsb29rIGF0IHRoYXQhDQoNClRoZSByZXN1bHRzIG9mIHRoZSBtb2RlbCBhcmUgdGhlIGZvbGxvd2luZzoNCg0KYGBge3J9DQpvcHRfbW9kIDwtIG1vZHNbWzEwXV0NCg0KdGlkeShvcHRfbW9kLCBtYXRyaXggPSAiYmV0YSIpICU+JSANCiAgZ3JvdXBfYnkodG9waWMpICU+JSANCiAgc2xpY2VfbWF4KGJldGEsbj0xMCkgJT4lIA0KICB1bmdyb3VwKCkgJT4lIA0KICBhcnJhbmdlKHRvcGljLC1iZXRhKSAlPiUgDQogIG11dGF0ZSh0ZXJtID0gcmVvcmRlcl93aXRoaW4odGVybSwgYmV0YSwgdG9waWMpKSAlPiUgDQogIGdncGxvdChhZXMoeD1iZXRhLCB5PSB0ZXJtLCBmaWxsPWZhY3Rvcih0b3BpYykpKSArIA0KICAgIGdlb21fY29sKHNob3cubGVnZW5kID0gRkFMU0UpICsNCiAgICBmYWNldF93cmFwKH50b3BpYywgc2NhbGVzID0gImZyZWUiKSArDQogICAgc2NhbGVfeV9yZW9yZGVyZWQoKQ0KDQpgYGANCg0KVGhhdCdzIG5vdCBiYWQhICBXZSBjb3VsZCBsaWtlbHkgaW1wcm92ZSBwZXJmb3JtYW5jZSBmdXJ0aGVyIGJ5IGRvaW5nIGFkZGl0aW9uYWwgY2xlYW5pbmcgcHJpb3IgdG8gZXN0aW1hdGluZyB0aGUgdG9waWMgbW9kZWxzLCBwZXJoYXBzIHVzaW5nIGEgY3VzdG9tIGRpY3Rpb25hcnkgdG8gcmVtb3ZlIG5vbi1wb2xpdGljYWwgd29yZHMuICBXZSBtaWdodCBhbHNvIHVzZSBhIGRpZmZlcmVudCBjcml0ZXJpYSBmb3Igc2VsZWN0aW5nIHRoZSBudW1iZXIgb2YgdG9waWNzLCBpbmNyZWFzZSB0aGUgbnVtYmVyIG9mIFR3ZWV0cyBhbmFseXplZCwgZXRjLg0KDQpCdXQgbm93IHRoYXQgd2UgaGF2ZSBhIGRlY2VudCB0b3BpYyBtb2RlbCwgd2UgbWlnaHQgd2FudCB0byBnbyBhaGVhZCBhbmQgYXNzb2NpYXRlIFR3ZWV0cyB3aXRoIHRoZWlyIHJlbGV2YW50IHRvcGljcy4gIFRoaXMgd2lsbCBoZWxwIHVzIGdvIGZ1cnRoZXIgdG8gZGV0ZXJtaW5lIHdoaWNoIHRleHRzIGFyZSBhc3NvY2lhdGVkIHdpdGggd2hpY2ggdG9waWNzLCBhbmQgd2lsbCBsaWtlbHkgaGVscCB1cyBmdXJ0aGVyIGRldGVybWluZSB0aGVpciBtZWFuaW5nLiAgVG8gZG8gdGhpcywgd2UgbmVlZCB0byBleHRyYWN0IGFub3RoZXIgcGFyYW1ldGVyIGZyb20gdGhlIGZpdHRlZCBtb2RlbDoNCg0KYGBge3J9DQp0aWR5KG9wdF9tb2QsIG1hdHJpeCA9ICJnYW1tYSIpDQpgYGANCg0KRWFjaCBvZiB0aGVzZSB2YWx1ZXMgaXMgYW4gZXN0aW1hdGVkIHByb3BvcnRpb24gb2Ygd29yZHMgZnJvbSB0aGF0IGRvY3VtZW50IHRoYXQgYXJlIGdlbmVyYXRlZCBmcm9tIHRoYXQgdG9waWMuICBNYW55IG9mIHRoZXNlIGFwcGVhciB0byBiZSBxdWl0ZSBuYXJyb3cgaW4gc2NvcGUsIHdoaWNoIG1ha2VzIHNlbnNlIGdpdmVuIHRoZSBuYXR1cmUgb2YgVHdlZXRzLCBidXQgYSBmZXcgbG9vayBsaWtlIHRoZXkgYXJlIGEgbWl4IG9mIHRvcGljcy4gTGV0J3MgdGFrZSBhIGxvb2sgYXQgYSBmZXcgVHdlZXRzIHRoYXQgcmFuayBoaWdobHkgb24gZWFjaCB0b3BpYy4gIEhlcmUgYXJlIHRoZSB0b3AgNSBmb3IgdG9waWMgMjoNCg0KYGBge3J9DQp0aWR5KG9wdF9tb2QsIG1hdHJpeCA9ICJnYW1tYSIpICU+JSANCiAgZmlsdGVyKHRvcGljID09IDIpICU+JSANCiAgc2xpY2VfbWF4KGdhbW1hLCBuID0gNSkgJT4lIA0KICBkcGx5cjo6c2VsZWN0KGRvY3VtZW50KSAlPiUgDQogIGFzLnZlY3RvcigpICU+JSANCiAgdW5saXN0KCkgJT4lIA0KICBhcy5udW1lcmljKCkgJT4lIA0KICBBT0NfcmVkdWNlZFsuLCJkb2N1bWVudCJdICU+JSANCiAgYXMudmVjdG9yKCkgJT4lIA0KICB1bmxpc3QoKSAlPiUgDQogIGFzLmNoYXJhY3RlcigpICU+JSANCiAgYXMubnVtZXJpYygpICU+JSANCiAgQU9DWy4sImRhdGEudGV4dCJdDQpgYGANCg0KV2hhdCBhYm91dCB0b3BpYyA5Pw0KDQpgYGB7cn0NCnRpZHkob3B0X21vZCwgbWF0cml4ID0gImdhbW1hIikgJT4lIA0KICBmaWx0ZXIodG9waWMgPT0gOSkgJT4lIA0KICBzbGljZV9tYXgoZ2FtbWEsIG4gPSA1KSAlPiUgDQogIGRwbHlyOjpzZWxlY3QoZG9jdW1lbnQpICU+JSANCiAgYXMudmVjdG9yKCkgJT4lIA0KICB1bmxpc3QoKSAlPiUgDQogIGFzLm51bWVyaWMoKSAlPiUgDQogIEFPQ19yZWR1Y2VkWy4sImRvY3VtZW50Il0gJT4lIA0KICBhcy52ZWN0b3IoKSAlPiUgDQogIHVubGlzdCgpICU+JSANCiAgYXMuY2hhcmFjdGVyKCkgJT4lIA0KICBhcy5udW1lcmljKCkgJT4lIA0KICBBT0NbLiwiZGF0YS50ZXh0Il0NCmBgYA0KDQpOb3QgYmFkISAgU29tZSBpbXByb3ZlbWVudHMgY291bGQgb2J2aW91c2x5IGJlIG1hZGUgdG8gdGhlIHByb2Nlc3Npbmcgb2YgdGhlIHRleHQgdG8gZ2V0IG1vcmUgcG9saXRpY2FsbHkgcmVsZXZhbnQgcmVzdWx0cywgYnV0IGFzIGFuIGV4cGxvcmF0b3J5IHRvb2wgZm9yIGFuc3dlcmluZyAid2hhdCB0b3BpY3MgbWlnaHQgZXhpc3QsIGFuZCB3aGF0IHRlcm1zIGFyZSBhc3NvY2lhdGVkIHdpdGggdGhlbT8iIHVuc3VwZXJ2aXNlZCBtZXRob2RzIGxpa2UgdG9waWMgbW9kZWxpbmcgYXJlIGhhcmQgdG8gYmVhdCENCg0KIyMgQm9udXMgQ29kZQ0KDQpBcyBwcm9taXNlZCwgaGVyZSBpcyBzb21lIGNvZGUgZm9yIGFjY2Vzc2luZyB0aGUgVHdpdHRlciB2b2x1bWUgc3RyZWFtLg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nID0gRiwgZXZhbCA9IEZ9DQpsaWJyYXJ5KGN1cmwpDQoNCnN0cmVhbV90d2VldHNfdjIgPC0gZnVuY3Rpb24oYmVhcmVyX3Rva2VuLCBtYXhfc2Vjb25kcywgbWF4X3R3ZWV0cyl7DQogIA0KICAjIFNldCB1cCBlbmRwb2ludCBhbmQgY29ubmVjdGlvbg0KICBoIDwtIG5ld19oYW5kbGUoKQ0KICBoYW5kbGVfc2V0aGVhZGVycyhoLCJBdXRob3JpemF0aW9uIiA9IHBhc3RlMCgiQmVhcmVyICIsYmVhcmVyX3Rva2VuKSkNCiAgZW5kcG9pbnQgPC0gImh0dHBzOi8vYXBpLnR3aXR0ZXIuY29tLzIvdHdlZXRzL3NhbXBsZS9zdHJlYW0iDQogIGNvbiA8LSBjdXJsKGVuZHBvaW50LCBoYW5kbGUgPSBoKQ0KICANCiAgIyBTZXQgdXAgdGVybWluYXRpb24gY29uZGl0aW9ucw0KICBtYXhfdGltZSA8LSBTeXMudGltZSgpICsgbWF4X3NlY29uZHMNCiAgDQogIGlmKG1heF90d2VldHMgPD0gMTAwKXsNCiAgICByZXF1ZXN0cyA8LSBtYXhfdHdlZXRzDQogIH1lbHNlew0KICAgIHJlcXVlc3RzIDwtIGMocmVwKDEwMCxmbG9vcihtYXhfdHdlZXRzLzEwMCkpLCBtYXhfdHdlZXRzICUlIDEwMCkNCiAgfQ0KICANCiAgbWF4X2l0ZXIgPC0gbGVuZ3RoKHJlcXVlc3RzKQ0KICBub3cgPC0gU3lzLnRpbWUoKQ0KICBpdGVyIDwtIDENCiAgDQogICMgQ29sbGVjdCBUd2VldHMNCiAgZHVtcHMgPC0gbGlzdCgpDQogIA0KICBvcGVuKGNvbikNCiAgd2hpbGUoIChtYXhfdGltZSA+IG5vdykgJiAobWF4X2l0ZXIgPj0gaXRlcikgKXsNCiAgICANCiAgICBkdW1wc1tbaXRlcl1dIDwtIHJlYWRMaW5lcyhjb24sbj1yZXF1ZXN0c1tpdGVyXSkNCiAgICANCiAgICBpdGVyIDwtIGl0ZXIgKyAxDQogICAgbm93IDwtIFN5cy50aW1lKCkNCiAgICANCiAgfQ0KICANCiAgIyBDbG9zZSBjb25uZWN0aW9uIGFuZCByZXR1cm4gDQogIA0KICBjbG9zZShjb24pDQogIA0KICBhbGwgPC0gdW5saXN0KGR1bXBzKQ0KICBvdXQgPC0gc3RyZWFtX2luKHRleHRDb25uZWN0aW9uKGFsbCksZmxhdHRlbiA9IFQpDQogIG91dA0KICANCn0NCg0KDQoNCiMgVGVzdCBvdXRwdXQgKHJlcXVpcmVzIGJlYXJlciB0b2tlbiEpDQoNCndkIDwtICJEOi9Ud2l0dGVyIg0Kc2V0d2Qod2QpDQoNCnR3aXR0ZXJfaW5mbyA8LSByZWFkLmNzdigidHdpdHRlcl9pbmZvMi5jc3YiLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHN0cmluZ3NBc0ZhY3RvcnMgPSBGKQ0KDQp0ZXN0IDwtIHN0cmVhbV90d2VldHNfdjIodHdpdHRlcl9pbmZvJGJlYXJlcl90b2tlbiwxMCwxMDAwKQ0KYXNfdGliYmxlKHRlc3QpDQpgYGANCg0KSW1hZ2luZyB3aGF0IHlvdSB3b3VsZCBoYXZlIHRvIGdvIHRocm91Z2ggdG8gcHJvY2VzcyB0aGF0IQ0KDQojIyBFeGVyY2lzZXMNCg0KQmVmb3JlIHdlIGdldCBhaGVhZCBvZiBvdXJzZWx2ZXMsIHdlIHdhbnQgdG8gbWFrZSBzdXJlIHRoYXQgeW91IGhhdmUgZnVuZGFtZW50YWxzIGluIG9yZGVyLiAgRG8gdGhlIGZvbGxvd2luZzoNCg0KV3JpdGUgYSBzY3JpcHQgd2hpY2guLi4NCg0KMS4gV2l0aCB0aGUgUmVwQU9DIFR3ZWV0cyBnaXZlbiBhYm92ZS4uLg0KICAgICsgQ29uZHVjdCBhIHNlbnRpbWVudCBhbmFseXNpcy4gIFdoaWNoIDEwIFR3ZWV0cyBhcmUgdGhlIG1vc3QgcG9zaXRpdmUgYW5kIG5lZ2F0aXZlPw0KICAgICsgV2hhdCBhcmUgdGhlIG9wdGltYWwgbnVtYmVyIG9mIHRvcGljcz8gIEludGVycHJldCB0aGVtLg0KICAgIA0KMi4gV2l0aCB0aGUgUmVwTVRHIFR3ZWV0cyBnaXZlbiBhYm92ZS4uLg0KICAgICsgQ29uZHVjdCBhIHNlbnRpbWVudCBhbmFseXNpcy4gIFdoaWNoIDEwIFR3ZWV0cyBhcmUgdGhlIG1vc3QgcG9zaXRpdmUgYW5kIG5lZ2F0aXZlPw0KICAgICsgV2hhdCBhcmUgdGhlIG9wdGltYWwgbnVtYmVyIG9mIHRvcGljcz8gIEludGVycHJldCB0aGVtLg0KDQozLiBDb2xsZWN0IGRhdGEgZnJvbSBlaXRoZXIgR29vZ2xlIE5ld3Mgb3IgUmVkZGl0IG9uIGF0IGxlYXN0IGZpdmUgZGlzdGluY3Qgc2VhcmNoIHRlcm1zIG9mIHlvdXIgY2hvaWNlLg0KICAgICsgQ29uZHVjdCBhIHNlbnRpbWVudCBhbmFseXNpcy4gIFdoaWNoIDEwIHRleHRzIGFyZSB0aGUgbW9zdCBwb3NpdGl2ZSBhbmQgbmVnYXRpdmU/DQogICAgKyBGaXQgYSB0b3BpYyBtb2RlbCwgYXMgYWJvdmUsIHRvIHRoaXMgY29ycHVzLiAgQXJlIHlvdSBhYmxlIHRvIGRpc3Rpbmd1aXNoIHRoZSB0b3BpY3M/DQoNClNhdmUgYW5kIHN1Ym1pdCB5b3VyIHdvcmtpbmcgUiBzY3JpcHQgdG8gdGhlIFtFeGVyY2lzZS9RdWl6IFN1Ym1pc3Npb24gTGlua10oaHR0cHM6Ly9mb3Jtcy5nbGUvelR6azZZS2o3cXZlTFJCTTYpIGJ5IHRoZSBlbmQgb2YgdGhlIGRheSAoaWRlYWxseSwgZW5kIG9mIGxhYiBzZXNzaW9uISkuDQo=