ScuttleBot

scuttlebot / pkg / client / topology.go
Source Blame History 185 lines
ce03bda… lmata 1 package client
ce03bda… lmata 2
ce03bda… lmata 3 import (
ce03bda… lmata 4 "bytes"
ce03bda… lmata 5 "context"
ce03bda… lmata 6 "encoding/json"
ce03bda… lmata 7 "fmt"
ce03bda… lmata 8 "net/http"
ce03bda… lmata 9 )
ce03bda… lmata 10
ce03bda… lmata 11 // ChannelInfo is the result of creating or looking up a channel.
ce03bda… lmata 12 type ChannelInfo struct {
ce03bda… lmata 13 // Channel is the full IRC channel name (e.g. "#task.gh-42").
ce03bda… lmata 14 Channel string `json:"channel"`
ce03bda… lmata 15
ce03bda… lmata 16 // Type is the channel type name from the topology policy (e.g. "task", "sprint").
ce03bda… lmata 17 // Empty if the channel does not match any configured type.
ce03bda… lmata 18 Type string `json:"type,omitempty"`
ce03bda… lmata 19
ce03bda… lmata 20 // Supervision is the coordination/supervision channel where summaries
ce03bda… lmata 21 // from this channel should also be posted (e.g. "#general"). Empty if none.
ce03bda… lmata 22 Supervision string `json:"supervision,omitempty"`
ce03bda… lmata 23
ce03bda… lmata 24 // Autojoin is the list of bot nicks that were invited when the channel was created.
ce03bda… lmata 25 Autojoin []string `json:"autojoin,omitempty"`
ce03bda… lmata 26 }
ce03bda… lmata 27
ce03bda… lmata 28 // ChannelTypeInfo describes a class of channels defined in the topology policy.
ce03bda… lmata 29 type ChannelTypeInfo struct {
ce03bda… lmata 30 // Name is the type identifier (e.g. "task", "sprint").
ce03bda… lmata 31 Name string `json:"name"`
ce03bda… lmata 32
ce03bda… lmata 33 // Prefix is the channel name prefix (e.g. "task.").
ce03bda… lmata 34 Prefix string `json:"prefix"`
ce03bda… lmata 35
ce03bda… lmata 36 // Autojoin is the list of bot nicks invited when a channel of this type is created.
ce03bda… lmata 37 Autojoin []string `json:"autojoin,omitempty"`
ce03bda… lmata 38
ce03bda… lmata 39 // Supervision is the coordination channel for this type, or empty.
ce03bda… lmata 40 Supervision string `json:"supervision,omitempty"`
ce03bda… lmata 41
ce03bda… lmata 42 // Ephemeral indicates channels of this type are automatically reaped.
ce03bda… lmata 43 Ephemeral bool `json:"ephemeral,omitempty"`
ce03bda… lmata 44
ce03bda… lmata 45 // TTLSeconds is the maximum lifetime in seconds for ephemeral channels, or zero.
ce03bda… lmata 46 TTLSeconds int64 `json:"ttl_seconds,omitempty"`
ce03bda… lmata 47 }
ce03bda… lmata 48
ce03bda… lmata 49 // TopologyClient calls the scuttlebot HTTP API to provision and discover channels.
ce03bda… lmata 50 // It complements the IRC-based Client for the dual-channel pattern: agents create
ce03bda… lmata 51 // a task channel here and get back the supervision channel where they should also post.
ce03bda… lmata 52 type TopologyClient struct {
ce03bda… lmata 53 apiURL string
ce03bda… lmata 54 token string
ce03bda… lmata 55 http *http.Client
ce03bda… lmata 56 }
ce03bda… lmata 57
ce03bda… lmata 58 // NewTopologyClient creates a TopologyClient.
ce03bda… lmata 59 // apiURL is the base URL of the scuttlebot API (e.g. "http://localhost:8080").
ce03bda… lmata 60 // token is the Bearer token issued by scuttlebot.
ce03bda… lmata 61 func NewTopologyClient(apiURL, token string) *TopologyClient {
ce03bda… lmata 62 return &TopologyClient{
ce03bda… lmata 63 apiURL: apiURL,
ce03bda… lmata 64 token: token,
ce03bda… lmata 65 http: &http.Client{},
ce03bda… lmata 66 }
ce03bda… lmata 67 }
ce03bda… lmata 68
ce03bda… lmata 69 type createChannelReq struct {
ce03bda… lmata 70 Name string `json:"name"`
ce03bda… lmata 71 Topic string `json:"topic,omitempty"`
ce03bda… lmata 72 Ops []string `json:"ops,omitempty"`
ce03bda… lmata 73 Voice []string `json:"voice,omitempty"`
ce03bda… lmata 74 Autojoin []string `json:"autojoin,omitempty"`
ce03bda… lmata 75 }
ce03bda… lmata 76
ce03bda… lmata 77 // CreateChannel provisions an IRC channel via the scuttlebot topology API.
ce03bda… lmata 78 // The server applies autojoin policy and invites the configured bots.
ce03bda… lmata 79 // Returns a ChannelInfo with the channel name, type, and supervision channel.
ce03bda… lmata 80 //
ce03bda… lmata 81 // Example: create a task channel for a GitHub issue.
ce03bda… lmata 82 //
ce03bda… lmata 83 // info, err := topo.CreateChannel(ctx, "#task.gh-42", "GitHub issue #42")
ce03bda… lmata 84 // if err != nil { ... }
ce03bda… lmata 85 // // post activity to info.Channel, summaries to info.Supervision
ce03bda… lmata 86 func (t *TopologyClient) CreateChannel(ctx context.Context, name, topic string) (ChannelInfo, error) {
ce03bda… lmata 87 body, err := json.Marshal(createChannelReq{Name: name, Topic: topic})
ce03bda… lmata 88 if err != nil {
ce03bda… lmata 89 return ChannelInfo{}, fmt.Errorf("topology: marshal request: %w", err)
ce03bda… lmata 90 }
ce03bda… lmata 91 req, err := http.NewRequestWithContext(ctx, http.MethodPost, t.apiURL+"/v1/channels", bytes.NewReader(body))
ce03bda… lmata 92 if err != nil {
ce03bda… lmata 93 return ChannelInfo{}, fmt.Errorf("topology: build request: %w", err)
ce03bda… lmata 94 }
ce03bda… lmata 95 req.Header.Set("Content-Type", "application/json")
ce03bda… lmata 96 req.Header.Set("Authorization", "Bearer "+t.token)
ce03bda… lmata 97
ce03bda… lmata 98 resp, err := t.http.Do(req)
ce03bda… lmata 99 if err != nil {
ce03bda… lmata 100 return ChannelInfo{}, fmt.Errorf("topology: create channel: %w", err)
ce03bda… lmata 101 }
ce03bda… lmata 102 defer resp.Body.Close()
ce03bda… lmata 103 if resp.StatusCode != http.StatusCreated {
336984b… lmata 104 var apiErr struct {
336984b… lmata 105 Error string `json:"error"`
336984b… lmata 106 }
ce03bda… lmata 107 _ = json.NewDecoder(resp.Body).Decode(&apiErr)
ce03bda… lmata 108 return ChannelInfo{}, fmt.Errorf("topology: create channel: %s", apiErr.Error)
ce03bda… lmata 109 }
ce03bda… lmata 110 var info ChannelInfo
ce03bda… lmata 111 if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
ce03bda… lmata 112 return ChannelInfo{}, fmt.Errorf("topology: decode response: %w", err)
ce03bda… lmata 113 }
ce03bda… lmata 114 return info, nil
ce03bda… lmata 115 }
ce03bda… lmata 116
ce03bda… lmata 117 // DropChannel drops an ephemeral channel via the scuttlebot topology API.
ce03bda… lmata 118 // The ChanServ registration is removed and the channel will be vacated.
ce03bda… lmata 119 func (t *TopologyClient) DropChannel(ctx context.Context, channel string) error {
ce03bda… lmata 120 if len(channel) < 2 || channel[0] != '#' {
ce03bda… lmata 121 return fmt.Errorf("topology: invalid channel name %q", channel)
ce03bda… lmata 122 }
ce03bda… lmata 123 slug := channel[1:] // strip leading #
ce03bda… lmata 124 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, t.apiURL+"/v1/topology/channels/"+slug, nil)
ce03bda… lmata 125 if err != nil {
ce03bda… lmata 126 return fmt.Errorf("topology: build request: %w", err)
ce03bda… lmata 127 }
ce03bda… lmata 128 req.Header.Set("Authorization", "Bearer "+t.token)
ce03bda… lmata 129 resp, err := t.http.Do(req)
ce03bda… lmata 130 if err != nil {
ce03bda… lmata 131 return fmt.Errorf("topology: drop channel: %w", err)
ce03bda… lmata 132 }
ce03bda… lmata 133 defer resp.Body.Close()
ce03bda… lmata 134 if resp.StatusCode != http.StatusNoContent {
336984b… lmata 135 var apiErr struct {
336984b… lmata 136 Error string `json:"error"`
336984b… lmata 137 }
ce03bda… lmata 138 _ = json.NewDecoder(resp.Body).Decode(&apiErr)
ce03bda… lmata 139 return fmt.Errorf("topology: drop channel: %s", apiErr.Error)
ce03bda… lmata 140 }
ce03bda… lmata 141 return nil
ce03bda… lmata 142 }
ce03bda… lmata 143
ce03bda… lmata 144 type topologyResp struct {
ce03bda… lmata 145 StaticChannels []string `json:"static_channels"`
ce03bda… lmata 146 Types []ChannelTypeInfo `json:"types"`
ce03bda… lmata 147 }
ce03bda… lmata 148
ce03bda… lmata 149 // GetTopology returns the channel type rules and static channels from the server.
ce03bda… lmata 150 func (t *TopologyClient) GetTopology(ctx context.Context) ([]string, []ChannelTypeInfo, error) {
ce03bda… lmata 151 req, err := http.NewRequestWithContext(ctx, http.MethodGet, t.apiURL+"/v1/topology", nil)
ce03bda… lmata 152 if err != nil {
ce03bda… lmata 153 return nil, nil, fmt.Errorf("topology: build request: %w", err)
ce03bda… lmata 154 }
ce03bda… lmata 155 req.Header.Set("Authorization", "Bearer "+t.token)
ce03bda… lmata 156 resp, err := t.http.Do(req)
ce03bda… lmata 157 if err != nil {
ce03bda… lmata 158 return nil, nil, fmt.Errorf("topology: get topology: %w", err)
ce03bda… lmata 159 }
ce03bda… lmata 160 defer resp.Body.Close()
ce03bda… lmata 161 if resp.StatusCode != http.StatusOK {
ce03bda… lmata 162 return nil, nil, fmt.Errorf("topology: get topology: status %d", resp.StatusCode)
ce03bda… lmata 163 }
ce03bda… lmata 164 var body topologyResp
ce03bda… lmata 165 if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
ce03bda… lmata 166 return nil, nil, fmt.Errorf("topology: decode response: %w", err)
ce03bda… lmata 167 }
ce03bda… lmata 168 return body.StaticChannels, body.Types, nil
ce03bda… lmata 169 }
ce03bda… lmata 170
ce03bda… lmata 171 // PostActivity sends a structured message to the task/activity channel.
ce03bda… lmata 172 // It is a convenience wrapper around client.Send for the dual-channel pattern.
ce03bda… lmata 173 func PostActivity(ctx context.Context, c *Client, channel, msgType string, payload any) error {
ce03bda… lmata 174 return c.Send(ctx, channel, msgType, payload)
ce03bda… lmata 175 }
ce03bda… lmata 176
ce03bda… lmata 177 // PostSummary sends a structured message to the supervision channel.
ce03bda… lmata 178 // supervision is the channel returned by CreateChannel (info.Supervision).
ce03bda… lmata 179 // It is a no-op if supervision is empty.
ce03bda… lmata 180 func PostSummary(ctx context.Context, c *Client, supervision, msgType string, payload any) error {
ce03bda… lmata 181 if supervision == "" {
ce03bda… lmata 182 return nil
ce03bda… lmata 183 }
ce03bda… lmata 184 return c.Send(ctx, supervision, msgType, payload)
ce03bda… lmata 185 }

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button