| | @@ -0,0 +1,141 @@ |
| 1 | +package topology
|
| 2 | +
|
| 3 | +import (
|
| 4 | + "context"
|
| 5 | + "strings"
|
| 6 | + "time"
|
| 7 | +
|
| 8 | + "github.com/conflicthq/scuttlebot/internal/config"
|
| 9 | +)
|
| 10 | +
|
| 11 | +// ChannelType is the resolved policy for a class of channels.
|
| 12 | +type ChannelType struct {
|
| 13 | + Name string
|
| 14 | + Prefix string
|
| 15 | + Autojoin []string
|
| 16 | + Supervision string
|
| 17 | + Ephemeral bool
|
| 18 | + TTL time.Duration
|
| 19 | +}
|
| 20 | +
|
| 21 | +// EventType classifies a channel lifecycle event.
|
| 22 | +type EventType string
|
| 23 | +
|
| 24 | +const (
|
| 25 | + EventCreated EventType = "created"
|
| 26 | + EventClosed EventType = "closed"
|
| 27 | + EventReopened EventType = "reopened"
|
| 28 | + EventTransferred EventType = "transferred"
|
| 29 | +)
|
| 30 | +
|
| 31 | +// ChannelEvent is emitted on channel lifecycle transitions.
|
| 32 | +type ChannelEvent struct {
|
| 33 | + Type EventType
|
| 34 | + Channel string
|
| 35 | + By string // nick that triggered the event
|
| 36 | + Meta map[string]string // optional domain-specific metadata
|
| 37 | +}
|
| 38 | +
|
| 39 | +// EventHook is implemented by anything that reacts to channel lifecycle events.
|
| 40 | +type EventHook interface {
|
| 41 | + OnChannelEvent(ctx context.Context, event ChannelEvent) error
|
| 42 | +}
|
| 43 | +
|
| 44 | +// Policy is the domain-agnostic evaluation layer between topology config and
|
| 45 | +// the runtime (IRC, bot invites, API). It answers questions like:
|
| 46 | +//
|
| 47 | +// - What type is #task.gh-42?
|
| 48 | +// - Which bots should join #incident.p1?
|
| 49 | +// - Where should summaries from #feature.auth surface?
|
| 50 | +//
|
| 51 | +// Rules come entirely from config — the Policy itself contains no hardcoded
|
| 52 | +// domain knowledge.
|
| 53 | +type Policy struct {
|
| 54 | + staticChannels []config.StaticChannelConfig
|
| 55 | + types []ChannelType
|
| 56 | +}
|
| 57 | +
|
| 58 | +// NewPolicy constructs a Policy from the topology section of the config.
|
| 59 | +func NewPolicy(cfg config.TopologyConfig) *Policy {
|
| 60 | + types := make([]ChannelType, 0, len(cfg.Types))
|
| 61 | + for _, t := range cfg.Types {
|
| 62 | + types = append(types, ChannelType{
|
| 63 | + Name: t.Name,
|
| 64 | + Prefix: t.Prefix,
|
| 65 | + Autojoin: append([]strinring(nil), t.Modes...),
|
| 66 | + Supervision: t.Supervision,
|
| 67 | + Ephemeral: t.Ephemeral,
|
| 68 | + TTL: t.TTL.Duration,
|
| 69 | + })
|
| 70 | + }
|
| 71 | + return &Policy{
|
| 72 | + staticChannels: append([]config.StaticChannelConfig(nil), cfg.Channels...),
|
| 73 | + types: types,
|
| 74 | + }
|
| 75 | +}
|
| 76 | +
|
| 77 | +// Match returns the ChannelType for the given channel name by prefix, or nil
|
| 78 | +// if no type matches. Channel names are matched after stripping the leading #.
|
| 79 | +func (p *Policy) Match(channel string) *ChannelType {
|
| 80 | + slug := strings.TrimPrefix(channel, "#")
|
| 81 | + for i := range p.types {
|
| 82 | + if strings.HasPrefix(slug, p.types[i].Prefix) {
|
| 83 | + return &p.types[i]
|
| 84 | + }
|
| 85 | + }
|
| 86 | + return nil
|
| 87 | +}
|
| 88 | +
|
| 89 | +// AutojoinFor returns the bot nicks that should join channel.
|
| 90 | +// For dynamic channels this comes from the matching ChannelType.
|
| 91 | +// For static channels it comes from the StaticChannelConfig.
|
| 92 | +// Returns nil if no rule matches.
|
| 93 | +func (p *Policy) AutojoinFor(channel string) []string {
|
| 94 | + // Check static channels first (exact match).
|
| 95 | + for _, sc := range p.staticChannels {
|
| 96 | + if strings.EqualFold(sc.Name, channel) {
|
| 97 | + return append([]string(nil), sc.Autojoin...)
|
| 98 | + }
|
| 99 | + }
|
| 100 | + if t := p.Match(channel); t != nil {
|
| 101 | + return append([]string(nil), t.Autojoin...)
|
| 102 | + }
|
| 103 | + return nil
|
| 104 | +}
|
| 105 | +
|
| 106 | +// SupervisionFor returns the coordination/supervision channel for the given
|
| 107 | +// channel, or an empty string if none is configured for its type.
|
| 108 | +func (p *Policy) SupervisionFor(channel string) string {
|
| 109 | + if t := p.Match(channel); t != nil {
|
| 110 | + return t.Supervision
|
| 111 | + }
|
| 112 | + return ""
|
| 113 | +}
|
| 114 | +
|
| 115 | +// TypeName returns the type name for the given channel, or "unknown".
|
| 116 | +func (p *Policy) TypeName(channel string) string {
|
| 117 | + if t := p.Match(channel); t != nil {
|
| 118 | + return t.Name
|
| 119 | + }
|
| 120 | + return ""
|
| 121 | +}
|
| 122 | +
|
| 123 | +// IsEphemeral reports whether channels of the matched type are ephemeral.
|
| 124 | +func (p *Policy) IsEphemeral(channel string) bool {
|
| 125 | + if t := p.Match(channel); t != nil {
|
| 126 | + return t.Ephemeral
|
| 127 | + }
|
| 128 | + return false
|
| 129 | +}
|
| 130 | +
|
| 131 | +// TTLFor returns the TTL for the matched channel type, or zero if none.
|
| 132 | +func (p *Policy) TTLFor(channel string) time.Duration {
|
| 133 | + if t := p.Match(cStaticChannels returns the list of channels to provision at startup.
|
| 134 | +func (p *Policy) StaticChannels() []config.StaticChannelConfig {
|
| 135 | + return append([]config.StaticChannelConfig(nil), p.staticChannels...)
|
| 136 | +}
|
| 137 | +
|
| 138 | +// Types returns all registered channel types.
|
| 139 | +func (p *Policy) Types() []ChannelType {
|
| 140 | + return append([]ChannelType(nil), p.types...)
|
| 141 | +}
|