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
2 changes: 2 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
^CLAUDE\.md$
^AGENTS\.md$
^cran-comments\.md$
^DESIGN\.md$
^tasks$
^\.git$
Expand Down
114 changes: 47 additions & 67 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ on:
pull_request:

env:
_R_CHECK_FORCE_SUGGESTS_: "false"
_R_CHECK_FORCE_SUGGESTS_: "true"

jobs:
ci:
strategy:
fail-fast: false
matrix:
include:
- {os: macos-latest}
Expand All @@ -24,79 +25,58 @@ jobs:
with:
backend: RAPT

- name: Dependencies
run: ./run.sh install_deps

# test_matrix_mxclient.R exit_file()s without mx.client, and that
# file holds the direct tests for the crypto boundary functions --
# the encryption-state lookup and the encrypted send, both of which
# leak if they fail open. They sit behind chat_matrix()'s .crypto
# seam, so nothing in test_matrix.R can reach them. install_deps
# takes hard dependencies only, so without this they skip
# themselves into a green check.
#
# curl and jsonlite come first and explicitly: chat.api has no
# dependencies of its own, so install_deps brings in nothing, and
# mx.api needs both.
- name: Install Matrix stack
run: |
Rscript -e 'install.packages(c("curl", "jsonlite"))'
for pkg in mx.api mx.client; do
git clone --depth 1 "https://github.com/cornball-ai/$pkg.git" "$RUNNER_TEMP/$pkg"
R CMD INSTALL "$RUNNER_TEMP/$pkg"
done

# The development crypto APIs come from drat after the upstream PR
# lands. Bypass rapt's older r2u binary and build the vendored sources
# with the Linux runner's Rust toolchain. Keep the existing platform
# split: macOS exercises the Matrix adapter without optional crypto.
- name: Install mx.crypto (Linux)
- name: Install Linux build prerequisites
if: runner.os == 'Linux'
run: sudo apt-get install -y --no-install-recommends libcurl4-openssl-dev

# Use CRAN sources for Matrix so binary rebuilds cannot select older
# versions. Include all adapter test packages from CRAN as well.
- name: Install CRAN dependencies
run: |
Rscript -e '
if (isTRUE(getOption("rapt.enabled"))) rapt::disable()
utils::install.packages("mx.crypto",
repos = "https://cornball-ai.github.io/drat",
type = "source", dependencies = FALSE)
if (!requireNamespace("mx.crypto", quietly = TRUE) ||
utils::packageVersion("mx.crypto") < "0.2.1.1")
stop("CI needs mx.crypto >= 0.2.1.1; publish it to drat first")'
remotes::install_cran(c("mx.api", "mx.crypto", "mx.client"),
repos = "https://cran.r-project.org",
type = "source", upgrade = "never")
remotes::install_deps(".", dependencies = TRUE,
repos = "https://cran.r-project.org",
upgrade = "never")'

# Fail loudly rather than letting the suite skip. mx.crypto is
# reported but not required, since the macOS leg does without it.
#
# The mx.client floor is enforced, not just declared: the adapter
# calls straight into mx_send_encrypted() and cannot work around
# what an older one gets wrong. Every step of reading that floor is
# checked, because a parse that quietly finds nothing would wave
# through exactly what this is here to catch -- which is what the
# first version of corteza's equivalent did on macOS.
- name: Verify Matrix stack
# A missing optional package would silently skip its adapter tests.
# Check every Suggests entry, including both Matrix version floors.
- name: Verify adapter dependencies
run: |
Rscript -e '
for (p in c("mx.api", "mx.client")) {
if (!requireNamespace(p, quietly = TRUE))
stop("chat.api CI needs ", p, ": without it ",
"test_matrix_mxclient.R skips itself", call. = FALSE)
}
sug <- read.dcf("DESCRIPTION")[1L, "Suggests"]
if (is.na(sug)) stop("no Suggests field in DESCRIPTION")
e <- grep("^mx[.]client",
trimws(strsplit(sug, ",")[[1L]]), value = TRUE)
if (!length(e)) stop("no mx.client entry in Suggests")
m <- regmatches(e, regexpr("[0-9][0-9.-]*", e))
if (!length(m)) stop("mx.client Suggests entry declares no floor")
floor <- package_version(m)
have <- utils::packageVersion("mx.client")
message("mx.client ", have, " (>= ", floor, ")")
if (have < floor)
stop("mx.client ", have, " is below the ", floor,
" chat.api declares", call. = FALSE)
message("mx.api ", utils::packageVersion("mx.api"))
message("mx.crypto ",
if (requireNamespace("mx.crypto", quietly = TRUE))
as.character(utils::packageVersion("mx.crypto"))
else "absent (crypto-init test will not run)")'
suggests <- read.dcf("DESCRIPTION")[1L, "Suggests"]
if (is.na(suggests) || !nzchar(suggests))
stop("No Suggests field in DESCRIPTION")
entries <- trimws(strsplit(suggests, ",", fixed = TRUE)[[1L]])
for (entry in entries) {
parts <- regmatches(entry, regexec(
"^([[:alnum:].]+)( [(]>= ([0-9.]+)[)])?$", entry))[[1L]]
if (!length(parts)) stop("Unsupported Suggests entry: ", entry)
pkg <- parts[[2L]]
if (!requireNamespace(pkg, quietly = TRUE))
stop("Adapter tests require ", pkg)
have <- utils::packageVersion(pkg)
floor <- parts[[4L]]
if (nzchar(floor) && have < package_version(floor))
stop(pkg, " ", have, " is below the required ", floor)
message(pkg, " ", have, " at ", find.package(pkg))
}'

- name: Test
run: ./run.sh run_tests

- name: Fail on check warnings
run: |
log=chat.api.Rcheck/00check.log
if [ ! -f "$log" ]; then
echo "Missing $log: R CMD check did not finish" >&2
exit 1
fi
grep -E '^Status:' "$log"
if grep -qE '^Status:.*(ERROR|WARNING)' "$log"; then
cat "$log" >&2
exit 1
fi
22 changes: 11 additions & 11 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
Package: chat.api
Type: Package
Title: Transport-Agnostic Chat Contract for R Agents
Version: 0.0.1.29
Date: 2026-09-08
Title: Transport-Agnostic Chat Contract
Version: 0.1.0
Date: 2026-09-11
Authors@R: c(
person("Troy", "Hernandez", role = c("aut", "cre"),
email = "troy@cornball.ai",
comment = c(ORCID = "0009-0005-4248-604X")),
person("cornball.ai", role = "cph"))
Description: A transport-agnostic contract for chat-room connectivity:
connect, poll, and send against one interface, with adapters for
'Matrix', 'Slack', 'Telegram', Internet Relay Chat (IRC), and other
platforms
supplied by optional packages. The contract is the union of the
parameters found across the R chat-client ecosystem, with per-adapter
capability flags for threads, markup dialects, encryption, and
per-message identity. Zero dependencies.
'Matrix' <https://spec.matrix.org/>, 'Slack' <https://api.slack.com/>,
'Telegram' <https://core.telegram.org/bots/api>, and Internet Relay
Chat (IRC). An in-memory adapter supports local testing. Capability
flags describe support for threads, markup dialects, encryption, and
per-message identity. Platform clients are supplied by optional
packages; the core interface uses only base R.
License: Apache License (>= 2)
Depends: R (>= 4.0)
URL: https://github.com/cornball-ai/chat.api
BugReports: https://github.com/cornball-ai/chat.api/issues
Suggests:
httr,
mx.api,
mx.client (>= 0.2.0.8),
mx.crypto (>= 0.2.1.1),
mx.client (>= 0.2.1),
mx.crypto (>= 0.2.2),
slackr,
telegram,
tinytest
Expand Down
68 changes: 16 additions & 52 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,52 +1,16 @@
# chat.api 0.0.1.29

* chat_telegram() accepts a telegram::TGBot as `bot`. Requests then go
through the class's public req(), so its proxy settings apply and the
token can stay inside the object; attachments are fetched from the
URL its getFile() returns. Only the transport is borrowed: the class's
own verbs cannot long-poll, choose update kinds, edit, react, or
leave, and its parser flattens updates into data frames. telegram
joins Suggests. The .download testing seam now takes
(file_id, file_path, dest).

# chat.api 0.0.1.28

* Slack gains user-token identity: chat_slack(user_token =) plus
as_user = TRUE on chat_send() and chat_whoami() authenticate as an
actual workspace member instead of the bot, so a send shows up under
that member's real name and photo rather than the bot's profile.
Distinct from identity/username, which only relabels the bot's own
post. Capability flag user_identity reports whether a given Slack
client was configured with a user token; every other adapter reports
FALSE.

# chat.api 0.0.1.27

* New Telegram adapter, chat_telegram(), over the Bot API with HTTP
delegated to the suggested httr package. getUpdates long polling is
the poll, with a single update offset as the cursor. Sends render
markdown to Telegram HTML and carry threads, replies, files, and
silent delivery; edits, emoji reactions and reaction events, typing,
chat info, leaving, identity, @username addressing, and attachment
fetch through getFile are wired. Capabilities report what the Bot
API lacks: history, member and chat lists, read markers, joining,
and creating.

# 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
unsent requests with their stable ids, and treats transport failures as
warnings so a decrypted sync batch is not lost or replayed.
* Same-user forwarded-key recovery requires a cross-signing chain matching
the master key in the local crypto store. Missing or unreadable local keys
leave the user's devices untrusted for recovery.
* Matrix E2EE now requires mx.client >= 0.2.0.8 and mx.crypto >= 0.2.1.1 for
durable request handling and local cross-signing key access.
# chat.api 0.1.0

* First CRAN release.
* A common interface for polling, sending, editing, reactions, attachments,
room membership, history, state, and identity, with capability flags for
adapter-specific support.
* An in-memory loopback adapter for local development and testing, plus
adapters for Matrix, IRC, Slack, and Telegram.
* Matrix supports encrypted messaging, durable room-key requests,
verification against local cross-signing keys, and credential persistence
through mx.client and mx.crypto.
* Slack supports channel creation and leaving, bot identity customization,
and posting as a workspace member when configured with a user token.
* Telegram supports long polling, threads, replies, files, edits, reactions,
and identity. Requests can use httr directly or a telegram::TGBot object
with its configured proxy.
16 changes: 16 additions & 0 deletions R/chat.api-package.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#' Transport-agnostic chat connectivity
#'
#' A common interface for chat messages, attachments, rooms, and identity.
#' Use \code{\link{chat_loopback}} for local development, or connect through
#' \code{\link{chat_matrix}}, \code{\link{chat_irc}},
#' \code{\link{chat_slack}}, or \code{\link{chat_telegram}}.
#' Inspect \code{\link{chat_capabilities}} before using optional operations.
#'
#' @name chat.api-package
#' @aliases chat.api
#' @keywords package
#' @examples
#' cl <- chat_loopback()
#' chat_send(cl, "general", "hello")
#' chat_poll(cl)$messages
NULL
Loading