diff --git a/DESCRIPTION b/DESCRIPTION index 895647f..74fd3af 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,11 +14,15 @@ Imports: magrittr, purrr, stringr -Suggests: +Suggests: + jsonlite, knitr, - rmarkdown + rmarkdown, + testthat (>= 3.0.0), + tibble +Config/testthat/edition: 3 VignetteBuilder: knitr Encoding: UTF-8 LazyData: true -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.3 diff --git a/NAMESPACE b/NAMESPACE index a428cef..cc9d3f7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,6 +9,7 @@ export(get_all_bookmarks_from_channel) export(get_all_bookmarks_id_from_channel) export(get_channel_id) export(get_user_id) +export(get_users_in_channel) export(invite_users_to_channel) export(remove_all_bookmarks_from_channel) export(remove_bookmark_starting_with) diff --git a/R/functions.R b/R/functions.R index e8836dd..bc62ee4 100644 --- a/R/functions.R +++ b/R/functions.R @@ -38,3 +38,40 @@ get_user_id <- function(name,users = slackr::slackr_users()){ get_id <- function(name,from){ from %>% select(name,id) %>% filter(name %in% !!name) %>% pull(id) } + +#' List the user ids already in a Slack channel +#' +#' Wraps the `conversations.members` Slack API endpoint. Used by +#' [invite_users_to_channel()] to skip users who are already in the +#' target channel. +#' +#' @param channel channel name (case-insensitive). +#' @param token slack api token. +#' @param all_channel cached `slackr::slackr_channels()` result, passed +#' through to [get_channel_id()]. +#' +#' @return character vector of Slack user ids. Empty when the API call +#' fails or the channel has no members. +#' @export +#' +#' @importFrom httr POST content +get_users_in_channel <- function(channel, + token = Sys.getenv("SLACK_API_TOKEN"), + all_channel = slackr::slackr_channels()) { + channel_id <- get_channel_id(name = tolower(channel), all_channel = all_channel) + if (length(channel_id) == 0L) return(character()) + + resp <- tryCatch( + httr::POST( + url = "https://slack.com/api/conversations.members", + body = list(token = token, channel = channel_id) + ), + error = function(e) NULL + ) + if (is.null(resp)) return(character()) + + body <- httr::content(resp) + if (!isTRUE(body$ok)) return(character()) + members <- unlist(body$members, use.names = FALSE) + if (is.null(members)) character() else as.character(members) +} diff --git a/R/invite_user_to_channel.R b/R/invite_user_to_channel.R index adf8d69..1717e61 100644 --- a/R/invite_user_to_channel.R +++ b/R/invite_user_to_channel.R @@ -19,10 +19,25 @@ invite_users_to_channel <- function(channel, users, token=Sys.getenv("SLACK_API_ # Sys.sleep(1) } + # Skip the current user (would be a self-invite error) AND anyone + # already in the channel (would otherwise produce a noisy + # 'already_in_channel' error and re-send a notification). + already_in <- get_users_in_channel( + channel = channel, token = token, all_channel = all_channel + ) + to_invite <- setdiff(get_user_id(users), c(current_user, already_in)) + + if (length(to_invite) == 0L) { + message( + "Nothing to do: all requested users are already in #", channel, "." + ) + return(invisible(channel)) + } + httr::POST(url="https://slack.com/api/conversations.invite", body=list( token=token, channel= get_channel_id(name = tolower(channel),all_channel = all_channel), - users=paste( setdiff( get_user_id(users),current_user ) ,collapse=","))) + users=paste(to_invite, collapse=","))) # Sys.sleep(1) invisible(channel) } diff --git a/man/get_users_in_channel.Rd b/man/get_users_in_channel.Rd new file mode 100644 index 0000000..32f3d74 --- /dev/null +++ b/man/get_users_in_channel.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/functions.R +\name{get_users_in_channel} +\alias{get_users_in_channel} +\title{List the user ids already in a Slack channel} +\usage{ +get_users_in_channel( + channel, + token = Sys.getenv("SLACK_API_TOKEN"), + all_channel = slackr::slackr_channels() +) +} +\arguments{ +\item{channel}{channel name (case-insensitive).} + +\item{token}{slack api token.} + +\item{all_channel}{cached `slackr::slackr_channels()` result, passed +through to [get_channel_id()].} +} +\value{ +character vector of Slack user ids. Empty when the API call + fails or the channel has no members. +} +\description{ +Wraps the `conversations.members` Slack API endpoint. Used by +[invite_users_to_channel()] to skip users who are already in the +target channel. +} diff --git a/man/invite_users_to_channel.Rd b/man/invite_users_to_channel.Rd index 9f171f1..8e4a1cd 100644 --- a/man/invite_users_to_channel.Rd +++ b/man/invite_users_to_channel.Rd @@ -9,7 +9,8 @@ invite_users_to_channel( users, token = Sys.getenv("SLACK_API_TOKEN"), create_channel = TRUE, - all_channel = slackr::slackr_channels() + all_channel = slackr::slackr_channels(), + current_user = slackr::auth_test()$user_id ) } \arguments{ @@ -20,6 +21,8 @@ invite_users_to_channel( \item{token}{slack api token} \item{create_channel}{boolean create chanel if needed} + +\item{current_user}{current user id} } \description{ Add user(s) to a channel diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..0cf3c42 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,4 @@ +library(testthat) +library(slack) + +test_check("slack") diff --git a/tests/testthat/test-invite_users_to_channel.R b/tests/testthat/test-invite_users_to_channel.R new file mode 100644 index 0000000..6db750f --- /dev/null +++ b/tests/testthat/test-invite_users_to_channel.R @@ -0,0 +1,118 @@ +# Mock-based tests for invite_users_to_channel + get_users_in_channel. +# We don't talk to the real Slack API; we stub httr::POST and the slackr +# helpers and inspect what would have been sent. + +fake_response <- function(content, ok = TRUE) { + body <- c(list(ok = ok), content) + enc <- charToRaw(jsonlite::toJSON(body, auto_unbox = TRUE)) + structure( + list( + status_code = 200L, + headers = structure(list(`content-type` = "application/json"), + class = "insensitive"), + content = enc + ), + class = "response" + ) +} + +# A tibble mimicking slackr::slackr_users() / slackr_channels() shape. +fake_users <- function() { + tibble::tibble(name = c("alice", "bob", "carol"), + id = c("U_ALICE", "U_BOB", "U_CAROL")) +} +fake_channels <- function() { + tibble::tibble(name = c("general", "team-r"), + id = c("C_GEN", "C_R")) +} + +# Whatever httr::POST stub the test installs returns this — recorded so +# the test can inspect both endpoints (.members, .invite). +post_log <- new.env(parent = emptyenv()) + +with_slack_mocks <- function(members_in_channel, code) { + post_log$calls <- list() + fake_post <- function(url, body = NULL, ...) { + post_log$calls <- c(post_log$calls, list(list(url = url, body = body))) + if (grepl("conversations.members$", url)) { + return(fake_response(list(members = as.list(members_in_channel)))) + } + if (grepl("conversations.invite$", url)) { + return(fake_response(list(channel = list(id = body$channel)))) + } + fake_response(list()) + } + testthat::with_mocked_bindings( + POST = fake_post, + .package = "httr", + { + testthat::with_mocked_bindings( + slackr_users = function(...) fake_users(), + slackr_channels = function(...) fake_channels(), + auth_test = function(...) list(user_id = "U_ALICE"), + .package = "slackr", + code + ) + } + ) +} + +test_that("get_users_in_channel returns the parsed members list", { + with_slack_mocks( + members_in_channel = c("U_ALICE", "U_BOB"), + code = { + ids <- get_users_in_channel("general", token = "fake") + expect_equal(sort(ids), c("U_ALICE", "U_BOB")) + } + ) +}) + +test_that("get_users_in_channel returns character() when the channel is unknown", { + with_slack_mocks( + members_in_channel = character(), + code = { + ids <- get_users_in_channel("does-not-exist", token = "fake") + expect_equal(ids, character()) + } + ) +}) + +test_that("invite_users_to_channel skips users already in the channel", { + with_slack_mocks( + members_in_channel = c("U_ALICE", "U_BOB"), + code = { + invite_users_to_channel( + channel = "general", + users = c("alice", "bob", "carol"), + token = "fake", + create_channel = FALSE + ) + } + ) + invite_call <- Filter( + function(x) grepl("invite$", x$url), + post_log$calls + ) + expect_length(invite_call, 1L) + expect_equal(invite_call[[1]]$body$users, "U_CAROL") +}) + +test_that("invite_users_to_channel does nothing when nobody is to add", { + with_slack_mocks( + members_in_channel = c("U_ALICE", "U_BOB", "U_CAROL"), + code = { + msg <- capture_messages(invite_users_to_channel( + channel = "general", + users = c("alice", "bob", "carol"), + token = "fake", + create_channel = FALSE + )) + expect_match(paste(msg, collapse = " "), "Nothing to do", fixed = TRUE) + } + ) + invite_call <- Filter( + function(x) grepl("invite$", x$url), + post_log$calls + ) + expect_length(invite_call, 0L) +})