source: code/trunk/service.go@ 220

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

Fill all fields of the service user prefix

On some IRC clients, NOTICE messages from a user which does not have a
user or host in its prefix (and therefore only have a Name, and look
like prefixes of servers), are treated as server notices rather than
user notices, and are treated differently. (For that matter, soju also
considers NOTICE messages from users with only a Name in their prefix as
special server messages). On most of these clients, NOTICE messages from
a user are formatted differently and stand out from the large flow of
incoming misceallenous server messages.

This fills the service user with fake User and Host values so that
NOTICE messages from it correctly appear as coming from a user. This
is particularly useful in the context of connection and disconnect
errors NOTICE messages that are broadcast from the service user to all
relevant downstreams.

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