source: code/trunk/service.go@ 219

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

Send the last error for disconnected networks in network status

This adds support for sending the exact error message of a network when
it is disconnected, in the reply to the service command `network
status`. This lets users easily examine why a network is currently
disconnected.

No lock is needed because all reads and writes of network.lastError are
made in the user goroutine.

Closes: https://todo.sr.ht/~emersion/soju/28

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