diff --git a/R/entities_objects.R b/R/entities_objects.R new file mode 100644 index 00000000..43054286 --- /dev/null +++ b/R/entities_objects.R @@ -0,0 +1,114 @@ +# Contains functions to parse the objects described here: +# https://developer.twitter.com/en/docs/twitter-api/v1/data-dictionary/object-model/entities + +# +hashtags <- function(x) { + if (NROW(x) == 0) { + data.frame(text = NA, indices = I(list(NA)), + stringsAsFactors = FALSE) + } else { + i <- indices_vec(x$indices) + data.frame(text = x$text, indices = I(i)) + } +} + +# PowerTrack $ text +# has:symbol +# They are the same +# +# parse_entities2 uses the name of the columns to match the appropriate function to parse it. +# It needs a symbols function that is the same as hashtags +symbols <- hashtags + +indices_vec <- function(x) { + lapply(x, function(y){ + matrix(y, ncol = 2, dimnames = list(NULL, c("start", "end")))}) +} + +# The extended entities is really for media +# +media <- function(x) { + df <- data.frame(id = NA, id_str = NA, indices = I(list(NA)), + media_url = NA, media_url_https = NA, + url = NA, display_url = NA, expanded_url = NA, + type = NA, sizes = I(list(NA)), ext_alt_text = NA, + stringsAsFactors = FALSE) + if (NROW(x) == 0) { + return(df) + } + indices <- as.data.frame(t(simplify2array(x$indices))) + colnames(indices) <- c("start", "end") + x$indices <- I(list(indices)) + sizes <- rbind(x$sizes$large, x$sizes$small, x$sizes$thumb, x$sizes$medium) + sizes$type <- c("large", "small", "thumb", "medium") + x$sizes <- list(sizes) + x[setdiff(colnames(df), colnames(x))] <- rep(NA, nrow(x)) + x +} + +urls <- function(x) { + df <- data.frame(url = NA, expanded_url = NA, display_url = NA, + indices = I(list(NA)), unwound = I(list(NA)), + stringsAsFactors = FALSE) + if (NROW(x) == 0) { + return(df) + } + indices <- as.data.frame(t(simplify2array(x$indices))) + colnames(indices) <- c("start", "end") + x$indices <- I(indices) + x[setdiff(colnames(df), colnames(x))] <- rep(NA, nrow(x)) + x +} + +# PowerTrack @ screen_name +# has:mentions +# +user_mentions <- function(x) { + df <- data.frame(screen_name = NA, name = NA, id = NA, id_str = NA, + indices = I(list(NA)), stringsAsFactors = FALSE) + if (NROW(x) == 0) { + return(df) + } + indices <- as.data.frame(t(simplify2array(x$indices))) + colnames(indices) <- c("start", "end") + x$indices <- indices + x[setdiff(colnames(df), colnames(x))] <- rep(NA, nrow(x)) + rownames(x) <- NULL + x +} + +# +# Not testable without fullarchive access +polls <- function(x) { + df <- data.frame(options= I(list(NA)), end_datetime = NA, + duration_minutes = NA, stringsAsFactors = FALSE) + if (NROW(x) == 0) { + return(df) + } + x[setdiff(colnames(df), colnames(x))] <- rep(NA, nrow(x)) + x +} + + +parse_entities <- function(x) { + + if (is.null(x)) { + return(list(description = urls(NULL), url = urls(NULL))) + } + + if (is.null(x$description$urls)) { + description <- list(description = urls(x$description$urls)) + } else { + description <- lapply(x$description$urls, urls) + + } + + if (is.null(x$url$urls)) { + url <- list(url = urls(x$url$urls)) + } else { + url <- lapply(x$url$urls, urls) + } + l <- Map(list, description, url) + lapply(l, `names<-`, value = c("description", "url")) +} + diff --git a/R/favorites.R b/R/favorites.R index f0beb16c..a26c80ce 100644 --- a/R/favorites.R +++ b/R/favorites.R @@ -45,9 +45,10 @@ get_favorites <- function(user, } get_favorites_user <- function(user, ..., parse = TRUE, token = NULL) { + stopifnot(length(user) == 1) params <- list( - tweet_mode = "extended", - include_ext_alt_text = "true" + # Undocumented parameter https://github.com/ropensci/rtweet/issues/575#issuecomment-829605892 + tweet_mode = "extended" ) params[[user_type(user)]] <- user diff --git a/R/geo_objects.R b/R/geo_objects.R new file mode 100644 index 00000000..0be69e7f --- /dev/null +++ b/R/geo_objects.R @@ -0,0 +1,56 @@ +# Contains functions to parse the objects described here: +# https://developer.twitter.com/en/docs/twitter-api/v1/data-dictionary/object-model/geo + +bounding_box <- function(x) { + empty <- data.frame(long = NA, lat = NA, type = NA) + if (is.null(x) || (identical(x, NA))) { + return(empty) + } + if (is.data.frame(x)) { + coord <- x$coordinates[[1]][1, , ] + if (is.null(coord)) { + return(empty) + } + return(data.frame(long = coord[, 1], lat = coord[, 2], type = x$type)) + } + m <- x$coordinates[1, , ] + colnames(m) <- c("long", "lat") + df <- as.data.frame(m) + df$type <- x$type + df +} + +# +coordinates <- function(x) { + if (is.null(x) || length(x) == 1 && is.na(x)) { + return(data.frame(long = NA, lat = NA, type = NA)) + } + if (has_name_children(x, "coordinates", "coordinates")) { + return(data.frame(long = x$coordinates[[1]], lat = x$coordinates[[2]], type = x$type)) + } + data.frame(long = x$coordinates[[1]], lat = x$coordinates[[2]], type = x$type) +} + +# +place <- function(x) { + if (is.null(x) || length(x) == 1 && is.na(x)) { + df <- data.frame(geo = I(list(coordinates(NA))), coordinates = I(list(coordinates(NA))), + place = I(list(NA))) + return(df) + } + + if (is.data.frame(x)) { + l <- simplify2array(x[!colnames(x) %in% c("geo", "coordinates", "bounding_box")]) + } else if (is.list(x)) { + l <- simplify2array(x[!names(x) %in% c("geo", "coordinates", "bounding_box")]) + if (nrow(l) != 1) { + l <- t(l) + } + } + place <- as.data.frame(l) + place$bounding_box <- list(bounding_box(x$bounding_box)) + + data.frame(geo = I(coordinates(x$geo)), + coordinates = I(coordinates(x$coordinates)), + place = I(place)) +} diff --git a/R/graph-network.R b/R/graph-network.R index 32b6a927..972b9e5a 100644 --- a/R/graph-network.R +++ b/R/graph-network.R @@ -1,79 +1,19 @@ -id_sn_index <- function(x) { - id <- character() - sn <- character() - if ("mentions_user_id" %in% names(x)) { - id <- unroll_users(x$mentions_user_id) - sn <- unroll_users(x$mentions_screen_name) - x$mentions_user_id <- NULL - x$mentions_screen_name <- NULL - } - id_vars <- grep("user_id$", names(x), value = TRUE) - sn_vars <- grep("screen_name$", names(x), value = TRUE) - id <- c(id, unlist(x[id_vars], use.names = FALSE)) - sn <- c(sn, unlist(x[sn_vars], use.names = FALSE)) - kp <- !duplicated(id) & !is.na(id) - list(id = id[kp], sn = sn[kp]) -} - - -unroll_users <- function(x) { - x <- unlist(x, use.names = FALSE) - x[!is.na(x)] -} - -id_sn_join <- function(x, ref) { - m <- match(x, ref$id) - ref$sn[m] -} - -unroll_connections <- function(x) { - ## initialize logical (TRUE) vector - kp <- !logical(nrow(x)) - - ## measure [and record] length of each 'to' field (list of character vector) - n <- lengths(x[[2]]) - n1 <- which(n == 1) - - ## if length == 1 & is.na(x[1]) - kp[n1[vapply(x[[2]][n1], is.na, logical(1))]] <- FALSE - - ## create 'from' and 'to' vectors - from <- unlist(mapply(rep, x[[1]][kp], n[kp]), use.names = FALSE) - to <- unlist(x[[2]][kp], use.names = FALSE) - - ## return as data frame - data.frame( - from = from, - to = to, - stringsAsFactors = FALSE - ) -} - -prep_from_to <- function(x, from, to) { - if (is.list(x[[to]])) { - unroll_connections(x[c(from, to)]) - } else { - x <- x[c(from, to)] - names(x) <- c("from", "to") - x <- x[!is.na(x[[2]]), ] - x - } -} - #' Network data +#' +#' Retrieve data to know which users are connected to which users. #' #' @description #' * `network_data()` returns a data frame that can easily be converted to #' various network classes. #' * `network_graph()` returns a igraph object #' -#' @param .x Data frame returned by rtweet function -#' @param .e Type of edge/link–i.e., "mention", "retweet", "quote", "reply". +#' @param x Data frame returned by rtweet function +#' @param e Type of edge/link–i.e., "mention", "retweet", "quote", "reply". #' This must be a character vector of length one or more. This value will be #' split on punctuation and space (so you can include multiple types in the #' same string separated by a comma or space). The values "all" and #' "semantic" are assumed to mean all edge types, which is equivalent to the -#' default value of `c("mention,retweet,reply,quote")` +#' default value of `c("mention", "retweet", "reply", "quote")` #' @return A from/to data edge data frame #' @seealso network_graph #' @examples @@ -83,7 +23,7 @@ prep_from_to <- function(x, from, to) { #' rstats <- search_tweets("#rstats", n = 200) #' #' ## create from-to data frame representing retweet/mention/reply connections -#' rstats_net <- network_data(rstats, "retweet,mention,reply") +#' rstats_net <- network_data(rstats, c("retweet","mention","reply")) #' #' ## view edge data frame #' rstats_net @@ -102,75 +42,144 @@ prep_from_to <- function(x, from, to) { #' } #' } #' @export -network_data <- function(.x, .e = c("mention,retweet,reply,quote")) { - if (isTRUE(.e)) { - .e <- "all" +network_data <- function(x, e = c("mention", "retweet", "reply", "quote")) { + if (isTRUE(e) || (length(e) == 1 && e %in% c("semantics", "all"))) { + e <- c("mention", "retweet", "reply", "quote") } - stopifnot(is.character(.e)) - .e <- sub("d$|s$", "", tolower(unlist(strsplit(.e, "[[:punct:] ]+")))) - if (length(.e) == 1 && grepl("^all$|^semantic$", .e, ignore.case = TRUE)) { - .e <- c("mention", "retweet", "reply", "quote") + + stopifnot(is.character(e)) + y <- users_data(x) + + ids <- character() + screen_names <- character() + if ("mention" %in% e) { + user_mentions <- lapply(x$entities, function(x){ + if (has_name_(x, "user_mentions")) { + y <- x$user_mentions + if (length(y$id_str) > 1 || !is.na(y$id_str)) { + return(y[, c("screen_name", "id_str")]) + } + } + NULL + }) + k <- vapply(user_mentions, is.null, logical(1L)) + # If no mention skip + if (!all(k)) { + + + r <- do.call("rbind", user_mentions[!k]) + + + ids <- c(ids, r$id_str, y[!k, "id_str", drop = TRUE]) + screen_names <- c(screen_names, r$screen_name, y[!k, "screen_name", drop = TRUE]) + + mention <- data.frame(from = rep(y[!k, "id_str", drop = TRUE], times = vapply(user_mentions[!k], nrow, numeric(1L))), + to = r$id_str, + type = "mention") + } else { + mention <- data.frame(from = NA, to = NA, type = NA)[0, , drop = FALSE] + } + } else { + mention <- data.frame(from = NA, to = NA, type = NA)[0, , drop = FALSE] } - .x <- lapply(.e, network_data_one, .x) - idsn <- lapply(.x, attr, "idsn") - idsn <- list( - id = unlist(lapply(idsn, `[[`, "id"), use.names = FALSE), - sn = unlist(lapply(idsn, `[[`, "sn"), use.names = FALSE) - ) - idsn$sn <- idsn$sn[!duplicated(idsn$id)] - idsn$id <- idsn$id[!duplicated(idsn$id)] - .x <- do.call(rbind, .x) - attr(.x, "idsn") <- idsn - .x -} + + if ("retweet" %in% e) { + # Retweets are those that the text start with RT and a mention but are not quoted + retweets <- startsWith(x$text, "RT @") + r <- x[retweets, ] + yr <- y[retweets, ] + + user_mentions <- lapply(r$entities, function(x){ + y <- x$user_mentions + # Pick the first mention that is the one the tweet is quoting + # Example: 1390785143615467524 + return(y[y$indices$start == 3, c("screen_name", "id_str")]) + }) + um <- do.call("rbind", user_mentions) + ur <- yr[, c("screen_name", "id_str")] -network_data_one <- function(.e, .x) { - stopifnot(.e %in% c("mention", "retweet", "reply", "quote")) - vars <- c("user_id", "screen_name", switch(.e, - mention = c("mentions_user_id", "mentions_screen_name"), - retweet = c("retweet_user_id", "retweet_screen_name"), - reply = c("reply_to_user_id", "reply_to_screen_name"), - quote = c("quoted_user_id", "quoted_screen_name"))) - .x <- .x[, vars] - idsn <- id_sn_index(.x) - v <- names(.x) - .x <- prep_from_to(.x, v[1], v[3]) - if (nrow(.x) > 0) { - .x$type <- .e + ids <- c(ids, ur$id_str, um$id_str) + screen_names <- c(screen_names, ur$screen_name, um$screen_name) + + retweet <- data.frame(from = um$id_str, + to = ur$id_str, + type = "retweet") + } else { + retweet <- data.frame(from = NA, to = NA, type = NA)[0, , drop = FALSE] + } + + if ("reply" %in% e && !all(is.na(x$in_reply_to_user_id_str))) { + reply_keep <- !is.na(x$in_reply_to_user_id_str) + + ids <- c(ids, y[["id_str"]][reply_keep], x[["in_reply_to_user_id_str"]][reply_keep]) + screen_names <- c(screen_names, y[["screen_name"]][reply_keep], x[["in_reply_to_screen_name"]][reply_keep]) + + reply <- data.frame(from = y[["id_str"]][reply_keep], + to = x[["in_reply_to_user_id_str"]][reply_keep], + type = "reply") + + } else { + reply <- data.frame(from = NA, to = NA, type = NA)[0, , drop = FALSE] } - attr(.x, "idsn") <- idsn - .x + if ("quote" %in% e && !all(is.na(x$is_quote_status))) { + r <- x[x$is_quote_status, ] + yr <- y[x$is_quote_status, c("screen_name", "id_str")] + # Quotes are from users on entities$user_mentions whose indices start at 3 + + if (is.data.frame(r$quoted_status$user)) { + um <- r$quoted_status$user[, c("screen_name", "id_str")] + } else { + user_mentions <- lapply(r$quoted_status$user, function(x){ + # Pick the first mention that is the one the tweet is quoting + # Example: 1390785143615467524 + return(x[, c("screen_name", "id_str")]) + }) + um <- do.call("rbind", user_mentions) + } + ums <- is.na(um[, 1]) + if (!is.null(nrow(ums))) { + um <- um[!ums, ] + yr <- yr[!ums, ] + ids <- c(ids, um$id_str, yr$id_str) + screen_names <- c(screen_names, um$screen_name, yr$screen_name) + + quote <- data.frame(from = um$id_str, + to = yr$id_str, + type = "quote") + } else { + quote <- data.frame(from = NA, to = NA, type = NA)[0, , drop = FALSE] + } + } else { + quote <- data.frame(from = NA, to = NA, type = NA)[0, , drop = FALSE] + } + + out <- rbind(mention, retweet, reply, quote) + out <- out[!is.na(out$type), ] + + idsn <- data.frame(id = ids, sn = screen_names) + idsn <- unique(idsn) + stopifnot(all(out$from %in% idsn$id)) + stopifnot(all(out$to %in% idsn$id)) + attr(out, "idsn") <- as.list(idsn) + out } + #' @return An igraph object #' @rdname network_data #' @export -network_graph <- function(.x, .e = c("mention,retweet,reply,quote")) { +network_graph <- function(x, e = c("mention", "retweet", "reply", "quote")) { if (!requireNamespace("igraph", quietly = TRUE)) { stop( "Please install the {igraph} package to use this function", call. = FALSE ) } - if (isTRUE(.e)) { - .e <- "all" - } - stopifnot(is.character(.e)) - .e <- sub("d$|s$", "", tolower(unlist(strsplit(.e, "[[:punct:] ]+")))) - if (length(.e) == 1 && grepl("^all$|^semantic$", .e, ignore.case = TRUE)) { - .e <- c("mention", "retweet", "reply", "quote") - } - .x <- network_data(.x, .e) - idsn <- attr(.x, "idsn") + nd <- network_data(x = x, e = e) + idsn <- attr(nd, "idsn") g <- igraph::make_empty_graph(n = 0, directed = TRUE) g <- igraph::add_vertices(g, length(idsn$id), attr = list(id = idsn$id, name = idsn$sn)) - edges <- rbind(match(.x[[1]], idsn$id), match(.x[[2]], idsn$id)) - igraph::add_edges(g, edges, attr = list(type = .x[[3]])) + edges <- rbind(match(nd[[1]], idsn$id), match(nd[[2]], idsn$id)) + igraph::add_edges(g, edges, attr = list(type = nd[[3]])) } - -# user_vars <- c("user_id", "screen_name", "name", "location", "description", -# "url", "protected", "followers_count", "friends_count", "listed_count", -# "statuses_count", "favourites_count", "account_created_at", "verified", -# "profile_url", "profile_expanded_url", "account_lang", -# "profile_banner_url", "profile_background_url", "profile_image_url") diff --git a/R/lists_statuses.R b/R/lists_statuses.R index 57b67142..d5909b7c 100644 --- a/R/lists_statuses.R +++ b/R/lists_statuses.R @@ -38,6 +38,7 @@ lists_statuses <- function(list_id = NULL, owner_user = owner_user, count = n, include_rts = include_rts, + # Undocumented parameter https://github.com/ropensci/rtweet/issues/575#issuecomment-829605892 tweet_mode = "extended" ) diff --git a/R/next_cursor.R b/R/next_cursor.R index b1080ad5..7027d129 100644 --- a/R/next_cursor.R +++ b/R/next_cursor.R @@ -77,10 +77,10 @@ find_id <- function(x, arg_name) { if (is.character(x)) { x } else if (is.data.frame(x)) { - if (!has_name(x, "status_id")) { - abort(paste0("`", arg_name, "` must contain a `status_id` column")) + if (!has_name(x, "id")) { + abort(paste0("`", arg_name, "` must contain a `id` column")) } - y <- x$status_id + y <- x$id if (is.factor(y)) { y <- as.numeric(levels(y))[y] } diff --git a/R/retweets.R b/R/retweets.R index eb4526d4..e84d4d2d 100644 --- a/R/retweets.R +++ b/R/retweets.R @@ -18,6 +18,8 @@ get_retweets <- function(status_id, n = 100, parse = TRUE, token = NULL, ...) { params <- list( id = status_id, count = n, + # Undocumented parameter https://github.com/ropensci/rtweet/issues/575#issuecomment-829605892 + tweet_mode = "extended", ... ) r <- TWIT_get(token, query, params) diff --git a/R/timeline.R b/R/timeline.R index fd5cc463..ab10ec42 100644 --- a/R/timeline.R +++ b/R/timeline.R @@ -93,7 +93,8 @@ get_my_timeline <- function(n = 100, parse = parse, retryonratelimit = retryonratelimit, verbose = verbose, - token = token + token = token, + ... ) } @@ -110,8 +111,8 @@ get_timeline_user <- function(user, api <- if (home) "/1.1/statuses/home_timeline" else "/1.1/statuses/user_timeline" params <- list( - # tweet_mode = "extended", - # include_ext_alt_text = "true", + # Undocumented parameter https://github.com/ropensci/rtweet/issues/575#issuecomment-829605892 + tweet_mode = "extended", ... ) params[[user_type(user)]] <- user diff --git a/R/tweet_object.R b/R/tweet_object.R new file mode 100644 index 00000000..f58107fd --- /dev/null +++ b/R/tweet_object.R @@ -0,0 +1,145 @@ +tweet <- function(x) { + empty <- data.frame(created_at = NA_character_, id = NA_integer_, + id_str = NA_character_, + text = NA_character_, + full_text = NA_character_, + truncated = NA, + entities = I(list(list())), + source = NA_character_, + in_reply_to_status_id = NA_integer_, + in_reply_to_status_id_str = NA_character_, + in_reply_to_user_id = NA_integer_, + in_reply_to_user_id_str = NA_character_, + in_reply_to_screen_name = NA_character_, + geo = NA, + coordinates = NA, place = NA, + contributors = NA, is_quote_status = NA, + retweet_count = 0, favorite_count = 0, + favorited = NA, favorited_by = NA, + retweeted = NA, + lang = NA_character_, + possibly_sensitive = NA, + display_text_width = NA, + display_text_range = NA, + retweeted_status = NA, + quoted_status = NA, + quoted_status_id = NA, + quoted_status_id_str = NA, + quoted_status_permalink = NA, + metadata = NA, + query = NA, + user = I(list(list())), + possibly_sensitive_appealable = NA) + if (NROW(x) == 0) { + return(empty) + } + + tb <- x + # Some fields seem to depend on what is needed + # possibly_sensitive, full_text, extended_entities + if (has_name_(x, "possibly_sensitive")) { + tb$possibly_sensitive <- x$possibly_sensitive + } else { + tb$possibly_sensitive <- list(NA) + } + + # Recursive and in a loop not good.. + if (has_name_(x, "quoted_status")) { + tb$quoted_status <- tweet(x$quoted_status) + } else { + tb$quoted_status <- list(NA) + } + + if (has_name_(x, "display_text_range")) { + # Handle missing display_text_range + tb$display_text_range <- display_text_range(x$display_text_range) + } + + if (has_name_(x, "quoted_status_permalink")){ + tb$quoted_status_permalink <- split_df(x$quoted_status_permalink) + } + if (has_name_(x, "retweeted_status")){ + tb$retweeted_status <- split_df(x$retweeted_status) + } + if (has_name_(x, "quoted_status")){ + tb$quoted_status <- split_df(x$quoted_status) + } + if (has_name_(x, "metadata")){ + tb$metadata <- split_df(x$metadata) + } + + if (has_name_(x, "text")) { + tb$text <- x$text + tb$display_text_width <- nchar(x$text) + } else if (has_name_(x, "full_text")) { + tb$text <- x$full_text + } + user <- user(x[["user"]]) + tb$user <- split_df(user) + + if (has_name_(x, "entities") && has_name_(x, "extended_entities")) { + ent <- parse_entities2(x$entities) + ext_ent <- parse_entities2(x$extended_entities) + tb$extended_entities <- NULL + for (i in NROW(x$entities)) { + ent[[i]][names(x$extended_entities)] <- ext_ent[[i]][names(x$extended_entities)] + } + + } else if (has_name_(x, "entities")) { + ent <- parse_entities2(x$entities) + } else if (has_name_(x, "extended_entities")) { + ent <- parse_entities2(x$extended_entities) + } else { + ent <- vector("list", NROW(x$entities)) + } + tb$entities <- ent + + tb$coordinates <- lapply(x$coordinates, coordinates) + if (is.data.frame(x$place)) { + l <- split_df(x$place) + tb$place <- lapply(l, place) + } else { + tb$place <- lapply(x$place, place) + } + + end <- setdiff(colnames(tb), colnames(empty)) + if (length(end) != 0) { + stop(end) + } + tb[setdiff(colnames(empty), colnames(tb))] <- NA + tb +} + +parse_entities2 <- function(y) { + l <- vector("list", NROW(y)) + for (col in seq_len(NCOL(y))) { + # Look for the function of said object and save it. + fun <- match.fun(colnames(y)[col]) + l[[col]] <- lapply(y[[col]], fun) + } + # Split and join + ll <- transpose_list(l) + lapply(ll, `names<-`, value = colnames(y)) +} + +# From https://stackoverflow.com/a/54970694/2886003 +# Assumes equal length for each list on the list +transpose_list <- function(l) { + l2 <- split(do.call(cbind, l), seq_len(length(l[[1]]))) + names(l2) <- NULL + l2 +} + + +split_df <- function(x) { + l <- split(x, seq_len(NROW(x))) + names(l) <- NULL + l +} + +display_text_range <- function(x) { + ldtr <- lengths(x) + dtr <- rep(NA, length.out = length(x)) + dtr[ldtr != 0] <- vapply(x[ldtr != 0], `[`, numeric(1), i = 2) + dtr +} diff --git a/R/tweet_threading.R b/R/tweet_threading.R index e07471cc..3aeb1008 100644 --- a/R/tweet_threading.R +++ b/R/tweet_threading.R @@ -45,9 +45,9 @@ tweet_threading_backwards <- function(tw, n = NULL, verbose = FALSE) { while (!last_found) { nr <- nrow(tw) - last_found <- is.na(tw$reply_to_status_id[nr]) + last_found <- is.na(tw$in_reply_to_status_id[nr]) - tw_tail <- lookup_tweets(tw$reply_to_status_id[nr]) + tw_tail <- lookup_tweets(tw$in_reply_to_status_id[nr]) tw <- rbind(tw, tw_tail) if (!last_found) { @@ -76,7 +76,7 @@ tweet_threading_forwards <- function(tw, n = 10, verbose = FALSE) { } timeline <- get_timeline(tw$screen_name[1], n = n) - from_id <- tw$status_id[1] + from_id <- tw$id[1] last_found <- FALSE @@ -87,11 +87,11 @@ tweet_threading_forwards <- function(tw, n = 10, verbose = FALSE) { counter <- 0 while (!last_found) { - idx <- which(timeline$reply_to_status_id %in% from_id) + idx <- which(timeline$in_reply_to_status_id %in% from_id) last_found <- length(idx) == 0 tw <- rbind(timeline[idx, ], tw) - from_id <- timeline$status_id[idx] + from_id <- timeline$id[idx] if (!last_found) { counter <- counter + 1 diff --git a/R/tweets_and_users.R b/R/tweets_and_users.R index b284185a..0f3c8ad4 100644 --- a/R/tweets_and_users.R +++ b/R/tweets_and_users.R @@ -7,580 +7,39 @@ #' @keywords internal #' @export tweets_with_users <- function(x) { - tweets_tbl <- lapply(x, tweets_to_tbl_) - tweets <- do.call("rbind", tweets_tbl) - - users_raw <- lapply(x, function(x) x[["user"]]) - users_tbl <- lapply(users_raw, users_to_tbl_) - users <- do.call("rbind", users_tbl) - + tweets <- do.call("rbind", lapply(x, tweet)) + if (has_name_(tweets, "user")) { + users <- do.call("rbind", tweets[["user"]]) + tweets <- tweets[!colnames(tweets) %in% "user"] + } else { + users <- user(NULL) + } + if (lengths(x)[1] == 0) { + tweets <- tweets[0, ] + users <- users[0, ] + } structure(tweets, users = users) } #' @rdname tweets_with_users #' @export users_with_tweets <- function(x) { - users_tbl <- lapply(x, users_to_tbl_) - users <- do.call("rbind", users_tbl) - - tweets_raw <- lapply(x, function(x) x[["status"]]) - tweets_tbl <- lapply(tweets_raw, tweets_to_tbl_) - tweets <- do.call("rbind", tweets_tbl) - tweets$user_id <- users$user_id - tweets$screen_name <- users$screen_name + users <- do.call("rbind", lapply(x, user)) + status <- lapply(x, `[[`, i = "status") + tweets <- do.call("rbind", lapply(status, tweet)) - structure(users, tweets = tweets) -} - -##----------------------------------------------------- -## safe extractors -##----------------------------------------------------- -`[[[` <- function(dat, var, NA_ = NA_character_) { - if (length(dat) == 0L) { - return(NA_) - } else if (!is.recursive(dat)) { - dat[lengths(dat) == 0L] <- NA_ - return(dat) - } - x <- `[[`(dat, var) - ## if empty give number of NAs equal to obs - if (length(x) == 0L && is.data.frame(dat)) { - return(rep(NA_, length.out = nrow(dat))) - } else if (length(x) == 0L && is.list(dat)) { - return(rep(NA_, length.out = length(dat))) - } else if (length(x) == 0L) { - return(NA_) - } - x[lengths(x) == 0L] <- NA_ - x -} - - -tweets_to_tbl_ <- function(dat) { - if (NROW(dat) == 0L) return(data.frame()) - dat$display_text_width <- display_text_range(dat) - ## extended entities > media - if (has_name_(dat, "extended_entities") && - has_name_(dat[['extended_entities']], "media")) { - dat$ext_media_url <- lapply( - dat$extended_entities$media, "[[[", "media_url") - dat$ext_media_expanded_url <- lapply( - dat$extended_entities$media, "[[[", "expanded_url") - dat$ext_media_t.co <- lapply(dat$extended_entities$media, "[[[", "url") - dat$ext_media_type <- lapply(dat$extended_entities$media, "[[[", "type") - dat$ext_alt_text <- lapply(dat$extended_entities$media, "[[[", "ext_alt_text") - } else { - dat$ext_media_url <- as.list(NA_character_) - dat$ext_media_expanded_url <- as.list(NA_character_) - dat$ext_media_t.co <- as.list(NA_character_) - dat$ext_media_type <- as.list(NA_character_) - dat$ext_alt_text <- as.list(NA_character_) - } - ## entities > urls - if (has_name_(dat, "entities") && has_name_(dat[['entities']], "urls")) { - dat$urls_url <- lapply(dat$entities$urls, "[[[", "display_url") - dat$urls_expanded_url <- lapply(dat$entities$urls, "[[[", "expanded_url") - dat$urls_t.co <- lapply(dat$entities$urls, "[[[", "url") - } else { - dat$urls_url <- as.list(NA_character_) - dat$urls_expanded_url <- as.list(NA_character_) - dat$urls_t.co <- as.list(NA_character_) + if (lengths(x)[1] == 0) { + tweets <- tweets[0, ] + users <- users[0, ] } - ## entities > media - if (has_name_(dat, "entities") && has_name_(dat[['entities']], "media")) { - dat$media_url <- lapply(dat$entities$media, "[[[", "media_url") - dat$media_expanded_url <- lapply(dat$entities$media, "[[[", "expanded_url") - dat$media_t.co <- lapply(dat$entities$media, "[[[", "url") - dat$media_type <- lapply(dat$entities$media, "[[[", "type") - } else { - dat$media_url <- as.list(NA_character_) - dat$media_expanded_url <- as.list(NA_character_) - dat$media_t.co <- as.list(NA_character_) - dat$media_type <- as.list(NA_character_) - } - ## entities > user_mentions - if (has_name_(dat, "entities") && - has_name_(dat[["entities"]], "user_mentions")) { - dat$mentions_user_id <- lapply( - dat$entities$user_mentions, "[[[", "id_str") - dat$mentions_screen_name <- lapply( - dat$entities$user_mentions, "[[[", "screen_name") - } else { - dat$mentions_user_id <- as.list(NA_character_) - dat$mentions_screen_name <- as.list(NA_character_) - } - ## entities > hashtags - if (has_name_(dat, "entities") && has_name_(dat[["entities"]], "hashtags")) { - dat$hashtags <- lapply(dat$entities$hashtags, "[[[", "text") - } else { - dat$hashtags <- as.list(NA_character_) - } - if (has_name_(dat, "entities") && has_name_(dat[["entities"]], "symbols")) { - dat$symbols <- lapply(dat$entities$symbols, "[[[", "text") - } else { - dat$symbols <- as.list(NA_character_) - } - if (has_name_(dat, "geo") && has_name_(dat[["geo"]], "coordinates")) { - dat$geo_coords <- lapply( - dat$geo$coordinates, `[[[`, 1, NA_ = c(NA_real_, NA_real_)) - } else { - dat$geo_coords <- list(c(NA_real_, NA_real_)) - } - if (has_name_(dat, "coordinates") && - has_name_(dat[["coordinates"]], "coordinates")) { - dat$coordinates_coords <- lapply( - dat$coordinates$coordinates, `[[[`, 1, NA_ = c(NA_real_, NA_real_)) - } else { - dat$coordinates_coords <- list(c(NA_real_, NA_real_)) - } - if (has_name_(dat, "place") && has_name_(dat[["place"]], "id")) { - dat$place_url <- `[[[`(dat$place, "url") - dat$place_full_name <- `[[[`(dat$place, "full_name") - dat$place_name <- `[[[`(dat$place, "name") - dat$country_code <- `[[[`(dat$place, "country_code") - dat$place_type <- `[[[`(dat$place, "place_type") - dat$country <- `[[[`(dat$place, "country") - if (has_name_(dat$place, "bounding_box") && - has_name_(dat$place[["bounding_box"]], "coordinates")) { - dat$bbox_coords <- lapply( - dat$place$bounding_box[["coordinates"]], function(i) { - if (is.array(i)) { - c(i[1, , 1], i[1, , 2]) - } else { - c(NA_real_, NA_real_, NA_real_, NA_real_, - NA_real_, NA_real_, NA_real_, NA_real_) - } - }) - - } else { - dat$bbox_coords <- list( - c(NA_real_, NA_real_, NA_real_, NA_real_, - NA_real_, NA_real_, NA_real_, NA_real_) - ) - } - } else { - dat$place_url <- NA_character_ - dat$place_full_name <- NA_character_ - dat$place_name <- NA_character_ - dat$country_code <- NA_character_ - dat$place_type <- NA_character_ - dat$country <- NA_character_ - dat$bbox_coords <- list( - c(NA_real_, NA_real_, NA_real_, NA_real_, - NA_real_, NA_real_, NA_real_, NA_real_) - ) - } - if (has_name_(dat, "user") && has_name_(dat[["user"]], "id_str")) { - dat$user_id <- `[[[`(dat$user, "id_str") - dat$screen_name <- `[[[`(dat$user, "screen_name") - } else if (has_name_(dat, "screen_name") && has_name_(dat, "id_str")) { - dat$user_id <- dat[["id_str"]] - dat$id_str <- NULL - - } else { - dat$user_id <- NA_character_ - dat$screen_name <- NA_character_ - } - ## full text - if (has_name_(dat, "full_text")) { - dat$text <- dat$full_text - } - if (has_name_(dat, "extended_tweet") && has_name_(dat$extended_tweet, "full_text")) { - is_et <- !is.na(dat$extended_tweet$full_text) - dat$text[is_et] <- dat$extended_tweet$full_text[is_et] - } - dat <- wrangle_quote_status(dat) - dat <- wrangle_retweet_status(dat) - statuscols <- statuscols_() - nacols <- statuscols[!statuscols %in% names(dat)] - for (i in nacols) { - if (grepl("_count$", i)) { - dat[[i]] <- NA_integer_ - } else if (grepl("^is_", i)) { - dat[[i]] <- NA - } else { - dat[[i]] <- NA_character_ - } - } - dat <- dat[, statuscols[statuscols %in% names(dat)]] - names(dat) <- names(statuscols)[statuscols %in% names(dat)] - dat$created_at <- format_date(dat$created_at) - dat$source <- clean_source_(dat$source) - dat <- status_url_(dat) - as_tbl(dat) -} - - -users_to_tbl_ <- function(dat) { - if (NROW(dat) == 0L) return(data.frame()) - urls <- `[[[`(dat, "entities") - urls <- `[[[`(urls, "url") - urls <- `[[[`(urls, "urls") - dat$profile_url <- unlist( - lapply(urls, function(x) { - if (is.data.frame(x)) x[["url"]] else NA_character_ - }), use.names = FALSE - ) - dat$profile_expanded_url <- unlist( - lapply(urls, function(x) { - if (is.data.frame(x)) x[["expanded_url"]] else NA_character_ - }), use.names = FALSE - ) - dat$created_at <- format_date(dat$created_at) - if (!has_name_(dat, "reply_count")) { - dat$reply_count <- NA_integer_ - } - if (!has_name_(dat, "quote_count")) { - dat$quote_count <- NA_integer_ - } - usercols <- usercols_() - nacols <- usercols[!usercols %in% names(dat)] - for (i in seq_along(nacols)) { - dat[[nacols[i]]] <- NA - } - dat <- dat[, usercols[usercols %in% names(dat)]] - names(dat) <- names(usercols)[usercols %in% names(dat)] - as_tbl(dat) -} - - - - - -##----------------------------------------------------- -## column names -##----------------------------------------------------- -usercols_ <- function() { - c( - user_id = "id_str", - name = "name", - screen_name = "screen_name", - location = "location", - description = "description", - url = "url", - protected = "protected", - followers_count = "followers_count", - friends_count = "friends_count", - listed_count = "listed_count", - statuses_count = "statuses_count", - favourites_count = "favourites_count", - account_created_at = "created_at", - verified = "verified", - profile_url = "profile_url", - profile_expanded_url = "profile_expanded_url", - account_lang = "lang", - profile_banner_url = "profile_banner_url", - profile_background_url = "profile_background_image_url", - profile_image_url = "profile_image_url" - ) + structure(users, tweets = tweets) } -statuscols_ <- function() { - c( - status_id = "id_str", - created_at = "created_at", - user_id = "user_id", - screen_name = "screen_name", - text = "text", - source = "source", - display_text_width = "display_text_width", - reply_to_status_id = "in_reply_to_status_id_str", - reply_to_user_id = "in_reply_to_user_id_str", - reply_to_screen_name = "in_reply_to_screen_name", - is_quote = "is_quote", - is_retweet = "is_retweet", - favorite_count = "favorite_count", - retweet_count = "retweet_count", - quote_count = "quote_count", - reply_count = "reply_count", - hashtags = "hashtags", - symbols = "symbols", - urls_url = "urls_url", - urls_t.co = "urls_t.co", - urls_expanded_url = "urls_expanded_url", - media_url = "media_url", - media_t.co = "media_t.co", - media_expanded_url = "media_expanded_url", - media_type = "media_type", - ext_media_url = "ext_media_url", - ext_media_t.co = "ext_media_t.co", - ext_media_expanded_url = "ext_media_expanded_url", - ext_media_type = "ext_media_expanded_type", - ext_alt_text = "ext_alt_text", - mentions_user_id = "mentions_user_id", - mentions_screen_name = "mentions_screen_name", - lang = "lang", - quoted_status_id = "quoted_status_id", - quoted_text = "quoted_text", - quoted_created_at = "quoted_created_at", - quoted_source = "quoted_source", - quoted_favorite_count = "quoted_favorite_count", - quoted_retweet_count = "quoted_retweet_count", - quoted_user_id = "quoted_user_id", - quoted_screen_name = "quoted_screen_name", - quoted_name = "quoted_name", - quoted_followers_count = "quoted_followers_count", - quoted_friends_count = "quoted_friends_count", - quoted_statuses_count = "quoted_statuses_count", - quoted_location = "quoted_location", - quoted_description = "quoted_description", - quoted_verified = "quoted_verified", - retweet_status_id = "retweet_status_id", - retweet_text = "retweet_text", - retweet_created_at = "retweet_created_at", - retweet_source = "retweet_source", - retweet_favorite_count = "retweet_favorite_count", - retweet_retweet_count = "retweet_retweet_count", - retweet_user_id = "retweet_user_id", - retweet_screen_name = "retweet_screen_name", - retweet_name = "retweet_name", - retweet_followers_count = "retweet_followers_count", - retweet_friends_count = "retweet_friends_count", - retweet_statuses_count = "retweet_statuses_count", - retweet_location = "retweet_location", - retweet_description = "retweet_description", - retweet_verified = "retweet_verified", - place_url = "place_url", - place_name = "place_name", - place_full_name = "place_full_name", - place_type = "place_type", - country = "country", - country_code = "country_code", - geo_coords = "geo_coords", - coords_coords = "coordinates_coords", - bbox_coords = "bbox_coords" - ) -} -display_text_range <- function(x) { - if (has_name_(x, "display_text_range")) { - vapply( - x$display_text_range, - function(x) ifelse(is.null(x), NA_integer_, diff(x)), double(1)) - } else { - rep(NA_integer_, nrow(x)) - } +as_tbl <- function(x, ...) { + tibble::as_tibble(x, ...) } - -##----------------------------------------------------- -## utility funs -##----------------------------------------------------- - -clean_source_ <- function(s) { +clean_source <- function(s) { sub("<\\/.*", "", sub("[^>]+>", "", s)) } - - -wrangle_retweet_status <- function(x) { - n <- nrow(x) - if (has_name_(x, "retweeted_status")) { - rst <- x[["retweeted_status"]] - } else { - rst <- data.frame() - } - ## user mentions - if (has_name_(rst, "id_str")) { - x$retweet_status_id <- rst$id_str - } else { - x$retweet_status_id <- NA_character_ - } - if (has_name_(rst, "retweet_count")) { - x$retweet_retweet_count <- rst$retweet_count - } else { - x$retweet_retweet_count <- NA_integer_ - } - if (has_name_(rst, "full_text")) { - x$retweet_text <- rst$full_text - } else if (has_name_(rst, "text")) { - x$retweet_text <- rst$text - } else { - x$retweet_text <- NA_character_ - } - if (has_name_(rst, "extended_tweet") && has_name_(rst$extended_tweet, "full_text")) { - is_et <- !is.na(rst$extended_tweet$full_text) - x$retweet_text[is_et] <- rst$extended_tweet$full_text[is_et] - } - ## make 'text' include full retweet text - rt_et <- !is.na(x$retweet_text) - x$text[rt_et] <- x$retweet_text[rt_et] - if (has_name_(rst, "source")) { - x$retweet_source <- clean_source_(rst$source) - } else { - x$retweet_source <- NA_character_ - } - if (has_name_(rst, "created_at")) { - x$retweet_created_at <- format_date(rst$created_at) - } else { - x$retweet_created_at <- as.POSIXct(NA_character_) - } - if (has_name_(rst, "favorite_count")) { - x$retweet_favorite_count <- rst$favorite_count - } else { - x$retweet_favorite_count <- NA_integer_ - } - if (has_name_(rst, "user") && has_name_(rst$user, "screen_name")) { - x$retweet_screen_name <- rst$user$screen_name - } else { - x$retweet_screen_name <- NA_character_ - } - if (has_name_(rst, "user") && has_name_(rst$user, "id_str")) { - x$retweet_user_id <- rst$user$id_str - } else { - x$retweet_user_id <- NA_character_ - } - ## - if (has_name_(rst, "user") && has_name_(rst$user, "name")) { - x$retweet_name <- rst$user$name - } else { - x$retweet_name <- NA_character_ - } - if (has_name_(rst, "user") && has_name_(rst$user, "description")) { - x$retweet_description <- rst$user$description - } else { - x$retweet_description <- NA_character_ - } - if (has_name_(rst, "user") && has_name_(rst$user, "followers_count")) { - x$retweet_followers_count <- rst$user$followers_count - } else { - x$retweet_followers_count <- NA_integer_ - } - if (has_name_(rst, "user") && has_name_(rst$user, "friends_count")) { - x$retweet_friends_count <- rst$user$friends_count - } else { - x$retweet_friends_count <- NA_integer_ - } - if (has_name_(rst, "user") && has_name_(rst$user, "statuses_count")) { - x$retweet_statuses_count <- rst$user$statuses_count - } else { - x$retweet_statuses_count <- NA_integer_ - } - if (has_name_(rst, "user") && has_name_(rst$user, "verified")) { - x$retweet_verified <- rst$user$verified - } else { - x$retweet_verified <- NA - } - if (has_name_(rst, "user") && has_name_(rst$user, "location")) { - x$retweet_location <- rst$user$location - } else { - x$retweet_location <- NA_character_ - } - x$is_retweet <- !is.na(x$retweet_status_id) - x[["retweeted_status"]] <- NULL - x -} - -wrangle_quote_status <- function(x) { - n <- nrow(x) - if (has_name_(x, "quoted_status")) { - qst <- x[["quoted_status"]] - } else { - qst <- data.frame() - } - ## user mentions - if (has_name_(qst, "id_str")) { - x$quoted_status_id <- qst$id_str - } else { - x$quoted_status_id <- NA_character_ - } - if (has_name_(qst, "full_text")) { - x$quoted_text <- qst$full_text - } else if (has_name_(qst, "text")) { - x$quoted_text <- qst$text - } else { - x$quoted_text <- NA_character_ - } - if (has_name_(qst, "extended_tweet") && has_name_(qst$extended_tweet, "full_text")) { - is_et <- !is.na(qst$extended_tweet$full_text) - x$quoted_text[is_et] <- qst$extended_tweet$full_text[is_et] - } - if (has_name_(qst, "source")) { - x$quoted_source <- clean_source_(qst$source) - } else { - x$quoted_source <- NA_character_ - } - if (has_name_(qst, "created_at")) { - x$quoted_created_at <- format_date(qst$created_at) - } else { - x$quoted_created_at <- as.POSIXct(NA_character_) - } - if (has_name_(qst, "favorite_count")) { - x$quoted_favorite_count <- qst$favorite_count - } else { - x$quoted_favorite_count <- NA_integer_ - } - if (has_name_(qst, "created_at")) { - x$quoted_retweet_count <- qst$retweet_count - } else { - x$quoted_retweet_count <- NA_integer_ - } - if (has_name_(qst, "user") && has_name_(qst$user, "screen_name")) { - x$quoted_screen_name <- qst$user$screen_name - } else { - x$quoted_screen_name <- NA_character_ - } - if (has_name_(qst, "user") && has_name_(qst$user, "id_str")) { - x$quoted_user_id <- qst$user$id_str - } else { - x$quoted_user_id <- NA_character_ - } - if (has_name_(qst, "user") && has_name_(qst$user, "name")) { - x$quoted_name <- qst$user$name - } else { - x$quoted_name <- NA_character_ - } - if (has_name_(qst, "user") && has_name_(qst$user, "description")) { - x$quoted_description <- qst$user$description - } else { - x$quoted_description <- NA_character_ - } - if (has_name_(qst, "user") && has_name_(qst$user, "followers_count")) { - x$quoted_followers_count <- qst$user$followers_count - } else { - x$quoted_followers_count <- NA_integer_ - } - if (has_name_(qst, "user") && has_name_(qst$user, "friends_count")) { - x$quoted_friends_count <- qst$user$friends_count - } else { - x$quoted_friends_count <- NA_integer_ - } - if (has_name_(qst, "user") && has_name_(qst$user, "statuses_count")) { - x$quoted_statuses_count <- qst$user$statuses_count - } else { - x$quoted_statuses_count <- NA_integer_ - } - if (has_name_(qst, "user") && has_name_(qst$user, "verified")) { - x$quoted_verified <- qst$user$verified - } else { - x$quoted_verified <- NA - } - if (has_name_(qst, "user") && has_name_(qst$user, "location")) { - x$quoted_location <- qst$user$location - } else { - x$quoted_location <- NA_character_ - } - x$is_quote <- !is.na(x$quoted_status_id) - x[["quoted_status"]] <- NULL - x -} - -status_url_ <- function(x) { - stopifnot(is.data.frame(x)) - if (all(c("screen_name", "status_id") %in% names(x))) { - nas <- apply( - x[, c("screen_name", "status_id")], - 1, function(x) - all(is.na(x)) - ) - x$status_url <- paste0( - "https://twitter.com/", - x$screen_name, - "/status/", - x$status_id - ) - x$status_url[nas] <- NA_character_ - } else { - x$status_url <- NA_character_ - } - x -} diff --git a/R/user_object.R b/R/user_object.R new file mode 100644 index 00000000..3eb482b6 --- /dev/null +++ b/R/user_object.R @@ -0,0 +1,39 @@ +# Documents + + +user <- function(x) { + empty <- data.frame( + "id" = NA_integer_, "id_str" = NA_character_, + "name" = NA_character_, "screen_name" = NA_character_, + "location" = NA_character_, "derived" = NA_character_, + "url" = NA_character_, "description" = NA_character_, + "protected" = NA, "verified" = NA, "followers_count" = NA_integer_, + "friends_count" = NA_integer_, "listed_count" = NA_character_, + "favourites_count" = NA_integer_, "statuses_count" = NA_integer_, + "created_at" = NA_character_, "profile_banner_url" = NA_character_, + "profile_image_url_https" = NA_character_, + "default_profile" = NA, "default_profile_image" = NA, + "withheld_in_countries" = I(list(list())), + "withheld_scope" = NA, stringsAsFactors = FALSE + ) + empty <- empty + if (NROW(x) == 0) { + return(empty) + } + + # Ignoring status, as it holds tweets + y <- x[ , colnames(x) %in% colnames(empty)] + + # Adding missing values. + missing <- setdiff(colnames(y), colnames(empty)) + if (length(missing) != 0 ) { + y[ , missing] <- empty[rep(1, nrow(y)), missing] + } + + if (has_name_(x, "entities")) { + y$entities <- parse_entities(x$entities) + } else { + y$entitites <- list(list()) + } + y +} diff --git a/R/utils.R b/R/utils.R index 2f22a139..7f9c0aa2 100644 --- a/R/utils.R +++ b/R/utils.R @@ -72,6 +72,10 @@ last <- function(x) { has_name_ <- function(x, name) isTRUE(name %in% names(x)) +has_name_children <- function(x, name, children) { + has_name_(x, name) && has_name_(x[[name]], children) +} + any_recursive <- function(x) { if (!is.recursive(x)) { return(FALSE) @@ -100,7 +104,7 @@ maybe_n <- function(x) { } is_testing <- function() { - identical(Sys.getenv("TESTTHAT"), "true") + identical(Sys.getenv("TESTTHAT"), "true") && requireNamespace("testthat", quietly = TRUE) } is_dev_mode <- function() { exists(".__DEVTOOLS__", .getNamespace("rtweet")) diff --git a/man/network_data.Rd b/man/network_data.Rd index 39dccdf8..f99f9b92 100644 --- a/man/network_data.Rd +++ b/man/network_data.Rd @@ -5,19 +5,19 @@ \alias{network_graph} \title{Network data} \usage{ -network_data(.x, .e = c("mention,retweet,reply,quote")) +network_data(x, e = c("mention", "retweet", "reply", "quote")) -network_graph(.x, .e = c("mention,retweet,reply,quote")) +network_graph(x, e = c("mention", "retweet", "reply", "quote")) } \arguments{ -\item{.x}{Data frame returned by rtweet function} +\item{x}{Data frame returned by rtweet function} -\item{.e}{Type of edge/link–i.e., "mention", "retweet", "quote", "reply". +\item{e}{Type of edge/link–i.e., "mention", "retweet", "quote", "reply". This must be a character vector of length one or more. This value will be split on punctuation and space (so you can include multiple types in the same string separated by a comma or space). The values "all" and "semantic" are assumed to mean all edge types, which is equivalent to the -default value of \code{c("mention,retweet,reply,quote")}} +default value of \code{c("mention", "retweet", "reply", "quote")}} } \value{ A from/to data edge data frame @@ -31,6 +31,9 @@ various network classes. \item \code{network_graph()} returns a igraph object } } +\details{ +Retrieve data to know which users are connected to which users. +} \examples{ \dontrun{ @@ -38,7 +41,7 @@ various network classes. rstats <- search_tweets("#rstats", n = 200) ## create from-to data frame representing retweet/mention/reply connections - rstats_net <- network_data(rstats, "retweet,mention,reply") + rstats_net <- network_data(rstats, c("retweet","mention","reply")) ## view edge data frame rstats_net diff --git a/tests/testthat/_snaps/next_cursor.md b/tests/testthat/_snaps/next_cursor.md index 815a94b1..50c956c7 100644 --- a/tests/testthat/_snaps/next_cursor.md +++ b/tests/testthat/_snaps/next_cursor.md @@ -18,7 +18,7 @@ Code max_id(mtcars) Error - `max_id` must contain a `status_id` column + `max_id` must contain a `id` column Code since_id(10) Error @@ -26,5 +26,5 @@ Code since_id(mtcars) Error - `since_id` must contain a `status_id` column + `since_id` must contain a `id` column diff --git a/tests/testthat/test-entities_objects.R b/tests/testthat/test-entities_objects.R new file mode 100644 index 00000000..15afaab3 --- /dev/null +++ b/tests/testthat/test-entities_objects.R @@ -0,0 +1,209 @@ + +test_that("hashtags works", { + hashtags <- jsonlite::fromJSON('{ + "hashtags": [ + { + "indices": [ + 32, + 38 + ], + "text": "nodejs" + } + ] +}') + out <- hashtags(hashtags$hashtags) + expect_s3_class(out, "data.frame") + expect_named(out, c("text", "indices")) + expect_length(out$indices[[1]], 2) +}) + + + + +test_that("media works", { + extended_media <- jsonlite::fromJSON('{ +"extended_entities": { + "media": [ + { + "id": 861627472244162561, + "id_str": "861627472244162561", + "indices": [ + 68, + 91 + ], + "media_url": "http://pbs.twimg.com/media/C_UdnvPUwAE3Dnn.jpg", + "media_url_https": "https://pbs.twimg.com/media/C_UdnvPUwAE3Dnn.jpg", + "url": "https://t.co/9r69akA484", + "display_url": "pic.twitter.com/9r69akA484", + "expanded_url": "https://twitter.com/FloodSocial/status/861627479294746624/photo/1", + "type": "photo", + "sizes": { + "medium": { + "w": 1200, + "h": 900, + "resize": "fit" + }, + "small": { + "w": 680, + "h": 510, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "large": { + "w": 2048, + "h": 1536, + "resize": "fit" + } + } + }, + { + "id": 861627472244203520, + "id_str": "861627472244203520", + "indices": [ + 68, + 91 + ], + "media_url": "http://pbs.twimg.com/media/C_UdnvPVYAAZbEs.jpg", + "media_url_https": "https://pbs.twimg.com/media/C_UdnvPVYAAZbEs.jpg", + "url": "https://t.co/9r69akA484", + "display_url": "pic.twitter.com/9r69akA484", + "expanded_url": "https://twitter.com/FloodSocial/status/861627479294746624/photo/1", + "type": "photo", + "sizes": { + "small": { + "w": 680, + "h": 680, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "medium": { + "w": 1200, + "h": 1200, + "resize": "fit" + }, + "large": { + "w": 2048, + "h": 2048, + "resize": "fit" + } + } + }, + { + "id": 861627474144149504, + "id_str": "861627474144149504", + "indices": [ + 68, + 91 + ], + "media_url": "http://pbs.twimg.com/media/C_Udn2UUQAADZIS.jpg", + "media_url_https": "https://pbs.twimg.com/media/C_Udn2UUQAADZIS.jpg", + "url": "https://t.co/9r69akA484", + "display_url": "pic.twitter.com/9r69akA484", + "expanded_url": "https://twitter.com/FloodSocial/status/861627479294746624/photo/1", + "type": "photo", + "sizes": { + "medium": { + "w": 1200, + "h": 900, + "resize": "fit" + }, + "small": { + "w": 680, + "h": 510, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "large": { + "w": 2048, + "h": 1536, + "resize": "fit" + } + } + }, + { + "id": 861627474760708096, + "id_str": "861627474760708096", + "indices": [ + 68, + 91 + ], + "media_url": "http://pbs.twimg.com/media/C_Udn4nUMAAgcIa.jpg", + "media_url_https": "https://pbs.twimg.com/media/C_Udn4nUMAAgcIa.jpg", + "url": "https://t.co/9r69akA484", + "display_url": "pic.twitter.com/9r69akA484", + "expanded_url": "https://twitter.com/FloodSocial/status/861627479294746624/photo/1", + "type": "photo", + "sizes": { + "small": { + "w": 680, + "h": 680, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "medium": { + "w": 1200, + "h": 1200, + "resize": "fit" + }, + "large": { + "w": 2048, + "h": 2048, + "resize": "fit" + } + } + } + ] + } +}') + out <- media(extended_media$extended_entities$media) + expect_s3_class(out, "data.frame") + expect_named(out, c("id", "id_str", "indices", "media_url", "media_url_https", + "url", "display_url", "expanded_url", "type", "sizes", + "ext_alt_text")) +}) + + + + +test_that("polls works", { + polls_media <- jsonlite::fromJSON('{"polls": [ + { + "options": [ + { + "position": 1, + "text": "I read documentation once." + }, + { + "position": 2, + "text": "I read documentation twice." + }, + { + "position": 3, + "text": "I read documentation over and over again." + } + ], + "end_datetime": "Thu May 25 22:20:27 +0000 2017", + "duration_minutes": 60 + } + ] + }') + out <- polls(polls_media$polls) + expect_s3_class(out, "data.frame") + expect_named(out, c("options", "end_datetime", "duration_minutes")) +}) diff --git a/tests/testthat/test-extractors.R b/tests/testthat/test-extractors.R index 31684028..79a29196 100644 --- a/tests/testthat/test-extractors.R +++ b/tests/testthat/test-extractors.R @@ -3,12 +3,12 @@ test_that("tweets_data works", { ## get data on most recent tweet from user(s) tweets <- tweets_data(jack) - expect_s3_class(tweets, "tbl_df") - expect_true("status_id" %in% names(tweets)) + expect_s3_class(tweets, "data.frame") + expect_true("id_str" %in% names(tweets)) }) test_that("users_data works", { tweets <- search_tweets("r") users <- users_data(tweets) - expect_s3_class(users, "tbl") + expect_s3_class(users, "data.frame") }) diff --git a/tests/testthat/test-favorites.R b/tests/testthat/test-favorites.R index 6b2c481f..0fe1ebae 100644 --- a/tests/testthat/test-favorites.R +++ b/tests/testthat/test-favorites.R @@ -2,8 +2,8 @@ test_that("can retrieve multiple users", { users <- c("hadleywickham", "jennybryan") out <- get_favorites(users, n = 20) - expect_s3_class(out, "tbl_df") - expect_s3_class(out$created_at, "POSIXct") + expect_s3_class(out, "data.frame") + expect_true(is.character(out$created_at)) expect_equal(unique(out$favorited_by), users) }) @@ -13,10 +13,10 @@ test_that("get_favorites returns tweets data", { expect_equal(is.data.frame(x), TRUE) expect_named(x) - expect_true("status_id" %in% names(x)) + expect_true("id" %in% names(x)) expect_gt(nrow(x), 10) expect_gt(ncol(x), 15) - expect_true(is.data.frame(data.frame(users_data(x)))) + expect_true(is.data.frame(users_data(x))) #expect_gt(nrow(users_data(x)), 0) #expect_gt(ncol(users_data(x)), 15) #expect_named(users_data(x)) diff --git a/tests/testthat/test-followers.R b/tests/testthat/test-followers.R index a8415910..22440afc 100644 --- a/tests/testthat/test-followers.R +++ b/tests/testthat/test-followers.R @@ -1,7 +1,7 @@ test_that("get_followers returns expected data", { users <- get_followers("KFC") - expect_s3_class(users, "tbl_df") + expect_s3_class(users, "data.frame") expect_named(users, "user_id") expect_equal(nrow(users), 5000) diff --git a/tests/testthat/test-friends.R b/tests/testthat/test-friends.R index a2c7691f..d72c3b93 100644 --- a/tests/testthat/test-friends.R +++ b/tests/testthat/test-friends.R @@ -21,7 +21,7 @@ test_that("friendships returns data", { test_that("get_friends works", { djt <- get_friends("ropensci") - expect_s3_class(djt, "tbl_df") + expect_s3_class(djt, "data.frame") }) test_that("lookup_friendships works", { diff --git a/tests/testthat/test-geo_objects.R b/tests/testthat/test-geo_objects.R new file mode 100644 index 00000000..1031d6e0 --- /dev/null +++ b/tests/testthat/test-geo_objects.R @@ -0,0 +1,90 @@ +test_that("bounding_box works", { + bb <- jsonlite::fromJSON('{ + "bounding_box": { + "coordinates": [ + [ + [ + -74.026675, + 40.683935 + ], + [ + -74.026675, + 40.877483 + ], + [ + -73.910408, + 40.877483 + ], + [ + -73.910408, + 40.3935 + ] + ] + ], + "type": "Polygon" + } +}') + out <- bounding_box(bb$bounding_box) + expect_s3_class(out, "data.frame") + expect_named(out, c("long", "lat", "type")) + expect_equal(ncol(out), 3) + expect_equal(nrow(out), 4) +}) + + +test_that("place works", { + exact_location <- jsonlite::fromJSON('{ + "geo": { + "type": "Point", + "coordinates": [ + 40.74118764, + -73.9998279 + ] + }, + "coordinates": { + "type": "Point", + "coordinates": [ + -73.9998279, + 40.74118764 + ] + }, + "place": { + "id": "01a9a39529b27f36", + "url": "https://api.twitter.com/1.1/geo/id/01a9a39529b27f36.json", + "place_type": "city", + "name": "Manhattan", + "full_name": "Manhattan, NY", + "country_code": "US", + "country": "United States", + "bounding_box": { + "type": "Polygon", + "coordinates": [ + [ + [ + -74.026675, + 40.683935 + ], + [ + -74.026675, + 40.877483 + ], + [ + -73.910408, + 40.877483 + ], + [ + -73.910408, + 40.683935 + ] + ] + ] + }, + "attributes": { + + } + } +}') + out <- place(exact_location) + expect_s3_class(out, "data.frame") + +}) diff --git a/tests/testthat/test-graph-network.R b/tests/testthat/test-graph-network.R index 613f29bf..80fc3437 100644 --- a/tests/testthat/test-graph-network.R +++ b/tests/testthat/test-graph-network.R @@ -1,3 +1,39 @@ +# Status (no retweet, no reply no quote) +test_that("network_data on status", { + status <- lookup_tweets("1333789433288540160") + nd <- network_data(status, "mention") + expect_s3_class(nd, "data.frame") +}) + +# Reply (no quote no retweet) +test_that("network_data on reply", { + reply <- lookup_tweets("1333789435482161153") + nd <- network_data(reply, "reply") + expect_s3_class(nd, "data.frame") +}) + +# Retweet with other tweet embedded quoting +test_that("network_data on retweet quoting", { + retweet_quoted <- lookup_tweets("1390610121743556609") + nd <- network_data(retweet_quoted, "quote") + expect_s3_class(nd, "data.frame") +}) + +# Retweet without adding anything new +test_that("network_data on retweet", { + retweet <- lookup_tweets("1390785143615467524") + nd_retweet <- network_data(retweet, "retweet") + expect_s3_class(nd_retweet, "data.frame") +}) + + +test_that("network_data on many", { + status <- lookup_tweets(c("1333789433288540160", "1333789435482161153", "1390610121743556609", "1390785143615467524")) + nd <- network_data(status) + expect_s3_class(nd, "data.frame") +}) + + test_that("graphing functions work", { x <- search_tweets("twitter filter:verified", n = 200) d <- network_data(x) @@ -10,18 +46,19 @@ test_that("graphing functions work", { expect_true( inherits(g, "igraph") ) - + }) + +# https://twitter.com/henrikbengtsson/status/1390403676057980928 test_that("network_data works", { - rstats <- search_tweets("#rstats", n = 20) - ## create from-to data frame representing retweet/mention/reply connections - rstats_net <- network_data(rstats, "retweet,mention,reply") - expect_s3_class(rstats_net, "data.frame") + rstats <- search_tweets("#rstats", n = 20) + ## create from-to data frame representing retweet/mention/reply connections + rstats_net <- network_data(rstats, c("retweet","mention","reply")) + expect_s3_class(rstats_net, "data.frame") expect_equal(colnames(rstats_net), c("from", "to", "type")) }) - test_that("network_graph works", { rstats <- search_tweets("#rstats", n = 20) diff --git a/tests/testthat/test-http.R b/tests/testthat/test-http.R index f3378a4e..1e2026a8 100644 --- a/tests/testthat/test-http.R +++ b/tests/testthat/test-http.R @@ -12,11 +12,11 @@ test_that("TWIT_paginte_max_id respects max_id and since_id", { # check that we can ask for older tweets older <- simple_timeline(max_id = base) - expect_true(min(older$created_at) < min(base$created_at)) + expect_true(min(format_date(older$created_at)) < min(format_date(base$created_at))) # asking for newer tweets should give back the original data base2 <- simple_timeline(since_id = older) - expect_length(intersect(base$status_id, base2$status_id), nrow(base)) + expect_length(intersect(base$id, base2$id), nrow(base)) }) test_that("TWIT_paginte_cursor respects cursor", { diff --git a/tests/testthat/test-lists_memberships.R b/tests/testthat/test-lists_memberships.R index 9e8ab239..73955dac 100644 --- a/tests/testthat/test-lists_memberships.R +++ b/tests/testthat/test-lists_memberships.R @@ -1,6 +1,6 @@ test_that("lists_memberships returns data frame with nrow > 1", { df <- lists_memberships("kearneymw", filter_to_owned_lists = TRUE) - expect_s3_class(df, "tbl_df") + expect_s3_class(df, "data.frame") expect_equal(df$name, "test-memberships") }) diff --git a/tests/testthat/test-mentions.R b/tests/testthat/test-mentions.R index 50df9853..09c69ef7 100644 --- a/tests/testthat/test-mentions.R +++ b/tests/testthat/test-mentions.R @@ -1,6 +1,6 @@ test_that("mentions returns tweets data", { suppressMessages(x <- get_mentions()) - expect_s3_class(x, "tbl_df") + expect_s3_class(x, "data.frame") expect_gt(nrow(x), 0) }) diff --git a/tests/testthat/test-next_cursor.R b/tests/testthat/test-next_cursor.R index cc4c678e..ffeef739 100644 --- a/tests/testthat/test-next_cursor.R +++ b/tests/testthat/test-next_cursor.R @@ -26,13 +26,13 @@ test_that("max_id and since_id work in bit64", { }) test_that("max_id and since_id work with data frames", { - df <- data.frame(status_id = "123", stringsAsFactors = FALSE) + df <- data.frame(id = "123", stringsAsFactors = FALSE) expect_equal(max_id(df), "122") expect_equal(since_id(df), "123") }) test_that("max_id and since_id work with data frames and factors", { - df <- data.frame(status_id = "123") + df <- data.frame(id = "123") expect_equal(max_id(df), "122") expect_equal(since_id(df), "123") }) diff --git a/tests/testthat/test-rate_limit.R b/tests/testthat/test-rate_limit.R index a320dbe8..02dc9e0a 100644 --- a/tests/testthat/test-rate_limit.R +++ b/tests/testthat/test-rate_limit.R @@ -1,10 +1,10 @@ test_that("rate_limit works", { rl <- rate_limit() - expect_s3_class(rl, "tbl_df") + expect_s3_class(rl, "data.frame") }) test_that("rate_limit works", { rl <- rate_limit("application/rate_limit_status") - expect_s3_class(rl, "tbl_df") + expect_s3_class(rl, "data.frame") expect_equal(nrow(rl), 1) }) diff --git a/tests/testthat/test-retweets.R b/tests/testthat/test-retweets.R index e91669cd..5ee8a4d8 100644 --- a/tests/testthat/test-retweets.R +++ b/tests/testthat/test-retweets.R @@ -2,7 +2,12 @@ test_that("get_retweets returns tweets data", { x <- get_retweets("1363488961537130497") expect_equal(is.data.frame(x), TRUE) expect_named(x) - expect_true("screen_name" %in% names(x)) + expect_true(all(colnames(x) %in% colnames(tweet(NULL)))) +}) + +test_that("get_retweets returns user data", { + x <- get_retweets("1363488961537130497") + expect_s3_class(users_data(x), "data.frame") }) test_that("get_retweeters returns users", { diff --git a/tests/testthat/test-save_as_csv.R b/tests/testthat/test-save_as_csv.R index 9e80f0a4..fb7ecb0f 100644 --- a/tests/testthat/test-save_as_csv.R +++ b/tests/testthat/test-save_as_csv.R @@ -1,4 +1,5 @@ test_that("save_as_csv saves tweets data", { + skip("Not ready yet") x <- search_tweets(q = "obama") write_as_csv(x, "csv_data.csv", prepend_ids = FALSE) expect_gt(ncol(utils::read.csv("csv_data.csv")), 15) diff --git a/tests/testthat/test-search_tweets.R b/tests/testthat/test-search_tweets.R index fe79086e..20d1badd 100644 --- a/tests/testthat/test-search_tweets.R +++ b/tests/testthat/test-search_tweets.R @@ -1,6 +1,6 @@ test_that("search_tweets returns tweets data and latlng", { df <- search_tweets("#rstats", n = 50) - expect_s3_class(df, "tbl_df") + expect_s3_class(df, "data.frame") expect_true(nrow(df) > 25) # should almost always be true # can extract lat_lng @@ -21,6 +21,6 @@ test_that("non-existent search returns empty data frame", { test_that("search_tweets2 can search for multiple queries", { df <- search_tweets2(c("#rstats", "open science"), n = 50) - expect_s3_class(df, "tbl_df") + expect_s3_class(df, "data.frame") expect_true(nrow(df) > 25) # should almost always be true }) diff --git a/tests/testthat/test-search_users.R b/tests/testthat/test-search_users.R index 099103a4..742f53aa 100644 --- a/tests/testthat/test-search_users.R +++ b/tests/testthat/test-search_users.R @@ -1,5 +1,5 @@ test_that("search_users returns users data", { x <- search_users("twitter", n = 20, verbose = FALSE) - expect_s3_class(x, "tbl_df") + expect_s3_class(x, "data.frame") expect_equal(nrow(x), 20) }) diff --git a/tests/testthat/test-statuses.R b/tests/testthat/test-statuses.R index f3732696..edec0007 100644 --- a/tests/testthat/test-statuses.R +++ b/tests/testthat/test-statuses.R @@ -1,7 +1,46 @@ +coln <- c("created_at", "id", "id_str", "full_text", "truncated", "display_text_range", + "entities", "source", "in_reply_to_status_id", "in_reply_to_status_id_str", + "in_reply_to_user_id", "in_reply_to_user_id_str", "in_reply_to_screen_name", + "geo", "coordinates", "place", "contributors", "retweeted_status", + "is_quote_status", "quoted_status_id", "quoted_status_id_str", + "quoted_status_permalink", "retweet_count", "favorite_count", + "favorited", "retweeted", "lang", "possibly_sensitive", "quoted_status", + "text") + test_that("lookup_statuses returns users data", { ids <- c("558115838503690243", "760182486005583872", "776053079540166657") x <- lookup_tweets(ids) + expect_true(all(coln %in% colnames(x))) - expect_s3_class(x, "tbl_df") + expect_s3_class(x, "data.frame") expect_equal(nrow(x), 2) # 558115838503690243 was deleted }) + + +test_that("lookup status no retweet, no reply no quote", { + status <- lookup_tweets("1333789433288540160") + expect_true(all(coln %in% colnames(status))) +}) + +test_that("lookup on reply, no quote no retweet", { + reply <- lookup_tweets("1333789435482161153") + expect_true(all(coln %in% colnames(reply))) +}) + +test_that("lookup on retweet quotting", { + retweet_quoted <- lookup_tweets("1390610121743556609") + expect_true(all(coln %in% colnames(retweet_quoted))) +}) + + +test_that("lookup on retweet", { + retweet <- lookup_tweets("1390785143615467524") + expect_true(all(coln %in% colnames(retweet))) +}) + + +test_that("lookup on users without tweets, #574", { + lu <- lookup_users("994659707766833153") + td <- tweets_data(lu) + expect_equal(nrow(td), 1) +}) diff --git a/tests/testthat/test-stream.R b/tests/testthat/test-stream.R index 6dee2bc3..f25458c6 100644 --- a/tests/testthat/test-stream.R +++ b/tests/testthat/test-stream.R @@ -1,7 +1,7 @@ test_that("stream_tweets returns tweets data", { path <- tempfile() x1 <- stream_tweets(timeout = 1, file_name = path, verbose = FALSE) - expect_s3_class(x1, "tbl_df") + expect_s3_class(x1, "data.frame") x2 <- stream_tweets(timeout = 1, file_name = path, verbose = FALSE) expect_true(nrow(x2) > nrow(x1)) diff --git a/tests/testthat/test-timeline.R b/tests/testthat/test-timeline.R index af6bbf22..7ab734f2 100644 --- a/tests/testthat/test-timeline.R +++ b/tests/testthat/test-timeline.R @@ -1,14 +1,14 @@ test_that("get_timeline works", { x <- get_timeline(c("cnnbrk", "cnn"), n = 400) - expect_s3_class(x, "tbl_df") - expect_true("status_id" %in% names(x)) + expect_s3_class(x, "data.frame") + expect_true("id" %in% names(x)) expect_gt(nrow(x), 100) - expect_gt(ncol(x), 25) + expect_gt(ncol(x), 20) }) test_that("get_my_timeline() works", { gmt <- get_my_timeline() - expect_s3_class(gmt, "tbl_df") + expect_s3_class(gmt, "data.frame") expect_true(nrow(gmt) > 50) }) @@ -16,3 +16,9 @@ test_that("get_timelines() is deprecated", { expect_snapshot(x <- get_timelines("cnn", n = 10)) }) +test_that("Doesn't trim at 280 characters, #575", { + timeline_users <- get_timeline(user = "mvabercron", n = 20) + text <- timeline_users$text[!is.na(timeline_users$text)] + expect_true(any(nchar(text) > 280)|| all(!endsWith(text, "..."))) +}) + diff --git a/tests/testthat/test-tweet_threading.R b/tests/testthat/test-tweet_threading.R index cdc703d1..4c4bc5a9 100644 --- a/tests/testthat/test-tweet_threading.R +++ b/tests/testthat/test-tweet_threading.R @@ -1,5 +1,5 @@ test_that("tweet_threading works", { tw <- lookup_tweets('1084143184664510470') tw_thread <- tweet_threading(tw) - expect_s3_class(tw_thread, "tbl_df") + expect_s3_class(tw_thread, "data.frame") }) diff --git a/tests/testthat/test-users.R b/tests/testthat/test-users.R index 356b12f0..3f2cb3c4 100644 --- a/tests/testthat/test-users.R +++ b/tests/testthat/test-users.R @@ -1,7 +1,7 @@ test_that("lookup_users returns users data", { x <- lookup_users(c("cnn", "potus", "twitter", "kearneymw")) - expect_s3_class(x, "tbl_df") + expect_s3_class(x, "data.frame") expect_equal(nrow(x), 4) }) @@ -11,7 +11,7 @@ test_that("lookup_users works", { "fivethirtyeight", "cnn", "espn", "twitter" ) usr_df <- lookup_users(users) - expect_s3_class(usr_df, "tbl") + expect_s3_class(usr_df, "data.frame") expect_equal(nrow(usr_df), 6) - expect_equal(ncol(usr_df), 20) + expect_equal(ncol(usr_df), 21) })