Documentation
¶
Overview ¶
Package ingest reads messages from a chat platform and does nothing else with them.
It is deliberately incapable of posting. The provider is constructed read-only, so it carries no Actor at all — not an Actor that refuses, but none — which removes moderation, interactive components and commands along with it. Shadow mode here is a property of the types rather than a flag somebody has to remember to check.
Index ¶
- Constants
- Variables
- type AuthConfig
- type Config
- type CredentialFunc
- type GuildConfig
- type HealthConfig
- type Option
- type ProviderFunc
- type Service
- func (s *Service) Ingested() int64
- func (s *Service) Live() error
- func (s *Service) Ready() error
- func (s *Service) ReconnectLosses() int64
- func (s *Service) Rejected() int64
- func (s *Service) Reload(cfg GuildConfig)
- func (s *Service) SetChannels(ids []chatplatform.ID)
- func (s *Service) Start(ctx context.Context) error
- func (s *Service) Stop(context.Context)
Constants ¶
const ( // DefaultTokenEnvVar names the environment variable holding the bot token. // The config records the NAME; the secret never lands in a config file. //nolint:gosec // G101: this is the NAME of an environment variable, not a // credential. Recording the name rather than the value is the entire point. DefaultTokenEnvVar = "DISCORD_PHPBOTSCOUT_TOKEN" // DefaultHealthHost binds the health listener to loopback. Anything wider // is a decision an operator has to make deliberately. DefaultHealthHost = "127.0.0.1" // DefaultHealthPort is the health listener's port. DefaultHealthPort = 8081 )
Defaults for the settings that have a safe one. A credential and an allowlist deliberately do not.
const DefaultStateInterval = 5 * time.Second
DefaultStateInterval is how often the connection is checked for a lossy reconnect.
It is polled rather than driven by traffic because the signal is about the connection, not about messages: a reconnect during a quiet spell is exactly the one nobody would otherwise notice, and waiting for the next message to report it would mean the quietest channels report last.
Variables ¶
var ( // ErrNoGuilds is returned when no guild is configured. ErrNoGuilds = errors.New("ingest: no guild configured") // ErrMultipleGuilds is returned when more than one guild is configured. // Multi-tenancy is a v1 non-goal; the config is a list so that becomes an // additive change rather than a breaking one. ErrMultipleGuilds = errors.New( "ingest: more than one guild configured, which is not supported in v1") // ErrNoSpace is returned when a guild carries no identifier. ErrNoSpace = errors.New("ingest: guild space must not be empty") // ErrNoChannels is returned for an empty allowlist. It fails closed: an // empty list permits nothing rather than everything. ErrNoChannels = errors.New("ingest: channel allowlist must not be empty") // ErrWildcardBind is returned when the health listener would bind every // interface. Binding all of them and trusting a firewall to be correct is // how private services become public ones. ErrWildcardBind = errors.New( "ingest: health host must be an explicit interface, not a wildcard") // ErrInvalidPort is returned for a port outside the usable range. ErrInvalidPort = errors.New("ingest: health port must be between 1 and 65535") // ErrNoAuth is returned when a guild carries no credential at all. ErrNoAuth = errors.New("ingest: no credential configured") // ErrAmbiguousAuth is returned when more than one credential mode is set. // Picking one silently is the wrong kind of helpful where a secret is // concerned. ErrAmbiguousAuth = errors.New( "ingest: exactly one of auth.env, auth.keychain or auth.value may be set") )
Configuration errors, as sentinels so a caller can tell a misconfiguration from a platform failure.
var ( // ErrNotStarted is reported by readiness before Start has run. ErrNotStarted = errors.New("ingest: not started") // ErrNotConnected is reported by readiness when the gateway is not // currently delivering messages. ErrNotConnected = errors.New("ingest: gateway not connected") // ErrLoopStopped is reported by liveness when the message loop has exited. ErrLoopStopped = errors.New("ingest: message loop stopped") )
Service errors.
Functions ¶
This section is empty.
Types ¶
type AuthConfig ¶
type AuthConfig struct {
// Env names an environment variable holding the token. The recommended
// default, and the only mode permitted under CI.
Env string `mapstructure:"env" yaml:"env" json:"env"`
// Keychain references an OS keychain entry as "service/account".
Keychain string `mapstructure:"keychain" yaml:"keychain" json:"keychain"`
// Value is a literal token. Supported for throwaway environments and
// refused under CI.
Value string `mapstructure:"value" yaml:"value" json:"value"`
}
AuthConfig records how the bot token is stored, following the estate's three-mode convention. Exactly one field is set.
The config never holds the secret in env or keychain mode — only a name or a reference — which is what keeps a token out of a file that gets committed by accident.
func (AuthConfig) Mode ¶
func (a AuthConfig) Mode() (credentials.Mode, error)
Mode reports which credential mode is configured, and whether exactly one is.
type Config ¶
type Config struct {
// Guilds is a list carrying exactly one entry in v1.
//
// The list shape is deliberate. A provider is scoped to one space at
// construction, so supporting several is N providers and N services rather
// than a contract change — but that is only cheap if the configuration did
// not have to break to express it. A list with one element costs nothing
// now; migrating every operator's config later does not.
Guilds []GuildConfig `mapstructure:"guilds" yaml:"guilds" json:"guilds"`
Health HealthConfig `mapstructure:"health" yaml:"health" json:"health"`
}
Config is the ingest service's configuration.
func (Config) Validate ¶
Validate reports whether the configuration can start a bot that does what the operator meant.
It is strict on purpose. Every check here catches a configuration that would otherwise start successfully and behave wrongly in a way nobody would notice — reading no channels, reading the wrong server, or listening where it should not.
func (Config) WithDefaults ¶
WithDefaults returns a copy with the safe defaults applied, leaving anything the operator set alone.
type CredentialFunc ¶
type CredentialFunc func(ctx context.Context, auth AuthConfig) (string, error)
CredentialFunc resolves a configured credential to a secret.
type GuildConfig ¶
type GuildConfig struct {
// Space is the guild, workspace or network identifier.
//
// It is not how the bot discovers where it is — the gateway supplies that.
// It is an authorisation boundary: an invite link can be shared, and this
// is what stops an unintended server becoming one the bot serves.
Space chatplatform.ID `mapstructure:"space" yaml:"space" json:"space"`
// Channels is the allowlist. Empty permits nothing.
Channels []chatplatform.ID `mapstructure:"channels" yaml:"channels" json:"channels"`
// Auth carries the credential, in whichever mode the operator chose.
Auth AuthConfig `mapstructure:"auth" yaml:"auth" json:"auth"`
}
GuildConfig is one platform space and the channels the bot may read in it.
type HealthConfig ¶
type HealthConfig struct {
// Host is the interface to bind. Explicit by requirement; wildcards are
// rejected rather than defaulted.
Host string `mapstructure:"host" yaml:"host" json:"host"`
Port int `mapstructure:"port" yaml:"port" json:"port"`
}
HealthConfig configures the health listener.
type Option ¶
type Option func(*Service)
Option configures a Service.
func WithCredential ¶
func WithCredential(fn CredentialFunc) Option
WithCredential overrides how the token reference is resolved.
func WithProvider ¶
func WithProvider(fn ProviderFunc) Option
WithProvider overrides how the platform provider is built.
func WithStateInterval ¶
WithStateInterval sets how often the connection state is polled for a reconnect that lost events.
type ProviderFunc ¶
type ProviderFunc func(chatplatform.Config) (*chatplatform.Provider, error)
ProviderFunc builds a provider from a platform config. It exists so the service can be driven by a fake in tests without a package-level variable, which would race under t.Parallel().
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service reads messages from one space and logs them.
It holds no Actor and cannot acquire one: the provider is built with ReadOnly set, so the capability to post does not exist rather than being withheld.
func New ¶
func New(cfg GuildConfig, opts ...Option) (*Service, error)
New builds an ingest service for one guild.
func (*Service) Ready ¶
Ready reports whether the bot is currently seeing messages.
A reconnecting session is live but not ready, which is the distinction that makes readiness worth having: a bot that is up but disconnected looks exactly like a quiet channel.
A resumed session that lost events stays ready. It is usable, and restarting it would not bring the lost messages back — the loss is reported separately rather than folded into health.
func (*Service) ReconnectLosses ¶
ReconnectLosses counts resumes that re-identified and therefore dropped the events buffered during the gap.
func (*Service) Rejected ¶
Rejected counts messages discarded for being outside the allowlist. It carries no labels by design.
func (*Service) Reload ¶
func (s *Service) Reload(cfg GuildConfig)
Reload applies a changed configuration.
The allowlist is applied. Identity is not: a changed space or credential needs a restart (R-OPS-5a), and is warned about by name so an ignored change is never a silent one. Applying it live would mean tearing down a working session on a config write, so a mistyped credential would take the bot off the platform at a time nobody chose.
func (*Service) SetChannels ¶
func (s *Service) SetChannels(ids []chatplatform.ID)
SetChannels replaces the allowlist. Pure filtering, so it applies to the next message with no session involved (R-OPS-5).