| Type: | Package |
| Title: | Transport-Agnostic Chat Contract |
| Version: | 0.1.0 |
| Date: | 2026-09-11 |
| Description: | A transport-agnostic contract for chat-room connectivity: connect, poll, and send against one interface, with adapters for '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.1), mx.crypto (≥ 0.2.2), slackr, telegram, tinytest |
| Encoding: | UTF-8 |
| NeedsCompilation: | no |
| Packaged: | 2026-09-15 23:19:44 UTC; troy |
| Author: | Troy Hernandez |
| Maintainer: | Troy Hernandez <troy@cornball.ai> |
| Repository: | CRAN |
| Date/Publication: | 2026-09-27 15:50:02 UTC |
Transport-agnostic chat connectivity
Description
A common interface for chat messages, attachments, rooms, and identity.
Use chat_loopback for local development, or connect through
chat_matrix, chat_irc,
chat_slack, or chat_telegram.
Inspect chat_capabilities before using optional operations.
Examples
cl <- chat_loopback()
chat_send(cl, "general", "hello")
chat_poll(cl)$messages
Does a message address this client?
Description
Answers the question a bot in a room full of people has to ask before replying. Two signals feed it, and which ones exist is the adapter's business rather than the caller's:
Usage
chat_addressed(client, message, ...)
Arguments
client |
A |
message |
A |
... |
Adapter-specific options. |
Details
The message's declared mentions – Matrix
m.mentions, a Slack user ref. Structured, unambiguous, and the only signal the default method reads.The plain-text conventions of the transport. Matrix has
@@botand the full user id, Slack has<@U0123>, IRC has a leadingnick:. These are the reason this is a verb and not a field: writing the Matrix form into a consumer is how that consumer ends up knowing it is talking to Matrix.
The default reads only the declared mentions, so an adapter that does not override it under-reports rather than over-reports. A bot that misses being addressed stays quiet; one that thinks it was addressed when it was not talks over people, and unprompted is worse than absent.
Value
TRUE or FALSE.
Examples
cl <- chat_loopback()
chat_send(cl, "general", "hi")
chat_addressed(cl, chat_poll(cl)$messages[[1L]])
Construct a normalized attachment record
Description
Inbound media on a chat_message: chat_poll()
and chat_history() put these in the message's
attachments on adapters whose
chat_capabilities()$attachments is TRUE.
Usage
chat_attachment(
id,
name = NA_character_,
mime = NA_character_,
bytes = NA_integer_,
url = NA_character_,
path = NA_character_,
sha256 = NA_character_,
raw = NULL
)
Arguments
id |
Adapter-native identifier for the content (a Matrix mxc URI, a Slack file id). The stable handle; everything else here is description. |
name |
Filename as the sender labeled it, or NA. |
mime |
MIME type, or NA when the transport does not say. |
bytes |
Size in bytes, or NA. |
url |
A fetchable location, or NA. It may require this client's credentials; a consumer must not assume it is public. |
path |
Local filesystem path when the content is already on disk, or NA. |
sha256 |
Content hash, or NA. Adapters fill it only when the transport carries one (Matrix encrypted attachments do); they must not compute it speculatively, because NA meaning "unverified" is what tells a consumer that needs provenance to hash at ingest and record the result. |
raw |
The adapter's platform-native payload. |
Value
A list with class chat_attachment.
Examples
chat_attachment("mxc://ex/abc", name = "plot.png", mime = "image/png")
Describe what a chat client's platform supports
Description
Describe what a chat client's platform supports
Usage
chat_capabilities(client, ...)
Arguments
client |
A |
... |
Adapter-specific options. |
Value
A list with at least: threads (can post into
threads), thread_replies (thread replies come back out of
chat_poll), edits, reactions
(chat_react works), reaction_events (reactions
come back out of chat_poll), channel_info
(chat_channel_info works), members
(chat_members works), invites (invitations come
back out of chat_poll), join
(chat_join works), whoami
(chat_whoami works, and with it the default
chat_addressed), channel_create
(chat_channel_create works), leave
(chat_leave works), set_state
(durable channel state: both chat_set_state and
chat_get_state work – a transport has the pair or
neither), files (outbound:
chat_send(files =) works), attachments (inbound:
media comes back out of chat_poll as
chat_attachment records), typing,
e2ee, identity_override (logicals),
user_identity (a send can authenticate as a real member of
the platform rather than as the bot – Slack's
chat_send(as_user = TRUE) with a user token; a property of
the client instance's configuration there, and FALSE everywhere
else), markup_dialects (character),
max_message_bytes (integer or NA).
Sending and receiving get separate flags wherever a platform does
one and not the other, which is why threads and
thread_replies are two entries rather than one. Reactions
split the same way: Slack can place one, and does not report anyone
else's through the history endpoint this adapter polls, so a
consumer reading a single flag would wait forever for events that
never arrive.
Examples
caps <- chat_capabilities(chat_loopback())
caps$threads
caps$e2ee
Create a channel
Description
Capability-gated: check chat_capabilities()$channel_create.
Bots open rooms about as often as they are invited to them; a
contract without creation forces every such consumer below the
seam, into adapter-native calls.
Usage
chat_channel_create(client, name, ...)
Arguments
client |
A |
name |
Character. Human-readable name for the new channel. |
... |
Adapter-specific options (topic, visibility, invitees). |
Value
The new channel's identifier, invisibly. Everything else – sends, joins, membership – takes the identifier, not the name, so the return value is the point of the call.
Examples
cl <- chat_loopback()
id <- chat_channel_create(cl, "general")
chat_send(cl, id, "hello")
Describe a channel
Description
Returns the channel's descriptive metadata: what it is called and what it is for.
Usage
chat_channel_info(client, channel, ...)
Arguments
client |
A |
channel |
Channel/room identifier. |
... |
Adapter-specific options. |
Details
Membership is deliberately not here, and chat_members is
a separate verb. The two look like one lookup and are not: a name and
a topic are two short strings that change rarely, while a member list
is unbounded and changes constantly. Bundling them makes every read of
a topic pay for a member list, which on a busy room is the expensive
part – and a consumer caches the two on different schedules for
exactly that reason.
A NULL field means the channel has no such thing: a Matrix room with
no m.room.name really has no name. An adapter that cannot
answer at all throws, so "cannot ask" and "asked, and there is none"
stay distinguishable. Check chat_capabilities()$channel_info
first.
Value
A list with id, name, and topic.
id is the channel as the platform addresses it; name
and topic are character or NULL.
Examples
## Not run:
# Requires a saved Matrix configuration and a joined room.
cl <- chat_matrix(app = "mybot")
chat_channel_info(cl, "#general:example.org")
## End(Not run)
List the channels this client is in
Description
The state half of the contract. chat_poll answers "what
changed since my cursor"; this and its siblings answer "what is true
now", which is the question a process asks when it starts up with no
useful cursor at all.
Usage
chat_channels(client, ...)
Arguments
client |
A |
... |
Adapter-specific options. |
Value
Character vector of channel identifiers.
Examples
chat_channels(chat_loopback())
Construct a chat_config
Description
Construct a chat_config
Usage
chat_config(x, app = NULL, path = NULL)
Arguments
x |
A named list of configuration fields. |
app |
Application namespace the config belongs to, or NULL. |
path |
File the config was read from, or NULL for one that has never been written. |
Value
A list with class chat_config.
Examples
chat_config(list(server = "https://ex.invalid", user = "bot"),
app = "demo")
Persist a configuration
Description
Writes to the file the config came from, at mode 0600.
Usage
chat_config_save(config, app = NULL, path = NULL)
Arguments
config |
A |
app |
Override the app namespace the config was loaded under. |
path |
Override the file to write. |
Value
The config, invisibly.
Examples
if (requireNamespace("mx.client", quietly = TRUE)) {
path <- tempfile(fileext = ".json")
cfg <- chat_config(list(server = "https://matrix.example.org",
token = "example-token",
user_id = "@bot:example.org"))
chat_config_save(cfg, path = path)
file.exists(path)
unlink(path)
}
Close a chat client's connection
Description
The default method is a no-op: HTTP-poll transports have nothing to close. Persistent-socket transports (IRC) override it.
Usage
chat_disconnect(client, ...)
Arguments
client |
A |
... |
Adapter-specific options. |
Value
TRUE, invisibly.
Examples
cl <- chat_loopback()
chat_disconnect(cl)
Fetch an attachment's bytes to a local file
Description
Inbound attachments name where their content lives, not what it is:
chat_attachment's url is a platform handle that
usually needs this client's credentials, so a consumer cannot simply
download it. This is the verb that turns one into a file on disk.
Usage
chat_download(client, attachment, dest = NULL, ...)
Arguments
client |
A |
attachment |
A |
dest |
Destination path. NULL picks a temporary file, keeping
the attachment's extension where it has one. The caller should remove
temporary downloads with |
... |
Adapter-specific options. |
Details
Capability-gated on chat_capabilities()$attachments, the same
flag that says inbound media arrives at all. The default method
throws, on chat_react's reasoning: a fetch that
quietly did nothing leaves the caller pointing at a path with no
bytes behind it.
An attachment already on disk (path set, as the loopback
adapter records) is copied rather than fetched, so a consumer needs
one code path for both.
Value
The destination path, invisibly.
Examples
cl <- chat_loopback()
src <- tempfile(fileext = ".txt")
writeLines("hello", src)
chat_send(cl, "general", "a file", files = src)
attachment <- chat_poll(cl)$messages[[1L]]$attachments[[1L]]
dest <- chat_download(cl, attachment)
readLines(dest)
unlink(c(src, dest))
Replace the text of a message already sent
Description
What makes a progress message possible: post "working on it", then keep replacing it as the work happens, instead of narrating into the channel one message at a time.
Usage
chat_edit(
client,
channel,
message_id,
text,
markup = c("plain", "markdown"),
rich = NULL,
kind = "message",
...
)
Arguments
client |
A |
channel |
Channel/room identifier. |
message_id |
The message to replace, as returned by
|
text |
The replacement text, in full. Not a delta: every platform that supports this takes the whole new body, and a contract that took a patch would have to reconstruct the old one to apply it. |
markup |
|
rich |
Adapter-native markup for the replacement, as
|
kind |
Message kind, as |
... |
Adapter-specific options. |
Details
The default throws. An edit that silently does nothing leaves the old
text on screen, and stale content is worse than a visible failure –
the reader has no way to tell that what they are looking at is no
longer true. Check chat_capabilities()$edits.
Value
The identifier of the event the edit created where the platform makes one (Matrix), or of the edited message where it does not (Slack), invisibly.
What a consumer must not assume
That the edit is what readers see. A client that does not implement edits shows the original and an "* edited" fallback beside it, and notifications almost always carry the text as first sent. So the first version has to stand on its own – "working on it" is a fine thing to be paged with, a half-finished sentence is not.
Examples
cl <- chat_loopback()
id <- chat_send(cl, "general", "Working on it")
chat_edit(cl, "general", id, "Finished")
chat_history(cl, "general")$messages
Read durable typed state from a channel
Description
The counterpart of chat_set_state, reading back what
it wrote. Capability-gated on the same
chat_capabilities()$set_state: a transport with durable
channel state has both halves or neither.
Usage
chat_get_state(client, channel, type, state_key = "", ...)
Arguments
client |
A |
channel |
Channel/room identifier. |
type |
Character. Namespaced event type. |
state_key |
Character. Sub-key within the type, defaulting to the empty string. |
... |
Adapter-specific options. |
Details
Absent state is NULL, not an error. "Nothing was ever
written here" is an ordinary answer for a caller checking whether a
marker is set, and one it should not have to wrap in a handler. A
state store that cannot be reached at all still errors, because
that is a different fact.
Value
The stored content as a named list, or NULL when no
state is set for that type/state_key pair.
Examples
cl <- chat_loopback()
chat_get_state(cl, "general", "m.room.topic") # NULL until written
chat_set_state(cl, "general", "m.room.topic", list(topic = "Planning"))
chat_get_state(cl, "general", "m.room.topic")
Read a channel's recent messages
Description
Independent of the poll cursor: a restarted process uses this to recover the context it lost, and asking for it must not move the cursor or consume anything.
Usage
chat_history(client, channel, limit = 50L, cursor = NULL, ...)
Arguments
client |
A |
channel |
Channel/room identifier. |
limit |
Maximum messages to return. |
cursor |
Opaque continuation token from a previous call's
|
... |
Adapter-specific options. |
Value
A list with messages (list of chat_message,
oldest first) and cursor (opaque; pass it back to read
further into the past, NULL when the channel has no more history).
The cursor is opaque, like chat_poll's
Not a message id. This started out taking one and it was wrong on the
reference transport: Matrix's /messages takes a pagination
token from a previous response, and handing it an event id does not
page from that event – it fails, or worse, silently returns the wrong
window. Slack pages by its own next_cursor. There is no id that
means the same thing on both, so the contract does what it already
does for chat_poll: the token is the adapter's, and a
consumer only ever passes back what it was given.
Order
Chronological, oldest first, whatever the platform's native direction
is. Matrix dir = "b" and Slack conversations.history
both hand back newest-first and every consumer replaying history into
a transcript has to flip it. One flip in the adapter beats one per
consumer, and a consumer that gets it wrong produces a transcript
that reads backwards without erroring.
Note that pages run backwards while each page runs forwards: call it twice and the second page's messages all precede the first page's. A consumer assembling a full transcript prepends.
Overlap with chat_poll
The same message can arrive from both, and adapters must return the
same id for it either way. That id is the only thing a consumer
has to deduplicate on – a startup backfill and the first poll after
it routinely cover the same events.
Examples
cl <- chat_loopback()
chat_send(cl, "general", "hello")
chat_history(cl, "general")$messages
Construct an identity record
Description
Construct an identity record
Usage
chat_identity(id, display = NA_character_, raw = NULL)
Arguments
id |
The account identifier, in whatever form the transport
uses. Comparable against |
display |
Human-readable name, or NA when the adapter would have to ask the server for it. Never used for matching – it is not unique, and on most transports any account can set it to any other account's. For logs and prompts only. |
raw |
The adapter's platform-native identity payload. |
Value
A list with class chat_identity.
Examples
chat_identity("@bot:example.org", display = "corteza")
Construct a normalized invitation record
Description
The record chat_poll returns in $invites on
adapters whose chat_capabilities()$invites is TRUE.
Usage
chat_invite(channel, inviter = NA_character_, raw = NULL)
Arguments
channel |
Channel/room identifier the client has been invited to
(character). Pass it to |
inviter |
Who issued the invitation, or |
raw |
The adapter's platform-native payload. |
Value
A list with class chat_invite.
No timestamp
Unlike chat_message and chat_reaction,
there is no ts. An invitation is a standing state rather than
an event at a moment, and Matrix's stripped invite state carries no
reliable origin_server_ts to report. A field that could only
ever be NA is worse than no field.
Examples
chat_invite("!room:example.org", inviter = "@alice:example.org")
Create an IRC chat client
Description
Connects, registers (NICK/USER), and joins channels. The
persistent socket buffers into chat_poll: each poll
drains available lines, answers server PINGs, and returns PRIVMSGs
as normalized messages. The cursor is a message counter.
Usage
chat_irc(host, port = 6667L, nick, channels = character(), realname = nick)
Arguments
host |
Server hostname. |
port |
Server port (plaintext; commonly 6667). |
nick |
Nickname to register. |
channels |
Character vector of channels to join (e.g.
|
realname |
Real-name field for USER registration. |
Value
A chat_client of class chat_irc.
Examples
## Not run:
# Requires a reachable IRC server and permission to join the channel.
cl <- chat_irc(host = "irc.example.org", nick = "example_bot",
channels = "#example")
chat_poll(cl, timeout = 1)
chat_disconnect(cl)
## End(Not run)
Join a channel
Description
Accepts a pending invitation, or joins an open channel where the platform allows it.
Usage
chat_join(client, channel, ...)
Arguments
client |
A |
channel |
Channel/room identifier, as carried on a
|
... |
Adapter-specific options. |
Details
The default method throws, on the same reasoning as
chat_react: a join that silently does nothing leaves the
caller believing it is in a room it will never hear from. Check
chat_capabilities()$join.
Value
The joined channel's identifier, invisibly.
Examples
## Not run:
# Requires a saved Matrix configuration and access to the room.
cl <- chat_matrix(app = "mybot")
chat_join(cl, "#general:example.org")
## End(Not run)
Leave a channel
Description
The inverse of chat_join: after it returns, the
client stops receiving the channel's traffic, on platforms where
membership is a thing at all. Capability-gated: check
chat_capabilities()$leave.
Usage
chat_leave(client, channel, ...)
Arguments
client |
A |
channel |
Channel/room identifier. |
... |
Adapter-specific options. |
Value
The left channel's identifier, invisibly.
Examples
## Not run:
# Requires a saved Matrix configuration and a joined room.
cl <- chat_matrix(app = "mybot")
chat_leave(cl, "#general:example.org")
## End(Not run)
Create a loopback chat client
Description
Messages sent to any channel are appended to an in-memory log and
come back out of chat_poll. The cursor is the integer
position in that log.
Usage
chat_loopback()
Value
A chat_client of class chat_loopback.
Examples
cl <- chat_loopback()
chat_send(cl, "general", "hello")
chat_poll(cl)$messages
Mark a message as read
Description
The default is a quiet FALSE, on chat_typing's
reasoning rather than chat_react's: a read marker that
does not appear costs a human a little context about what the bot has
seen, and nothing more. Nobody is waiting on it the way they wait on
an acknowledgement.
Usage
chat_mark_read(client, channel, message_id, ...)
Arguments
client |
A |
channel |
Channel/room identifier. |
message_id |
The message to mark read, and everything before it. |
... |
Adapter-specific options. |
Details
Write-only. Reading other participants' read state is a much larger surface – per-user, per-device, and absent entirely on some platforms – and no consumer needs it yet.
Value
TRUE if the marker was sent, FALSE otherwise, invisibly.
Examples
cl <- chat_loopback()
id <- chat_send(cl, "general", "hello")
chat_mark_read(cl, "general", id) # FALSE: no read markers
Create a Matrix chat client
Description
Wraps an mx.client client config (see
mx.client::mx_client_load()). The sync cursor lives inside the
mx.client config; with save_cursor = TRUE every poll persists
it, so a restarted process resumes where it left off.
Usage
chat_matrix(
app = NULL,
path = NULL,
save_cursor = TRUE,
mx = NULL,
relogin = TRUE,
e2ee = FALSE,
crypto_store = NULL,
.sync = NULL,
.extract = NULL,
.send = NULL,
.media = NULL,
.typing = NULL,
.crypto = NULL,
.save = NULL,
.react = NULL,
.info = NULL,
.members = NULL,
.join = NULL,
.create = NULL,
.leave = NULL,
.state = NULL,
.get_state = NULL,
.extract_media = NULL,
.download = NULL,
.channels = NULL,
.history = NULL,
.pending = NULL,
.read = NULL,
.identity = NULL,
.edit = NULL,
.rich = NULL
)
Arguments
app |
Application namespace passed to
|
path |
Explicit config path, or NULL for the app default. |
save_cursor |
Logical. Persist the sync token after each poll,
through mx.client, into the config file the wrapped client points
at. Pass FALSE only if you persist |
mx |
A ready mx.client client config to wrap, for consumers
that already load and manage one; NULL (default) loads via
|
relogin |
Logical. Wrap each sync in
|
e2ee |
Logical. Own Olm/Megolm state in the adapter, so
Three things change when it is on. The crypto state is built on
first use rather than here: from |
crypto_store |
Character or NULL. Directory holding the pickled
Olm account and sessions. NULL derives it from A Matrix device has one Olm identity for its whole life, and three
things hold that. The store records the That last check distinguishes three cases: no published device is a
first run, a published device whose keys match is this account, and
a published device that either differs or fails signature
verification is an error. A A Windows drive-relative |
.sync |
Testing seam: replacement for
|
.extract |
Testing seam: replacement for
|
.send |
Testing seam: replacement for
|
.media |
Testing seam: replacement for
|
.typing |
Testing seam: replacement for
|
.crypto |
Testing seam: a named list overriding any of the
adapter's four crypto operations – |
.save |
Testing seam: replacement for
|
.react |
Testing seam: replacement for |
.info |
Testing seam: a list with |
.members |
Testing seam: replacement for
|
.join |
Testing seam: replacement for
|
.create |
Testing seam: replacement for
|
.leave |
Testing seam: replacement for
|
.state |
Testing seam: replacement for
|
.get_state |
Testing seam: replacement for
|
.extract_media |
Testing seam: replacement for
|
.download |
Testing seam: replacement for
|
.channels |
Testing seam: replacement for
|
.history |
Testing seam: replacement for
|
.pending |
Testing seam: replacement for |
.read |
Testing seam: replacement for
|
.identity |
Testing seam: replacement for
|
.edit |
Testing seam: replacement for |
.rich |
Testing seam: replacement for |
Value
A chat_client of class chat_matrix.
chat_poll on this class returns first_run and
client alongside messages, cursor, and
raw. first_run is TRUE when the sync started from no
cursor, so the messages are a backfill baseline rather than new
traffic. client is the post-sync mx.client config, which a
consumer needs whenever it drives mx.api directly (read receipts,
member lookups) because a relogin may have replaced the token this
poll cycle.
Examples
## Not run:
# Requires mx.client and saved Matrix credentials for this application.
cl <- chat_matrix(app = "mybot")
chat_capabilities(cl)
chat_poll(cl, timeout = 0)
## End(Not run)
Load a Matrix configuration
Description
Reads the credentials an application saved, and returns them as a
chat_config: a list carrying the transport's own fields plus
whatever else the application stored alongside them, with the app
namespace and file path attached as attributes so
chat_config_save can write it back where it came from.
Usage
chat_matrix_config(app = NULL, path = NULL, env_var = NULL)
Arguments
app |
Application namespace, e.g. |
path |
Explicit file path, overriding |
env_var |
Name of an environment variable that, when set, overrides both. |
Value
A list with class chat_config.
Why the extra fields survive
Applications keep their own settings in the same file – which accounts count as bots, who may open a private conversation, a preferred model. Those are the application's, not the transport's, and a loader that dropped them would make the file unreadable by its owner. They pass through untouched and unvalidated.
Examples
if (requireNamespace("mx.client", quietly = TRUE)) {
path <- tempfile(fileext = ".json")
cfg <- chat_config(list(server = "https://matrix.example.org",
token = "example-token",
user_id = "@bot:example.org"))
chat_config_save(cfg, path = path)
chat_matrix_config(path = path)
unlink(path)
}
Where a Matrix configuration lives
Description
Where a Matrix configuration lives
Usage
chat_matrix_config_path(app, env_var = NULL, legacy = FALSE)
Arguments
app |
Application namespace. |
env_var |
Name of an environment variable that overrides the default path, or NULL. |
legacy |
Return the pre- |
Value
The file path (character).
Examples
if (requireNamespace("mx.client", quietly = TRUE)) {
chat_matrix_config_path("demo")
}
Configure a Matrix account interactively
Description
Logs in to a homeserver, resolves the room, and writes the resulting credentials.
Usage
chat_matrix_configure(
server,
user,
password,
room = NULL,
app = NULL,
path = NULL,
device_id = NULL,
extra = list()
)
Arguments
server |
Homeserver base URL. |
user |
Localpart or full user id. |
password |
Account password. |
room |
Room id or alias to record as the default. |
app |
Application namespace to save under. |
path |
Explicit path to save to, overriding |
device_id |
Device id to log in as. Reusing one keeps an existing E2EE identity; a new one starts a new device. |
extra |
Named list of application fields to store alongside the credentials. |
Value
A chat_config, invisibly.
Examples
## Not run:
# Requires a real homeserver, account password, and access to the room.
pw <- Sys.getenv("MATRIX_PASSWORD")
cfg <- chat_matrix_configure(server = "https://matrix.example.org",
user = "bot", password = pw,
room = "#lab:example.org", app = "mybot")
## End(Not run)
List a channel's members
Description
Separate from chat_channel_info because it is the
expensive half: a member list is unbounded where a name and a topic
are two short strings, and it goes stale on a different schedule.
Usage
chat_members(client, channel, ...)
Arguments
client |
A |
channel |
Channel/room identifier. |
... |
Adapter-specific options. |
Value
Character vector of member identifiers. Empty when the channel
has none; an adapter that cannot answer throws, so an empty room is
never confused with an unanswerable question. Check
chat_capabilities()$members first.
Examples
## Not run:
# Requires a saved Matrix configuration and a joined room.
cl <- chat_matrix(app = "mybot")
chat_members(cl, "#general:example.org")
## End(Not run)
Construct a normalized chat message
Description
The record every adapter's chat_poll returns.
Usage
chat_message(
id,
channel,
sender,
body,
ts,
thread = NULL,
markup = "plain",
kind = "message",
self = NULL,
mentions = NULL,
raw = NULL,
encrypted = FALSE,
sender_verified = NULL,
attachments = NULL
)
Arguments
id |
Message identifier (character). |
channel |
Channel/room identifier (character). |
sender |
Sender identifier (character). |
body |
Message text (character). |
ts |
POSIXct timestamp of when the platform recorded the
message, or |
thread |
Thread identifier or NULL. |
markup |
Source markup hint (character, e.g. "plain", "html"). |
kind |
Message kind (character; "message" default). Contract
vocabulary, not platform vocabulary: |
self |
Logical: did this client send the message? Poll returns the bot's own traffic like any other, so a consumer that replies to inbound mail needs this to avoid answering itself. NULL when the adapter cannot tell. |
mentions |
Character vector of user identifiers the message
explicitly mentioned (Matrix |
raw |
The adapter's platform-native payload for this message, exactly as the transport layer handed it over. Shape is 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. |
encrypted |
Logical: did this message arrive end-to-end encrypted? FALSE on transports without E2EE and on cleartext messages in rooms that have it. |
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. |
attachments |
List of |
Value
A list with class chat_message.
Examples
chat_message("m1", "general", "alice", "hello",
ts = as.POSIXct("2026-01-01", tz = "UTC"))
Read standing state that is not tied to a cursor
Description
Today: pending invitations. chat_poll reports an
invitation when it arrives, which is no help to a client that was not
running at the time – and some homeservers only report invites newer
than the since token, so the poll loop never sees them again.
Usage
chat_pending(client, ...)
Arguments
client |
A |
... |
Adapter-specific options. |
Details
This is a separate verb rather than a mode of chat_poll
deliberately. Overloading the cursor would make "start from nothing"
and "tell me what is standing" the same call, and a client that asked
for pending invitations and thereby reset its read position would
replay every channel it is in.
Value
A list with invites, a list of chat_invite.
Examples
## Not run:
# Requires a saved Matrix configuration and a homeserver connection.
client <- chat_matrix(app = "mybot")
pending <- chat_pending(client)
for (iv in pending$invites) chat_join(client, iv$channel)
## End(Not run)
Poll a chat client for new messages
Description
Poll-shaped everywhere: long-poll transports (Matrix /sync, Telegram
getUpdates) map directly; persistent-socket transports (IRC) buffer
into the poll. The cursor is opaque and adapter-specific; pass the
returned cursor back as since on the next call.
Usage
chat_poll(client, since = NULL, timeout = NULL, ...)
Arguments
client |
A |
since |
Opaque cursor from the previous poll, or NULL to start. |
timeout |
Seconds to wait for activity; NULL for the adapter default. |
... |
Adapter-specific options. |
Value
A list with messages (list of chat_message) and
cursor (opaque, for the next since).
Examples
cl <- chat_loopback()
chat_send(cl, "general", "hello")
batch <- chat_poll(cl)
batch$messages
chat_poll(cl, since = batch$cursor)$messages
React to a message
Description
Places a reaction (an emoji or short key) on an existing message.
Usage
chat_react(client, channel, message_id, key, ...)
Arguments
client |
A |
channel |
Channel/room identifier. |
message_id |
Identifier of the message being reacted to, as
returned by |
key |
The reaction itself. Platforms differ on what they accept:
Matrix takes any string and conventionally an emoji character,
Slack takes a short name without colons ( |
... |
Adapter-specific options. |
Details
The default method throws. A reaction that silently does nothing is
worse than one that fails: the caller believes it acknowledged
something and no one can see that it did not. This differs from
chat_typing, whose default is a quiet FALSE, because a
missing typing indicator costs nothing and a missing acknowledgement
can be the whole message. Check chat_capabilities()$reactions
before calling on an unknown adapter.
Value
The reaction's identifier where the platform gives it one
(Matrix), invisibly; TRUE where it does not (Slack).
Examples
## Not run:
# Requires a saved Matrix configuration and a joined room.
cl <- chat_matrix(app = "mybot")
room <- chat_resolve(cl, "#general:example.org")
id <- chat_send(cl, room, "hello")
chat_react(cl, room, id, "+1")
## End(Not run)
Construct a normalized reaction record
Description
The record chat_poll returns in $reactions on
adapters whose chat_capabilities()$reaction_events is TRUE.
Usage
chat_reaction(id, channel, sender, target, key, ts, self = NULL, raw = NULL)
Arguments
id |
The reaction's own identifier, or NULL where the platform
does not give it one. Not the identifier of the message it is
attached to – see |
channel |
Channel/room identifier (character). |
sender |
Sender identifier (character). |
target |
Identifier of the message being reacted to (character). |
key |
The reaction itself (character): an emoji on Matrix, a short name on Slack. |
ts |
POSIXct timestamp, or |
self |
Logical: did this client place the reaction? A consumer that reacts to acknowledge needs this to avoid answering its own acknowledgement. NULL when the adapter cannot tell. |
raw |
The adapter's platform-native payload. |
Details
Deliberately not a chat_message. A reaction has a target
and no body, and the message record has a body and no target;
thread is the closest slot and it means something else, so
folding one into the other would make every consumer disambiguate by
inspecting fields.
Value
A list with class chat_reaction.
Examples
chat_reaction("r1", "general", "alice", target = "m1",
key = "+1", ts = as.POSIXct("2026-01-01", tz = "UTC"))
Refresh this client's credentials
Description
Forces the re-authentication that adapters otherwise perform on demand. The refreshed credentials stay inside the client.
Usage
chat_relogin(client, ...)
Arguments
client |
A |
... |
Adapter-specific options. |
Details
The default throws rather than returning quietly. "I could not refresh" and "there was nothing to refresh" look identical to a caller that gets FALSE, and the first means the next call will fail with a stale token.
Value
TRUE, invisibly.
Examples
## Not run:
# Requires a saved Matrix configuration with login credentials.
cl <- chat_matrix(app = "mybot")
chat_relogin(cl)
## End(Not run)
Resolve a human channel name to its identifier
Description
Resolve a human channel name to its identifier
Usage
chat_resolve(client, name, ...)
Arguments
client |
A |
name |
Channel name, alias, or identifier. |
... |
Adapter-specific options. |
Value
The adapter-native channel identifier (character).
Examples
chat_resolve(chat_loopback(), "general")
Send a message through a chat client
Description
Send a message through a chat client
Usage
chat_send(
client,
channel,
text,
markup = c("plain", "markdown"),
thread = NULL,
reply_to = NULL,
identity = NULL,
files = NULL,
kind = "message",
notify = TRUE,
rich = NULL,
...
)
Arguments
client |
A |
channel |
Channel/room identifier (adapter-native or resolved
via |
text |
Message text. |
markup |
|
thread |
Thread identifier to post into, or NULL. |
reply_to |
Message id being replied to, or NULL. |
identity |
Optional per-message identity override
( |
files |
Character vector of file paths to attach, or NULL. |
kind |
Message kind; |
notify |
Logical; FALSE requests a silent delivery where supported. |
rich |
Adapter-native markup for the platforms that accept it,
or NULL. Matrix takes an HTML fragment and sends it as
Ignored rather than refused where unsupported, on
|
... |
Adapter-specific options. |
Value
Character vector of the message ids this call created, in the order they were sent, invisibly. Usually length one. A platform that splits a send into several events returns one id per event: the Matrix adapter sends each attachment as its own event, so a send with files returns the attachment ids followed by the text id. Callers that track their own traffic by id must handle every element, or an unclaimed event reads as somebody else's message.
Examples
cl <- chat_loopback()
id <- chat_send(cl, "general", "hello", markup = "plain")
chat_send(cl, "general", "a reply", thread = id)
chat_poll(cl)$messages
Set this client's persistent identity
Description
The account's own display name, as everyone in every channel sees it
until it is changed again. Distinct from chat_send's
identity argument, which decorates a single message on
platforms that allow it.
Usage
chat_set_identity(client, display, ...)
Arguments
client |
A |
display |
New display name. |
... |
Adapter-specific options. |
Details
Owning this matters beyond tidiness. On Matrix the rename is an authenticated call that can rotate the access token underneath the caller, and a consumer that made that call itself had to notice the rotation and get the new token back into its client – usually via whatever file both of them happened to share. Behind the contract the rotation lands in the client that performed it, and nothing outside has to know it happened.
Value
TRUE if the identity was changed, invisibly.
Examples
## Not run:
# Requires a saved Matrix configuration and account credentials.
cl <- chat_matrix(app = "mybot")
chat_set_identity(cl, "Example Bot")
## End(Not run)
Set durable typed state on a channel
Description
Attaches a typed, durable piece of metadata to a channel, readable
by every client in it and replaced by the next write to the same
type and state_key. On Matrix this is a state event;
most platforms have no equivalent, which is why it is
capability-gated: check chat_capabilities()$set_state.
Usage
chat_set_state(client, channel, type, content, state_key = "", ...)
Arguments
client |
A |
channel |
Channel/room identifier. |
type |
Character. Namespaced event type, e.g.
|
content |
Named list. The state content. A write replaces the
whole content for its |
state_key |
Character. Sub-key within the type. Most state is keyed by the empty string, the default. |
... |
Adapter-specific options. |
Details
The default method throws, on chat_react's reasoning:
a state write that silently did nothing leaves the caller believing
a marker is set that no reader will ever see.
Value
The state event's identifier where the platform gives one
(Matrix), invisibly; TRUE where it does not.
Examples
cl <- chat_loopback()
chat_set_state(cl, "general", "m.room.topic", list(topic = "Planning"))
chat_get_state(cl, "general", "m.room.topic")
Create a Slack chat client
Description
Requires the suggested slackr package and a bot token.
Channel names are normalized to bare names (no #), matching
what slackr's channel translation accepts.
Usage
chat_slack(
channels = character(),
token = Sys.getenv("SLACK_TOKEN"),
user_token = Sys.getenv("SLACK_USER_TOKEN"),
username = NULL,
.history = NULL,
.post = NULL,
.react = NULL,
.api = NULL
)
Arguments
channels |
Character vector of channels to poll. |
token |
Bot token; defaults to the |
user_token |
User token ( |
username |
Default display-name override for sends, or NULL (default) to post as the bot's own identity. |
.history |
Testing seam: replacement for
|
.post |
Testing seam: replacement for
|
.react |
Testing seam: replacement for
|
.api |
Testing seam: replacement for
|
Details
Sends explicitly suppress slackr's SLACK_USERNAME /
SLACK_ICON_EMOJI environment defaults: a plain
chat_send() posts as the bot's own identity, and authorship
is only overridden through username here or
chat_send(identity =) (both need the chat:write.customize
scope).
user_token is a different kind of authorship than
identity: identity/username relabel a bot's own
post with a cosmetic name and icon, while a user token
(xoxp-..., obtained via a Slack app's User Token Scopes
rather than its Bot Token Scopes) authenticates as an actual
workspace member, so chat_send(..., as_user = TRUE) and
chat_whoami(..., as_user = TRUE) post and resolve identity as
that member – Slack shows their real name and photo, not a bot
profile. Optional: leave unset if you only ever post as the bot.
A post made as_user = TRUE is the member's own message in
every respect, including to this client's own chat_poll:
Slack messages carry no self, so nothing distinguishes it from
something the member typed, and a consumer that replies to the
member's traffic will reply to it.
Value
A chat_client of class chat_slack.
Examples
## Not run:
# Requires slackr, SLACK_TOKEN, and a channel the token can access.
cl <- chat_slack(channels = "C0123456789")
chat_send(cl, "C0123456789", "hello")
## End(Not run)
Create a Telegram chat client
Description
Requires the suggested httr package and a bot token from
BotFather, or a telegram::TGBot that carries one.
Usage
chat_telegram(
token = Sys.getenv("TELEGRAM_BOT_TOKEN"),
timeout = 30L,
api_url = "https://api.telegram.org",
bot = NULL,
.api = NULL,
.download = NULL
)
Arguments
token |
Bot token; defaults to the |
timeout |
Long-poll wait in seconds, used by
|
api_url |
Base URL of the Bot API. The default is Telegram's;
a local Bot API server takes its own. Ignored when |
bot |
A |
.api |
Testing seam: replacement for the HTTP layer, a
|
.download |
Testing seam: replacement for the file fetch, a
|
Details
Channels are chat identifiers as Telegram reports them – a
positive number for a private chat, a negative one for a group or
channel – always as character. chat_resolve turns a
public @@username into one.
The first poll returns whatever updates Telegram is still holding
for the bot (it keeps them for 24 hours). That is the mail that
arrived while the bot was down rather than channel history, so it
comes out as ordinary traffic. Passing the returned cursor back as
since confirms it; Telegram re-sends anything unconfirmed.
Value
A chat_client of class chat_telegram.
Examples
## Not run:
# Requires httr, TELEGRAM_BOT_TOKEN, and access to the target chat.
cl <- chat_telegram()
chat_whoami(cl)
chat_send(cl, "@example_channel", "hello")
## End(Not run)
Signal typing state in a channel
Description
Capability-gated: the default method is a no-op so adapters without typing indicators need not implement it.
Usage
chat_typing(client, channel, on = TRUE, ...)
## S3 method for class 'chat_matrix'
chat_typing(client, channel, on = TRUE, timeout = 30, ...)
Arguments
client |
A |
channel |
Channel/room identifier. |
on |
Logical. |
... |
Adapter-specific options. |
timeout |
Seconds the typing indicator should stand before the
homeserver clears it, for |
Value
TRUE if the signal was sent, FALSE otherwise, invisibly.
Examples
cl <- chat_loopback()
chat_typing(cl, "general") # FALSE: loopback has no typing indicator
Who is this client logged in as?
Description
The client's own account, as the transport identifies it.
Usage
chat_whoami(client, ...)
Arguments
client |
A |
... |
Adapter-specific options. |
Details
The default method throws rather than guessing. Every use of an identity is a comparison – is this message mine, did someone address me – and a wrong answer to either is silent: the bot answers itself in a loop, or never answers anyone. An adapter that cannot say who it is should say so.
Value
Examples
chat_whoami(chat_loopback())