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: mx.api
Type: Package
Title: Minimal Matrix Client-Server API
Version: 0.3.0
Version: 0.3.0.1
Date: 2026-06-10
Authors@R: c(
person("Troy", "Hernandez", role = c("aut", "cre"),
Expand Down
8 changes: 8 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# mx.api 0.3.0.1

* `mx_room_create()` gains `creation_content`, merged into the
`m.room.create` event. This is the only way to set properties fixed at
creation and immutable afterwards -- notably `type = "m.space"`, which
makes the room a space. A space cannot be converted from an ordinary
room later, so it has to be requested at creation or not at all.

# mx.api 0.3.0

* Generic room-event and state plumbing: `mx_send_event()` sends any
Expand Down
23 changes: 22 additions & 1 deletion R/rooms.R
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,26 @@ mx_rooms <- function(session) {
#' @param preset Character or NULL. A Matrix room preset
#' ("private_chat", "trusted_private_chat", "public_chat").
#' @param invite Character vector. Matrix IDs to invite.
#' @param creation_content Named list or NULL. Merged into the
#' \code{m.room.create} event content. This is the only way to set
#' properties that are fixed at creation and immutable afterwards --
#' notably \code{type = "m.space"}, which makes the room a space rather
#' than a conversation. A space cannot be converted from an ordinary
#' room later, so it has to be requested here or not at all.
#'
#' @return The new room ID as a character string.
#' @examples
#' \dontrun{
#' room_id <- mx_room_create(s, name = "test", topic = "hello")
#'
#' # A space, which holds other rooms instead of messages
#' space_id <- mx_room_create(s, name = "Topics",
#' creation_content = list(type = "m.space"))
#' }
#' @export
mx_room_create <- function(session, name = NULL, topic = NULL,
visibility = "private", preset = NULL,
invite = character()) {
invite = character(), creation_content = NULL) {
body <- list(visibility = visibility)
if (!is.null(name)) {
body$name <- name
Expand All @@ -50,6 +60,17 @@ mx_room_create <- function(session, name = NULL, topic = NULL,
if (length(invite)) {
body$invite <- as.list(invite)
}
if (length(creation_content)) {
if (!is.list(creation_content) ||
is.null(names(creation_content)) ||
!all(nzchar(names(creation_content)))) {
stop("creation_content must be a fully named list", call. = FALSE)
}
# Sent as an object even with one entry: unnamed or auto-unboxed
# here would serialize to a bare string and the server would
# reject the create rather than quietly making a normal room.
body$creation_content <- creation_content
}

resp <- mx_http(
session$server, "POST", "/_matrix/client/v3/createRoom",
Expand Down
64 changes: 64 additions & 0 deletions inst/tinytest/test_room_create_body.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# What mx_room_create() actually puts on the wire.
#
# The existence checks in test_rooms.R cannot see this, and a live server
# is not needed to: the request body is built entirely in R. `mx_http` is
# stubbed and the body captured, which is the only way to assert the shape
# of a field whose wrong shape the server accepts.

library(tinytest)

ns <- asNamespace("mx.api")
capture_body <- function(expr) {
seen <- NULL
orig <- get("mx_http", envir = ns, inherits = FALSE)
assignInNamespace("mx_http", function(base_url, method, path, body = NULL,
query = NULL, token = NULL) {
seen <<- body
list(room_id = "!captured:example.org")
}, ns = "mx.api")
on.exit(assignInNamespace("mx_http", orig, ns = "mx.api"), add = TRUE)
force(expr)
seen
}

s <- mx.api::mx_session("https://example.org", "tok", "@bot:example.org", "DEV")

# ---- the default body is unchanged ----------------------------------
# creation_content is absent, not empty. An empty object in m.room.create
# is a different request from no object at all.
b <- capture_body(mx.api::mx_room_create(s, name = "plain"))
expect_equal(b$name, "plain")
expect_equal(b$visibility, "private")
expect_null(b$creation_content)

# ---- a space carries type through -----------------------------------
b <- capture_body(mx.api::mx_room_create(s, name = "Topics",
creation_content = list(type = "m.space")))
expect_equal(b$creation_content, list(type = "m.space"))

# THE SHAPE, not just the value. `type = "m.space"` has to serialize as
# an object; mx_http auto-unboxes, and an unnamed list(...) of one string
# goes out as a bare "m.space" instead. The server's response to that is
# an ordinary room, so the failure is a working space-less space rather
# than an error. Asserted on the JSON because that is where it goes wrong.
json <- jsonlite::toJSON(b, auto_unbox = TRUE, null = "null")
expect_true(grepl('"creation_content":\\{"type":"m.space"\\}', json))

# ---- an unnamed list is refused rather than sent ---------------------
# This is the mistake the assertion above describes, caught at the call
# instead of becoming a room that looks fine and holds nothing.
expect_error(mx.api::mx_room_create(s, creation_content = list("m.space")),
"fully named")
expect_error(mx.api::mx_room_create(s, creation_content = list(type = "m.space", "x")),
"fully named")

# ---- an empty creation_content is treated as absent ------------------
b <- capture_body(mx.api::mx_room_create(s, creation_content = list()))
expect_null(b$creation_content)

# ---- invite and creation_content coexist ----------------------------
b <- capture_body(mx.api::mx_room_create(s, name = "Topics",
invite = c("@troy:example.org"),
creation_content = list(type = "m.space")))
expect_equal(b$invite, list("@troy:example.org"))
expect_equal(b$creation_content, list(type = "m.space"))
8 changes: 6 additions & 2 deletions man/mx_keys_upload.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
\alias{mx_keys_upload}
\title{Upload device identity and one-time keys}
\usage{
mx_keys_upload(session, device_keys = NULL, one_time_keys = NULL,
fallback_keys = NULL)
mx_keys_upload(
session,
device_keys = NULL,
one_time_keys = NULL,
fallback_keys = NULL
)
}
\arguments{
\item{session}{An \code{mx_session}.}
Expand Down
8 changes: 6 additions & 2 deletions man/mx_read_receipt.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
\alias{mx_read_receipt}
\title{Send a read receipt for a room event}
\usage{
mx_read_receipt(session, room_id, event_id,
receipt_type = c("m.read", "m.read.private"))
mx_read_receipt(
session,
room_id,
event_id,
receipt_type = c("m.read", "m.read.private")
)
}
\arguments{
\item{session}{An "mx_session" object.}
Expand Down
10 changes: 8 additions & 2 deletions man/mx_register.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
\alias{mx_register}
\title{Register a new account on a Matrix homeserver}
\usage{
mx_register(server, username, password, device_id = NULL,
initial_device_display_name = NULL, inhibit_login = FALSE)
mx_register(
server,
username,
password,
device_id = NULL,
initial_device_display_name = NULL,
inhibit_login = FALSE
)
}
\arguments{
\item{server}{Character. Homeserver base URL.}
Expand Down
22 changes: 20 additions & 2 deletions man/mx_room_create.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
\alias{mx_room_create}
\title{Create a room}
\usage{
mx_room_create(session, name = NULL, topic = NULL, visibility = "private",
preset = NULL, invite = character())
mx_room_create(
session,
name = NULL,
topic = NULL,
visibility = "private",
preset = NULL,
invite = character(),
creation_content = NULL
)
}
\arguments{
\item{session}{An "mx_session" object.}
Expand All @@ -19,6 +26,13 @@ mx_room_create(session, name = NULL, topic = NULL, visibility = "private",
("private_chat", "trusted_private_chat", "public_chat").}

\item{invite}{Character vector. Matrix IDs to invite.}

\item{creation_content}{Named list or NULL. Merged into the
\code{m.room.create} event content. This is the only way to set
properties that are fixed at creation and immutable afterwards --
notably \code{type = "m.space"}, which makes the room a space rather
than a conversation. A space cannot be converted from an ordinary
room later, so it has to be requested here or not at all.}
}
\value{
The new room ID as a character string.
Expand All @@ -29,5 +43,9 @@ Create a room
\examples{
\dontrun{
room_id <- mx_room_create(s, name = "test", topic = "hello")

# A space, which holds other rooms instead of messages
space_id <- mx_room_create(s, name = "Topics",
creation_content = list(type = "m.space"))
}
}
51 changes: 41 additions & 10 deletions man/mx_send_media.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,51 @@
\alias{mx_send_video}
\title{Send a media file to a room}
\usage{
mx_send_media(session, room_id, path, body = basename(path), msgtype = NULL,
content_type = NULL, info = list())
mx_send_media(
session,
room_id,
path,
body = basename(path),
msgtype = NULL,
content_type = NULL,
info = list()
)

mx_send_file(session, room_id, path, body = basename(path),
content_type = NULL, info = list())
mx_send_file(
session,
room_id,
path,
body = basename(path),
content_type = NULL,
info = list()
)

mx_send_image(session, room_id, path, body = basename(path),
content_type = NULL, info = list())
mx_send_image(
session,
room_id,
path,
body = basename(path),
content_type = NULL,
info = list()
)

mx_send_audio(session, room_id, path, body = basename(path),
content_type = NULL, info = list())
mx_send_audio(
session,
room_id,
path,
body = basename(path),
content_type = NULL,
info = list()
)

mx_send_video(session, room_id, path, body = basename(path),
content_type = NULL, info = list())
mx_send_video(
session,
room_id,
path,
body = basename(path),
content_type = NULL,
info = list()
)
}
\arguments{
\item{session}{An "mx_session" object.}
Expand Down
Loading