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: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: chat.api
Type: Package
Title: Transport-Agnostic Chat Contract for R Agents
Version: 0.0.1.19
Version: 0.0.1.20
Date: 2026-08-05
Authors@R: c(
person("Troy", "Hernandez", role = c("aut", "cre"),
Expand Down
9 changes: 7 additions & 2 deletions R/contract.R
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,10 @@ chat_relogin.default <- function(client, ...) {
#' apply it.
#' @param markup \code{"plain"} or \code{"markdown"}, as
#' \code{\link{chat_send}}.
#' @param kind Message kind, as \code{\link{chat_send}}. Must match what
#' the message was sent as: Matrix carries the msgtype inside the
#' replacement content, so editing an \code{m.notice} without saying so
#' turns it into an ordinary message.
#' @param ... Adapter-specific options.
#' @return The identifier of the event the edit created where the
#' platform makes one (Matrix), or of the edited message where it does
Expand All @@ -788,14 +792,15 @@ chat_relogin.default <- function(client, ...) {
#' thing to be paged with, a half-finished sentence is not.
#' @export
chat_edit <- function(client, channel, message_id, text,
markup = c("plain", "markdown"), rich = NULL, ...) {
markup = c("plain", "markdown"), rich = NULL,
kind = "message", ...) {
UseMethod("chat_edit")
}

#' @export
chat_edit.default <- function(client, channel, message_id, text,
markup = c("plain", "markdown"), rich = NULL,
...) {
kind = "message", ...) {
stop("chat_edit() is not supported by this adapter (",
paste(class(client), collapse = "/"),
"). Check chat_capabilities()$edits.", call. = FALSE)
Expand Down
2 changes: 1 addition & 1 deletion R/loopback.R
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ chat_history.chat_loopback <- function(client, channel, limit = 50L,
#' @export
chat_edit.chat_loopback <- function(client, channel, message_id, text,
markup = c("plain", "markdown"),
rich = NULL, ...) {
rich = NULL, kind = "message", ...) {
markup <- match.arg(markup)
pos <- which(vapply(client$env$log,
function(m) identical(m$id, message_id), logical(1)))
Expand Down
49 changes: 29 additions & 20 deletions R/matrix.R
Original file line number Diff line number Diff line change
Expand Up @@ -550,23 +550,7 @@ chat_send.chat_matrix <- function(client, channel, text,
media_ids <- c(media_ids, as.character(event))
}
}
# The contract's three kinds, plus a documented way past them: a
# kind already spelled as a Matrix msgtype goes out as itself. The
# contract has no word for an image or a file, and mapping those to
# "m.text" -- which is what the else branch below does to anything
# unrecognized -- posts a text message whose body is a filename.
# Better a caller that names a Matrix type explicitly than a caller
# forced around the contract entirely to send one.
msgtype <- if (identical(kind, "notice")) {
"m.notice"
} else if (identical(kind, "emote")) {
"m.emote"
} else if (is.character(kind) && length(kind) == 1L &&
startsWith(kind, "m.")) {
kind
} else {
"m.text"
}
msgtype <- matrix_msgtype(kind)
# An attachment-only send is the uploads and nothing else. Matrix
# accepts an empty body and clients render it as a blank bubble, so
# posting one after every file leaves visible litter in the room.
Expand Down Expand Up @@ -923,7 +907,7 @@ chat_mark_read.chat_matrix <- function(client, channel, message_id, ...) {
#' @export
chat_edit.chat_matrix <- function(client, channel, message_id, text,
markup = c("plain", "markdown"),
rich = NULL, ...) {
rich = NULL, kind = "message", ...) {
markup <- match.arg(markup)
# Refused in encrypted rooms, and chat_capabilities() reports
# edits = FALSE on an e2ee client to match -- the same bargain
Expand All @@ -950,7 +934,8 @@ chat_edit.chat_matrix <- function(client, channel, message_id, text,
} else {
NULL
}
new_content <- list(msgtype = "m.text", body = text)
msgtype <- matrix_msgtype(kind)
new_content <- list(msgtype = msgtype, body = text)
# A supplied fragment wins over one rendered from markdown: the
# caller has markup the renderer cannot express, which is the only
# reason to pass one.
Expand All @@ -972,7 +957,7 @@ chat_edit.chat_matrix <- function(client, channel, message_id, text,
extra$formatted_body <- paste0("* ", html)
}
invisible(as.character(fn(sess, channel, paste0("* ", text),
msgtype = "m.text", extra = extra)))
msgtype = msgtype, extra = extra)))
}

# A send carrying a caller-supplied HTML fragment. mx_send_text() builds
Expand All @@ -989,3 +974,27 @@ matrix_send_rich <- function(client, channel, text, rich, msgtype) {
fn(sess, channel, text, msgtype = msgtype,
extra = list(format = "org.matrix.custom.html", formatted_body = rich))
}

# The contract's kind vocabulary as a Matrix msgtype, plus a documented
# way past it: a kind already spelled as a msgtype goes out as itself.
# The contract has no word for an image or a file, and mapping those to
# m.text -- which is what an unrecognized kind falls to -- posts a text
# message whose body is a filename. Better a caller that names a Matrix
# type explicitly than one forced around the contract to send it.
#
# Shared by chat_send() and chat_edit(): an edit carries the msgtype in
# its m.new_content, so an edit that assumed m.text would quietly turn
# an m.notice into an ordinary message the first time it fired -- and
# m.notice is what stops a bot's own output triggering other bots.
matrix_msgtype <- function(kind) {
if (identical(kind, "notice")) {
"m.notice"
} else if (identical(kind, "emote")) {
"m.emote"
} else if (is.character(kind) && length(kind) == 1L &&
startsWith(kind, "m.")) {
kind
} else {
"m.text"
}
}
2 changes: 1 addition & 1 deletion R/slack.R
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ chat_set_identity.chat_slack <- function(client, display, ...) {
#' @export
chat_edit.chat_slack <- function(client, channel, message_id, text,
markup = c("plain", "markdown"),
rich = NULL, ...) {
rich = NULL, kind = "message", ...) {
markup <- match.arg(markup)
api <- client$api_fn %||% slackr::call_slack_api
resp <- api("/api/chat.update", .method = "POST", token = client$token,
Expand Down
33 changes: 33 additions & 0 deletions inst/tinytest/test_matrix.R
Original file line number Diff line number Diff line change
Expand Up @@ -1923,3 +1923,36 @@ local({
expect_identical(chat_capabilities(cl)$rich_markup, character())
})
expect_identical(chat_capabilities(seam_client())$rich_markup, "html")

# ---- An edit keeps the message's kind ----
# Matrix carries the msgtype inside m.new_content, so an edit that
# assumed m.text would turn an m.notice into an ordinary message the
# first time it fired -- and m.notice is what keeps a bot's own output
# from triggering other bots.
local({
seen <- NULL
cl <- seam_client(.edit = function(session, room_id, body, msgtype = "m.text",
extra = NULL) {
seen <<- list(msgtype = msgtype, extra = extra)
"$e"
})
chat_edit(cl, "!a:ex", "$o", "Ran 3 commands", kind = "notice")
expect_identical(seen$msgtype, "m.notice")
expect_identical(seen$extra$`m.new_content`$msgtype, "m.notice")
})
local({
# Default is unchanged.
seen <- NULL
cl <- seam_client(.edit = function(session, room_id, body, msgtype = "m.text",
...) {
seen <<- msgtype
"$e"
})
chat_edit(cl, "!a:ex", "$o", "x")
expect_identical(seen, "m.text")
})
expect_identical(chat.api:::matrix_msgtype("notice"), "m.notice")
expect_identical(chat.api:::matrix_msgtype("emote"), "m.emote")
expect_identical(chat.api:::matrix_msgtype("message"), "m.text")
expect_identical(chat.api:::matrix_msgtype("m.image"), "m.image")
expect_identical(chat.api:::matrix_msgtype(NULL), "m.text")
6 changes: 6 additions & 0 deletions man/chat_edit.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ chat_edit(
text,
markup = c("plain", "markdown"),
rich = NULL,
kind = "message",
...
)
}
Expand All @@ -29,6 +30,11 @@ apply it.}
\item{markup}{\code{"plain"} or \code{"markdown"}, as
\code{\link{chat_send}}.}

\item{kind}{Message kind, as \code{\link{chat_send}}. Must match what
the message was sent as: Matrix carries the msgtype inside the
replacement content, so editing an \code{m.notice} without saying so
turns it into an ordinary message.}

\item{...}{Adapter-specific options.}
}
\value{
Expand Down
Loading