This is to give you some tools for calculating the Fightin Words statistic, extracting top-ranked terms, and plotting. Bare bones at the moment.

load libraries

library(tidyr)
library(dplyr)
library(ggplot2)
library(ggrepel)

load the FW functions

fwgroups <- function(dtm, groups, pair = NULL, weights = rep(1,nrow(dtm)), k.prior = .1) {
  
  weights[is.na(weights)] <- 0
  
  weights <- weights/mean(weights)
  
  zero.doc <- rowSums(dtm)==0 | weights==0
  zero.term <- colSums(dtm[!zero.doc,])==0
  
  dtm.nz <- apply(dtm[!zero.doc,!zero.term],2,"*", weights[!zero.doc])
  
  g.prior <- tcrossprod(rowSums(dtm.nz),colSums(dtm.nz))/sum(dtm.nz)
  
  # 
  
  g.posterior <- as.matrix(dtm.nz + k.prior*g.prior)
  
  groups <- groups[!zero.doc]
  groups <- droplevels(groups)
  
  g.adtm <- as.matrix(aggregate(x=g.posterior,by=list(groups=groups),FUN=sum)[,-1])
  rownames(g.adtm) <- levels(groups)
  
  g.ladtm <- log(g.adtm)
  
  g.delta <- t(scale( t(scale(g.ladtm, center=T, scale=F)), center=T, scale=F))
  
  g.adtm_w <- -sweep(g.adtm,1,rowSums(g.adtm)) # terms not w spoken by k
  g.adtm_k <- -sweep(g.adtm,2,colSums(g.adtm)) # w spoken by groups other than k
  g.adtm_kw <- sum(g.adtm) - g.adtm_w - g.adtm_k - g.adtm # total terms not w or k 
  
  g.se <- sqrt(1/g.adtm + 1/g.adtm_w + 1/g.adtm_k + 1/g.adtm_kw)
  
  g.zeta <- g.delta/g.se
  
  g.counts <- as.matrix(aggregate(x=dtm.nz, by = list(groups=groups), FUN=sum)[,-1])
  
  if (!is.null(pair)) {
    pr.delta <- t(scale( t(scale(g.ladtm[pair,], center = T, scale =F)), center=T, scale=F))
    pr.adtm_w <- -sweep(g.adtm[pair,],1,rowSums(g.adtm[pair,]))
    pr.adtm_k <- -sweep(g.adtm[pair,],2,colSums(g.adtm[pair,])) # w spoken by groups other than k
    pr.adtm_kw <- sum(g.adtm[pair,]) - pr.adtm_w - pr.adtm_k - g.adtm[pair,] # total terms not w or k
    pr.se <- sqrt(1/g.adtm[pair,] + 1/pr.adtm_w + 1/pr.adtm_k + 1/pr.adtm_kw)
    pr.zeta <- pr.delta/pr.se
    
    return(list(zeta=pr.zeta[1,], delta=pr.delta[1,],se=pr.se[1,], counts = colSums(dtm.nz), acounts = colSums(g.adtm)))
  } else {
    return(list(zeta=g.zeta,delta=g.delta,se=g.se,counts=g.counts,acounts=g.adtm))
  }
}
############## FIGHTIN' WORDS PLOTTING FUNCTION
# helper function
makeTransparent<-function(someColor, alpha=100)
{
  newColor<-col2rgb(someColor)
  apply(newColor, 2, function(curcoldata){rgb(red=curcoldata[1], green=curcoldata[2],
                                              blue=curcoldata[3],alpha=alpha, maxColorValue=255)})
}
fw.ggplot.groups <- function(fw.ch, groups.use = as.factor(rownames(fw.ch$zeta)), max.words = 50, max.countrank = 400, colorpalette=rep("black",length(groups.use)), sizescale=2, title="Comparison of Terms by Groups", subtitle = "", caption = "Group-specific terms are ordered by Fightin' Words statistic (Monroe, et al. 2008)") {
  if (is.null(dim(fw.ch$zeta))) {## two-group fw object consists of vectors, not matrices
    zetarankmat <- cbind(rank(-fw.ch$zeta),rank(fw.ch$zeta))
    colnames(zetarankmat) <- groups.use
    countrank <- rank(-(fw.ch$counts))
  } else {
    zetarankmat <- apply(-fw.ch$zeta[groups.use,],1,rank)
    countrank <- rank(-colSums(fw.ch$counts))
  }
  wideplotmat <- as_tibble(cbind(zetarankmat,countrank=countrank))
  wideplotmat$term=names(countrank)
  #rankplot <- gather(wideplotmat, party, zetarank, 1:ncol(zetarankmat))
  rankplot <- gather(wideplotmat, groups.use, zetarank, 1:ncol(zetarankmat))
  rankplot$plotsize <- sizescale*(50/(rankplot$zetarank))^(1/4)
  rankplot <- rankplot[rankplot$zetarank < max.words + 1 & rankplot$countrank<max.countrank+1,]
  rankplot$groups.use <- factor(rankplot$groups.use,levels=groups.use)
  
  p <- ggplot(rankplot, aes((nrow(rankplot)-countrank)^1, -(zetarank^1), colour=groups.use)) + 
    geom_point(show.legend=F,size=sizescale/2) + 
    theme_classic() +
    theme(axis.ticks=element_blank(), axis.text=element_blank() ) +
    ylim(-max.words,40) +
    facet_grid(groups.use ~ .) +
    geom_text_repel(aes(label = term), size = rankplot$plotsize, point.padding=.05,
                    box.padding = unit(0.20, "lines"), show.legend=F) +
    scale_colour_manual(values = alpha(colorpalette, .7)) + 
#    labs(x="Terms used more frequently overall →", y="Terms used more frequently by group →",  title=title, subtitle=subtitle , caption = caption) 
    labs(x=paste("Terms used more frequently overall -->"), y=paste("Terms used more frequently by group -->"),  title=title, subtitle=subtitle , caption = caption) 
  
}
fw.keys <- function(fw.ch,n.keys=10) {
  n.groups <- nrow(fw.ch$zeta)
  keys <- matrix("",n.keys,n.groups)
  colnames(keys) <- rownames(fw.ch$zeta)
  
  for (g in 1:n.groups) {
    keys[,g] <- names(sort(fw.ch$zeta[g,],dec=T)[1:n.keys])
  }
  keys
}

Compare “Poliblog” data by Ideological Rating

Load the data

poliblog.dfm <- readRDS("poliblog5k.dfm.rds")
poliblog.meta <- readRDS("poliblog5k.fullmeta.rds")

Calculate FW.

fw.blogideo <- fwgroups(poliblog.dfm,groups = poliblog5k.meta$rating)

Get and show the top words per group by zeta.

library(knitr)
fwkeys.blogideo <- fw.keys(fw.blogideo, n.keys=20)
kable(fwkeys.blogideo)
Conservative Liberal
israel mccain
obama bush
eastern said
wright sen
chicago presid
especi linktocom
isra postcount
hama postcounttb
russian tortur
exit iraq
ed john
v campaign
may today
tip administr
rezko â
via pm
border ad
palestinian know
either dem
pakistani rove

Plot

p.fw.blogideo <- fw.ggplot.groups(fw.blogideo,sizescale=4,max.words=200,max.countrank=400,colorpalette=c("red","blue"))
p.fw.blogideo

Calculate by individual blog

Calculate FW and keys

fw.blogs <- fwgroups(poliblog.dfm,groups = poliblog5k.meta$blog)

Get and show the top words per group by zeta.

library(knitr)
fwkeys.blogs <- fw.keys(fw.blogs, n.keys=15)
kable(fwkeys.blogs)
at db ha mm tp tpm
israel linktocom especi eastern bush obama
mr postcount exit r sen campaign
isra postcounttb v updat iraq hillari
russian pm franken pentagon said dem
chicago tortur although dead mccain ad
hama digbi obama romney u. poll
jewish film sadr illeg rove mccain
palestinian dday either alien administr â
tip matthew hillari confirm watch franken
wright like begun legisl presid senat
obama think may immigr iraqi et
iranian villag want build tortur camp
iran religi rezko bailout http race
jew republican rather paulson r-az lead
pakistani thing least la percent late

Plot

p.fw.blogs <- fw.ggplot.groups(fw.blogs,sizescale=3,max.words=200,max.countrank=400)
p.fw.blogs

Use to identify topic keywords

FW makes a better (in my opinion) extractor of keywords than FREX, Lift, Score, etc.

Load a topic model (The no metadata 20-topic model from the STM notebook).

stm.nm <- readRDS("poliblog5k.fit.nometa.rds")

Calculate expected word frequency per topic across corpus:

stm.nm.top_word_tots <- colSums(sweep(stm.nm$theta,1,rowSums(poliblog.dfm),"*"))
stm.nm.top_word_assigns <- sweep(exp(stm.nm.beta),1,stm.nm.top_word_tots,"*")

Calculate FW

fw.topics.stm <- fwgroups(stm.nm.top_word_assigns,groups = as.factor(1:20))

Get and show the top words per topic by zeta.

library(knitr)
fwkeys.stm.nm <- fw.keys(fw.topics.stm, n.keys=15)
kable(fwkeys.stm.nm)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
law democrat think women don obama race attack obama tax obama war stori bush mccain world voter governor money clinton
congress republican know health didn barack gop terrorist senat economi mccain iraq report presid john america elect offici billion hillari
bill parti thing life doesn campaign dem nuclear debat econom poll militari global administr campaign countri vote report million win
administr elect peopl care show polit seat terror said american voter forc york white palin war organ depart crisi campaign
court conserv like church updat black senat iran polici health percent troop articl news romney foreign group offic market democrat
bush gop realli children get chicago rep weapon foreign job barack iraqi book hous governor nation union case hous primari
justic vote right educ eastern church sen bomb barack compani lead armi polic said attack unit illeg committe spend convent
legal polit liber famili reader wright ballot israel presid cut state govern build watch sen georgia state email feder race
rule candid way school isn michell poll threat joe energi among secur dead interview alaska nuclear communiti charg govern romney
intellig win media human video race campaign kill biden cost florida qaeda confirm don experi peac california former fund candid
presid hous say univers love america count east meet price number afghanistan publish report sarah south ballot staff congress parti
judg progress guy social night team candid muslim campaign will ohio pakistan journalist sen talk power worker investig dollar nomin
protect congress someth god exit advis vote qaeda committe plan ralli surg climat press raz intern border inform compani florida
act voter linktocommentspostcount research see candid district intellig bill oil margin command warm fox huckabe must immigr senat street michigan
constitut reagan postcounttb christian sure associ challeng peac sen increas virginia withdraw news vice gov govern regist justic wall carolina

Plot

p.stm.nm <- fw.ggplot.topics(fw.topics.stm,sizescale=2,max.words=50,max.countrank=400)
p.stm.nm

LS0tCnRpdGxlOiAiQW4gSW50cm9kdWN0aW9uIHRvIEZpZ2h0aW4nIFdvcmRzIGluIFIiCnN1YnRpdGxlOiAiUExTQyA1OTc6IFRleHQgYXMgRGF0YSwgUGVubiBTdGF0ZSIKYXV0aG9yOiBCdXJ0IEwuIE1vbnJvZQpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazoKICAgIGNvZGVfZm9sZGluZzogc2hvdwogICAgZGZfcHJpbnQ6IHBhZ2VkCiAgICBoaWdobGlnaHQ6IHRhbmdvCiAgICB0aGVtZTogdW5pdGVkCiAgICB0b2M6IHllcwotLS0KVGhpcyBpcyB0byBnaXZlIHlvdSBzb21lIHRvb2xzIGZvciBjYWxjdWxhdGluZyB0aGUgRmlnaHRpbiBXb3JkcyBzdGF0aXN0aWMsIGV4dHJhY3RpbmcgdG9wLXJhbmtlZCB0ZXJtcywgYW5kIHBsb3R0aW5nLiBCYXJlIGJvbmVzIGF0IHRoZSBtb21lbnQuCgojIyBsb2FkIGxpYnJhcmllcwoKYGBge3J9CmxpYnJhcnkodGlkeXIpCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkoZ2dwbG90MikKbGlicmFyeShnZ3JlcGVsKQpgYGAKCiMjIGxvYWQgdGhlIEZXIGZ1bmN0aW9ucwoKYGBge3J9CmZ3Z3JvdXBzIDwtIGZ1bmN0aW9uKGR0bSwgZ3JvdXBzLCBwYWlyID0gTlVMTCwgd2VpZ2h0cyA9IHJlcCgxLG5yb3coZHRtKSksIGsucHJpb3IgPSAuMSkgewogIAogIHdlaWdodHNbaXMubmEod2VpZ2h0cyldIDwtIDAKICAKICB3ZWlnaHRzIDwtIHdlaWdodHMvbWVhbih3ZWlnaHRzKQogIAogIHplcm8uZG9jIDwtIHJvd1N1bXMoZHRtKT09MCB8IHdlaWdodHM9PTAKICB6ZXJvLnRlcm0gPC0gY29sU3VtcyhkdG1bIXplcm8uZG9jLF0pPT0wCiAgCiAgZHRtLm56IDwtIGFwcGx5KGR0bVshemVyby5kb2MsIXplcm8udGVybV0sMiwiKiIsIHdlaWdodHNbIXplcm8uZG9jXSkKICAKICBnLnByaW9yIDwtIHRjcm9zc3Byb2Qocm93U3VtcyhkdG0ubnopLGNvbFN1bXMoZHRtLm56KSkvc3VtKGR0bS5ueikKICAKICAjIAogIAogIGcucG9zdGVyaW9yIDwtIGFzLm1hdHJpeChkdG0ubnogKyBrLnByaW9yKmcucHJpb3IpCiAgCiAgZ3JvdXBzIDwtIGdyb3Vwc1shemVyby5kb2NdCiAgZ3JvdXBzIDwtIGRyb3BsZXZlbHMoZ3JvdXBzKQogIAogIGcuYWR0bSA8LSBhcy5tYXRyaXgoYWdncmVnYXRlKHg9Zy5wb3N0ZXJpb3IsYnk9bGlzdChncm91cHM9Z3JvdXBzKSxGVU49c3VtKVssLTFdKQogIHJvd25hbWVzKGcuYWR0bSkgPC0gbGV2ZWxzKGdyb3VwcykKICAKICBnLmxhZHRtIDwtIGxvZyhnLmFkdG0pCiAgCiAgZy5kZWx0YSA8LSB0KHNjYWxlKCB0KHNjYWxlKGcubGFkdG0sIGNlbnRlcj1ULCBzY2FsZT1GKSksIGNlbnRlcj1ULCBzY2FsZT1GKSkKICAKICBnLmFkdG1fdyA8LSAtc3dlZXAoZy5hZHRtLDEscm93U3VtcyhnLmFkdG0pKSAjIHRlcm1zIG5vdCB3IHNwb2tlbiBieSBrCiAgZy5hZHRtX2sgPC0gLXN3ZWVwKGcuYWR0bSwyLGNvbFN1bXMoZy5hZHRtKSkgIyB3IHNwb2tlbiBieSBncm91cHMgb3RoZXIgdGhhbiBrCiAgZy5hZHRtX2t3IDwtIHN1bShnLmFkdG0pIC0gZy5hZHRtX3cgLSBnLmFkdG1fayAtIGcuYWR0bSAjIHRvdGFsIHRlcm1zIG5vdCB3IG9yIGsgCiAgCiAgZy5zZSA8LSBzcXJ0KDEvZy5hZHRtICsgMS9nLmFkdG1fdyArIDEvZy5hZHRtX2sgKyAxL2cuYWR0bV9rdykKICAKICBnLnpldGEgPC0gZy5kZWx0YS9nLnNlCiAgCiAgZy5jb3VudHMgPC0gYXMubWF0cml4KGFnZ3JlZ2F0ZSh4PWR0bS5ueiwgYnkgPSBsaXN0KGdyb3Vwcz1ncm91cHMpLCBGVU49c3VtKVssLTFdKQogIAogIGlmICghaXMubnVsbChwYWlyKSkgewogICAgcHIuZGVsdGEgPC0gdChzY2FsZSggdChzY2FsZShnLmxhZHRtW3BhaXIsXSwgY2VudGVyID0gVCwgc2NhbGUgPUYpKSwgY2VudGVyPVQsIHNjYWxlPUYpKQogICAgcHIuYWR0bV93IDwtIC1zd2VlcChnLmFkdG1bcGFpcixdLDEscm93U3VtcyhnLmFkdG1bcGFpcixdKSkKICAgIHByLmFkdG1fayA8LSAtc3dlZXAoZy5hZHRtW3BhaXIsXSwyLGNvbFN1bXMoZy5hZHRtW3BhaXIsXSkpICMgdyBzcG9rZW4gYnkgZ3JvdXBzIG90aGVyIHRoYW4gawogICAgcHIuYWR0bV9rdyA8LSBzdW0oZy5hZHRtW3BhaXIsXSkgLSBwci5hZHRtX3cgLSBwci5hZHRtX2sgLSBnLmFkdG1bcGFpcixdICMgdG90YWwgdGVybXMgbm90IHcgb3IgawogICAgcHIuc2UgPC0gc3FydCgxL2cuYWR0bVtwYWlyLF0gKyAxL3ByLmFkdG1fdyArIDEvcHIuYWR0bV9rICsgMS9wci5hZHRtX2t3KQogICAgcHIuemV0YSA8LSBwci5kZWx0YS9wci5zZQogICAgCiAgICByZXR1cm4obGlzdCh6ZXRhPXByLnpldGFbMSxdLCBkZWx0YT1wci5kZWx0YVsxLF0sc2U9cHIuc2VbMSxdLCBjb3VudHMgPSBjb2xTdW1zKGR0bS5ueiksIGFjb3VudHMgPSBjb2xTdW1zKGcuYWR0bSkpKQogIH0gZWxzZSB7CiAgICByZXR1cm4obGlzdCh6ZXRhPWcuemV0YSxkZWx0YT1nLmRlbHRhLHNlPWcuc2UsY291bnRzPWcuY291bnRzLGFjb3VudHM9Zy5hZHRtKSkKICB9Cn0KCiMjIyMjIyMjIyMjIyMjIEZJR0hUSU4nIFdPUkRTIFBMT1RUSU5HIEZVTkNUSU9OCgojIGhlbHBlciBmdW5jdGlvbgptYWtlVHJhbnNwYXJlbnQ8LWZ1bmN0aW9uKHNvbWVDb2xvciwgYWxwaGE9MTAwKQp7CiAgbmV3Q29sb3I8LWNvbDJyZ2Ioc29tZUNvbG9yKQogIGFwcGx5KG5ld0NvbG9yLCAyLCBmdW5jdGlvbihjdXJjb2xkYXRhKXtyZ2IocmVkPWN1cmNvbGRhdGFbMV0sIGdyZWVuPWN1cmNvbGRhdGFbMl0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBibHVlPWN1cmNvbGRhdGFbM10sYWxwaGE9YWxwaGEsIG1heENvbG9yVmFsdWU9MjU1KX0pCn0KCmZ3LmdncGxvdC5ncm91cHMgPC0gZnVuY3Rpb24oZncuY2gsIGdyb3Vwcy51c2UgPSBhcy5mYWN0b3Iocm93bmFtZXMoZncuY2gkemV0YSkpLCBtYXgud29yZHMgPSA1MCwgbWF4LmNvdW50cmFuayA9IDQwMCwgY29sb3JwYWxldHRlPXJlcCgiYmxhY2siLGxlbmd0aChncm91cHMudXNlKSksIHNpemVzY2FsZT0yLCB0aXRsZT0iQ29tcGFyaXNvbiBvZiBUZXJtcyBieSBHcm91cHMiLCBzdWJ0aXRsZSA9ICIiLCBjYXB0aW9uID0gIkdyb3VwLXNwZWNpZmljIHRlcm1zIGFyZSBvcmRlcmVkIGJ5IEZpZ2h0aW4nIFdvcmRzIHN0YXRpc3RpYyAoTW9ucm9lLCBldCBhbC4gMjAwOCkiKSB7CiAgaWYgKGlzLm51bGwoZGltKGZ3LmNoJHpldGEpKSkgeyMjIHR3by1ncm91cCBmdyBvYmplY3QgY29uc2lzdHMgb2YgdmVjdG9ycywgbm90IG1hdHJpY2VzCiAgICB6ZXRhcmFua21hdCA8LSBjYmluZChyYW5rKC1mdy5jaCR6ZXRhKSxyYW5rKGZ3LmNoJHpldGEpKQogICAgY29sbmFtZXMoemV0YXJhbmttYXQpIDwtIGdyb3Vwcy51c2UKICAgIGNvdW50cmFuayA8LSByYW5rKC0oZncuY2gkY291bnRzKSkKICB9IGVsc2UgewogICAgemV0YXJhbmttYXQgPC0gYXBwbHkoLWZ3LmNoJHpldGFbZ3JvdXBzLnVzZSxdLDEscmFuaykKICAgIGNvdW50cmFuayA8LSByYW5rKC1jb2xTdW1zKGZ3LmNoJGNvdW50cykpCiAgfQogIHdpZGVwbG90bWF0IDwtIGFzX3RpYmJsZShjYmluZCh6ZXRhcmFua21hdCxjb3VudHJhbms9Y291bnRyYW5rKSkKICB3aWRlcGxvdG1hdCR0ZXJtPW5hbWVzKGNvdW50cmFuaykKICAjcmFua3Bsb3QgPC0gZ2F0aGVyKHdpZGVwbG90bWF0LCBwYXJ0eSwgemV0YXJhbmssIDE6bmNvbCh6ZXRhcmFua21hdCkpCiAgcmFua3Bsb3QgPC0gZ2F0aGVyKHdpZGVwbG90bWF0LCBncm91cHMudXNlLCB6ZXRhcmFuaywgMTpuY29sKHpldGFyYW5rbWF0KSkKICByYW5rcGxvdCRwbG90c2l6ZSA8LSBzaXplc2NhbGUqKDUwLyhyYW5rcGxvdCR6ZXRhcmFuaykpXigxLzQpCiAgcmFua3Bsb3QgPC0gcmFua3Bsb3RbcmFua3Bsb3QkemV0YXJhbmsgPCBtYXgud29yZHMgKyAxICYgcmFua3Bsb3QkY291bnRyYW5rPG1heC5jb3VudHJhbmsrMSxdCiAgcmFua3Bsb3QkZ3JvdXBzLnVzZSA8LSBmYWN0b3IocmFua3Bsb3QkZ3JvdXBzLnVzZSxsZXZlbHM9Z3JvdXBzLnVzZSkKICAKICBwIDwtIGdncGxvdChyYW5rcGxvdCwgYWVzKChucm93KHJhbmtwbG90KS1jb3VudHJhbmspXjEsIC0oemV0YXJhbmteMSksIGNvbG91cj1ncm91cHMudXNlKSkgKyAKICAgIGdlb21fcG9pbnQoc2hvdy5sZWdlbmQ9RixzaXplPXNpemVzY2FsZS8yKSArIAogICAgdGhlbWVfY2xhc3NpYygpICsKICAgIHRoZW1lKGF4aXMudGlja3M9ZWxlbWVudF9ibGFuaygpLCBheGlzLnRleHQ9ZWxlbWVudF9ibGFuaygpICkgKwogICAgeWxpbSgtbWF4LndvcmRzLDQwKSArCiAgICBmYWNldF9ncmlkKGdyb3Vwcy51c2UgfiAuKSArCiAgICBnZW9tX3RleHRfcmVwZWwoYWVzKGxhYmVsID0gdGVybSksIHNpemUgPSByYW5rcGxvdCRwbG90c2l6ZSwgcG9pbnQucGFkZGluZz0uMDUsCiAgICAgICAgICAgICAgICAgICAgYm94LnBhZGRpbmcgPSB1bml0KDAuMjAsICJsaW5lcyIpLCBzaG93LmxlZ2VuZD1GKSArCiAgICBzY2FsZV9jb2xvdXJfbWFudWFsKHZhbHVlcyA9IGFscGhhKGNvbG9ycGFsZXR0ZSwgLjcpKSArIAojICAgIGxhYnMoeD0iVGVybXMgdXNlZCBtb3JlIGZyZXF1ZW50bHkgb3ZlcmFsbCDihpIiLCB5PSJUZXJtcyB1c2VkIG1vcmUgZnJlcXVlbnRseSBieSBncm91cCDihpIiLCAgdGl0bGU9dGl0bGUsIHN1YnRpdGxlPXN1YnRpdGxlICwgY2FwdGlvbiA9IGNhcHRpb24pIAogICAgbGFicyh4PXBhc3RlKCJUZXJtcyB1c2VkIG1vcmUgZnJlcXVlbnRseSBvdmVyYWxsIC0tPiIpLCB5PXBhc3RlKCJUZXJtcyB1c2VkIG1vcmUgZnJlcXVlbnRseSBieSBncm91cCAtLT4iKSwgIHRpdGxlPXRpdGxlLCBzdWJ0aXRsZT1zdWJ0aXRsZSAsIGNhcHRpb24gPSBjYXB0aW9uKSAKICAKfQoKZncua2V5cyA8LSBmdW5jdGlvbihmdy5jaCxuLmtleXM9MTApIHsKICBuLmdyb3VwcyA8LSBucm93KGZ3LmNoJHpldGEpCiAga2V5cyA8LSBtYXRyaXgoIiIsbi5rZXlzLG4uZ3JvdXBzKQogIGNvbG5hbWVzKGtleXMpIDwtIHJvd25hbWVzKGZ3LmNoJHpldGEpCiAgCiAgZm9yIChnIGluIDE6bi5ncm91cHMpIHsKICAgIGtleXNbLGddIDwtIG5hbWVzKHNvcnQoZncuY2gkemV0YVtnLF0sZGVjPVQpWzE6bi5rZXlzXSkKICB9CiAga2V5cwp9CmBgYAoKCiMjIENvbXBhcmUgIlBvbGlibG9nIiBkYXRhIGJ5IElkZW9sb2dpY2FsIFJhdGluZwoKTG9hZCB0aGUgZGF0YQpgYGB7cn0KcG9saWJsb2cuZGZtIDwtIHJlYWRSRFMoInBvbGlibG9nNWsuZGZtLnJkcyIpCnBvbGlibG9nLm1ldGEgPC0gcmVhZFJEUygicG9saWJsb2c1ay5mdWxsbWV0YS5yZHMiKQpgYGAKCkNhbGN1bGF0ZSBGVy4KCmBgYHtyfQpmdy5ibG9naWRlbyA8LSBmd2dyb3Vwcyhwb2xpYmxvZy5kZm0sZ3JvdXBzID0gcG9saWJsb2c1ay5tZXRhJHJhdGluZykKYGBgCgpHZXQgYW5kIHNob3cgdGhlIHRvcCB3b3JkcyBwZXIgZ3JvdXAgYnkgemV0YS4KCmBgYHtyIGVjaG89VFJVRSwgcmVzdWx0cz0iYXNpcyJ9CmxpYnJhcnkoa25pdHIpCmZ3a2V5cy5ibG9naWRlbyA8LSBmdy5rZXlzKGZ3LmJsb2dpZGVvLCBuLmtleXM9MjApCmthYmxlKGZ3a2V5cy5ibG9naWRlbykKYGBgCgpQbG90CmBgYHtyLCBmaWcuaGVpZ2h0PTUsIGZpZy53aWR0aD00fQpwLmZ3LmJsb2dpZGVvIDwtIGZ3LmdncGxvdC5ncm91cHMoZncuYmxvZ2lkZW8sc2l6ZXNjYWxlPTQsbWF4LndvcmRzPTIwMCxtYXguY291bnRyYW5rPTQwMCxjb2xvcnBhbGV0dGU9YygicmVkIiwiYmx1ZSIpKQpwLmZ3LmJsb2dpZGVvCmBgYAoKIyMgQ2FsY3VsYXRlIGJ5IGluZGl2aWR1YWwgYmxvZwoKQ2FsY3VsYXRlIEZXIGFuZCBrZXlzCmBgYHtyfQpmdy5ibG9ncyA8LSBmd2dyb3Vwcyhwb2xpYmxvZy5kZm0sZ3JvdXBzID0gcG9saWJsb2c1ay5tZXRhJGJsb2cpCmBgYAoKR2V0IGFuZCBzaG93IHRoZSB0b3Agd29yZHMgcGVyIGdyb3VwIGJ5IHpldGEuCgpgYGB7ciBlY2hvPVRSVUUsIHJlc3VsdHM9ImFzaXMifQpsaWJyYXJ5KGtuaXRyKQpmd2tleXMuYmxvZ3MgPC0gZncua2V5cyhmdy5ibG9ncywgbi5rZXlzPTE1KQprYWJsZShmd2tleXMuYmxvZ3MpCmBgYAoKUGxvdApgYGB7ciwgZmlnLmhlaWdodD04LCBmaWcud2lkdGg9Nn0KcC5mdy5ibG9ncyA8LSBmdy5nZ3Bsb3QuZ3JvdXBzKGZ3LmJsb2dzLHNpemVzY2FsZT0zLG1heC53b3Jkcz0yMDAsbWF4LmNvdW50cmFuaz00MDApCnAuZncuYmxvZ3MKYGBgCgoKIyMgVXNlIHRvIGlkZW50aWZ5IHRvcGljIGtleXdvcmRzCgpGVyBtYWtlcyBhIGJldHRlciAoaW4gbXkgb3BpbmlvbikgZXh0cmFjdG9yIG9mIGtleXdvcmRzIHRoYW4gRlJFWCwgTGlmdCwgU2NvcmUsIGV0Yy4KCkxvYWQgYSB0b3BpYyBtb2RlbCAoVGhlIG5vIG1ldGFkYXRhIDIwLXRvcGljIG1vZGVsIGZyb20gdGhlIFNUTSBub3RlYm9vaykuCgpgYGB7cn0Kc3RtLm5tIDwtIHJlYWRSRFMoInBvbGlibG9nNWsuZml0Lm5vbWV0YS5yZHMiKQpgYGAKCkNhbGN1bGF0ZSBleHBlY3RlZCB3b3JkIGZyZXF1ZW5jeSBwZXIgdG9waWMgYWNyb3NzIGNvcnB1czoKCmBgYHtyfQpzdG0ubm0udG9wX3dvcmRfdG90cyA8LSBjb2xTdW1zKHN3ZWVwKHN0bS5ubSR0aGV0YSwxLHJvd1N1bXMocG9saWJsb2cuZGZtKSwiKiIpKQpzdG0ubm0udG9wX3dvcmRfYXNzaWducyA8LSBzd2VlcChleHAoc3RtLm5tLmJldGEpLDEsc3RtLm5tLnRvcF93b3JkX3RvdHMsIioiKQpgYGAKCgpDYWxjdWxhdGUgRlcKYGBge3J9CmZ3LnRvcGljcy5zdG0gPC0gZndncm91cHMoc3RtLm5tLnRvcF93b3JkX2Fzc2lnbnMsZ3JvdXBzID0gYXMuZmFjdG9yKDE6MjApKQpgYGAKCkdldCBhbmQgc2hvdyB0aGUgdG9wIHdvcmRzIHBlciB0b3BpYyBieSB6ZXRhLgoKYGBge3IgZWNobz1UUlVFLCByZXN1bHRzPSJhc2lzIn0KbGlicmFyeShrbml0cikKZndrZXlzLnN0bS5ubSA8LSBmdy5rZXlzKGZ3LnRvcGljcy5zdG0sIG4ua2V5cz0xNSkKa2FibGUoZndrZXlzLnN0bS5ubSkKYGBgCgpQbG90CmBgYHtyLCBmaWcuaGVpZ2h0PTIwLCBmaWcud2lkdGg9Nn0KcC5zdG0ubm0gPC0gZncuZ2dwbG90LnRvcGljcyhmdy50b3BpY3Muc3RtLHNpemVzY2FsZT0yLG1heC53b3Jkcz01MCxtYXguY291bnRyYW5rPTQwMCkKcC5zdG0ubm0KYGBg