Reading and acting are separate¶
Reader observes. Actor changes what people see. They are separate
interfaces, and Config.ReadOnly yields a Provider whose Actor is nil.
The alternative, and why not¶
The obvious design is one interface with a flag: a provider that knows it is read-only and refuses at call time.
// Not this.
func (p *provider) DeleteMessage(...) error {
if p.readOnly {
return ErrReadOnly
}
...
}
That moves a deployment-shaped guarantee into a runtime check. Every call site has to handle a failure that is not about the platform, the network or the request — only about a flag set somewhere else. And a missed check is a message deleted in production by a bot that was supposed to be watching.
With a nil Actor there is nothing to call. Shadow mode is enforced by the type
system.
What this buys¶
A bot that cannot post cannot moderate. Optional capabilities hang off
Actor, so removing it removes Moderator, Interactive and Commands with
it. There is no combination of settings that yields something able to delete a
message but not to reply — the dangerous half is never the half that survives.
Rolling a new bot out is a config change. Run it read-only, watch what it would have done, then turn it on. The code path is identical; only the construction differs.
The types document the deployment. A function taking a chatplatform.Reader
visibly cannot act. One taking a *chatplatform.Provider might.
The cost¶
You have to nil-check Actor, or construct in a way that guarantees it. In
practice a bot that answers questions is constructed once at startup, so the
check happens once.
That is the trade: one check at construction, in exchange for no check at any call site.