Skip to content

Optional capabilities

Moderation, member lookup, interactive components and slash commands are optional interfaces, discovered by type assertion rather than declared in Actor.

if mod, ok := chatplatform.AsModerator(p); ok {
    err := mod.TimeoutMember(ctx, userID, time.Hour, "repeated abuse")
}

Why not put them in Actor

Because a provider implementing none of them is legitimate.

(Two of them are not on Actor at all. VoiceParticipants and ReactionObserver hang off Reader, because asking who is in a voice channel and hearing a reaction land both observe and change nothing, and a read-only deployment enforcing consent, or noticing a reaction on somebody else's message, is precisely the caller that needs them. Everything below applies to them equally; only the half of the Provider they are asserted on differs.)

A bridge that reads an IRC channel and posts replies is a useful provider. If Actor required TimeoutMember, that provider would have to stub it and return "unsupported", telling the caller at runtime what the type system could have told them at compile time. Multiply by ten capabilities and every minimal provider carries a dozen lying methods.

Type assertion inverts it. The provider implements what it can do; the caller asks. Neither pretends.

Declare what you implement

The conformance harness checks capability declarations in both directions, because both mistakes are silent:

  • A claimed capability that is missing makes the harness look for something impossible.
  • An implemented capability nobody declares is one nobody discovers. It works, and no consumer ever finds it.

Why voice is two interfaces and everything else is one

VoiceReceiver and VoiceSender are discovered separately. Every other capability is a single interface, so the split needs a reason.

It is not an authorisation mechanism. The platform's own permission decides whether a bot may speak, and a Send method on a bot without that permission fails at the platform. Nothing about holding the interface grants the authority, and a consumer that assumes otherwise has misread it.

The reason is that a consumer should be able to choose its own posture. A bot that records a meeting and never speaks, one that speaks and ignores what it hears, and one that does both are all legitimate, and which of them you are building is not a fact this contract knows. Offering only the combination its first consumer happened to need would decide that for everyone.

So the library's job is to make the halves separable; deciding which halves to hold is the consumer's.

A capability can also be absent because of how you built the binary

Voice is the first capability where "does this provider offer it" depends on the build rather than only on the platform. The Discord provider needs a C library to negotiate the platform's end-to-end encryption, so a binary built without CGO has no voice capability at all.

That deliberately produces the same answer as a platform with no voice concept: AsVoiceReceiver returns false, and the caller asks one question instead of having to distinguish "cannot" from "was not built to". The alternative, a capability that exists and always errors, would put a dead branch in every consumer, one nobody tests and everybody assumes is handled.

It is worth knowing the limit of that guarantee, because it is easy to overstate. It holds at build time and no further: the library is linked dynamically, so a CGO-enabled binary built where it is missing at run time does not fall back to no-voice. It fails to start. "Built with CGO" and "the library is present" are two different guarantees and only the first is visible to the type system.

Why both halves hang off Actor

Receiving audio looks like observation, which is an argument for putting it on Reader beside Messages. It was considered and rejected, for a reason that only shows up when you try it.

Actor says what belongs to it: "Actor changes what people see. Everything here is observable by somebody." Joining a voice channel passes that test: the bot appears in the channel, and everyone in it can see that it did. And receiving is unreachable without joining.

So putting the receiving half on Reader would offer an observe-only deployment an interface it can discover and never use, because it has no Actor to join with. Discovery that answers "yes" where the answer is "not really" is worse than an honest no.

Where the interactive surface stops

Interactive deliberately stops short of any platform's component model. No rows, no custom-ID encoding, no message flags, and styling reduced to an advisory hint that an unrecognised value renders as neutral rather than failing.

Reproducing a component model would make this one vendor's API with different names. The contract carries what a support bot actually needs: offer some labelled choices, open a small form, answer, and replace the thing that was acted on.

UpdateSource earns its place for that last one. Without it, buttons stay live after they have been used and a second moderator actions the same report.

Validation belongs in the spec types

PromptSpec, FormSpec and CommandSpec validate their own keys, and providers call Validate before touching the wire.

Duplicate or empty keys are the failure worth catching early. A moderation card carries dismiss, delete and ban; routing an ambiguous key means taking the wrong action against a person. Better to refuse to post the card.