ScuttleBot

scuttlebot / internal / api / channels_topology.go
Blame History Raw 235 lines
1
package api
2
3
import (
4
"encoding/json"
5
"net/http"
6
7
"github.com/conflicthq/scuttlebot/internal/topology"
8
)
9
10
// TopologyManager is the interface the API server uses to provision channels
11
// and query the channel policy. Satisfied by *topology.Manager.
12
type TopologyManager = topologyManager
13
type topologyManager interface {
14
ProvisionChannel(ch topology.ChannelConfig) error
15
DropChannel(channel string)
16
Policy() *topology.Policy
17
GrantAccess(nick, channel, level string)
18
RevokeAccess(nick, channel string)
19
ListChannels() []topology.ChannelInfo
20
}
21
22
type provisionChannelRequest struct {
23
Name string `json:"name"`
24
Topic string `json:"topic,omitempty"`
25
Ops []string `json:"ops,omitempty"`
26
Voice []string `json:"voice,omitempty"`
27
Autojoin []string `json:"autojoin,omitempty"`
28
Instructions string `json:"instructions,omitempty"`
29
MirrorDetail string `json:"mirror_detail,omitempty"`
30
}
31
32
type provisionChannelResponse struct {
33
Channel string `json:"channel"`
34
Type string `json:"type,omitempty"`
35
Supervision string `json:"supervision,omitempty"`
36
Autojoin []string `json:"autojoin,omitempty"`
37
}
38
39
// handleProvisionChannel handles POST /v1/channels.
40
// It provisions an IRC channel via ChanServ, applies the autojoin policy for
41
// the channel's type, and returns the channel name, type, and supervision channel.
42
func (s *Server) handleProvisionChannel(w http.ResponseWriter, r *http.Request) {
43
var req provisionChannelRequest
44
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
45
writeError(w, http.StatusBadRequest, "invalid request body")
46
return
47
}
48
if err := topology.ValidateName(req.Name); err != nil {
49
writeError(w, http.StatusBadRequest, err.Error())
50
return
51
}
52
if s.topoMgr == nil {
53
writeError(w, http.StatusServiceUnavailable, "topology not configured")
54
return
55
}
56
57
policy := s.topoMgr.Policy()
58
59
// Merge autojoin and modes from policy if the caller didn't specify any.
60
autojoin := req.Autojoin
61
if len(autojoin) == 0 && policy != nil {
62
autojoin = policy.AutojoinFor(req.Name)
63
}
64
var modes []string
65
if policy != nil {
66
modes = policy.ModesFor(req.Name)
67
}
68
69
ch := topology.ChannelConfig{
70
Name: req.Name,
71
Topic: req.Topic,
72
Ops: req.Ops,
73
Voice: req.Voice,
74
Autojoin: autojoin,
75
Modes: modes,
76
}
77
if err := s.topoMgr.ProvisionChannel(ch); err != nil {
78
s.log.Error("provision channel", "channel", req.Name, "err", err)
79
writeError(w, http.StatusInternalServerError, "provision failed")
80
return
81
}
82
83
// Save instructions to policies if provided.
84
if req.Instructions != "" && s.policies != nil {
85
p := s.policies.Get()
86
if p.OnJoinMessages == nil {
87
p.OnJoinMessages = make(map[string]string)
88
}
89
p.OnJoinMessages[req.Name] = req.Instructions
90
if req.MirrorDetail != "" {
91
if p.Bridge.ChannelDisplay == nil {
92
p.Bridge.ChannelDisplay = make(map[string]ChannelDisplayConfig)
93
}
94
cfg := p.Bridge.ChannelDisplay[req.Name]
95
cfg.MirrorDetail = req.MirrorDetail
96
p.Bridge.ChannelDisplay[req.Name] = cfg
97
}
98
_ = s.policies.Set(p)
99
}
100
101
resp := provisionChannelResponse{
102
Channel: req.Name,
103
Autojoin: autojoin,
104
}
105
if policy != nil {
106
resp.Type = policy.TypeName(req.Name)
107
resp.Supervision = policy.SupervisionFor(req.Name)
108
}
109
writeJSON(w, http.StatusCreated, resp)
110
}
111
112
type channelTypeInfo struct {
113
Name string `json:"name"`
114
Prefix string `json:"prefix"`
115
Autojoin []string `json:"autojoin,omitempty"`
116
Supervision string `json:"supervision,omitempty"`
117
Ephemeral bool `json:"ephemeral,omitempty"`
118
TTLSeconds int64 `json:"ttl_seconds,omitempty"`
119
}
120
121
type topologyResponse struct {
122
StaticChannels []string `json:"static_channels"`
123
Types []channelTypeInfo `json:"types"`
124
ActiveChannels []topology.ChannelInfo `json:"active_channels,omitempty"`
125
}
126
127
// handleDropChannel handles DELETE /v1/topology/channels/{channel}.
128
// Drops the ChanServ registration of an ephemeral channel.
129
func (s *Server) handleDropChannel(w http.ResponseWriter, r *http.Request) {
130
channel := "#" + r.PathValue("channel")
131
if err := topology.ValidateName(channel); err != nil {
132
writeError(w, http.StatusBadRequest, err.Error())
133
return
134
}
135
if s.topoMgr == nil {
136
writeError(w, http.StatusServiceUnavailable, "topology not configured")
137
return
138
}
139
s.topoMgr.DropChannel(channel)
140
w.WriteHeader(http.StatusNoContent)
141
}
142
143
// handleGetInstructions handles GET /v1/channels/{channel}/instructions.
144
func (s *Server) handleGetInstructions(w http.ResponseWriter, r *http.Request) {
145
channel := "#" + r.PathValue("channel")
146
if s.policies == nil {
147
writeJSON(w, http.StatusOK, map[string]string{"channel": channel, "instructions": ""})
148
return
149
}
150
p := s.policies.Get()
151
msg := p.OnJoinMessages[channel]
152
writeJSON(w, http.StatusOK, map[string]string{"channel": channel, "instructions": msg})
153
}
154
155
// handlePutInstructions handles PUT /v1/channels/{channel}/instructions.
156
func (s *Server) handlePutInstructions(w http.ResponseWriter, r *http.Request) {
157
channel := "#" + r.PathValue("channel")
158
if s.policies == nil {
159
writeError(w, http.StatusServiceUnavailable, "policies not configured")
160
return
161
}
162
var req struct {
163
Instructions string `json:"instructions"`
164
}
165
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
166
writeError(w, http.StatusBadRequest, "invalid request body")
167
return
168
}
169
p := s.policies.Get()
170
if p.OnJoinMessages == nil {
171
p.OnJoinMessages = make(map[string]string)
172
}
173
p.OnJoinMessages[channel] = req.Instructions
174
if err := s.policies.Set(p); err != nil {
175
writeError(w, http.StatusInternalServerError, "save failed")
176
return
177
}
178
w.WriteHeader(http.StatusNoContent)
179
}
180
181
// handleDeleteInstructions handles DELETE /v1/channels/{channel}/instructions.
182
func (s *Server) handleDeleteInstructions(w http.ResponseWriter, r *http.Request) {
183
channel := "#" + r.PathValue("channel")
184
if s.policies == nil {
185
writeError(w, http.StatusServiceUnavailable, "policies not configured")
186
return
187
}
188
p := s.policies.Get()
189
delete(p.OnJoinMessages, channel)
190
if err := s.policies.Set(p); err != nil {
191
writeError(w, http.StatusInternalServerError, "save failed")
192
return
193
}
194
w.WriteHeader(http.StatusNoContent)
195
}
196
197
// handleGetTopology handles GET /v1/topology.
198
// Returns the channel type rules and static channel names declared in config.
199
func (s *Server) handleGetTopology(w http.ResponseWriter, r *http.Request) {
200
if s.topoMgr == nil {
201
writeJSON(w, http.StatusOK, topologyResponse{})
202
return
203
}
204
policy := s.topoMgr.Policy()
205
if policy == nil {
206
writeJSON(w, http.StatusOK, topologyResponse{})
207
return
208
}
209
210
statics := policy.StaticChannels()
211
staticNames := make([]string, len(statics))
212
for i, sc := range statics {
213
staticNames[i] = sc.Name
214
}
215
216
types := policy.Types()
217
typeInfos := make([]channelTypeInfo, len(types))
218
for i, t := range types {
219
typeInfos[i] = channelTypeInfo{
220
Name: t.Name,
221
Prefix: t.Prefix,
222
Autojoin: t.Autojoin,
223
Supervision: t.Supervision,
224
Ephemeral: t.Ephemeral,
225
TTLSeconds: int64(t.TTL.Seconds()),
226
}
227
}
228
229
writeJSON(w, http.StatusOK, topologyResponse{
230
StaticChannels: staticNames,
231
Types: typeInfos,
232
ActiveChannels: s.topoMgr.ListChannels(),
233
})
234
}
235

Keyboard Shortcuts

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