What this contract does not do¶
This module is a contract for a support and moderation bot, not a general chat client. A lot of what a chat platform can do is deliberately not here, and the absence is usually load-bearing rather than an oversight.
Each answer below stands on its own. If the thing you want is not listed, check the reference before assuming it exists.
Can the bot post a plain message in a channel?¶
No. Actor has three methods and none of them posts into a channel body.
ReplyInThread is the only way to say something, and it always says it in a
thread, creating one on the message it is replying to if there is not one
already. A bot that answers in the channel body is a bot talking over the people
in it, and once several conversations are in flight nobody can tell which answer
belongs to which question.
If you need a channel announcement, that is a different job from answering a question and belongs to whatever posts your announcements.
Can the bot edit or delete a message?¶
Other people's, it can delete and only through the Moderator capability.
DeleteMessage takes a reason, which gets recorded in the platform's audit log
where one exists. That is moderation, not editing, and it lives behind an
optional capability precisely so a deployment can be built that cannot reach it.
Its own, it can replace or delete through the Author capability, which is
optional for the same reason and covers only what the bot posted.
Author.Replace is wholesale, content and choices together, and a provider
must not re-notify anyone to achieve it.
There is no general message edit: nothing rewrites a message somebody else
wrote. The other thing that can be rewritten after the fact is the message an
interaction came from, via Interactive.UpdateSource, which exists so a
moderation card stops offering buttons once somebody has pressed one.
Can it send files, images, embeds or formatted cards?¶
No. Everything the Actor sends is a plain string.
There are no attachments, no embeds, no rich cards, and no Markdown helpers. Prompts carry text and labelled buttons; forms carry text inputs. That is the whole outbound surface.
The reasoning is the same one that keeps a vendor SDK out of the core: a component model is the part of a chat API that differs most between platforms, and reproducing one would make this Discord's API with different names. See Where the interactive surface stops.
Can it read a channel's history?¶
Only a thread's. Actor.ThreadHistory returns up to limit messages from one
thread, oldest first, and there is nothing that reads a channel's backlog.
There is also no pagination: no cursor, no "before this message" argument. The method exists to carry a conversation's context somewhere it can be read by people who were never in the thread, not to archive anything.
Does it support direct messages?¶
No. The contract has no concept of a direct message, and no provider offers one.
Everything is scoped to a space and a channel allowlist. A DM is neither, so there is nowhere for one to arrive and no way to send one.
Can one provider serve two guilds or workspaces?¶
No. A Provider is one space, and no method on it takes a space argument. But
that is a statement about a scope, not about a connection, and the two are
different things.
To watch two spaces, mint two Providers from one Client. They have
separate readers and separate allowlists, and merging their message feeds is your
code's job. But they share one connection, which is what a platform carrying
many spaces over a single socket expects. Serving four tenants costs one transport, not
four.
New still exists and still gives a Provider that owns a transport of its own,
which is why it is deprecated: looping it is how a consumer ends up dialling once
per tenant.
A Provider is a scope, not a transport, and keeping those separate is what
lets a heterogeneous set of tenants share a connection without any of them being
able to see another's channels.
Scoping the space at construction keeps the concept out of every method signature, and out of a contract that would otherwise have to pick one platform's word for it.
Can one bot use two platforms at once?¶
Yes, this one it does support. Blank-import each provider module, then look both up by name and run both readers:
for _, name := range chatplatform.Registered() {
c, err := chatplatform.NewClient(ctx, name, clientConfigFor(name))
...
}
Your code is written against Reader and Actor, so the answering logic is the
same for every platform. What you cannot do is register two providers under the
same name, because Register refuses a duplicate rather than overwriting it.
Today only one provider exists, so this is a property of the design rather than something you can exercise yet.
Does the allowlist stop the bot posting somewhere unexpected?¶
Not reliably, and this is worth being precise about.
A scope's allowlist governs what a Reader may emit. Providers are
required to enforce it there, and they do. The contract also defines
ErrChannelDenied for a provider refusing to act on a channel outside the list.
But nothing forces an Actor to check a Ref, and the shipping Discord
provider does not: a Ref naming any channel the bot can see will be posted
to, deleted from or reacted to. The two methods that do refuse are the ones
that take a bare channel id rather than a Ref, VoiceReceiver.Join and
VoiceParticipants.Participants, because nothing upstream has checked that id
and a voice channel is a channel. Every Ref-taking method returns
ErrChannelDenied from nowhere.
So treat the allowlist as an input filter, not an output guard. If your bot
constructs a Ref from anything other than a message it received, check the
channel yourself with Scope.ChannelAllowed.
The output guard that is structural is
WithReadOnly: no Actor at all, so there is
nothing to post through.
Is message delivery guaranteed?¶
No, in two distinct ways, and neither raises an error.
A slow consumer loses messages. Providers buffer the inbound feed and drop when it is full, because blocking the gateway read loop eventually costs the session. Losing one message is bad; losing the session loses every message after it. Nothing reports the drop.
A reconnect can lose everything buffered during the gap. That one is
surfaced, through ConnState.LastReconnectLostEvents. It is the single failure
this contract goes furthest out of its way to make visible, and
has its own page.
There is no acknowledgement, no replay, no dead-letter queue and no way to ask
for the messages you missed. If your consumer needs to keep up, read from
Messages() into your own queue promptly and do the slow work elsewhere.
Does it retry, back off or handle rate limits?¶
No. There is no retry policy, no backoff, no circuit breaker and no
ErrRateLimited sentinel.
Whatever the platform SDK does about rate limiting is what happens, and its error comes back wrapped. This module has no dependencies at all, which rules out shipping a retry library, and a retry policy imposed on every caller would be wrong for most of them.
Does it log, emit metrics or trace anything?¶
No. This module writes nothing anywhere.
There is no logger parameter, no metrics registry and no tracing hooks, again a consequence of the zero-dependency rule, and a deliberate one: a library that logs on your behalf logs in a format you did not choose, at a level you did not set.
Reader.State() is the observability surface. It is cheap, safe to call on
every health check, and it is what you build a gauge and an alert from.
Does it sanitise message content?¶
No, and it goes out of its way not to.
Message carries Content as a raw string with no Markdown rendering and no
mention resolution. Those conveniences were left out on purpose: offering them
would invite a caller to treat the result as safe.
Every field on an inbound Message is untrusted. The content reaches an LLM
prompt, an issue body and a log line, and this module does nothing to any of
those paths. Redaction, mention neutralisation and prompt-injection defence
belong to the application, and none of them is optional just because the
contract is quiet about them.
Does it ship mocks or a fake provider?¶
No. There is no mocks/ directory and no in-memory provider.
Reader has four methods and Actor has three, which is small enough to
hand-roll a fake in a test file. What the module does ship is the
conformance harness, which is the
other half of the problem: it checks a real provider against the contract,
offline.
Is the API stable?¶
Not yet. This is pre-1.0, and the public API may change in a minor release.
The contract has already been through one round of contact with a real provider, which is what most of the "why" pages record. Expect additions rather than removals, but do not expect a compatibility promise before 1.0.