Skip to content
Merged
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: mx.api
Type: Package
Title: Minimal Matrix Client-Server API
Version: 0.2.0.1
Version: 0.3.0
Date: 2026-05-13
Authors@R: c(
person("Troy", "Hernandez", role = c("aut", "cre"),
Expand Down
18 changes: 18 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,28 +1,46 @@
# tinyrox says don't edit this manually, but it can't stop you!

export(mx_canonical_json)
export(mx_delete_device)
export(mx_devices)
export(mx_download)
export(mx_get_account_data)
export(mx_get_state)
export(mx_guess_mime)
export(mx_keys_claim)
export(mx_keys_query)
export(mx_keys_upload)
export(mx_login)
export(mx_logout)
export(mx_media_config)
export(mx_messages)
export(mx_profile)
export(mx_react)
export(mx_read_receipt)
export(mx_redact)
export(mx_register)
export(mx_room_create)
export(mx_room_invite)
export(mx_room_join)
export(mx_room_leave)
export(mx_room_members)
export(mx_room_name)
export(mx_room_topic)
export(mx_rooms)
export(mx_send)
export(mx_send_audio)
export(mx_send_event)
export(mx_send_file)
export(mx_send_image)
export(mx_send_media)
export(mx_send_to_device)
export(mx_send_video)
export(mx_session)
export(mx_set_account_data)
export(mx_set_avatar_url)
export(mx_set_displayname)
export(mx_set_state)
export(mx_sync)
export(mx_typing)
export(mx_upload)
export(mx_whoami)
30 changes: 30 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
# mx.api 0.3.0

* Generic room-event and state plumbing: `mx_send_event()` sends any
event type (e.g. `m.room.encrypted`), `mx_set_state()` /
`mx_get_state()` write and read state events (e.g.
`m.room.encryption`). Together these are what an external
end-to-end-encryption layer (such as 'mx.client') needs.
* Media messages: `mx_send_media()` uploads a file and posts the
referencing `m.room.message`, deriving m.image/m.audio/m.video from
the MIME type by default; `mx_send_file()`, `mx_send_image()`,
`mx_send_audio()`, and `mx_send_video()` fix the msgtype explicitly.
Metadata beyond mimetype and size is caller-supplied; mx.api does not
inspect media files. `mx_guess_mime()` is now exported, and
`mx_media_config()` reports the server's upload cap.
* `mx_upload()` streams files from disk instead of reading them into
memory, and `mx_guess_mime()` falls back to octet-stream on unknown
extensions as documented (was NA).
* HTTP failures signal classed conditions
(`mx_error_<ERRCODE>` / `mx_error`) carrying `$errcode`, `$status`,
and the parsed `$body`, so callers can react to specific failures
without parsing message strings. Message text is unchanged.
* Bot lifecycle endpoints: `mx_room_invite()` (invite into an existing
room), `mx_redact()` (Matrix deletion), `mx_typing()` (typing
indicator), and profile helpers `mx_profile()`,
`mx_set_displayname()`, `mx_set_avatar_url()`.
* Account data: `mx_get_account_data()` / `mx_set_account_data()`,
kept generic (no DM-semantics helpers).
* Devices: `mx_devices()` lists; `mx_delete_device()` deletes, passing
any user-interactive auth payload through verbatim.

# mx.api 0.2.0

* New transport endpoints for end-to-end-encryption coordination:
Expand Down
53 changes: 53 additions & 0 deletions R/account.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Account data: per-user key-value storage on the homeserver.

#' Get account data
#'
#' Reads a global account-data event for a user, e.g. \code{"m.direct"}
#' (the DM room map) or any custom namespaced type.
#'
#' @param session An "mx_session" object.
#' @param type Character. Event type, e.g. \code{"m.direct"}.
#' @param user_id Character. Defaults to the session's own user.
#' @return The account-data content as a list, or NULL when the type has
#' never been set.
#' @examples
#' \dontrun{
#' direct <- mx_get_account_data(s, "m.direct")
#' }
#' @export
mx_get_account_data <- function(session, type, user_id = session$user_id) {
path <- sprintf(
"/_matrix/client/v3/user/%s/account_data/%s",
mx_encode_id(user_id), mx_encode_id(type)
)
tryCatch(
mx_http(session$server, "GET", path, token = session$token),
mx_error_M_NOT_FOUND = function(e) NULL
)
}

#' Set account data
#'
#' Writes a global account-data event for a user. The content replaces
#' whatever was stored under \code{type}.
#'
#' @param session An "mx_session" object.
#' @param type Character. Event type, e.g. \code{"m.direct"}.
#' @param content List. The content, sent as-is.
#' @param user_id Character. Defaults to the session's own user.
#' @return Invisibly TRUE on success.
#' @examples
#' \dontrun{
#' mx_set_account_data(s, "ai.cornball.notes", list(theme = "dark"))
#' }
#' @export
mx_set_account_data <- function(session, type, content,
user_id = session$user_id) {
path <- sprintf(
"/_matrix/client/v3/user/%s/account_data/%s",
mx_encode_id(user_id), mx_encode_id(type)
)
mx_http(session$server, "PUT", path, body = content,
token = session$token)
invisible(TRUE)
}
55 changes: 55 additions & 0 deletions R/devices.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Device management.

#' List this account's devices
#'
#' @param session An "mx_session" object.
#' @return A list of devices, each with \code{device_id},
#' \code{display_name}, \code{last_seen_ip}, and \code{last_seen_ts}.
#' @examples
#' \dontrun{
#' vapply(mx_devices(s), function(d) d$device_id, character(1))
#' }
#' @export
mx_devices <- function(session) {
resp <- mx_http(session$server, "GET", "/_matrix/client/v3/devices",
token = session$token)
resp$devices %||% list()
}

#' Delete a device
#'
#' Removes a device and invalidates its access token. Most homeservers
#' protect this with user-interactive authentication: the first call
#' fails with M_FORBIDDEN or a 401 carrying a \code{flows} object, and
#' the caller retries with a completed \code{auth} payload, e.g.
#' \code{list(type = "m.login.password", identifier = list(type =
#' "m.id.user", user = "bot"), password = "...", session = "<from the
#' 401>")}. mx.api deliberately does not automate that exchange.
#'
#' @param session An "mx_session" object.
#' @param device_id Character. The device to delete.
#' @param auth List or NULL. Completed user-interactive auth payload.
#' @return Invisibly TRUE on success.
#' @examples
#' \dontrun{
#' mx_delete_device(s, "OLDDEVICE", auth = list(
#' type = "m.login.password",
#' identifier = list(type = "m.id.user", user = "bot"),
#' password = "secret"
#' ))
#' }
#' @export
mx_delete_device <- function(session, device_id, auth = NULL) {
body <- if (is.null(auth)) {
mx_empty_body()
} else {
list(auth = auth)
}
path <- sprintf(
"/_matrix/client/v3/devices/%s",
mx_encode_id(device_id)
)
mx_http(session$server, "DELETE", path, body = body,
token = session$token)
invisible(TRUE)
}
28 changes: 25 additions & 3 deletions R/http.R
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,36 @@ mx_http <- function(base_url, method, path, body = NULL, query = NULL,
}

if (resp$status_code >= 400) {
errcode <- parsed$errcode %||% "HTTP"
msg <- parsed$error %||% paste("HTTP", resp$status_code)
stop(sprintf("Matrix error [%s]: %s", errcode, msg), call. = FALSE)
mx_raise(parsed$errcode %||% "HTTP",
parsed$error %||% paste("HTTP", resp$status_code),
status = resp$status_code, body = parsed)
}

parsed
}

# Raise a classed Matrix error. Code can catch specific failures by
# class -- e.g. tryCatch(..., mx_error_M_NOT_FOUND = function(e) NULL)
# or test inherits(e, "mx_error_M_UNKNOWN_TOKEN") to re-login -- instead
# of grepl()ing the message. The message text keeps the historical
# "Matrix error [CODE]: msg" shape, and $errcode, $status, and $body
# carry the structured details.
mx_raise <- function(errcode, msg, status = NULL, body = NULL) {
cond <- structure(
class = c(paste0("mx_error_", errcode), "mx_error",
"error", "condition"),
list(
message = sprintf("Matrix error [%s]: %s",
errcode, msg),
call = NULL,
errcode = errcode,
status = status,
body = body
)
)
stop(cond)
}

`%||%` <- function(a, b) if (is.null(a)) b else a

mx_txn_id <- function() {
Expand Down
Loading