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