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
63 changes: 63 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,68 @@ jobs:
- 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

# mx.crypto needs a Rust toolchain to build from source, so it is
# Linux-only here, off the r2u binary rapt already has sources for.
# Its one test -- that matrix_crypto_init() binds the store before
# opening the account -- is conditional and does not run on macOS.
- name: Install mx.crypto (Linux)
if: runner.os == 'Linux'
run: Rscript -e 'install.packages("mx.crypto")'

# 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
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)")'

- name: Test
run: ./run.sh run_tests
6 changes: 3 additions & 3 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.2
Date: 2026-07-20
Version: 0.0.1.11
Date: 2026-08-05
Authors@R: c(
person("Troy", "Hernandez", role = c("aut", "cre"),
email = "troy@cornball.ai",
Expand All @@ -21,7 +21,7 @@ URL: https://github.com/cornball-ai/chat.api
BugReports: https://github.com/cornball-ai/chat.api/issues
Suggests:
mx.api,
mx.client (>= 0.1.1.2),
mx.client (>= 0.2.0.2),
mx.crypto,
slackr,
tinytest
Expand Down
17 changes: 15 additions & 2 deletions R/contract.R
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,30 @@ chat_disconnect.default <- function(client, ...) {
#' adapter-specific and may already be normalized by the transport
#' package (Matrix hands over an extracted record, not the timeline
#' event), so it is an escape hatch, not a guarantee of completeness.
#' @param encrypted Logical: did this message arrive end-to-end
#' encrypted? FALSE on transports without E2EE and on cleartext
#' messages in rooms that have it.
#' @param sender_verified Logical: does the sender identifier bind to a
#' device whose keys this client verified? NULL on cleartext messages,
#' where the transport asserts the sender and there is nothing to
#' verify. On an encrypted message FALSE is a real answer, not a
#' missing one: the payload decrypted, but its claimed sender could not
#' be tied to a verified device, so the identifier is the homeserver's
#' word rather than cryptographic fact.
#' @return A list with class \code{chat_message}.
#' @export
chat_message <- function(id, channel, sender, body, ts, thread = NULL,
markup = "plain", kind = "message", self = NULL,
mentions = NULL, raw = NULL) {
mentions = NULL, raw = NULL, encrypted = FALSE,
sender_verified = NULL) {
stopifnot(is.character(id), is.character(channel), is.character(sender),
is.character(body))
structure(list(id = id, channel = channel, sender = sender,
body = body, ts = ts, thread = thread,
markup = markup, kind = kind, self = self,
mentions = mentions, raw = raw),
mentions = mentions, raw = raw,
encrypted = isTRUE(encrypted),
sender_verified = sender_verified),
class = "chat_message")
}

Expand Down
Loading
Loading