source: code/trunk/service.go@ 266

Last change on this file since 266 was 263, checked in by delthas, 5 years ago

Add support for custom network on-connect commands

Some servers use custom IRC bots with custom commands for registering to
specific services after connection.

This adds support for setting custom raw IRC messages, that will be
sent after registering to a network.

It also adds support for a custom flag.Value type for string
slice flags (flags taking several string values).

File size: 6.9 KB
Line 
1package soju
2
3import (
4 "flag"
5 "fmt"
6 "io/ioutil"
7 "strings"
8
9 "github.com/google/shlex"
10 "golang.org/x/crypto/bcrypt"
11 "gopkg.in/irc.v3"
12)
13
14const serviceNick = "BouncerServ"
15
16var servicePrefix = &irc.Prefix{
17 Name: serviceNick,
18 User: serviceNick,
19 Host: serviceNick,
20}
21
22type serviceCommandSet map[string]*serviceCommand
23
24type serviceCommand struct {
25 usage string
26 desc string
27 handle func(dc *downstreamConn, params []string) error
28 children serviceCommandSet
29}
30
31func sendServiceNOTICE(dc *downstreamConn, text string) {
32 dc.SendMessage(&irc.Message{
33 Prefix: servicePrefix,
34 Command: "NOTICE",
35 Params: []string{dc.nick, text},
36 })
37}
38
39func sendServicePRIVMSG(dc *downstreamConn, text string) {
40 dc.SendMessage(&irc.Message{
41 Prefix: servicePrefix,
42 Command: "PRIVMSG",
43 Params: []string{dc.nick, text},
44 })
45}
46
47func handleServicePRIVMSG(dc *downstreamConn, text string) {
48 words, err := shlex.Split(text)
49 if err != nil {
50 sendServicePRIVMSG(dc, fmt.Sprintf("error: failed to parse command: %v", err))
51 return
52 }
53
54 cmd, params, err := serviceCommands.Get(words)
55 if err != nil {
56 sendServicePRIVMSG(dc, fmt.Sprintf(`error: %v (type "help" for a list of commands)`, err))
57 return
58 }
59
60 if err := cmd.handle(dc, params); err != nil {
61 sendServicePRIVMSG(dc, fmt.Sprintf("error: %v", err))
62 }
63}
64
65func (cmds serviceCommandSet) Get(params []string) (*serviceCommand, []string, error) {
66 if len(params) == 0 {
67 return nil, nil, fmt.Errorf("no command specified")
68 }
69
70 name := params[0]
71 params = params[1:]
72
73 cmd, ok := cmds[name]
74 if !ok {
75 for k := range cmds {
76 if !strings.HasPrefix(k, name) {
77 continue
78 }
79 if cmd != nil {
80 return nil, params, fmt.Errorf("command %q is ambiguous", name)
81 }
82 cmd = cmds[k]
83 }
84 }
85 if cmd == nil {
86 return nil, params, fmt.Errorf("command %q not found", name)
87 }
88
89 if len(params) == 0 || len(cmd.children) == 0 {
90 return cmd, params, nil
91 }
92 return cmd.children.Get(params)
93}
94
95var serviceCommands serviceCommandSet
96
97func init() {
98 serviceCommands = serviceCommandSet{
99 "help": {
100 usage: "[command]",
101 desc: "print help message",
102 handle: handleServiceHelp,
103 },
104 "network": {
105 children: serviceCommandSet{
106 "create": {
107 usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [[-connect-command command] ...]",
108 desc: "add a new network",
109 handle: handleServiceCreateNetwork,
110 },
111 "status": {
112 desc: "show a list of saved networks and their current status",
113 handle: handleServiceNetworkStatus,
114 },
115 "delete": {
116 usage: "<name>",
117 desc: "delete a network",
118 handle: handleServiceNetworkDelete,
119 },
120 },
121 },
122 "change-password": {
123 usage: "<new password>",
124 desc: "change your password",
125 handle: handlePasswordChange,
126 },
127 }
128}
129
130func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, l *[]string) {
131 for name, cmd := range cmds {
132 words := append(prefix, name)
133 if len(cmd.children) == 0 {
134 s := strings.Join(words, " ")
135 *l = append(*l, s)
136 } else {
137 appendServiceCommandSetHelp(cmd.children, words, l)
138 }
139 }
140}
141
142func handleServiceHelp(dc *downstreamConn, params []string) error {
143 if len(params) > 0 {
144 cmd, rest, err := serviceCommands.Get(params)
145 if err != nil {
146 return err
147 }
148 words := params[:len(params)-len(rest)]
149
150 if len(cmd.children) > 0 {
151 var l []string
152 appendServiceCommandSetHelp(cmd.children, words, &l)
153 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
154 } else {
155 text := strings.Join(words, " ")
156 if cmd.usage != "" {
157 text += " " + cmd.usage
158 }
159 text += ": " + cmd.desc
160
161 sendServicePRIVMSG(dc, text)
162 }
163 } else {
164 var l []string
165 appendServiceCommandSetHelp(serviceCommands, nil, &l)
166 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
167 }
168 return nil
169}
170
171func newFlagSet() *flag.FlagSet {
172 fs := flag.NewFlagSet("", flag.ContinueOnError)
173 fs.SetOutput(ioutil.Discard)
174 return fs
175}
176
177type stringSliceVar []string
178
179func (v *stringSliceVar) String() string {
180 return fmt.Sprint([]string(*v))
181}
182
183func (v *stringSliceVar) Set(s string) error {
184 *v = append(*v, s)
185 return nil
186}
187
188func handleServiceCreateNetwork(dc *downstreamConn, params []string) error {
189 fs := newFlagSet()
190 addr := fs.String("addr", "", "")
191 name := fs.String("name", "", "")
192 username := fs.String("username", "", "")
193 pass := fs.String("pass", "", "")
194 realname := fs.String("realname", "", "")
195 nick := fs.String("nick", "", "")
196 var connectCommands stringSliceVar
197 fs.Var(&connectCommands, "connect-command", "")
198
199 if err := fs.Parse(params); err != nil {
200 return err
201 }
202 if *addr == "" {
203 return fmt.Errorf("flag -addr is required")
204 }
205
206 for _, command := range connectCommands {
207 _, err := irc.ParseMessage(command)
208 if err != nil {
209 return fmt.Errorf("flag -connect-command must be a valid raw irc command string: %q: %v", command, err)
210 }
211 }
212
213 if *nick == "" {
214 *nick = dc.nick
215 }
216
217 var err error
218 network, err := dc.user.createNetwork(&Network{
219 Addr: *addr,
220 Name: *name,
221 Username: *username,
222 Pass: *pass,
223 Realname: *realname,
224 Nick: *nick,
225 ConnectCommands: connectCommands,
226 })
227 if err != nil {
228 return fmt.Errorf("could not create network: %v", err)
229 }
230
231 sendServicePRIVMSG(dc, fmt.Sprintf("created network %q", network.GetName()))
232 return nil
233}
234
235func handleServiceNetworkStatus(dc *downstreamConn, params []string) error {
236 dc.user.forEachNetwork(func(net *network) {
237 var statuses []string
238 var details string
239 if uc := net.upstream(); uc != nil {
240 statuses = append(statuses, "connected as "+uc.nick)
241 details = fmt.Sprintf("%v channels", len(uc.channels))
242 } else {
243 statuses = append(statuses, "disconnected")
244 if net.lastError != nil {
245 details = net.lastError.Error()
246 }
247 }
248
249 if net == dc.network {
250 statuses = append(statuses, "current")
251 }
252
253 name := net.GetName()
254 if name != net.Addr {
255 name = fmt.Sprintf("%v (%v)", name, net.Addr)
256 }
257
258 s := fmt.Sprintf("%v [%v]", name, strings.Join(statuses, ", "))
259 if details != "" {
260 s += ": " + details
261 }
262 sendServicePRIVMSG(dc, s)
263 })
264 return nil
265}
266
267func handleServiceNetworkDelete(dc *downstreamConn, params []string) error {
268 if len(params) != 1 {
269 return fmt.Errorf("expected exactly one argument")
270 }
271
272 net := dc.user.getNetwork(params[0])
273 if net == nil {
274 return fmt.Errorf("unknown network %q", params[0])
275 }
276
277 if err := dc.user.deleteNetwork(net.ID); err != nil {
278 return err
279 }
280
281 sendServicePRIVMSG(dc, fmt.Sprintf("deleted network %q", net.GetName()))
282 return nil
283}
284
285func handlePasswordChange(dc *downstreamConn, params []string) error {
286 if len(params) != 1 {
287 return fmt.Errorf("expected exactly one argument")
288 }
289
290 hashed, err := bcrypt.GenerateFromPassword([]byte(params[0]), bcrypt.DefaultCost)
291 if err != nil {
292 return fmt.Errorf("failed to hash password: %v", err)
293 }
294 if err := dc.user.updatePassword(string(hashed)); err != nil {
295 return err
296 }
297
298 sendServicePRIVMSG(dc, "password updated")
299 return nil
300}
Note: See TracBrowser for help on using the repository browser.