ingest

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 12 Imported by: 0

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

View Source
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.

View Source
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

View Source
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.

View Source
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

func (c Config) Validate() error

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

func (c Config) WithDefaults() Config

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 WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the logger.

func WithProvider

func WithProvider(fn ProviderFunc) Option

WithProvider overrides how the platform provider is built.

func WithStateInterval

func WithStateInterval(d time.Duration) Option

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) Ingested

func (s *Service) Ingested() int64

Ingested counts messages accepted from allowlisted channels.

func (*Service) Live

func (s *Service) Live() error

Live reports whether the process should keep running.

func (*Service) Ready

func (s *Service) Ready() error

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

func (s *Service) ReconnectLosses() int64

ReconnectLosses counts resumes that re-identified and therefore dropped the events buffered during the gap.

func (*Service) Rejected

func (s *Service) Rejected() int64

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).

func (*Service) Start

func (s *Service) Start(ctx context.Context) error

Start resolves the credential, connects, and runs the message loop.

It returns rather than retrying. Restart policy belongs to the controller, which can back off and give up; a service that retries internally hides its own failure from the thing supervising it.

func (*Service) Stop

func (s *Service) Stop(context.Context)

Stop closes the reader, which ends the loop.

Safe on a service that never started and safe to call twice: shutdown runs after failures, and a panic here buries the error that caused them.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL