Skip to content
Merged
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
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
34 changes: 31 additions & 3 deletions R/slack.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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, set_state = FALSE,
channel_create = TRUE, leave = TRUE, set_state = FALSE,
files = FALSE, attachments = FALSE, typing = FALSE, e2ee = FALSE,
identity_override = TRUE, rich_markup = character(),
markup_dialects = c("plain", "markdown"),
Expand Down
53 changes: 53 additions & 0 deletions inst/tinytest/test_slack.R
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down