Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
37 changes: 37 additions & 0 deletions R/functions.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
17 changes: 16 additions & 1 deletion R/invite_user_to_channel.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
29 changes: 29 additions & 0 deletions man/get_users_in_channel.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion man/invite_users_to_channel.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions tests/testthat.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
library(testthat)
library(slack)

test_check("slack")
118 changes: 118 additions & 0 deletions tests/testthat/test-invite_users_to_channel.R
Original file line number Diff line number Diff line change
@@ -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)
})