I caught this tweet by Terence Eden about using Twitter image alt-text to “PGP sign” tweet and my mind immediately went to “how can I abuse this for covert communications, malicious command-and-control, and embedding R code in tweets?”.
When you paste or upload an image to tweet (web interface, at least) you have an opportunity to add “alt” text which is — in theory — supposed to help communicate the content of the image to folks using assistive technology. Terence figured out the alt-text limit on Twitter is large (~1K) which is plenty of room for useful R code.
I poked around for something to use as an example and settled on using data from COVID Stimulus Watch. The following makes the chart in this tweet — https://twitter.com/hrbrmstr/status/1261641887603179520.
I’m not posting the chart here b/c it’s nothing special, but the code for it is below.
library(hrbrthemes);
x <- read.csv("https://data.covidstimuluswatch.org/prog.php?&detail=export_csv")[,3:5];
x[,3] <- as.numeric(gsub("[$,]","",x[,3]));
x <- x[(x[,1]>20200400)&x[,3]>0,];
x[,1] <- as.Date(as.character(x[,1]),"%Y%m%d");
ggplot(x, aes(Award.Date, Grant.Amount, fill=Award.Type)) +
geom_col() +
scale_y_comma(
labels = c("$0", "$5bn", "$10bn", "$15bn")
) +
labs(
title = "COVID Stimulus Watch: Grants",
caption = "Source: https://data.covidstimuluswatch.org/prog.php?detail=opening"
) +
theme_ipsum_es(grid="XY")
Semicolons are necessary b/c newlines are going to get stripped when we paste that code block into the alt-text entry box.
We can read that code back into R with some help from read_html() & {styler}:
library(rtweet)
library(rvest)
library(stringi)
library(magrittr)
pg <- read_html("https://twitter.com/hrbrmstr/status/1261641887603179520")
html_nodes(pg, "img") %>%
html_attr("alt") %>%
keep(stri_detect_fixed, "library") %>%
styler::style_text()
library(hrbrthemes)
x <- read.csv("https://data.covidstimuluswatch.org/prog.php?&detail=export_csv")[, 3:5]
x[, 3] <- as.numeric(gsub("[$,]", "", x[, 3]))
x <- x[(x[, 1] > 20200400) & x[, 3] > 0, ]
x[, 1] <- as.Date(as.character(x[, 1]), "%Y%m%d")
ggplot(x, aes(Award.Date, Grant.Amount, fill = Award.Type)) +
geom_col() +
scale_y_comma(
labels = c("$0", "$5bn", "$10bn", "$15bn")
) +
labs(
title = "COVID Stimulus Watch: Grants",
caption = "Source: https://data.covidstimuluswatch.org/prog.php?detail=opening"
) +
theme_ipsum_es(grid = "XY")
Twitter’s API does not seem to return alt-text: (see UPDATE)
rtweet::lookup_statuses("1261641887603179520") %>%
jsonlite::toJSON(pretty=TRUE)
## [
## {
## "user_id": "5685812",
## "status_id": "1261641887603179520",
## "created_at": "2020-05-16 12:57:20",
## "screen_name": "hrbrmstr",
## "text": "Twitter's img alt-text limit is YUGE! So, we can abuse it for semi-covert comms channels, C2, or for \"embedding\" the code ## that makes this chart!\n\nUse `read_html()` on URL of this tweet; find 'img' nodes w/html_nodes(); extract 'alt' attr text w/## html_attr(). #rstats \n\nh/t @edent https://t.co/v5Ut8TzlRO",
## "source": "Twitter Web App",
## "display_text_width": 278,
## "is_quote": false,
## "is_retweet": false,
## "favorite_count": 8,
## "retweet_count": 2,
## "hashtags": ["rstats"],
## "symbols": [null],
## "urls_url": [null],
## "urls_t.co": [null],
## "urls_expanded_url": [null],
## "media_url": ["http://pbs.twimg.com/media/EYI_W-xWsAAZFeP.png"],
## "media_t.co": ["https://t.co/v5Ut8TzlRO"],
## "media_expanded_url": ["https://twitter.com/hrbrmstr/status/1261641887603179520/photo/1"],
## "media_type": ["photo"],
## "ext_media_url": ["http://pbs.twimg.com/media/EYI_W-xWsAAZFeP.png"],
## "ext_media_t.co": ["https://t.co/v5Ut8TzlRO"],
## "ext_media_expanded_url": ["https://twitter.com/hrbrmstr/status/1261641887603179520/photo/1"],
## "mentions_user_id": ["14054507"],
## "mentions_screen_name": ["edent"],
## "lang": "en",
## "geo_coords": ["NA", "NA"],
## "coords_coords": ["NA", "NA"],
## "bbox_coords": ["NA", "NA", "NA", "NA", "NA", "NA", "NA", "NA"],
## "status_url": "https://twitter.com/hrbrmstr/status/1261641887603179520",
## "name": "boB • Everywhere is Baltimore • Rudis",
## "location": "Doors & Corners",
## "description": "Don't look at me…I do what he does—just slower. ? #rstats avuncular • pampa • #tired • ?? • ✝️ • Prìomh ## Neach-saidheans Dàta @ @rapid7",
## "url": "https://t.co/RgY1wHjoqM",
## "protected": false,
## "followers_count": 11886,
## "friends_count": 458,
## "listed_count": 667,
## "statuses_count": 84655,
## "favourites_count": 15140,
## "account_created_at": "2007-05-01 14:04:24",
## "verified": true,
## "profile_url": "https://t.co/RgY1wHjoqM",
## "profile_expanded_url": "https://rud.is/b",
## "profile_banner_url": "https://pbs.twimg.com/profile_banners/5685812/1398248552",
## "profile_background_url": "http://abs.twimg.com/images/themes/theme15/bg.png",
## "profile_image_url": "http://pbs.twimg.com/profile_images/824974380803334144/Vpmh_s3x_normal.jpg"
## }
## ]
but I still need to poke over at the API docs to figure out if there is a way to get it more programmatically. (see UPDATE)
If we want to be incredibly irresponsible and daft (like a recently semi-shuttered R package installation service) we can throw caution to the wind and just plot it outright:
library(rtweet)
library(rvest)
library(stringi)
library(magrittr)
pg <- read_html("https://twitter.com/hrbrmstr/status/1261641887603179520")
html_nodes(pg, "img") %>%
html_attr("alt") %>%
keep(stri_detect_fixed, "library") %>%
textConnection() %>%
source() %>% # THIS IS DANGEROUS DO NOT TRY THIS AT HOME
print()
Seriously, though, don’t do that. Lots of bad things can happen when you source() from the internet.
UPDATE (2020-05-17)
You can use:
paste(as.character(parse(text = "...")), collapse = "; ")
to “minify” R code for the alt-text.
And, you can use https://github.com/hrbrmstr/rtweet until it is PR’d back into {rtweet} proper to send tweets with image alt-text. The “status” functions also return any alt-text in a new ext_alt_text column.
FIN
Now, you can make your Twitter charts reproducible on-platform (until Twitter does something to thwart this new communication and file-sharing channel).
Since twitter status URLs are just GET requests, orgs should consider running the content of those URLs through alt-text extractors just in case there’s some funny business going on across user endpoints.
The Avatar Is Blue
I’ve changed my years-long avatar to the blue Cap’ shield because the colors of our flag have no business being displayed in any venue until Donald Trump is no longer President (one way or another). The red/white/blue triad has been coopted by an authoritarian, sociopathic puppet and is now a symbol of fear, greed, hate, and evil. I refuse to be associated with it until the principles it is supposed to stand for are even remotely embodied by those who serve our country. I would like to hope that is in mid-January 2021, but I’m not optimistic we’ll have a peaceful change of power.
Be safe. Be well. Be an ally.