2

I want to update my Twitter banner profle (the header image) using the Twitter API v1.1 from within R. I don’t care how it’s done, it can be a function from an existing R package (the ones I know of, do not have this kind of functionality) or it can be a call to system2("curl", args = args_strng) or any other approach - as long as it is done in R.

Below I will explain in detail what approaches I have tried so far.


Approach 1: Using {rtweet} (v0.7.0)

{rtweet} is the most popular R package used to interact with Twitter API v1.1. It comes with a lot of functions, but updating the banner profile is not supported yet.

The idea was create a function building on {rtweet} that allows linking a file path to an image of appropriate size (1500x500 px) to upload it as new Twitter banner.

update_profile_banner <- function (banner_file, token = NULL) {

  token <- rtweet:::check_token(token)

  banner_uri <- base64enc::base64encode(banner_file)

  query <- "account/update_profile_banner"

  # build params
  params <- list(banner = banner_uri)

  # make URL
  url <- rtweet:::make_url(query = query, param = params)

  # send
  r <- rtweet:::TWIT(get = FALSE, url, token)

  if (!r$status_code %in% c(200,201,202)) {
    return(httr::content(r))
  }

  message("your profile banner image has been updated!")
}

# Now all we need is a new banner image (see `test_png` below) and a valid Twitter token:

library(rtweet) # v.0.7.0
library(httr) # v.1.4.2

# Create a token containing Twitter keys
mytoken <- rtweet::create_token(
  app = "the name of the Twitter app", 
  consumer_key = Sys.getenv("MY_CONSUMER_API_KEY"),
  consumer_secret = Sys.getenv("MY_CONSUMER_API_SECRET"),
  access_token = Sys.getenv("MY_ACCESS_TOKEN"),
  access_secret = Sys.getenv("MY_ACCESS_TOKEN_SECRET"),
  set_renv = FALSE
)

test_png <- "https://raw.githubusercontent.com/TimTeaFan/dynamicTwitterHeader/main/data/test.png"

update_profile_banner(test_png, mytoken)
#> Error in curl::curl_fetch_memory(url, handle = handle): HTTP/2 stream 0 was not closed cleanly: INTERNAL_ERROR (err 2)

The error message isn’t very helpful but looking it up and changing HTTP version 2 to 1.1 throws a more meaningful message telling us that the header is too long. I suspect that this is due to the fact that under the hood {httr} calls the following curl command:

POST https://api.twitter.com/1.1/account/update_profile_banner.json?banner=super_long_image_string_goes_here+oauth_credentials_come_last

It's just a simple POST command not differentiating between the header and the body, which (my guess) makes the header too long due to the image string.

To check whether this assumption is true (or at least not wrong), and to exclude the possibility that the approach itself is faulty, I created a similar function to get a banner image:

get_profile_banner <- function (screen_name, token) {

  token <- rtweet:::check_token(token)

  query <- "users/profile_banner"

  # build params
  params <- list(screen_name = screen_name)

  # make URL
  url <- rtweet:::make_url(query = query, param = params)
  r <- rtweet:::TWIT(get = TRUE, url, token)

    if (!r$status_code %in% c(200,201,202)) {
    return(httr::content(r))
  }

  rtweet:::from_js(r)
}

get_profile_banner("timteafan", mytoken)

#> $sizes
#> $sizes$ipad
#> $sizes$ipad$h
#> [1] 313
#> 
#> $sizes$ipad$w
#> [1] 626
#> 
#> $sizes$ipad$url
#> [1] "https://pbs.twimg.com/profile_banners/188839854/1653511325/ipad"
#> 
#> ...
#> [truncated]

Since get_profile_banner() is working fine, I suspect that the problem is indeed due to the long base64 encoded image string we are sending to the Twitter API in a simple call to curl POST.


Approach 2: Using system2("curl")

So a possible workaround might be to just call curl directly from R using system2() by building a more sophisticated call separating the header containing the authorization from the body containing the image string.

The only problem here is that we’d have to create our own signature. I tried this, but failed, so I decided to use {httr} to create the signature and all oauth credentials and then use those as header.

Let’s first start with get_banner_profile() to see whether this approach works.

For this we need some functions and helper functions:

This is the main function

get_banner <- function(screen_name, token) {

  query <- "users/profile_banner"

  url <- rtweet:::make_url(query = query,
                           param = list(screen_name = screen_name))

  # lets create the oauth credentials
  oauth <- oauth_GET(url, token)
  oauth <- oauth[[which(names(oauth) == "Authorization")]]

  # this is the initial curl command
  req <- "--get 'https://api.twitter.com/1.1/users/profile_banner.json?'"

  # this is the data
  data <- paste0("--data 'screen_name=", screen_name, "'")

  # and here goes the header
  header <- paste0("--header 'Authorization:", oauth, "'")

  # lets collapse everything into one string
  args <- paste(req, data, header, collapse = " ")
  system2("curl", args = args)
}

Some helper functions:

oauth_GET <- function (url = NULL, config = list(), ..., handle = NULL) {
  hu <- httr:::handle_url(handle, url, ...)
  req <- httr:::request_build("GET", hu$url, as.token.request(config), ...) 
  oauth_req(req, hu$handle$handle)
}

oauth_req <- function (req, handle) {
  stopifnot(httr:::is.request(req), inherits(handle, "curl_handle"))
  req <- httr:::request_prepare(req)
  req$headers
}

# httr methods
as.token.request <- function(x) auth_req(auth_token = x)

auth_req <- function (auth_token) {
  structure(list(auth_token = auth_token), class = "request")
}

library(httr)
get_banner("timteafan", mytoken)
#> {"sizes":{"ipad":{"h":313,"w":626,"url":"https:\/\/pbs.twimg.com\/profile_banners\/188839854\/1653511325\/ipad"},"ipad_retina":{"h":626,"w":1252,"url":"https:\/\/pbs.twimg.com\/profile_banners\/188839854\/1653511325\/ipad_retina"},"web":{"h":260,"w":520,"url":"https:\/\/pbs.twimg.com\/profile_banners\/188839854\/1653511325\/web"},"web_retina":{"h":520,"w":1040,"url":"https:\/\/pbs.twimg.com\/profile_banners\/188839854\/1653511325\/web_retina"},"mobile":{"h":160,"w":320,"url":"https:\/\/pbs.twimg.com\/profile_banners\/188839854\/1653511325\/mobile"},"mobile_retina":{"h":320,"w":640,"url":"https:\/\/pbs.twimg.com\/profile_banners\/188839854\/1653511325\/mobile_retina"},"300x100":{"h":100,"w":300,"url":"https:\/\/pbs.twimg.com\/profile_banners\/188839854\/1653511325\/300x100"},"600x200":{"h":200,"w":600,"url":"https:\/\/pbs.twimg.com\/profile_banners\/188839854\/1653511325\/600x200"},"1500x500":{"h":500,"w":1500,"url":"https:\/\/pbs.twimg.com\/profile_banners\/188839854\/1653511325\/1500x500"},"1080x360":{"h":360,"w":1080,"url":"https:\/\/pbs.twimg.com\/profile_banners\/188839854\/1653511325\/1080x360"}}}

Since this is working, lets try the same with update_banner():

upload_banner <- function(banner_file, token) {
  query <- "account/update_profile_banner"

  banner_uri <- base64enc::base64encode(banner_file)

  url <- rtweet:::make_url(query = query,
                           param = list(banner = banner_uri))

  oauth <- oauth_POST(url, token)
  oauth <- oauth[names(oauth) == "Authorization"]

  req <- "--request POST 'https://api.twitter.com/1.1/account/update_profile_banner.json?'"

  header <- paste0("--header 'Authorization: ", oauth, "'")
  header2 <- paste0("--header 'Content-Type: application/json'")
  header3 <- paste0("--header 'Accept: application/json, text/xml, application/xml, */*'")
  data <- paste0('--data "banner=', banner_uri,'"')

  myargs <- paste(req, header2, header3, header, data, collapse = " ")
  system2("curl", args = myargs)
}

oauth_POST <- function (url = NULL, config = list(), ...,
                        body = NULL, encode = c("multipart", "form", "json", "raw"),
                        handle = NULL) {

  encode <- match.arg(encode)
  hu <- httr:::handle_url(handle, url, ...)
  req <- httr:::request_build("POST", hu$url, httr:::body_config(body, match.arg(encode)),
                      auth_req(config), ...)

  oauth_req(req, hu$handle$handle)
}

upload_banner(test_png, mytoken)
#>   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
#>                                 Dload  Upload   Total   Spent    Left  Speed
#> 100 16279  100    64  100 16215     97  24728 --:--:-- --:--:-- --:--:-- 25083
#> {"errors":[{"message":"Could not authenticate you","code":32}]}

Created on 2022-06-16 by the reprex package (v0.3.0)

Since this is failing, I suspect that the problem stems from the authorization via {httr} and my guess is that the long image string is the cause.


What I have not tried yet:

  • The development version of {rtweet}: From what I've seen on Github the underyling calls to {httr} haven't changed much, which is why I expect the problem to persist.

  • The {httr2} package: I think I read somewhere that it doesn't work with OAUTH 1.0

  • Other R packages such as using only {curl} or {Rcurl} or any other package that works with the Twitter API in R.


Is this endpoint still working?

As @llrs points out the user endpoints are on the migration map and will be available in Twitter's API 2.0 some time down the road. However, the endpoints are still open under API 1.1, we can use the folllowing code to update the profile banner in python:

import os
import tweepy

# Get environment variables
CONKEY = os.getenv('MYTWITTER_CONSUMER_API_KEY')
CONSEC = os.environ.get('MYTWITTER_CONSUMER_API_SECRET')
ACCKEY = os.getenv('MYTWITTER_ACCESS_TOKEN')
ACCSEC = os.environ.get('MYTWITTER_ACCESS_TOKEN_SECRET')

auth = tweepy.OAuth1UserHandler(
   CONKEY,
   CONSEC,
   ACCKEY,
   ACCSEC
)

api = tweepy.API(auth)

image_url = 'https://raw.githubusercontent.com/TimTeaFan/dynamicTwitterHeader/main/data/test.png'

api.update_profile_banner(image_url)
TimTeaFan
  • 17,549
  • 4
  • 18
  • 39
  • 1
    I think it is not currently possible because the endpoint seems closed, I posted this on the issue on rtweet: https://github.com/ropensci/rtweet/issues/188#issuecomment-1159576794 – llrs Jun 18 '22 at 22:42
  • 1
    @llrs: Thanks for looking into this! The endpoints are still open, I just updated my profile banner using the tweepy package in python and it works. I suspect the problem lies in the way {httr} is building the curl calls. – TimTeaFan Jun 19 '22 at 21:38

0 Answers0