1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
|
#' Posts status update to user's Twitter account
#'
#' `r lifecycle::badge("deprecated")`
#' @inheritParams lookup_users
#' @param status Character, tweet status. Must be 280 characters or less.
#' @param media Length 1 character vector with a file path to video media **OR**
#' up-to length 4 character vector with file paths to static images to be included in tweet.
#' **The caller is responsible for managing this.**
#' @param in_reply_to_status_id Status ID of tweet to which you'd like to reply.
#' Note: in line with the Twitter API, this parameter is ignored unless the
#' author of the tweet this parameter references is mentioned within the
#' status text.
#' @param destroy_id To delete a status, supply the single status ID here. If a
#' character string is supplied, overriding the default (NULL), then a destroy
#' request is made (and the status text and media attachments) are irrelevant.
#' @param retweet_id To retweet a status, supply the single status ID here. If a
#' character string is supplied, overriding the default (NULL), then a retweet
#' request is made (and the status text and media attachments) are irrelevant.
#' @param auto_populate_reply_metadata If set to TRUE and used with
#' in_reply_to_status_id, leading @mentions will be looked up from the
#' original Tweet, and added to the new Tweet from there. Defaults to FALSE.
#' @param media_alt_text attach additional [alt text](https://en.wikipedia.org/wiki/Alt_attribute)
#' metadata to the `media` you are uploading. Should be same length as
#' `media` (i.e. as many alt text entries as there are `media` entries). See
#' [the official API documentation](https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-metadata-create)
#' for more information.
#' @param lat A numeric value representing the latitude of the location the
#' tweet refers to. Range should be between -90 and 90 (north). Note that you
#' should enable the "Precise location" option in your account via *Settings
#' and privacy > Privacy and Safety > Location*. See
#' [the official Help Center section](https://help.twitter.com/en/safety-and-security/x-location-services-for-mobile).
#' @param long A numeric value representing the longitude of the location the
#' tweet refers to. Range should be between -180 and 180 (west). See
#' `lat` parameter.
#' @param display_coordinates Put a pin on the exact coordinates a tweet has
#' been sent from. Value should be TRUE or FALSE. This parameter would apply
#' only if you have provided a valid `lat/long` pair of valid values.
#' @family post
#' @aliases post_status
#' @seealso [tweet_post()], [`rtweet-deprecated`]
#' @export
#' @references
#' Tweet: <https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-statuses-update>
#' Retweet: <https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-statuses-retweet-id>
#' Media: <https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-metadata-create>
#' Alt-text: <https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-metadata-create>
post_tweet <- function(status = "my first rtweet #rstats",
media = NULL,
token = NULL,
in_reply_to_status_id = NULL,
destroy_id = NULL,
retweet_id = NULL,
auto_populate_reply_metadata = FALSE,
media_alt_text = NULL,
lat = NULL,
long = NULL,
display_coordinates = FALSE) {
## if delete
if (!is.null(destroy_id)) {
lifecycle::deprecate_warn("1.0.0", "post_tweet(destroy_id)", "post_destroy()")
return(post_destroy(destroy_id, token=token))
}
## if retweet
if (!is.null(retweet_id)) {
stopifnot(is.character(retweet_id) && length(retweet_id) == 1)
query <- sprintf("/1.1/statuses/retweet/%s", retweet_id)
r <- TWIT_post(token, query)
message("the tweet has been retweeted!")
return(invisible(r))
}
stopifnot(is.character(status), length(status) == 1)
## media if provided
if (!is.null(media)) {
check_media(media, media_alt_text)
media_id_string <- character(length(media))
for (i in seq_along(media)) {
media_id_string[[i]] <- upload_media_to_twitter(media[[i]], token, media_alt_text[[i]])
}
media_id_string <- paste(media_id_string, collapse = ",")
params <- list(
status = status,
media_ids = media_id_string
)
} else {
params <- list(
status = status
)
}
## geotag if provided
if (!is.null(lat) && !is.null(long)) {
# Validate inputs
if (!is.numeric(lat)) stop("`lat` must be numeric.")
if (!is.numeric(long)) stop("`long` must be numeric.")
if (!is_logical(display_coordinates)) {
stop("`display_coordinates` must be TRUE/FALSE.")
}
if (abs(lat) > 90) stop("`lat` must be between -90 and 90 degrees.")
if (abs(long) > 180) stop("`long` must be between -180 and 180 degrees.")
params[["lat"]] <- as.double(lat)
params[["long"]] <- as.double(long)
if (display_coordinates) {
params[["display_coordinates"]] <- "true"
} else {
params[["display_coordinates"]] <- "false"
}
}
if (!is.null(in_reply_to_status_id)) {
params[["in_reply_to_status_id"]] <- in_reply_to_status_id
}
if (auto_populate_reply_metadata) {
params[["auto_populate_reply_metadata"]] <- "true"
}
r <- TWIT_post(token, "/1.1/statuses/update", params)
message("Your tweet has been posted!")
class(r) <- c("post_tweet", class(r))
invisible(r)
}
#' Uploads media using chunked media endpoint
#'
#' @param media Path to media file (image or movie) to upload.
#' @inheritParams lookup_users
#' @noRd
upload_media_to_twitter <- function(media,
token = NULL,
alt_text = NULL,
chunk_size = 5 * 1024 * 1024) {
media_type <- switch(tools::file_ext(media),
jpg = ,
jpeg = "image/jpeg",
png = "image/png",
gif = "image/gif",
mp4 = "video/mp4",
stop("Unsupported file extension", call. = FALSE)
)
file_size <- file.size(media)
if (file_size <= chunk_size && media_type != "video/mp4") {
resp <- TWIT_upload(token, "/1.1/media/upload", list(
media = httr::upload_file(media)
))
media_id <- from_js(resp)$media_id_string
} else {
# https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/uploading-media/chunked-media-upload
# Initialize upload
resp <- TWIT_upload(token, "/1.1/media/upload", list(
command = "INIT",
media_type = media_type,
total_bytes = file_size
))
media_id <- from_js(resp)$media_id_string
# Send chunks
bytes_sent <- 0
videofile <- file(media, open = "rb")
withr::defer(close(videofile))
segment_id <- 0
while (bytes_sent < file_size) {
chunk <- readBin(videofile, chunk_size, what = "raw")
resp <- TWIT_upload(token, "/1.1/media/upload", list(
command = "APPEND",
media_id = media_id,
segment_index = segment_id,
media = chunk
))
segment_id <- segment_id + 1
bytes_sent <- bytes_sent + chunk_size
}
# Finalize
resp <- TWIT_upload(token, "/1.1/media/upload", list(
command = "FINALIZE",
media_id = media_id
))
wait_for_chunked_media(resp, media_id, token)
}
if (!is.null(alt_text)) {
# https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-metadata-create
TWIT_upload(token, "/1.1/media/metadata/create",
list(
media_id = media_id,
alt_text = list(text = substr(as.character(alt_text), 1, 1000))
),
encode = "json"
)
}
media_id
}
TWIT_upload <- function(token, api, body, ...) {
TWIT_post(token, api, body = body, ..., host = "upload.twitter.com")
}
wait_for_chunked_media <- function(resp, media_id, token = NULL) {
json <- from_js(resp)
if (is.null(json$process_info)) {
return()
}
params <- list(
command = "STATUS",
media_id = media_id
)
while (!json$processing_info$state %in% c("pending", "in_progress")) {
Sys.sleep(json$processing_info$check_after_secs)
json <- TWIT_get(token, "/1.1/media/upload",
params = params,
host = "upload.twitter.com"
)
}
invisible()
}
check_media <- function(media, alt_text) {
if (!is.character(media) || !is.character(alt_text)) {
stop("Media and alt_text must be character vectors.", call. = FALSE)
}
media_type <- tools::file_ext(media)
if (length(media) > 4) {
stop("At most 4 images per plot can be uploaded.", call. = FALSE)
}
if (all(media_type %in% c("gif", "mp4")) && length(media) > 1) {
stop("Cannot upload more than one gif or video per tweet.", call. = TRUE)
}
if (!is.null(alt_text) && length(alt_text) != length(media)) {
stop("Alt text for media isn't provided for each image.", call. = TRUE)
}
if (!any(media_type %in% c("jpg", "jpeg", "png", "gif", "mp4"))) {
stop("Media type format not recognized.", call. = TRUE)
}
if (any(nchar(alt_text) > 1000)) {
stop("Alt text cannot be longer than 1000 characters.", call. = TRUE)
}
}
|