-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Basic Functionality for Daily Archives (#1)
* Basic functionality for archiving conversations. * Users functionality. * Specify response parser. Archive users.
- Loading branch information
1 parent
470f06d
commit c0da4e6
Showing
31 changed files
with
1,524 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,4 @@ | |
^_pkgdown\.yml$ | ||
^docs$ | ||
^pkgdown$ | ||
^data-raw$ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,23 @@ | ||
# Generated by roxygen2: do not edit by hand | ||
|
||
S3method(as_slack_ts,POSIXct) | ||
S3method(as_slack_ts,character) | ||
S3method(as_slack_ts,default) | ||
S3method(as_slack_ts,numeric) | ||
export(slack_call_api) | ||
export(slack_conversations_history) | ||
export(slack_conversations_list) | ||
export(slack_conversations_replies) | ||
export(slack_users_info) | ||
export(slack_users_list) | ||
importFrom(rlang,"%||%") | ||
importFrom(rlang,.data) | ||
importFrom(tibblify,tib_chr) | ||
importFrom(tibblify,tib_dbl) | ||
importFrom(tibblify,tib_df) | ||
importFrom(tibblify,tib_int) | ||
importFrom(tibblify,tib_lgl) | ||
importFrom(tibblify,tib_row) | ||
importFrom(tibblify,tib_unspecified) | ||
importFrom(tibblify,tib_variant) | ||
importFrom(tibblify,tspec_df) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
# Set up the basic call once at package build. | ||
slack_req_base <- nectar::req_setup( | ||
"https://slack.com/api", | ||
user_agent = "slackapi (https://github.com/jonthegeek/slackapi)" | ||
) | ||
|
||
#' Call the Slack Web API | ||
#' | ||
#' Generate and perform request to a Slack Web API method. | ||
#' | ||
#' @inheritParams nectar::req_modify | ||
#' @inheritParams .slack_req_perform | ||
#' @inheritParams rlang::args_error_context | ||
#' @param response_parser (`function`) A function to parse the server response. | ||
#' Defaults to [slack_response_parser()]. Set this to `NULL` to return the raw | ||
#' response from [httr2::req_perform()]. | ||
#' @param token (`character`) A bearer token provided by Slack. A later | ||
#' enhancement will add the ability to generate this token. Slack token are | ||
#' long-lasting, and should be carefully guarded. | ||
#' | ||
#' @return A tibble with the results of the API call. | ||
#' @export | ||
slack_call_api <- function(path, | ||
query = list(), | ||
body = NULL, | ||
method = NULL, | ||
pagination = c("none", "cursor"), | ||
max_results = Inf, | ||
max_reqs = Inf, | ||
response_parser = slack_response_parser, | ||
token = Sys.getenv("SLACK_API_TOKEN"), | ||
call = rlang::caller_env()) { | ||
# Don't pass token in query or body if provided as parameters; we'll instead | ||
# pass it in the header. | ||
token <- token %||% body$token %||% query$token | ||
if (length(body)) { | ||
body$token <- NULL | ||
} | ||
if (length(query)) { | ||
query$token <- NULL | ||
} | ||
|
||
req <- nectar::req_modify( | ||
slack_req_base, | ||
path = path, | ||
query = query, | ||
body = body, | ||
method = method | ||
) | ||
req <- .slack_req_auth(req, token = token) | ||
|
||
resps <- .slack_req_perform( | ||
req, | ||
pagination = pagination, | ||
max_results = max_results, | ||
max_reqs = max_reqs, | ||
call = call | ||
) | ||
|
||
nectar::resp_parse(resps, response_parser = response_parser) | ||
} | ||
|
||
#' Choose and apply pagination strategy | ||
#' | ||
#' @inheritParams rlang::args_error_context | ||
#' @inheritParams nectar::req_perform_opinionated | ||
#' @param req (`httr2_request`) The request object to modify. | ||
#' @param pagination (`character`) The pagination scheme to use. Currently either | ||
#' "none" (the default) or "cursor" (a scheme that uses `cursor`-based | ||
#' pagination; see [Pagination through | ||
#' collections](https://api.slack.com/apis/pagination) in the Slack API | ||
#' documentation. We do not currently support "Classic pagination". | ||
#' @param max_results (`integer` or `Inf`) The maximum number of results to | ||
#' return. Note that slightly more results may be returned if `max_results` is | ||
#' not evenly divisible by 100. | ||
#' | ||
#' @inherit nectar::req_perform_opinionated return | ||
#' @keywords internal | ||
.slack_req_perform <- function(req, | ||
pagination, | ||
max_results, | ||
max_reqs, | ||
call) { | ||
next_req <- NULL | ||
if (max_reqs > 1) { | ||
next_req <- .choose_pagination_fn(pagination, call = call) | ||
if (!is.null(next_req)) { | ||
# Use Slack's recommended limit when paginating. | ||
per_page <- 200L | ||
max_reqs <- min(max_reqs, ceiling(max_results / per_page)) | ||
req <- httr2::req_url_query(req, limit = per_page) | ||
} | ||
} | ||
|
||
# nectar respects it if we put our own retry mechanism on. Slack has retry | ||
# tiers, so it MIGHT make sense to implement those specifically. Dunno if | ||
# Slack sends back info around that, need to check; it seems to always wait 10 | ||
# seconds. | ||
|
||
nectar::req_perform_opinionated( | ||
req, | ||
next_req = next_req, | ||
max_reqs = max_reqs | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.slack_req_auth <- function(req, token = NULL) { | ||
if (!is.null(token)) { | ||
req <- httr2::req_auth_bearer_token(req, token) | ||
} | ||
return(req) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
#' Basic stop-gap parsing | ||
#' | ||
#' @inheritParams httr2::resp_body_json | ||
#' | ||
#' @return A tibble of results. | ||
#' @export | ||
slack_response_parser <- function(resp) { | ||
# This will only work for a few things right now. We need more complete | ||
# parsing. | ||
# | ||
# I intentionally left the targeting of specific parsers in here for other | ||
# endpoints that return those same types of objects. | ||
results <- httr2::resp_body_json(resp) | ||
if (length(results)) { | ||
if ("messages" %in% names(results)) { | ||
return( | ||
# TODO: If I (even ~naively) specify the spec, I think I can silence the | ||
# messages about unspecified. | ||
tibblify::tibblify( | ||
results[["messages"]], unspecified = "list" | ||
) | ||
) | ||
} | ||
if ("channels" %in% names(results)) { | ||
return(.parse_channel(results$channels)) | ||
} | ||
if ("user" %in% names(results)) { | ||
obj <- results[["user"]] | ||
profile <- obj$profile | ||
obj$profile <- NULL | ||
profile$real_name <- NULL | ||
to_return <- tibble::as_tibble(obj) |> | ||
dplyr::mutate( | ||
profile = list(profile) | ||
) |> | ||
tidyr::unnest_wider(profile) | ||
return(to_return) | ||
} | ||
if ("members" %in% names(results)) { | ||
return(.parse_members(results$members)) | ||
} | ||
} | ||
cli::cli_abort(c( | ||
"Don't know how to parse this response.", | ||
i = "Response pieces: {names(results)}" | ||
)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
.validate_pagination <- function(pagination, call = rlang::caller_env()) { | ||
rlang::arg_match0( | ||
pagination, | ||
c("none", "cursor"), | ||
error_call = call | ||
) | ||
} | ||
|
||
.iterator_fn_cursor <- function() { | ||
httr2::iterate_with_cursor( | ||
"cursor", | ||
resp_param_value = function(resp) { | ||
cursor <- httr2::resp_body_json(resp)$response_metadata$next_cursor | ||
if (!length(cursor) || cursor == "") { | ||
cursor <- NULL | ||
} | ||
return(cursor) | ||
} | ||
) | ||
} | ||
|
||
.choose_pagination_fn <- function(pagination, call = rlang::caller_env()) { | ||
pagination <- .validate_pagination(pagination, call) | ||
switch(pagination, | ||
cursor = .iterator_fn_cursor(), | ||
none = NULL | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
as_slack_ts <- function(x, | ||
arg = rlang::caller_arg(x), | ||
call = rlang::caller_env()) { | ||
UseMethod("as_slack_ts") | ||
} | ||
|
||
#' @export | ||
as_slack_ts.POSIXct <- function(x, | ||
arg = rlang::caller_arg(x), | ||
call = rlang::caller_env()) { | ||
as_slack_ts(as.numeric(x), arg = arg, call = call) | ||
} | ||
|
||
#' @export | ||
as_slack_ts.numeric <- function(x, | ||
arg = rlang::caller_arg(x), | ||
call = rlang::caller_env()) { | ||
stbl::stabilize_chr_scalar(x, allow_na = FALSE, x_arg = arg, call = call) | ||
} | ||
|
||
#' @export | ||
as_slack_ts.character <- function(x, | ||
arg = rlang::caller_arg(x), | ||
call = rlang::caller_env()) { | ||
# TODO: Ideally this should detect whether this is datetime-y or double-y. It | ||
# should also probably use a stbl::to_dbl() function that doesn't exist yet, | ||
# to give better errors about NA. | ||
as_slack_ts(as.double(x), arg = arg, call = call) | ||
} | ||
|
||
#' @export | ||
as_slack_ts.default <- function(x, | ||
arg = rlang::caller_arg(x), | ||
call = rlang::caller_env()) { | ||
cli::cli_abort( | ||
c( | ||
"Cannot convert object to Slack timestamp", | ||
i = "{.arg {arg}} is {.obj_type_friendly {x}}." | ||
), | ||
call = call | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
#' Get conversation history | ||
#' | ||
#' Fetches a conversation's history of messages and events. | ||
#' | ||
#' @inheritParams slack_call_api | ||
#' @param channel (`character`) Conversation ID to fetch history for. | ||
#' @param latest (`datetime` or `double`) End of time range of messages to | ||
#' include in results. | ||
#' @param oldest (`datetime` or `double`) Start of time range of messages to | ||
#' include in results. | ||
#' @param inclusive (`logical`) Include messages with `latest` or `oldest` | ||
#' timestamp in results only when either timestamp is specified. | ||
#' @param include_all_metadata (`logical`) Return all metadata associated with | ||
#' this message. | ||
#' | ||
#' @return A channel's messages as a tibble. | ||
#' @export | ||
slack_conversations_history <- function(channel, | ||
latest = lubridate::now(), | ||
oldest = 0, | ||
inclusive = TRUE, | ||
include_all_metadata = FALSE, | ||
max_results = Inf, | ||
max_reqs = Inf, | ||
token = Sys.getenv("SLACK_API_TOKEN")) { | ||
latest <- as_slack_ts(latest) | ||
oldest <- as_slack_ts(oldest) | ||
slack_call_api( | ||
path = "/conversations.history", | ||
method = "get", | ||
token = token, | ||
query = list( | ||
channel = channel, | ||
latest = latest, | ||
oldest = oldest, | ||
inclusive = inclusive, | ||
include_all_metadata = include_all_metadata | ||
), | ||
pagination = "cursor", | ||
max_results = max_results, | ||
max_reqs = max_reqs | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
#' Get conversations list | ||
#' | ||
#' Lists all channels in a Slack team. | ||
#' | ||
#' @inheritParams slack_call_api | ||
#' @param team_id (`character`) Encoded team id to list channels in, required if | ||
#' token belongs to org-wide app. | ||
#' @param exclude_archived (`logical`) Set to `TRUE` to exclude archived | ||
#' channels from the list. | ||
#' @param types (`character`) Mix and match channel types by | ||
#' providing a vector of any combination of `public_channel`, | ||
#' `private_channel`, `mpim`, `im`. | ||
#' | ||
#' @return A thread of messages posted to a conversation as a tibble. Note: The | ||
#' parent message is always included in the response. | ||
#' @export | ||
slack_conversations_list <- function(team_id = NULL, | ||
exclude_archived = FALSE, | ||
types = c( | ||
"public_channel", | ||
"private_channel", | ||
"mpim", | ||
"im" | ||
), | ||
max_results = Inf, | ||
max_reqs = Inf, | ||
token = Sys.getenv("SLACK_API_TOKEN")) { | ||
types <- rlang::arg_match(types, multiple = TRUE) | ||
slack_call_api( | ||
path = "/conversations.list", | ||
method = "get", | ||
token = token, | ||
query = list( | ||
team_id = team_id, | ||
exclude_archived = exclude_archived, | ||
types = types, | ||
.multi = "comma" | ||
), | ||
pagination = "cursor", | ||
response_parser = .parse_channel, | ||
max_results = max_results, | ||
max_reqs = max_reqs | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
#' Get conversations members | ||
#' | ||
#' Retrieve members of a conversation. | ||
#' | ||
#' @inheritParams slack_call_api | ||
#' @param team_id (`character`) Encoded team id to list channels in, required if | ||
#' token belongs to org-wide app. | ||
#' @param exclude_archived (`logical`) Set to `TRUE` to exclude archived | ||
#' channels from the list. | ||
#' @param types (`character`) Mix and match channel types by | ||
#' providing a vector of any combination of `public_channel`, | ||
#' `private_channel`, `mpim`, `im`. | ||
#' | ||
#' @return A thread of messages posted to a conversation as a tibble. Note: The | ||
#' parent message is always included in the response. | ||
#' @export | ||
slack_conversations_members <- function(channel, | ||
max_results = Inf, | ||
max_reqs = Inf, | ||
token = Sys.getenv("SLACK_API_TOKEN")) { | ||
slack_call_api( | ||
path = "/conversations.members", | ||
method = "get", | ||
token = token, | ||
query = list(channel = channel), | ||
pagination = "cursor", | ||
max_results = max_results, | ||
max_reqs = max_reqs | ||
) | ||
} |
Oops, something went wrong.