The tutorial will work through practical examples showing you how to extract and visualize Twitter data using R.

Part 1. Extracting Tweets

Prerequisites

  1. You have already installed R and are using RStudio.

  2. In order to extract tweets, you will need a Twitter application and hence a Twitter account. If you don’t have a Twitter account, please sign up.

  3. Use your Twitter login ID and password to sign in at Twitter Developers.

Create a Twitter Application

  1. Navigate to Twitter Developers. Click the button Create New App in the upper right corner. Create New App

  2. Fill in the required application details, including Name, Description, and Website. Note that the Name must be unique. If your chosen name has been taken, try a new one. Click the button Create your Twitter application in the lower-left corner. App Details

  3. Click the Keys and Access Tokens tab under your created Twitter Application. Then click the button Create my access token in the lower-left corner Token Tab Create Tokens

  4. Note the values of Consumer Key (API Key), Consumer Secret (API Secret), Access Token, and Access Token Secret handy for future use. You should keep these secret. If anyone was to get these keys, they could effectively access your Twitter account. Access Tokens


Install and Load R Packages

For the purpose of this tutorial, we will need the following packages:

  • ROAuth: Provides an interface to the OAuth 1.0 specification, allowing users to authenticate via OAuth to the server of their choice.

  • TwitteR: Provides an interface to the Twitter web API.

if (!require(twitteR)) {install.packages("twitteR")}
if (!require(ROAuth)) {install.packages("ROAuth")}
library(twitteR)
library(ROAuth)

Authentication with OAuth

Authorize App to use your account, i.e., established handshake between Twitter and R.

# you must get the following information from the Twitter App you just created
my.consumer.key = "fH4IijcQUrwxEQ3mmb6G2gzUc"
my.consumer.secret = "FxkuV6ePyFaia2LmyxetoH50IxGrQcEYbwnLe3EjVDWsCdPrhJ"
my.access.token = "99989439-wG82y2hMmAmlJ1iIlQgNu0l65ZOKVuVscj4Idm9Xu"
my.access.token.secret = "QNehvUZOGNNZyqFYDCAP6tlWEHWIBbKIhiqPEKAM2SOoT"
my_oauth <- setup_twitter_oauth(consumer_key = my.consumer.key, consumer_secret = my.consumer.secret, access_token = my.access.token, access_secret = my.access.token.secret)
[1] "Using direct authentication"
save(my_oauth, file = "my_oauth.Rdata")

Extract Tweets Using a Search Term

search.string <- "#HurricaneNate"
result.term <- searchTwitter(search.string, n = 100)
head(result.term)
[[1]]
[1] "mdorsey92: RT @jacobdickeywx: The eyewall approaches #HURRICANENATE #NATE #GULFPORT https://t.co/uFaobimvjY"

[[2]]
[1] "Mi_Puerto_Rico: RT @Readygov: Before heading to bed make sure you have different ways to receive emergency alerts throughout the night. #HurricaneNate http…"

[[3]]
[1] "SVH2: RT @FoxNews: .@AdamKlotzFNC on #HurricaneNate: \"Currently you're looking at 90 mph winds, that's a Category 1 storm.\" https://t.co/RnTJDFp7…"

[[4]]
[1] "YusefforPeace: RT @MeritLaw: Our volunteers at #AmericanBlackCross are preparing for #HurricaneNate. Visit https://t.co/jCrBVhYPN4 to pitch in! #ItsOnUs #…"

[[5]]
[1] "Torchbug: RT @realDonaldTrump: Our great team at @FEMA is prepared for #HurricaneNate. Everyone in LA, MS, AL, and FL please listen to your local aut…"

[[6]]
[1] "Butch2763: RT @realDonaldTrump: Our great team at @FEMA is prepared for #HurricaneNate. Everyone in LA, MS, AL, and FL please listen to your local aut…"

Convert Results to Data Frame

df.term <- twListToDF(result.term)
write.csv(df.term, "HurricaneNate.csv")

Search Tweets Using lat/lon

result.latlon <- searchTwitter('nba', geocode='29.8174,-95.6814,20mi', n = 100)
head(result.latlon)
[[1]]
[1] "ELOSSports: Maurice Evans @1MoEvans shares his personal story, #ELOSSports, and #NBA. #Entrepreneurs #Athletes #sportstech… https://t.co/TbLAUKylGk"

[[2]]
[1] "Jabari316: I added a video to a @YouTube playlist https://t.co/m07ktuDRxH NBA 2K18 iOS My Career - WE LIT - BACK WITH THE PLAYMAKER"

[[3]]
[1] "jessielantz: RT @HoustonRockets: Throughout October and November, we will honor the Heroes of Harvey that YOU nominate &amp; recognize them on-court: https:…"

[[4]]
[1] "SWHTown30: You called this man Gary Harris one of the worse defending guards in the NBA https://t.co/bwceHj9HXJ"

[[5]]
[1] "SWHTown30: Tatum is not ready for the NBA and will be hard to notice the talent right now https://t.co/PCjGjhDwje"

[[6]]
[1] "kerstindonota_9: RT @HoustonRockets: Squad is up 69-37 with 3:41 left in the first-half. \n\n\xed愼㸰戼㹤\xed戼㸳\u008a Gordon 14pts\n\xed愼㸰戼㹤\xed戼㸳\u008a Capela 12pts 9reb \n\xed愼㸰戼㹤\xed戼㸳\u008a CP3 11pts/7ast \n\nLive at ht…"
df.latlon <- twListToDF(result.latlon)
write.csv(df.latlon, "NBA.csv")

Extract Tweets from a Specific User

To take a closer look at a Twitter user (including yourself!), run the command getUser. This will only work correctly with users who have their profiles public, or if you’re authenticated and granted access. You can also see things such as a user’s followers, who they follow, retweets, and more. The getUser function returns a user object, which can then be polled for further information.

test_user <- getUser("binghamtonu")
test_user$id
[1] "23790666"
test_user$getDescription()
[1] "Official #BinghamtonU Twitter! Founded in 1946, Binghamton University is the premier public university in the Northeast."
test_user$getFollowersCount()
[1] 29537
test_user$getFriends(n=5)
$`3420478114`
[1] "BingPharmacy"

$`755044097874882560`
[1] "DrACR24"

$`2720523253`
[1] "allisonkhin"

$`3282859598`
[1] "TwitterVideo"

$`3376435420`
[1] "AtlantaBlaze"

The userTimeline function will allow you to retrieve various timelines within the Twitter universe.

userTimeline(user = "realDonaldTrump", n = 5)
[[1]]
[1] "realDonaldTrump: Leaving the White House for the Great State of North Carolina. Big progress being made on many fronts!"

[[2]]
[1] "realDonaldTrump: Thanks for your support! https://t.co/iqUM1RfQso"

[[3]]
[1] "realDonaldTrump: ...hasn't worked, agreements violated before the ink was dry, makings fools of U.S. negotiators. Sorry, but only one thing will work!"

[[4]]
[1] "realDonaldTrump: Presidents and their administrations have been talking to North Korea for 25 years, agreements made and massive amounts of money paid......"

[[5]]
[1] "realDonaldTrump: Will be joining @GovMikeHuckabee tonight at 8pmE on @TBN. Enjoy! https://t.co/Y5hGPpYZfl"

Part 2. Creating Word Cloud

In this part we will use R to visualize tweets as a word cloud to find out what people are tweeting about the NBA (#nba). A word cloud is a visual representation showing the most relevant words (i.e., the more times a word appears in our tweet sampling the bigger the word).

Authentication with OAuth

if (!require(twitteR)) {install.packages("twitteR")}
if (!require(ROAuth)) {install.packages("ROAuth")}
library(twitteR)
library(ROAuth)
# you must get the following information from the Twitter App you just created
my.consumer.key = "fH4IijcQUrwxEQ3mmb6G2gzUc"
my.consumer.secret = "FxkuV6ePyFaia2LmyxetoH50IxGrQcEYbwnLe3EjVDWsCdPrhJ"
my.access.token = "99989439-wG82y2hMmAmlJ1iIlQgNu0l65ZOKVuVscj4Idm9Xu"
my.access.token.secret = "QNehvUZOGNNZyqFYDCAP6tlWEHWIBbKIhiqPEKAM2SOoT"
my_oauth <- setup_twitter_oauth(consumer_key = my.consumer.key, consumer_secret = my.consumer.secret, access_token = my.access.token, access_secret = my.access.token.secret)
[1] "Using direct authentication"

Extract Tweets

tweets <- searchTwitter("#nba", n=1000, lang="en")
tweets.text <- sapply(tweets, function(x) x$getText())

Clean Up Text

We have already been authenticated and successfully retrieved the text from the tweets using #nba. The first step in creating a word cloud is to clean up the text by using lowercase and removing punctuation, usernames, links, etc. We are using the function gsub to replace unwanted text. Gsub will replace all occurrences of any given pattern. Although there are alternative packages that can perform this operation, we have chosen gsub because of its simplicity and readability.

# Replace blank space (“rt”)
tweets.text <- gsub("rt", "", tweets.text)
# Replace @UserName
tweets.text <- gsub("@\\w+", "", tweets.text)
# Remove punctuation
tweets.text <- gsub("[[:punct:]]", "", tweets.text)
# Remove links
tweets.text <- gsub("http\\w+", "", tweets.text)
# Remove tabs
tweets.text <- gsub("[ |\t]{2,}", "", tweets.text)
# Remove blank spaces at the beginning
tweets.text <- gsub("^ ", "", tweets.text)
# Remove blank spaces at the end
tweets.text <- gsub(" $", "", tweets.text)
 
# #convert all text to lower case
tweets.text <- tolower(tweets.text)

Remove Stop Words

In the next step we will use the text mining package tm to remove stop words. A stop word is a commonly used word such as “the”. Stop words should not be included in the analysis.

if(!require(tm)) {install.packages("tm")}
library(tm)
#create corpus
#clean up by removing stop words
tweets.text.corpus <- tm_map(tweets.text.corpus, function(x) removeWords(x,stopwords()))

Generate Word Cloud

Now we’ll generate the word cloud using the wordcloud package. For this example we are concerned with plotting no more than 150 words that occur more than once with random color, order, and position.

if(!require(wordcloud)) {install.packages("wordcloud")}
library(wordcloud)
#generate wordcloud
wordcloud(tweets.text.corpus,min.freq = 2, scale=c(7,0.5),colors=brewer.pal(8, "Dark2"),  random.color= TRUE, random.order = FALSE, max.words = 150)

Part 3. Sentiment Analysis

Sentiment analyses classify communications as positive, negative, or neutral. Determining sentiment ranges from very simple classification methods to very complex algorithms. For ease and transparency in this example, we will classify the sentiment of a tweet based on the polarity of the individual words. Each word will be given a score of +1 if classified as positive, -1 if negative, and 0 if classified as neutral. This will be determined using positive and negative lexicon lists compiled by Minqing Hu and Bing Liu for their work “Mining and Summarizing Customer Reviews”. The total polarity score of a given tweet will result in adding together the scores of all the individual words. Once you go to the page, click on Opinion Lexicon and then download the rar file.

Install and Load R Packages

 # Install packages for sentiment analysis
if (!require(twitteR)) {install.packages("twitteR")}
if (!require(ROAuth)) {install.packages("ROAuth")}
if (!require(plyr)) {install.packages("plyr")}
if (!require(stringr)) {install.packages("stringr")}
if (!require(ggplot2)) {install.packages("ggplot2")}
library(twitteR)
library(ROAuth)
library(plyr)
library(dplyr)
library(stringr)
library(ggplot2)

Authentication with OAuth

# you must get the following information from the Twitter App you just created
my.consumer.key = "fH4IijcQUrwxEQ3mmb6G2gzUc"
my.consumer.secret = "FxkuV6ePyFaia2LmyxetoH50IxGrQcEYbwnLe3EjVDWsCdPrhJ"
my.access.token = "99989439-wG82y2hMmAmlJ1iIlQgNu0l65ZOKVuVscj4Idm9Xu"
my.access.token.secret = "QNehvUZOGNNZyqFYDCAP6tlWEHWIBbKIhiqPEKAM2SOoT"
my_oauth <- setup_twitter_oauth(consumer_key = my.consumer.key, consumer_secret = my.consumer.secret, access_token = my.access.token, access_secret = my.access.token.secret)
[1] "Using direct authentication"
save(my_oauth, file = "my_oauth.Rdata")

Read Positive and Negative Words

neg = scan("negative-words.txt", what="character", comment.char=";")
Read 4783 items
pos = scan("positive-words.txt", what="character", comment.char=";")
Read 2006 items

Function for Scoring Tweets

score.sentiment = function(tweets, pos.words, neg.words)
{
scores = laply(tweets, function(tweet, pos.words, neg.words) {
tweet = gsub('https://','',tweet) # removes https://
tweet = gsub('http://','',tweet) # removes http://
tweet=gsub('[^[:graph:]]', ' ',tweet) ## removes graphic characters  #like emoticons 
tweet = gsub('[[:punct:]]', '', tweet) # removes punctuation 
tweet = gsub('[[:cntrl:]]', '', tweet) # removes control characters
tweet = gsub('\\d+', '', tweet) # removes numbers
tweet=str_replace_all(tweet,"[^[:graph:]]", " ") 
tweet = tolower(tweet) # makes all letters lowercase
word.list = str_split(tweet, '\\s+') # splits the tweets by word in a list
 
words = unlist(word.list) # turns the list into vector
 
pos.matches = match(words, pos.words) ## returns matching 
          #values for words from list 
neg.matches = match(words, neg.words)
 
pos.matches = !is.na(pos.matches) ## converts matching values to true of false
neg.matches = !is.na(neg.matches)
 
score = sum(pos.matches) - sum(neg.matches) # true and false are 
                #treated as 1 and 0 so they can be added
 
return(score)
 
}, pos.words, neg.words )
 
scores.df = data.frame(score=scores, text=tweets)
 
return(scores.df)
 
}

Extract Tweets

tweets = searchTwitter('Trump',n=2500)
Tweets.text = laply(tweets,function(t)t$getText()) # gets text from Tweets
analysis = score.sentiment(Tweets.text, pos, neg) # calls sentiment function

Plot Sentiment Scores

table(analysis$score)

 -5  -4  -3  -2  -1   0   1   2   3   4   5 
  1   2  55 121 406 597 826 295 181  14   2 
hist(analysis$score)

LS0tDQp0aXRsZTogIlR3aXR0ZXIgQW5hbHl0aWNzIFVzaW5nIFIiDQphdXRob3I6ICJEci4gUWl1c2hlbmcgV3UiDQpvdXRwdXQ6IA0KICBodG1sX25vdGVib29rOg0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KLS0tDQoNClRoZSB0dXRvcmlhbCB3aWxsIHdvcmsgdGhyb3VnaCBwcmFjdGljYWwgZXhhbXBsZXMgc2hvd2luZyB5b3UgaG93IHRvIGV4dHJhY3QgYW5kIHZpc3VhbGl6ZSBUd2l0dGVyIGRhdGEgdXNpbmcgUi4gDQoNCiMjIFBhcnQgMS4gRXh0cmFjdGluZyBUd2VldHMNCg0KIyMjIFByZXJlcXVpc2l0ZXMNCg0KMS4gWW91IGhhdmUgYWxyZWFkeSBpbnN0YWxsZWQgW1JdKGh0dHA6Ly93d3cuci1wcm9qZWN0Lm9yZy8pIGFuZCBhcmUgdXNpbmcgW1JTdHVkaW9dKGh0dHBzOi8vd3d3LnJzdHVkaW8uY29tL3Byb2R1Y3RzL3JzdHVkaW8vZG93bmxvYWQvKS4NCg0KMi4gSW4gb3JkZXIgdG8gZXh0cmFjdCB0d2VldHMsIHlvdSB3aWxsIG5lZWQgYSBUd2l0dGVyIGFwcGxpY2F0aW9uIGFuZCBoZW5jZSBhIFR3aXR0ZXIgYWNjb3VudC4gSWYgeW91IGRvbuKAmXQgaGF2ZSBhIFR3aXR0ZXIgYWNjb3VudCwgcGxlYXNlIFtzaWduIHVwXShodHRwczovL3R3aXR0ZXIuY29tL3NpZ251cCkuDQoNCjMuIFVzZSB5b3VyIFR3aXR0ZXIgbG9naW4gSUQgYW5kIHBhc3N3b3JkIHRvIHNpZ24gaW4gYXQgW1R3aXR0ZXIgRGV2ZWxvcGVyc10oaHR0cHM6Ly9hcHBzLnR3aXR0ZXIuY29tLykuDQoNCiMjIyBDcmVhdGUgYSBUd2l0dGVyIEFwcGxpY2F0aW9uDQoNCjEuIE5hdmlnYXRlIHRvIFtUd2l0dGVyIERldmVsb3BlcnNdKGh0dHBzOi8vYXBwcy50d2l0dGVyLmNvbS8pLiBDbGljayB0aGUgYnV0dG9uICoqQ3JlYXRlIE5ldyBBcHAqKiBpbiB0aGUgdXBwZXIgcmlnaHQgY29ybmVyLg0KIVtDcmVhdGUgTmV3IEFwcF0oaW1hZ2VzL2NyZWF0ZV9uZXdfYXBwLnBuZykNCg0KMi4gRmlsbCBpbiB0aGUgcmVxdWlyZWQgYXBwbGljYXRpb24gZGV0YWlscywgaW5jbHVkaW5nICoqTmFtZSoqLCAqKkRlc2NyaXB0aW9uKiosIGFuZCAqKldlYnNpdGUqKi4gTm90ZSB0aGF0IHRoZSAqKk5hbWUqKiBtdXN0IGJlIHVuaXF1ZS4gSWYgeW91ciBjaG9zZW4gbmFtZSBoYXMgYmVlbiB0YWtlbiwgdHJ5IGEgbmV3IG9uZS4gQ2xpY2sgdGhlIGJ1dHRvbiAqKkNyZWF0ZSB5b3VyIFR3aXR0ZXIgYXBwbGljYXRpb24qKiBpbiB0aGUgbG93ZXItbGVmdCBjb3JuZXIuDQohW0FwcCBEZXRhaWxzXShpbWFnZXMvYXBwX2RldGFpbHMucG5nKQ0KDQozLiBDbGljayB0aGUgKipLZXlzIGFuZCBBY2Nlc3MgVG9rZW5zKiogdGFiIHVuZGVyIHlvdXIgY3JlYXRlZCBUd2l0dGVyIEFwcGxpY2F0aW9uLiBUaGVuIGNsaWNrIHRoZSBidXR0b24gKipDcmVhdGUgbXkgYWNjZXNzIHRva2VuKiogaW4gdGhlIGxvd2VyLWxlZnQgY29ybmVyDQohW1Rva2VuIFRhYl0oaW1hZ2VzL3Rva2VuX3RhYi5wbmcpDQohW0NyZWF0ZSBUb2tlbnNdKGltYWdlcy9jcmVhdGVfdG9rZW5zLnBuZykNCg0KNC4gTm90ZSB0aGUgdmFsdWVzIG9mICoqQ29uc3VtZXIgS2V5IChBUEkgS2V5KSoqLCAqKkNvbnN1bWVyIFNlY3JldCAoQVBJIFNlY3JldCkqKiwgKipBY2Nlc3MgVG9rZW4qKiwgYW5kICoqQWNjZXNzIFRva2VuIFNlY3JldCoqIGhhbmR5IGZvciBmdXR1cmUgdXNlLiBZb3Ugc2hvdWxkIGtlZXAgdGhlc2Ugc2VjcmV0LiBJZiBhbnlvbmUgd2FzIHRvIGdldCB0aGVzZSBrZXlzLCB0aGV5IGNvdWxkIGVmZmVjdGl2ZWx5IGFjY2VzcyB5b3VyIFR3aXR0ZXIgYWNjb3VudC4NCiFbQWNjZXNzIFRva2Vuc10oaW1hZ2VzL2FjY2Vzc190b2tlbnMucG5nKQ0KDQo8YnI+DQoNCiMjIyBJbnN0YWxsIGFuZCBMb2FkIFIgUGFja2FnZXMNCg0KRm9yIHRoZSBwdXJwb3NlIG9mIHRoaXMgdHV0b3JpYWwsIHdlIHdpbGwgbmVlZCB0aGUgZm9sbG93aW5nIHBhY2thZ2VzOg0KDQoqICAqKlJPQXV0aCoqOiBQcm92aWRlcyBhbiBpbnRlcmZhY2UgdG8gdGhlIE9BdXRoIDEuMCBzcGVjaWZpY2F0aW9uLCBhbGxvd2luZyB1c2VycyB0byBhdXRoZW50aWNhdGUgdmlhIE9BdXRoIHRvIHRoZSBzZXJ2ZXIgb2YgdGhlaXIgY2hvaWNlLg0KDQoqICAqKlR3aXR0ZVIqKjogUHJvdmlkZXMgYW4gaW50ZXJmYWNlIHRvIHRoZSBUd2l0dGVyIHdlYiBBUEkuDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KaWYgKCFyZXF1aXJlKHR3aXR0ZVIpKSB7aW5zdGFsbC5wYWNrYWdlcygidHdpdHRlUiIpfQ0KaWYgKCFyZXF1aXJlKFJPQXV0aCkpIHtpbnN0YWxsLnBhY2thZ2VzKCJST0F1dGgiKX0NCmxpYnJhcnkodHdpdHRlUikNCmxpYnJhcnkoUk9BdXRoKQ0KYGBgDQoNCg0KIyMjIEF1dGhlbnRpY2F0aW9uIHdpdGggT0F1dGggDQoNCkF1dGhvcml6ZSBBcHAgdG8gdXNlIHlvdXIgYWNjb3VudCwgaS5lLiwgZXN0YWJsaXNoZWQgaGFuZHNoYWtlIGJldHdlZW4gVHdpdHRlciBhbmQgUi4NCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQojIHlvdSBtdXN0IGdldCB0aGUgZm9sbG93aW5nIGluZm9ybWF0aW9uIGZyb20gdGhlIFR3aXR0ZXIgQXBwIHlvdSBqdXN0IGNyZWF0ZWQNCm15LmNvbnN1bWVyLmtleSA9ICJmSDRJaWpjUVVyd3hFUTNtbWI2RzJnelVjIg0KbXkuY29uc3VtZXIuc2VjcmV0ID0gIkZ4a3VWNmVQeUZhaWEyTG15eGV0b0g1MEl4R3JRY0VZYnduTGUzRWpWRFdzQ2RQcmhKIg0KbXkuYWNjZXNzLnRva2VuID0gIjk5OTg5NDM5LXdHODJ5MmhNbUFtbEoxaUlsUWdOdTBsNjVaT0tWdVZzY2o0SWRtOVh1Ig0KbXkuYWNjZXNzLnRva2VuLnNlY3JldCA9ICJRTmVodlVaT0dOTlp5cUZZRENBUDZ0bFdFSFdJQmJLSWhpcVBFS0FNMlNPb1QiDQoNCm15X29hdXRoIDwtIHNldHVwX3R3aXR0ZXJfb2F1dGgoY29uc3VtZXJfa2V5ID0gbXkuY29uc3VtZXIua2V5LCBjb25zdW1lcl9zZWNyZXQgPSBteS5jb25zdW1lci5zZWNyZXQsIGFjY2Vzc190b2tlbiA9IG15LmFjY2Vzcy50b2tlbiwgYWNjZXNzX3NlY3JldCA9IG15LmFjY2Vzcy50b2tlbi5zZWNyZXQpDQoNCnNhdmUobXlfb2F1dGgsIGZpbGUgPSAibXlfb2F1dGguUmRhdGEiKQ0KYGBgDQohW10oaW1hZ2VzL2F1dGhvcml6ZV9hcHAucG5nKQ0KDQojIyMgRXh0cmFjdCBUd2VldHMgVXNpbmcgYSBTZWFyY2ggVGVybQ0KDQpgYGB7cn0NCnNlYXJjaC5zdHJpbmcgPC0gIiNIdXJyaWNhbmVOYXRlIg0KcmVzdWx0LnRlcm0gPC0gc2VhcmNoVHdpdHRlcihzZWFyY2guc3RyaW5nLCBuID0gMTAwKQ0KaGVhZChyZXN1bHQudGVybSkNCg0KYGBgDQoNCiMjIyBDb252ZXJ0IFJlc3VsdHMgdG8gRGF0YSBGcmFtZQ0KYGBge3J9DQpkZi50ZXJtIDwtIHR3TGlzdFRvREYocmVzdWx0LnRlcm0pDQp3cml0ZS5jc3YoZGYudGVybSwgIkh1cnJpY2FuZU5hdGUuY3N2IikNCmBgYA0KDQoNCiMjIyBTZWFyY2ggVHdlZXRzIFVzaW5nIGxhdC9sb24NCmBgYHtyfQ0KcmVzdWx0LmxhdGxvbiA8LSBzZWFyY2hUd2l0dGVyKCduYmEnLCBnZW9jb2RlPScyOS44MTc0LC05NS42ODE0LDIwbWknLCBuID0gMTAwKQ0KaGVhZChyZXN1bHQubGF0bG9uKQ0KZGYubGF0bG9uIDwtIHR3TGlzdFRvREYocmVzdWx0LmxhdGxvbikNCndyaXRlLmNzdihkZi5sYXRsb24sICJOQkEuY3N2IikNCmBgYA0KDQojIyMgSWRlbnRpZnkgVHJlbmRpbmcgVG9waWNzDQoNCllvdSBjYW4gdXNlIFR3aXR0ZVIgdG8gaWRlbnRpZnkgd2hhdCBpcyBjdXJyZW50bHkgInRyZW5kaW5nIiBvbiBUd2l0dGVyIGluIGEgc3BlY2lmaWMgbG9jYXRpb24gYnkgdXNpbmcgWWFob28ncyBXaGVyZSBPbiBFYXJ0aCBJRCwgb3IgKndvZWlkKi4gWW91IGNhbiBsb29rIGF0IGFsbCBwbGFjZXMgYXJvdW5kIHRoZSB3b3JsZCB0aGF0IGhhdmUgYSB3b2VpZCBieSBlbnRlcmluZyB0aGUgZm9sbG93aW5nIFIgc2NyaXB0Og0KDQpgYGB7cn0NCmF2YWlsYWJsZVRyZW5kTG9jYXRpb25zKCkNCmBgYA0KDQpZb3UgY2FuIGFsc28gZmluZCB0aGUgd29laWQgZm9yIGFueSBwbGFjZXMgbmVhciBhIHBhcnRpY3VsYXIgbGF0aXR1ZGUtbG9uZ2l0dWRlIGNvb3JkaW5hdGUgcGFpci4gVG8gZmluZCB0aGUgd29laWQgZm9yIE5ldyBZb3JrIENpdHksIHlvdSBjYW4gZW50ZXIgdGhlIGZvbGxvd2luZyBSIHNjcmlwdDoNCg0KYGBge3J9DQpjbG9zZXN0VHJlbmRMb2NhdGlvbnMoNDAuNzM2ODgxLC03My45ODg4NykNCmBgYA0KDQpMZXQncyB1c2UgdGhlIHdvZWlkIGZvciBOZXcgWW9yayB0byBjb2xsZWN0IGRhdGEgb24gd2hhdCBpcyB0cmVuZGluZyBpbiBOZXcgWW9yay4gDQpgYGB7cn0NCm55IDwtIGdldFRyZW5kcygyNDU5MTE1KQ0KaGVhZChueSxuID0gMTApDQp3cml0ZS5jc3YobnksICJOWXRyZW5kcy5jc3YiKQ0KYGBgDQoNCiMjIyBFeHRyYWN0IFR3ZWV0cyBmcm9tIGEgU3BlY2lmaWMgVXNlcg0KDQpUbyB0YWtlIGEgY2xvc2VyIGxvb2sgYXQgYSBUd2l0dGVyIHVzZXIgKGluY2x1ZGluZyB5b3Vyc2VsZiEpLCBydW4gdGhlIGNvbW1hbmQgZ2V0VXNlci4gVGhpcyB3aWxsIG9ubHkgd29yayBjb3JyZWN0bHkgd2l0aCB1c2VycyB3aG8gaGF2ZSB0aGVpciBwcm9maWxlcyBwdWJsaWMsIG9yIGlmIHlvdeKAmXJlIGF1dGhlbnRpY2F0ZWQgYW5kIGdyYW50ZWQgYWNjZXNzLiBZb3UgY2FuIGFsc28gc2VlIHRoaW5ncyBzdWNoIGFzIGEgdXNlcuKAmXMgZm9sbG93ZXJzLCB3aG8gdGhleSBmb2xsb3csIHJldHdlZXRzLCBhbmQgbW9yZS4gVGhlIGdldFVzZXIgZnVuY3Rpb24gcmV0dXJucyBhIHVzZXIgb2JqZWN0LCB3aGljaCBjYW4gdGhlbiBiZSBwb2xsZWQgZm9yIGZ1cnRoZXIgaW5mb3JtYXRpb24uDQoNCmBgYHtyfQ0KdGVzdF91c2VyIDwtIGdldFVzZXIoImJpbmdoYW10b251IikNCnRlc3RfdXNlciRpZA0KdGVzdF91c2VyJGdldERlc2NyaXB0aW9uKCkNCnRlc3RfdXNlciRnZXRGb2xsb3dlcnNDb3VudCgpDQp0ZXN0X3VzZXIkZ2V0RnJpZW5kcyhuPTUpDQpgYGANCg0KVGhlIHVzZXJUaW1lbGluZSBmdW5jdGlvbiB3aWxsIGFsbG93IHlvdSB0byByZXRyaWV2ZSB2YXJpb3VzIHRpbWVsaW5lcyB3aXRoaW4gdGhlIFR3aXR0ZXIgdW5pdmVyc2UuDQpgYGB7cn0NCnVzZXJUaW1lbGluZSh1c2VyID0gInJlYWxEb25hbGRUcnVtcCIsIG4gPSA1KQ0KYGBgDQoNCg0KDQojIyBQYXJ0IDIuIENyZWF0aW5nIFdvcmQgQ2xvdWQNCkluIHRoaXMgcGFydCB3ZSB3aWxsIHVzZSBSIHRvIHZpc3VhbGl6ZSB0d2VldHMgYXMgYSB3b3JkIGNsb3VkIHRvIGZpbmQgb3V0IHdoYXQgcGVvcGxlIGFyZSB0d2VldGluZyBhYm91dCB0aGUgTkJBICgjbmJhKS4gQSB3b3JkIGNsb3VkIGlzIGEgdmlzdWFsIHJlcHJlc2VudGF0aW9uIHNob3dpbmcgdGhlIG1vc3QgcmVsZXZhbnQgd29yZHMgKGkuZS4sIHRoZSBtb3JlIHRpbWVzIGEgd29yZCBhcHBlYXJzIGluIG91ciB0d2VldCBzYW1wbGluZyB0aGUgYmlnZ2VyIHRoZSB3b3JkKS4NCg0KIyMjIEF1dGhlbnRpY2F0aW9uIHdpdGggT0F1dGgNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KaWYgKCFyZXF1aXJlKHR3aXR0ZVIpKSB7aW5zdGFsbC5wYWNrYWdlcygidHdpdHRlUiIpfQ0KaWYgKCFyZXF1aXJlKFJPQXV0aCkpIHtpbnN0YWxsLnBhY2thZ2VzKCJST0F1dGgiKX0NCmxpYnJhcnkodHdpdHRlUikNCmxpYnJhcnkoUk9BdXRoKQ0KDQojIHlvdSBtdXN0IGdldCB0aGUgZm9sbG93aW5nIGluZm9ybWF0aW9uIGZyb20gdGhlIFR3aXR0ZXIgQXBwIHlvdSBqdXN0IGNyZWF0ZWQNCm15LmNvbnN1bWVyLmtleSA9ICJmSDRJaWpjUVVyd3hFUTNtbWI2RzJnelVjIg0KbXkuY29uc3VtZXIuc2VjcmV0ID0gIkZ4a3VWNmVQeUZhaWEyTG15eGV0b0g1MEl4R3JRY0VZYnduTGUzRWpWRFdzQ2RQcmhKIg0KbXkuYWNjZXNzLnRva2VuID0gIjk5OTg5NDM5LXdHODJ5MmhNbUFtbEoxaUlsUWdOdTBsNjVaT0tWdVZzY2o0SWRtOVh1Ig0KbXkuYWNjZXNzLnRva2VuLnNlY3JldCA9ICJRTmVodlVaT0dOTlp5cUZZRENBUDZ0bFdFSFdJQmJLSWhpcVBFS0FNMlNPb1QiDQoNCm15X29hdXRoIDwtIHNldHVwX3R3aXR0ZXJfb2F1dGgoY29uc3VtZXJfa2V5ID0gbXkuY29uc3VtZXIua2V5LCBjb25zdW1lcl9zZWNyZXQgPSBteS5jb25zdW1lci5zZWNyZXQsIGFjY2Vzc190b2tlbiA9IG15LmFjY2Vzcy50b2tlbiwgYWNjZXNzX3NlY3JldCA9IG15LmFjY2Vzcy50b2tlbi5zZWNyZXQpDQpgYGANCg0KIyMjIEV4dHJhY3QgVHdlZXRzDQpgYGB7cn0NCnR3ZWV0cyA8LSBzZWFyY2hUd2l0dGVyKCIjbmJhIiwgbj0xMDAwLCBsYW5nPSJlbiIpDQp0d2VldHMudGV4dCA8LSBzYXBwbHkodHdlZXRzLCBmdW5jdGlvbih4KSB4JGdldFRleHQoKSkNCmBgYA0KDQojIyMgQ2xlYW4gVXAgVGV4dA0KV2UgaGF2ZSBhbHJlYWR5IGJlZW4gYXV0aGVudGljYXRlZCBhbmQgc3VjY2Vzc2Z1bGx5IHJldHJpZXZlZCB0aGUgdGV4dCBmcm9tIHRoZSB0d2VldHMgdXNpbmcgI25iYS4gVGhlIGZpcnN0IHN0ZXAgaW4gY3JlYXRpbmcgYSB3b3JkIGNsb3VkIGlzIHRvIGNsZWFuIHVwIHRoZSB0ZXh0IGJ5IHVzaW5nIGxvd2VyY2FzZSBhbmQgcmVtb3ZpbmcgcHVuY3R1YXRpb24sIHVzZXJuYW1lcywgbGlua3MsIGV0Yy4gV2UgYXJlIHVzaW5nIHRoZSBmdW5jdGlvbiBnc3ViIHRvIHJlcGxhY2UgdW53YW50ZWQgdGV4dC4gR3N1YiB3aWxsIHJlcGxhY2UgYWxsIG9jY3VycmVuY2VzIG9mIGFueSBnaXZlbiBwYXR0ZXJuLiBBbHRob3VnaCB0aGVyZSBhcmUgYWx0ZXJuYXRpdmUgcGFja2FnZXMgdGhhdCBjYW4gcGVyZm9ybSB0aGlzIG9wZXJhdGlvbiwgd2UgaGF2ZSBjaG9zZW4gZ3N1YiBiZWNhdXNlIG9mIGl0cyBzaW1wbGljaXR5IGFuZCByZWFkYWJpbGl0eS4NCg0KYGBge3J9DQojIFJlcGxhY2UgYmxhbmsgc3BhY2UgKOKAnHJ04oCdKQ0KdHdlZXRzLnRleHQgPC0gZ3N1YigicnQiLCAiIiwgdHdlZXRzLnRleHQpDQoNCiMgUmVwbGFjZSBAVXNlck5hbWUNCnR3ZWV0cy50ZXh0IDwtIGdzdWIoIkBcXHcrIiwgIiIsIHR3ZWV0cy50ZXh0KQ0KDQojIFJlbW92ZSBwdW5jdHVhdGlvbg0KdHdlZXRzLnRleHQgPC0gZ3N1YigiW1s6cHVuY3Q6XV0iLCAiIiwgdHdlZXRzLnRleHQpDQoNCiMgUmVtb3ZlIGxpbmtzDQp0d2VldHMudGV4dCA8LSBnc3ViKCJodHRwXFx3KyIsICIiLCB0d2VldHMudGV4dCkNCg0KIyBSZW1vdmUgdGFicw0KdHdlZXRzLnRleHQgPC0gZ3N1YigiWyB8XHRdezIsfSIsICIiLCB0d2VldHMudGV4dCkNCg0KIyBSZW1vdmUgYmxhbmsgc3BhY2VzIGF0IHRoZSBiZWdpbm5pbmcNCnR3ZWV0cy50ZXh0IDwtIGdzdWIoIl4gIiwgIiIsIHR3ZWV0cy50ZXh0KQ0KDQojIFJlbW92ZSBibGFuayBzcGFjZXMgYXQgdGhlIGVuZA0KdHdlZXRzLnRleHQgPC0gZ3N1YigiICQiLCAiIiwgdHdlZXRzLnRleHQpDQogDQojICNjb252ZXJ0IGFsbCB0ZXh0IHRvIGxvd2VyIGNhc2UNCnR3ZWV0cy50ZXh0IDwtIHRvbG93ZXIodHdlZXRzLnRleHQpDQpgYGANCg0KIyMjIFJlbW92ZSBTdG9wIFdvcmRzDQpJbiB0aGUgbmV4dCBzdGVwIHdlIHdpbGwgdXNlIHRoZSB0ZXh0IG1pbmluZyBwYWNrYWdlIHRtIHRvIHJlbW92ZSBzdG9wIHdvcmRzLiBBIHN0b3Agd29yZCBpcyBhIGNvbW1vbmx5IHVzZWQgd29yZCBzdWNoIGFzIOKAnHRoZeKAnS4gU3RvcCB3b3JkcyBzaG91bGQgbm90IGJlIGluY2x1ZGVkIGluIHRoZSBhbmFseXNpcy4NCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KaWYoIXJlcXVpcmUodG0pKSB7aW5zdGFsbC5wYWNrYWdlcygidG0iKX0NCmxpYnJhcnkodG0pDQoNCiNjcmVhdGUgY29ycHVzDQp0d2VldHMudGV4dC5jb3JwdXMgPC0gQ29ycHVzKFZlY3RvclNvdXJjZSh0d2VldHMudGV4dCkpDQoNCiNjbGVhbiB1cCBieSByZW1vdmluZyBzdG9wIHdvcmRzDQp0d2VldHMudGV4dC5jb3JwdXMgPC0gdG1fbWFwKHR3ZWV0cy50ZXh0LmNvcnB1cywgZnVuY3Rpb24oeCkgcmVtb3ZlV29yZHMoeCxzdG9wd29yZHMoKSkpDQpgYGANCg0KDQojIyMgR2VuZXJhdGUgV29yZCBDbG91ZA0KTm93IHdl4oCZbGwgZ2VuZXJhdGUgdGhlIHdvcmQgY2xvdWQgdXNpbmcgdGhlIHdvcmRjbG91ZCBwYWNrYWdlLiBGb3IgdGhpcyBleGFtcGxlIHdlIGFyZSBjb25jZXJuZWQgd2l0aCBwbG90dGluZyBubyBtb3JlIHRoYW4gMTUwIHdvcmRzIHRoYXQgb2NjdXIgbW9yZSB0aGFuIG9uY2Ugd2l0aCByYW5kb20gY29sb3IsIG9yZGVyLCBhbmQgcG9zaXRpb24uDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KaWYoIXJlcXVpcmUod29yZGNsb3VkKSkge2luc3RhbGwucGFja2FnZXMoIndvcmRjbG91ZCIpfQ0KbGlicmFyeSh3b3JkY2xvdWQpDQoNCiNnZW5lcmF0ZSB3b3JkY2xvdWQNCndvcmRjbG91ZCh0d2VldHMudGV4dC5jb3JwdXMsbWluLmZyZXEgPSAyLCBzY2FsZT1jKDcsMC41KSxjb2xvcnM9YnJld2VyLnBhbCg4LCAiRGFyazIiKSwgIHJhbmRvbS5jb2xvcj0gVFJVRSwgcmFuZG9tLm9yZGVyID0gRkFMU0UsIG1heC53b3JkcyA9IDE1MCkNCmBgYA0KDQojIyBQYXJ0IDMuIFNlbnRpbWVudCBBbmFseXNpcw0KDQpTZW50aW1lbnQgYW5hbHlzZXMgY2xhc3NpZnkgY29tbXVuaWNhdGlvbnMgYXMgcG9zaXRpdmUsIG5lZ2F0aXZlLCBvciBuZXV0cmFsLiBEZXRlcm1pbmluZyBzZW50aW1lbnQgcmFuZ2VzIGZyb20gdmVyeSBzaW1wbGUgY2xhc3NpZmljYXRpb24gbWV0aG9kcyB0byB2ZXJ5IGNvbXBsZXggYWxnb3JpdGhtcy4gRm9yIGVhc2UgYW5kIHRyYW5zcGFyZW5jeSBpbiB0aGlzIGV4YW1wbGUsIHdlIHdpbGwgY2xhc3NpZnkgdGhlIHNlbnRpbWVudCBvZiBhIHR3ZWV0IGJhc2VkIG9uIHRoZSBwb2xhcml0eSBvZiB0aGUgaW5kaXZpZHVhbCB3b3Jkcy4gRWFjaCB3b3JkIHdpbGwgYmUgZ2l2ZW4gYSBzY29yZSBvZiArMSBpZiBjbGFzc2lmaWVkIGFzIHBvc2l0aXZlLCAtMSBpZiBuZWdhdGl2ZSwgYW5kIDAgaWYgY2xhc3NpZmllZCBhcyBuZXV0cmFsLiBUaGlzIHdpbGwgYmUgZGV0ZXJtaW5lZCB1c2luZyBwb3NpdGl2ZSBhbmQgbmVnYXRpdmUgbGV4aWNvbiBsaXN0cyBjb21waWxlZCBieSBNaW5xaW5nIEh1IGFuZCBCaW5nIExpdSBmb3IgdGhlaXIgd29yayDigJxbTWluaW5nIGFuZCBTdW1tYXJpemluZyBDdXN0b21lciBSZXZpZXdzXShodHRwczovL3d3dy5jcy51aWMuZWR1L35saXViL0ZCUy9zZW50aW1lbnQtYW5hbHlzaXMuaHRtbCnigJ0uIFRoZSB0b3RhbCBwb2xhcml0eSBzY29yZSBvZiBhIGdpdmVuIHR3ZWV0IHdpbGwgcmVzdWx0IGluIGFkZGluZyB0b2dldGhlciB0aGUgc2NvcmVzIG9mIGFsbCB0aGUgaW5kaXZpZHVhbCB3b3Jkcy4gT25jZSB5b3UgZ28gdG8gdGhlIHBhZ2UsIGNsaWNrIG9uIE9waW5pb24gTGV4aWNvbiBhbmQgdGhlbiBkb3dubG9hZCB0aGUgcmFyIGZpbGUuDQoNCg0KIyMjIEluc3RhbGwgYW5kIExvYWQgUiBQYWNrYWdlcw0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQogIyBJbnN0YWxsIHBhY2thZ2VzIGZvciBzZW50aW1lbnQgYW5hbHlzaXMNCmlmICghcmVxdWlyZSh0d2l0dGVSKSkge2luc3RhbGwucGFja2FnZXMoInR3aXR0ZVIiKX0NCmlmICghcmVxdWlyZShST0F1dGgpKSB7aW5zdGFsbC5wYWNrYWdlcygiUk9BdXRoIil9DQppZiAoIXJlcXVpcmUocGx5cikpIHtpbnN0YWxsLnBhY2thZ2VzKCJwbHlyIil9DQppZiAoIXJlcXVpcmUoZHBseXIpKSB7aW5zdGFsbC5wYWNrYWdlcygiZHBseXIiKX0NCmlmICghcmVxdWlyZShzdHJpbmdyKSkge2luc3RhbGwucGFja2FnZXMoInN0cmluZ3IiKX0NCmlmICghcmVxdWlyZShnZ3Bsb3QyKSkge2luc3RhbGwucGFja2FnZXMoImdncGxvdDIiKX0NCg0KbGlicmFyeSh0d2l0dGVSKQ0KbGlicmFyeShST0F1dGgpDQpsaWJyYXJ5KHBseXIpDQpsaWJyYXJ5KGRwbHlyKQ0KbGlicmFyeShzdHJpbmdyKQ0KbGlicmFyeShnZ3Bsb3QyKQ0KYGBgDQoNCg0KIyMjIEF1dGhlbnRpY2F0aW9uIHdpdGggT0F1dGggDQpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCiMgeW91IG11c3QgZ2V0IHRoZSBmb2xsb3dpbmcgaW5mb3JtYXRpb24gZnJvbSB0aGUgVHdpdHRlciBBcHAgeW91IGp1c3QgY3JlYXRlZA0KbXkuY29uc3VtZXIua2V5ID0gImZINElpamNRVXJ3eEVRM21tYjZHMmd6VWMiDQpteS5jb25zdW1lci5zZWNyZXQgPSAiRnhrdVY2ZVB5RmFpYTJMbXl4ZXRvSDUwSXhHclFjRVlid25MZTNFalZEV3NDZFByaEoiDQpteS5hY2Nlc3MudG9rZW4gPSAiOTk5ODk0Mzktd0c4MnkyaE1tQW1sSjFpSWxRZ051MGw2NVpPS1Z1VnNjajRJZG05WHUiDQpteS5hY2Nlc3MudG9rZW4uc2VjcmV0ID0gIlFOZWh2VVpPR05OWnlxRllEQ0FQNnRsV0VIV0lCYktJaGlxUEVLQU0yU09vVCINCg0KbXlfb2F1dGggPC0gc2V0dXBfdHdpdHRlcl9vYXV0aChjb25zdW1lcl9rZXkgPSBteS5jb25zdW1lci5rZXksIGNvbnN1bWVyX3NlY3JldCA9IG15LmNvbnN1bWVyLnNlY3JldCwgYWNjZXNzX3Rva2VuID0gbXkuYWNjZXNzLnRva2VuLCBhY2Nlc3Nfc2VjcmV0ID0gbXkuYWNjZXNzLnRva2VuLnNlY3JldCkNCg0Kc2F2ZShteV9vYXV0aCwgZmlsZSA9ICJteV9vYXV0aC5SZGF0YSIpDQpgYGANCg0KIyMjIFJlYWQgUG9zaXRpdmUgYW5kIE5lZ2F0aXZlIFdvcmRzDQpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCm5lZyA9IHNjYW4oIm5lZ2F0aXZlLXdvcmRzLnR4dCIsIHdoYXQ9ImNoYXJhY3RlciIsIGNvbW1lbnQuY2hhcj0iOyIpDQpwb3MgPSBzY2FuKCJwb3NpdGl2ZS13b3Jkcy50eHQiLCB3aGF0PSJjaGFyYWN0ZXIiLCBjb21tZW50LmNoYXI9IjsiKQ0KYGBgDQoNCiMjIyBGdW5jdGlvbiBmb3IgU2NvcmluZyBUd2VldHMNCmBgYHtyfQ0Kc2NvcmUuc2VudGltZW50ID0gZnVuY3Rpb24odHdlZXRzLCBwb3Mud29yZHMsIG5lZy53b3JkcykNCg0Kew0Kc2NvcmVzID0gbGFwbHkodHdlZXRzLCBmdW5jdGlvbih0d2VldCwgcG9zLndvcmRzLCBuZWcud29yZHMpIHsNCg0KdHdlZXQgPSBnc3ViKCdodHRwczovLycsJycsdHdlZXQpICMgcmVtb3ZlcyBodHRwczovLw0KdHdlZXQgPSBnc3ViKCdodHRwOi8vJywnJyx0d2VldCkgIyByZW1vdmVzIGh0dHA6Ly8NCnR3ZWV0PWdzdWIoJ1teWzpncmFwaDpdXScsICcgJyx0d2VldCkgIyMgcmVtb3ZlcyBncmFwaGljIGNoYXJhY3RlcnMgICNsaWtlIGVtb3RpY29ucyANCnR3ZWV0ID0gZ3N1YignW1s6cHVuY3Q6XV0nLCAnJywgdHdlZXQpICMgcmVtb3ZlcyBwdW5jdHVhdGlvbiANCnR3ZWV0ID0gZ3N1YignW1s6Y250cmw6XV0nLCAnJywgdHdlZXQpICMgcmVtb3ZlcyBjb250cm9sIGNoYXJhY3RlcnMNCnR3ZWV0ID0gZ3N1YignXFxkKycsICcnLCB0d2VldCkgIyByZW1vdmVzIG51bWJlcnMNCnR3ZWV0PXN0cl9yZXBsYWNlX2FsbCh0d2VldCwiW15bOmdyYXBoOl1dIiwgIiAiKSANCnR3ZWV0ID0gdG9sb3dlcih0d2VldCkgIyBtYWtlcyBhbGwgbGV0dGVycyBsb3dlcmNhc2UNCg0Kd29yZC5saXN0ID0gc3RyX3NwbGl0KHR3ZWV0LCAnXFxzKycpICMgc3BsaXRzIHRoZSB0d2VldHMgYnkgd29yZCBpbiBhIGxpc3QNCndvcmRzID0gdW5saXN0KHdvcmQubGlzdCkgIyB0dXJucyB0aGUgbGlzdCBpbnRvIHZlY3Rvcg0KcG9zLm1hdGNoZXMgPSBtYXRjaCh3b3JkcywgcG9zLndvcmRzKSAjIyByZXR1cm5zIG1hdGNoaW5nIHZhbHVlcyBmb3Igd29yZHMgZnJvbSBsaXN0IA0KbmVnLm1hdGNoZXMgPSBtYXRjaCh3b3JkcywgbmVnLndvcmRzKQ0KcG9zLm1hdGNoZXMgPSAhaXMubmEocG9zLm1hdGNoZXMpICMjIGNvbnZlcnRzIG1hdGNoaW5nIHZhbHVlcyB0byB0cnVlIG9mIGZhbHNlDQpuZWcubWF0Y2hlcyA9ICFpcy5uYShuZWcubWF0Y2hlcykNCiANCnNjb3JlID0gc3VtKHBvcy5tYXRjaGVzKSAtIHN1bShuZWcubWF0Y2hlcykgIyB0cnVlIGFuZCBmYWxzZSBhcmUgdHJlYXRlZCBhcyAxIGFuZCAwIHNvIHRoZXkgY2FuIGJlIGFkZGVkDQogDQpyZXR1cm4oc2NvcmUpDQogDQp9LCBwb3Mud29yZHMsIG5lZy53b3JkcyApDQogDQpzY29yZXMuZGYgPSBkYXRhLmZyYW1lKHNjb3JlPXNjb3JlcywgdGV4dD10d2VldHMpDQogDQpyZXR1cm4oc2NvcmVzLmRmKQ0KIA0KfQ0KYGBgDQoNCiMjIyBFeHRyYWN0IFR3ZWV0cw0KYGBge3J9DQp0d2VldHMgPSBzZWFyY2hUd2l0dGVyKCdUcnVtcCcsbj0yNTAwKQ0KVHdlZXRzLnRleHQgPSBsYXBseSh0d2VldHMsZnVuY3Rpb24odCl0JGdldFRleHQoKSkgIyBnZXRzIHRleHQgZnJvbSBUd2VldHMNCg0KYW5hbHlzaXMgPSBzY29yZS5zZW50aW1lbnQoVHdlZXRzLnRleHQsIHBvcywgbmVnKSAjIGNhbGxzIHNlbnRpbWVudCBmdW5jdGlvbg0KYGBgDQoNCg0KIyMjIFBsb3QgU2VudGltZW50IFNjb3Jlcw0KYGBge3J9DQp0YWJsZShhbmFseXNpcyRzY29yZSkNCmhpc3QoYW5hbHlzaXMkc2NvcmUpDQpgYGANCg==