Skip to content

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?

Yes, into a channel of the space it serves, and only one its allowlist names. From chat-platform v0.28.0 that is Poster (spec 0034).

It is for what the bot says in its own right: a greeting in a channel it has just created, or a summary. An answer to a question still belongs in a thread, through ReplyInThread. A bot that answers in the channel body talks over the people in it, and once several conversations are in flight nobody can tell which answer belongs to which question. Nothing in the API can tell an answer from a post, so that rule is yours to keep.

What stays out is announcing from outside the space: a release job posting through a webhook, or anything without a bot already serving the space. That 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, and only one the scope reads. Actor.ThreadHistory returns up to limit messages from one thread whose parent channel is in the allowlist, oldest first. Asked for a channel that is not a thread it refuses with ErrInvalidArgument, and for a thread outside the allowlist with ErrChannelDenied. Nothing 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, the single-space path that gave each Provider a transport of its own, was removed in v0.18.0 for this reason: looping it was how a consumer ended 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 comms.Registered() {
    c, err := comms.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?

Partly, and this is worth being precise about.

A scope's allowlist governs what a Reader may emit, and every call that takes a bare channel id: ThreadHistory, ChannelInspector, VoiceReceiver.Join and VoiceParticipants.Participants, which read, and Poster.Post, ChannelManager and Floor, which write. Each refuses a channel outside the allowlist with ErrChannelDenied before reaching the platform.

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 replied to, prompted in, deleted from or reacted to.

The same holds under PublicChannels, where the scope is "every channel everyone can read" rather than a list.

With WithInteractionsAnywhere, interactions arrive from any channel of the space, so the Refs a consumer holds may name unread channels routinely. The bare-id calls still refuse them; the Ref-taking ones still act.

So treat the allowlist as the read boundary, not an output guard. If your bot constructs a Ref from anything other than a message it received, check the channel yourself, or read Interaction.OutsideAllowlist.

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 dependency beyond the toolkit's error package, 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 same 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.

Can the bot mention someone?

Only a prompt or a post, only the members it names, and only once the consumer has said so.

By default nothing the bot posts notifies anybody. That holds whatever the text says, which matters because some of that text is a user's words echoed back: a question quoted in an answer can carry @everyone, and without this it would ping the whole server in the bot's name.

A consumer that needs to reach someone turns on ClientConfig.Mentions.Members when it builds the client, then lists the people in PromptSpec.Notify or PostSpec.Notify. The provider writes their mentions and notifies exactly them. Nothing is stripped from the content, so this is not sanitising: it controls what the platform does with the text, not the text.

Roles, @everyone, and mentions from replies and interaction answers are not offered. Nor is reaching someone privately, which is the direct-message question above.

Can the bot create channels or roles?

It can create them, and it never deletes them.

A consumer running an activity in a space, such as a game table, can create a category and the text and voice channels inside it, decide who can take part in each, create a role, and give it to members and take it away. That is ChannelManager and RoleManager.

Deleting a channel or a role is left out on purpose, because it cannot be undone, and everything the capabilities do can be. Taking a role away and removing someone's access can both be reversed. Tearing down a space is a job for a person.

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 been shaped by contact with a real provider and its consumers, which is what most of the "why" pages record. Removals and retypes have happened, so do not expect a compatibility promise before 1.0.