From 3930c7ccac3ee41fab34fd0a1b81d46f242fe10d Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Tue, 18 Aug 2026 13:16:05 -0500 Subject: [PATCH 1/4] Wire Slack channel lifecycle chat_channel_create.chat_slack posts conversations.create, with adapter options (is_private) riding through ... into the body, and returns the name from Slack's answer, since names are what every other verb in this adapter takes. chat_leave.chat_slack posts conversations.leave. Refusals propagate through slack_stop_for_error in both directions: no phantom channels, no phantom departures. The created channel is not added to the poll set; channels= is fixed at construction, the same bargain chat_join() makes. --- R/slack.R | 34 +++++++++++++++++++++--- inst/tinytest/test_slack.R | 53 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/R/slack.R b/R/slack.R index 2764f02..249789e 100644 --- a/R/slack.R +++ b/R/slack.R @@ -317,6 +317,36 @@ chat_join.chat_slack <- function(client, channel, ...) { invisible(sub("^#", "", channel)) } +#' @export +chat_channel_create.chat_slack <- function(client, name, ...) { + # conversations.create. The name is normalized the way every other + # channel argument here is; Slack's own naming rules (lowercase, no + # spaces or periods) are enforced by the API and its refusal + # propagates. Adapter options (is_private = TRUE) ride through ... + # into the request body. The created channel is NOT added to the + # poll set: channels= is fixed at construction, the same bargain + # chat_join() makes. + api <- client$api_fn %||% slackr::call_slack_api + body <- slack_body(api("/api/conversations.create", .method = "POST", + token = client$token, + body = list(name = sub("^#", "", name), ...))) + slack_stop_for_error(body, "conversations.create") + invisible(as.character(body$channel$name %||% body$channel$id)) +} + +#' @export +chat_leave.chat_slack <- function(client, channel, ...) { + # conversations.leave. A refusal (not_in_channel) propagates: a + # leave that quietly failed keeps delivering a channel the caller + # believes it has left. + api <- client$api_fn %||% slackr::call_slack_api + body <- slack_body(api("/api/conversations.leave", .method = "POST", + token = client$token, + body = list(channel = sub("^#", "", channel)))) + slack_stop_for_error(body, "conversations.leave") + invisible(sub("^#", "", channel)) +} + #' @export chat_members.chat_slack <- function(client, channel, ...) { api <- client$api_fn %||% slackr::call_slack_api @@ -387,9 +417,7 @@ chat_capabilities.chat_slack <- function(client, ...) { invites = FALSE, join = TRUE, whoami = TRUE, channels = TRUE, history = TRUE, pending = FALSE, mark_read = TRUE, set_identity = TRUE, relogin = FALSE, - # conversations.create and conversations.leave exist in the - # Web API; this adapter has no verbs for them yet. - channel_create = FALSE, leave = FALSE, + channel_create = TRUE, leave = TRUE, files = FALSE, attachments = FALSE, typing = FALSE, e2ee = FALSE, identity_override = TRUE, rich_markup = character(), markup_dialects = c("plain", "markdown"), diff --git a/inst/tinytest/test_slack.R b/inst/tinytest/test_slack.R index dee62d4..17c3347 100644 --- a/inst/tinytest/test_slack.R +++ b/inst/tinytest/test_slack.R @@ -406,6 +406,59 @@ local({ expect_true(caps$join) }) +# ---- Creating ---- +# conversations.create; Slack's answer names the channel, and that name +# is the return value, since names are what every other verb in this +# adapter takes. Adapter options (is_private) ride into the body. + +local({ + seen <- NULL + cl <- slack_api_client(function(path, ..., body = NULL, .method, token) { + seen <<- list(path = path, body = body, .method = .method) + list(ok = TRUE, channel = list(id = "C9", name = "warroom")) + }) + expect_identical(chat_channel_create(cl, "#warroom", is_private = TRUE), + "warroom") + expect_identical(seen$path, "/api/conversations.create") + expect_identical(seen$.method, "POST") + expect_identical(seen$body$name, "warroom") + expect_true(seen$body$is_private) +}) + +# A refusal is an error, not a phantom channel: the caller must not walk +# away with a name it believes is a room. +expect_error(chat_channel_create(slack_api_client(function(...) { + list(ok = FALSE, error = "name_taken") +}), "warroom"), "Slack refused conversations.create: name_taken") + +# ---- Leaving ---- + +local({ + seen <- NULL + cl <- slack_api_client(function(path, ..., body = NULL, .method, token) { + seen <<- list(path = path, body = body, .method = .method) + list(ok = TRUE) + }) + expect_identical(chat_leave(cl, "#lab"), "lab") + expect_identical(seen$path, "/api/conversations.leave") + expect_identical(seen$.method, "POST") + expect_identical(seen$body$channel, "lab") +}) + +# A refusal propagates: a leave that quietly failed keeps delivering a +# channel the caller believes it has left. +expect_error(chat_leave(slack_api_client(function(...) { + list(ok = FALSE, error = "not_in_channel") +}), "lab"), "Slack refused conversations.leave: not_in_channel") + +local({ + caps <- chat_capabilities(chat_slack(channels = "lab", token = "t", + .history = function(...) NULL, + .post = function(...) "1")) + expect_true(caps$channel_create) + expect_true(caps$leave) +}) + # ---- Identity ---- slack_msg <- function(body = "", mentions = NULL) { chat_message(id = "1.1", channel = "lab", sender = "U999", From 640d836600c74d1e9a9f10c4de8254cbf89eebdc Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Tue, 18 Aug 2026 13:16:32 -0500 Subject: [PATCH 2/4] Bump version to 0.0.1.22 --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 6a48f25..8191952 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: chat.api Type: Package Title: Transport-Agnostic Chat Contract for R Agents -Version: 0.0.1.21 +Version: 0.0.1.22 Date: 2026-08-18 Authors@R: c( person("Troy", "Hernandez", role = c("aut", "cre"), From 04fb062cdb9eecce34bef467c4b1f0d4b2cb3539 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 7 Sep 2026 17:57:46 -0500 Subject: [PATCH 3/4] Register the Slack lifecycle methods in NAMESPACE chat_channel_create.chat_slack and chat_leave.chat_slack carried @export tags but NAMESPACE was never regenerated, so the installed package fell through to the default method and the Slack tests failed under R CMD check. --- NAMESPACE | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NAMESPACE b/NAMESPACE index b38b00e..8c5f1d1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -49,6 +49,7 @@ S3method(chat_capabilities,chat_matrix) S3method(chat_capabilities,chat_slack) S3method(chat_channel_create,chat_loopback) S3method(chat_channel_create,chat_matrix) +S3method(chat_channel_create,chat_slack) S3method(chat_channel_create,default) S3method(chat_channel_info,chat_matrix) S3method(chat_channel_info,chat_slack) @@ -77,6 +78,7 @@ S3method(chat_join,chat_matrix) S3method(chat_join,chat_slack) S3method(chat_join,default) S3method(chat_leave,chat_matrix) +S3method(chat_leave,chat_slack) S3method(chat_leave,default) S3method(chat_mark_read,chat_matrix) S3method(chat_mark_read,chat_slack) From e2a459ca31055bc6617b1d67f82ada09618e3732 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 7 Sep 2026 17:59:23 -0500 Subject: [PATCH 4/4] Bump version to 0.0.1.26 --- DESCRIPTION | 4 ++-- NEWS.md | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index e03c9e0..b654a58 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: chat.api Type: Package Title: Transport-Agnostic Chat Contract for R Agents -Version: 0.0.1.25 -Date: 2026-09-04 +Version: 0.0.1.26 +Date: 2026-09-07 Authors@R: c( person("Troy", "Hernandez", role = c("aut", "cre"), email = "troy@cornball.ai", diff --git a/NEWS.md b/NEWS.md index 925f1fa..4a6a6aa 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,11 @@ +# chat.api 0.0.1.26 + +* Slack gains chat_channel_create() and chat_leave(), posting + conversations.create and conversations.leave through the adapter's API + seam. Adapter options such as is_private = TRUE pass through to the + request body, and Slack's own refusals (name_taken, invalid_name, + not_in_channel) propagate as errors. Both capability flags are now TRUE. + # chat.api 0.0.1.25 * Matrix E2EE saves ratchet state before room-key request transport, retries