source: code/trunk/service.go@ 486

Last change on this file since 486 was 478, checked in by hubert, 4 years ago

Implement casemapping

TL;DR: supports for casemapping, now logs are saved in
casemapped/canonical/tolower form
(eg. in the #channel directory instead of #Channel... or something)

What is casemapping?

see <https://modern.ircdocs.horse/#casemapping-parameter>

Casemapping and multi-upstream

Since each upstream does not necessarily use the same casemapping, and
since casemappings cannot coexist [0],

  1. soju must also update the database accordingly to upstreams' casemapping, otherwise it will end up inconsistent,
  2. soju must "normalize" entity names and expose only one casemapping that is a subset of all supported casemappings (here, ascii).

[0] On some upstreams, "emersion[m]" and "emersion{m}" refer to the same
user (upstreams that advertise rfc1459 for example), while on others
(upstreams that advertise ascii) they don't.

Once upstream's casemapping is known (default to rfc1459), entity names
in map keys are made into casemapped form, for upstreamConn,
upstreamChannel and network.

downstreamConn advertises "CASEMAPPING=ascii", and always casemap map
keys with ascii.

Some functions require the caller to casemap their argument (to avoid
needless calls to casemapping functions).

Message forwarding and casemapping

downstream message handling (joins and parts basically):
When relaying entity names from downstreams to upstreams, soju uses the
upstream casemapping, in order to not get in the way of the user. This
does not brings any issue, as long as soju replies with the ascii
casemapping in mind (solves point 1.).

marshalEntity/marshalUserPrefix:
When relaying entity names from upstreams with non-ascii casemappings,
soju *partially* casemap them: it only change the case of characters
which are not ascii letters. ASCII case is thus kept intact, while
special symbols like []{} are the same every time soju sends them to
downstreams (solves point 2.).

Casemapping changes

Casemapping changes are not fully supported by this patch and will
result in loss of history. This is a limitation of the protocol and
should be solved by the RENAME spec.

File size: 19.4 KB
RevLine 
[117]1package soju
2
3import (
[307]4 "crypto"
5 "crypto/ecdsa"
6 "crypto/ed25519"
7 "crypto/elliptic"
8 "crypto/rand"
9 "crypto/rsa"
10 "crypto/sha1"
11 "crypto/sha256"
12 "crypto/x509"
13 "crypto/x509/pkix"
14 "encoding/hex"
15 "errors"
[120]16 "flag"
[117]17 "fmt"
[120]18 "io/ioutil"
[307]19 "math/big"
[339]20 "sort"
[117]21 "strings"
[307]22 "time"
[117]23
24 "github.com/google/shlex"
[252]25 "golang.org/x/crypto/bcrypt"
[117]26 "gopkg.in/irc.v3"
27)
28
29const serviceNick = "BouncerServ"
[478]30const serviceNickCM = "bouncerserv"
[343]31const serviceRealname = "soju bouncer service"
[117]32
[220]33var servicePrefix = &irc.Prefix{
34 Name: serviceNick,
35 User: serviceNick,
36 Host: serviceNick,
37}
38
[150]39type serviceCommandSet map[string]*serviceCommand
40
[117]41type serviceCommand struct {
[150]42 usage string
43 desc string
44 handle func(dc *downstreamConn, params []string) error
45 children serviceCommandSet
[328]46 admin bool
[117]47}
48
[218]49func sendServiceNOTICE(dc *downstreamConn, text string) {
50 dc.SendMessage(&irc.Message{
[220]51 Prefix: servicePrefix,
[218]52 Command: "NOTICE",
53 Params: []string{dc.nick, text},
54 })
55}
56
[117]57func sendServicePRIVMSG(dc *downstreamConn, text string) {
58 dc.SendMessage(&irc.Message{
[220]59 Prefix: servicePrefix,
[117]60 Command: "PRIVMSG",
61 Params: []string{dc.nick, text},
62 })
63}
64
65func handleServicePRIVMSG(dc *downstreamConn, text string) {
66 words, err := shlex.Split(text)
67 if err != nil {
68 sendServicePRIVMSG(dc, fmt.Sprintf("error: failed to parse command: %v", err))
69 return
70 }
71
[150]72 cmd, params, err := serviceCommands.Get(words)
73 if err != nil {
74 sendServicePRIVMSG(dc, fmt.Sprintf(`error: %v (type "help" for a list of commands)`, err))
[117]75 return
76 }
[328]77 if cmd.admin && !dc.user.Admin {
78 sendServicePRIVMSG(dc, fmt.Sprintf(`error: you must be an admin to use this command`))
79 return
80 }
[117]81
[334]82 if cmd.handle == nil {
83 if len(cmd.children) > 0 {
84 var l []string
[335]85 appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
[334]86 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
87 } else {
88 // Pretend the command does not exist if it has neither children nor handler.
89 // This is obviously a bug but it is better to not die anyway.
90 dc.logger.Printf("command without handler and subcommands invoked:", words[0])
91 sendServicePRIVMSG(dc, fmt.Sprintf("command %q not found", words[0]))
92 }
93 return
94 }
95
[117]96 if err := cmd.handle(dc, params); err != nil {
97 sendServicePRIVMSG(dc, fmt.Sprintf("error: %v", err))
98 }
99}
100
[150]101func (cmds serviceCommandSet) Get(params []string) (*serviceCommand, []string, error) {
102 if len(params) == 0 {
103 return nil, nil, fmt.Errorf("no command specified")
104 }
[117]105
[150]106 name := params[0]
107 params = params[1:]
108
109 cmd, ok := cmds[name]
110 if !ok {
111 for k := range cmds {
112 if !strings.HasPrefix(k, name) {
113 continue
114 }
115 if cmd != nil {
116 return nil, params, fmt.Errorf("command %q is ambiguous", name)
117 }
118 cmd = cmds[k]
119 }
120 }
121 if cmd == nil {
122 return nil, params, fmt.Errorf("command %q not found", name)
123 }
124
125 if len(params) == 0 || len(cmd.children) == 0 {
126 return cmd, params, nil
127 }
128 return cmd.children.Get(params)
129}
130
[339]131func (cmds serviceCommandSet) Names() []string {
132 l := make([]string, 0, len(cmds))
133 for name := range cmds {
134 l = append(l, name)
135 }
136 sort.Strings(l)
137 return l
138}
139
[150]140var serviceCommands serviceCommandSet
141
[117]142func init() {
[150]143 serviceCommands = serviceCommandSet{
[117]144 "help": {
145 usage: "[command]",
146 desc: "print help message",
147 handle: handleServiceHelp,
148 },
[150]149 "network": {
150 children: serviceCommandSet{
151 "create": {
[313]152 usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-connect-command command]...",
[150]153 desc: "add a new network",
[325]154 handle: handleServiceNetworkCreate,
[150]155 },
[151]156 "status": {
157 desc: "show a list of saved networks and their current status",
158 handle: handleServiceNetworkStatus,
159 },
[313]160 "update": {
[374]161 usage: "<name> [-addr addr] [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-connect-command command]...",
[315]162 desc: "update a network",
[313]163 handle: handleServiceNetworkUpdate,
164 },
[202]165 "delete": {
166 usage: "<name>",
167 desc: "delete a network",
168 handle: handleServiceNetworkDelete,
169 },
[150]170 },
[120]171 },
[307]172 "certfp": {
173 children: serviceCommandSet{
174 "generate": {
175 usage: "[-key-type rsa|ecdsa|ed25519] [-bits N] <network name>",
176 desc: "generate a new self-signed certificate, defaults to using RSA-3072 key",
177 handle: handleServiceCertfpGenerate,
178 },
179 "fingerprint": {
180 usage: "<network name>",
181 desc: "show fingerprints of certificate associated with the network",
182 handle: handleServiceCertfpFingerprints,
183 },
184 },
185 },
[363]186 "sasl": {
187 children: serviceCommandSet{
188 "set-plain": {
189 usage: "<network name> <username> <password>",
190 desc: "set SASL PLAIN credentials",
191 handle: handleServiceSASLSetPlain,
192 },
[364]193 "reset": {
194 usage: "<network name>",
195 desc: "disable SASL authentication and remove stored credentials",
196 handle: handleServiceSASLReset,
197 },
[363]198 },
199 },
[329]200 "user": {
201 children: serviceCommandSet{
202 "create": {
203 usage: "-username <username> -password <password> [-admin]",
204 desc: "create a new soju user",
205 handle: handleUserCreate,
206 admin: true,
207 },
[379]208 "delete": {
209 usage: "<username>",
210 desc: "delete a user",
211 handle: handleUserDelete,
212 admin: true,
213 },
[329]214 },
215 admin: true,
216 },
[252]217 "change-password": {
218 usage: "<new password>",
219 desc: "change your password",
220 handle: handlePasswordChange,
221 },
[436]222 "channel": {
223 children: serviceCommandSet{
224 "update": {
225 usage: "<name> [-relay-detached <default|none|highlight|message>] [-reattach-on <default|none|highlight|message>] [-detach-after <duration>] [-detach-on <default|none|highlight|message>]",
226 desc: "update a channel",
227 handle: handleServiceChannelUpdate,
228 },
229 },
230 },
[117]231 }
232}
233
[328]234func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, admin bool, l *[]string) {
[339]235 for _, name := range cmds.Names() {
236 cmd := cmds[name]
[328]237 if cmd.admin && !admin {
238 continue
239 }
[150]240 words := append(prefix, name)
241 if len(cmd.children) == 0 {
242 s := strings.Join(words, " ")
243 *l = append(*l, s)
244 } else {
[328]245 appendServiceCommandSetHelp(cmd.children, words, admin, l)
[150]246 }
247 }
248}
249
[117]250func handleServiceHelp(dc *downstreamConn, params []string) error {
251 if len(params) > 0 {
[150]252 cmd, rest, err := serviceCommands.Get(params)
253 if err != nil {
254 return err
[117]255 }
[150]256 words := params[:len(params)-len(rest)]
[117]257
[150]258 if len(cmd.children) > 0 {
259 var l []string
[328]260 appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
[150]261 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
262 } else {
263 text := strings.Join(words, " ")
264 if cmd.usage != "" {
265 text += " " + cmd.usage
266 }
267 text += ": " + cmd.desc
268
269 sendServicePRIVMSG(dc, text)
[117]270 }
271 } else {
272 var l []string
[328]273 appendServiceCommandSetHelp(serviceCommands, nil, dc.user.Admin, &l)
[117]274 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
275 }
276 return nil
277}
[120]278
[202]279func newFlagSet() *flag.FlagSet {
[120]280 fs := flag.NewFlagSet("", flag.ContinueOnError)
281 fs.SetOutput(ioutil.Discard)
[202]282 return fs
283}
284
[313]285type stringSliceFlag []string
[263]286
[313]287func (v *stringSliceFlag) String() string {
[263]288 return fmt.Sprint([]string(*v))
289}
290
[313]291func (v *stringSliceFlag) Set(s string) error {
[263]292 *v = append(*v, s)
293 return nil
294}
295
[313]296// stringPtrFlag is a flag value populating a string pointer. This allows to
297// disambiguate between a flag that hasn't been set and a flag that has been
298// set to an empty string.
299type stringPtrFlag struct {
300 ptr **string
301}
302
303func (f stringPtrFlag) String() string {
[333]304 if f.ptr == nil || *f.ptr == nil {
[313]305 return ""
306 }
307 return **f.ptr
308}
309
310func (f stringPtrFlag) Set(s string) error {
311 *f.ptr = &s
312 return nil
313}
314
315type networkFlagSet struct {
316 *flag.FlagSet
317 Addr, Name, Nick, Username, Pass, Realname *string
[315]318 ConnectCommands []string
[313]319}
320
321func newNetworkFlagSet() *networkFlagSet {
322 fs := &networkFlagSet{FlagSet: newFlagSet()}
323 fs.Var(stringPtrFlag{&fs.Addr}, "addr", "")
324 fs.Var(stringPtrFlag{&fs.Name}, "name", "")
325 fs.Var(stringPtrFlag{&fs.Nick}, "nick", "")
326 fs.Var(stringPtrFlag{&fs.Username}, "username", "")
327 fs.Var(stringPtrFlag{&fs.Pass}, "pass", "")
328 fs.Var(stringPtrFlag{&fs.Realname}, "realname", "")
329 fs.Var((*stringSliceFlag)(&fs.ConnectCommands), "connect-command", "")
330 return fs
331}
332
333func (fs *networkFlagSet) update(network *Network) error {
334 if fs.Addr != nil {
335 if addrParts := strings.SplitN(*fs.Addr, "://", 2); len(addrParts) == 2 {
336 scheme := addrParts[0]
337 switch scheme {
[358]338 case "ircs", "irc+insecure", "unix":
[313]339 default:
[358]340 return fmt.Errorf("unknown scheme %q (supported schemes: ircs, irc+insecure, unix)", scheme)
[313]341 }
342 }
343 network.Addr = *fs.Addr
344 }
345 if fs.Name != nil {
346 network.Name = *fs.Name
347 }
348 if fs.Nick != nil {
349 network.Nick = *fs.Nick
350 }
351 if fs.Username != nil {
352 network.Username = *fs.Username
353 }
354 if fs.Pass != nil {
355 network.Pass = *fs.Pass
356 }
357 if fs.Realname != nil {
358 network.Realname = *fs.Realname
359 }
360 if fs.ConnectCommands != nil {
361 if len(fs.ConnectCommands) == 1 && fs.ConnectCommands[0] == "" {
362 network.ConnectCommands = nil
363 } else {
364 for _, command := range fs.ConnectCommands {
365 _, err := irc.ParseMessage(command)
366 if err != nil {
367 return fmt.Errorf("flag -connect-command must be a valid raw irc command string: %q: %v", command, err)
368 }
369 }
370 network.ConnectCommands = fs.ConnectCommands
371 }
372 }
373 return nil
374}
375
[325]376func handleServiceNetworkCreate(dc *downstreamConn, params []string) error {
[313]377 fs := newNetworkFlagSet()
[120]378 if err := fs.Parse(params); err != nil {
379 return err
380 }
[313]381 if fs.Addr == nil {
[150]382 return fmt.Errorf("flag -addr is required")
[120]383 }
384
[313]385 record := &Network{
386 Addr: *fs.Addr,
387 Nick: dc.nick,
[269]388 }
[313]389 if err := fs.update(record); err != nil {
390 return err
[263]391 }
392
[313]393 network, err := dc.user.createNetwork(record)
[120]394 if err != nil {
395 return fmt.Errorf("could not create network: %v", err)
396 }
397
[202]398 sendServicePRIVMSG(dc, fmt.Sprintf("created network %q", network.GetName()))
[120]399 return nil
400}
[151]401
402func handleServiceNetworkStatus(dc *downstreamConn, params []string) error {
403 dc.user.forEachNetwork(func(net *network) {
404 var statuses []string
405 var details string
[279]406 if uc := net.conn; uc != nil {
[271]407 if dc.nick != uc.nick {
408 statuses = append(statuses, "connected as "+uc.nick)
409 } else {
410 statuses = append(statuses, "connected")
411 }
[478]412 details = fmt.Sprintf("%v channels", uc.channels.Len())
[151]413 } else {
414 statuses = append(statuses, "disconnected")
[219]415 if net.lastError != nil {
416 details = net.lastError.Error()
417 }
[151]418 }
419
420 if net == dc.network {
421 statuses = append(statuses, "current")
422 }
423
[224]424 name := net.GetName()
425 if name != net.Addr {
426 name = fmt.Sprintf("%v (%v)", name, net.Addr)
427 }
428
429 s := fmt.Sprintf("%v [%v]", name, strings.Join(statuses, ", "))
[151]430 if details != "" {
431 s += ": " + details
432 }
433 sendServicePRIVMSG(dc, s)
434 })
435 return nil
436}
[202]437
[313]438func handleServiceNetworkUpdate(dc *downstreamConn, params []string) error {
439 if len(params) < 1 {
[436]440 return fmt.Errorf("expected at least one argument")
[313]441 }
442
443 fs := newNetworkFlagSet()
444 if err := fs.Parse(params[1:]); err != nil {
445 return err
446 }
447
448 net := dc.user.getNetwork(params[0])
449 if net == nil {
450 return fmt.Errorf("unknown network %q", params[0])
451 }
452
453 record := net.Network // copy network record because we'll mutate it
454 if err := fs.update(&record); err != nil {
455 return err
456 }
457
458 network, err := dc.user.updateNetwork(&record)
459 if err != nil {
460 return fmt.Errorf("could not update network: %v", err)
461 }
462
463 sendServicePRIVMSG(dc, fmt.Sprintf("updated network %q", network.GetName()))
464 return nil
465}
466
[202]467func handleServiceNetworkDelete(dc *downstreamConn, params []string) error {
468 if len(params) != 1 {
469 return fmt.Errorf("expected exactly one argument")
470 }
471
472 net := dc.user.getNetwork(params[0])
473 if net == nil {
474 return fmt.Errorf("unknown network %q", params[0])
475 }
476
477 if err := dc.user.deleteNetwork(net.ID); err != nil {
478 return err
479 }
480
481 sendServicePRIVMSG(dc, fmt.Sprintf("deleted network %q", net.GetName()))
482 return nil
483}
[252]484
[325]485func handleServiceCertfpGenerate(dc *downstreamConn, params []string) error {
486 fs := newFlagSet()
487 keyType := fs.String("key-type", "rsa", "key type to generate (rsa, ecdsa, ed25519)")
488 bits := fs.Int("bits", 3072, "size of key to generate, meaningful only for RSA")
489
490 if err := fs.Parse(params); err != nil {
491 return err
492 }
493
494 if len(fs.Args()) != 1 {
495 return errors.New("exactly one argument is required")
496 }
497
498 net := dc.user.getNetwork(fs.Arg(0))
499 if net == nil {
500 return fmt.Errorf("unknown network %q", fs.Arg(0))
501 }
502
503 var (
504 privKey crypto.PrivateKey
505 pubKey crypto.PublicKey
506 )
507 switch *keyType {
508 case "rsa":
509 key, err := rsa.GenerateKey(rand.Reader, *bits)
510 if err != nil {
511 return err
512 }
513 privKey = key
514 pubKey = key.Public()
515 case "ecdsa":
516 key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
517 if err != nil {
518 return err
519 }
520 privKey = key
521 pubKey = key.Public()
522 case "ed25519":
523 var err error
524 pubKey, privKey, err = ed25519.GenerateKey(rand.Reader)
525 if err != nil {
526 return err
527 }
528 }
529
530 // Using PKCS#8 allows easier extension for new key types.
531 privKeyBytes, err := x509.MarshalPKCS8PrivateKey(privKey)
532 if err != nil {
533 return err
534 }
535
536 notBefore := time.Now()
537 // Lets make a fair assumption nobody will use the same cert for more than 20 years...
538 notAfter := notBefore.Add(24 * time.Hour * 365 * 20)
539 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
540 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
541 if err != nil {
542 return err
543 }
544 cert := &x509.Certificate{
545 SerialNumber: serialNumber,
546 Subject: pkix.Name{CommonName: "soju auto-generated certificate"},
547 NotBefore: notBefore,
548 NotAfter: notAfter,
549 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
550 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
551 }
552 derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, pubKey, privKey)
553 if err != nil {
554 return err
555 }
556
557 net.SASL.External.CertBlob = derBytes
558 net.SASL.External.PrivKeyBlob = privKeyBytes
559 net.SASL.Mechanism = "EXTERNAL"
560
[421]561 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
[325]562 return err
563 }
564
565 sendServicePRIVMSG(dc, "certificate generated")
566
567 sha1Sum := sha1.Sum(derBytes)
568 sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
569 sha256Sum := sha256.Sum256(derBytes)
570 sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
571
572 return nil
573}
574
575func handleServiceCertfpFingerprints(dc *downstreamConn, params []string) error {
576 if len(params) != 1 {
577 return fmt.Errorf("expected exactly one argument")
578 }
579
580 net := dc.user.getNetwork(params[0])
581 if net == nil {
582 return fmt.Errorf("unknown network %q", params[0])
583 }
584
585 sha1Sum := sha1.Sum(net.SASL.External.CertBlob)
586 sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
587 sha256Sum := sha256.Sum256(net.SASL.External.CertBlob)
588 sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
589 return nil
590}
591
[364]592func handleServiceSASLSetPlain(dc *downstreamConn, params []string) error {
593 if len(params) != 3 {
594 return fmt.Errorf("expected exactly 3 arguments")
[325]595 }
596
597 net := dc.user.getNetwork(params[0])
598 if net == nil {
599 return fmt.Errorf("unknown network %q", params[0])
600 }
601
[364]602 net.SASL.Plain.Username = params[1]
603 net.SASL.Plain.Password = params[2]
604 net.SASL.Mechanism = "PLAIN"
[325]605
[421]606 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
[325]607 return err
608 }
609
[364]610 sendServicePRIVMSG(dc, "credentials saved")
[325]611 return nil
612}
613
[364]614func handleServiceSASLReset(dc *downstreamConn, params []string) error {
615 if len(params) != 1 {
616 return fmt.Errorf("expected exactly one argument")
[363]617 }
618
619 net := dc.user.getNetwork(params[0])
620 if net == nil {
621 return fmt.Errorf("unknown network %q", params[0])
622 }
623
[364]624 net.SASL.Plain.Username = ""
625 net.SASL.Plain.Password = ""
626 net.SASL.External.CertBlob = nil
627 net.SASL.External.PrivKeyBlob = nil
628 net.SASL.Mechanism = ""
[363]629
[421]630 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
[363]631 return err
632 }
633
[364]634 sendServicePRIVMSG(dc, "credentials reset")
[363]635 return nil
636}
637
[252]638func handlePasswordChange(dc *downstreamConn, params []string) error {
639 if len(params) != 1 {
640 return fmt.Errorf("expected exactly one argument")
641 }
642
643 hashed, err := bcrypt.GenerateFromPassword([]byte(params[0]), bcrypt.DefaultCost)
644 if err != nil {
645 return fmt.Errorf("failed to hash password: %v", err)
646 }
647 if err := dc.user.updatePassword(string(hashed)); err != nil {
648 return err
649 }
650
651 sendServicePRIVMSG(dc, "password updated")
652 return nil
653}
[329]654
655func handleUserCreate(dc *downstreamConn, params []string) error {
656 fs := newFlagSet()
657 username := fs.String("username", "", "")
658 password := fs.String("password", "", "")
659 admin := fs.Bool("admin", false, "")
660
661 if err := fs.Parse(params); err != nil {
662 return err
663 }
664 if *username == "" {
665 return fmt.Errorf("flag -username is required")
666 }
667 if *password == "" {
668 return fmt.Errorf("flag -password is required")
669 }
670
671 hashed, err := bcrypt.GenerateFromPassword([]byte(*password), bcrypt.DefaultCost)
672 if err != nil {
673 return fmt.Errorf("failed to hash password: %v", err)
674 }
675
676 user := &User{
677 Username: *username,
678 Password: string(hashed),
679 Admin: *admin,
680 }
681 if _, err := dc.srv.createUser(user); err != nil {
682 return fmt.Errorf("could not create user: %v", err)
683 }
684
685 sendServicePRIVMSG(dc, fmt.Sprintf("created user %q", *username))
686 return nil
687}
[379]688
689func handleUserDelete(dc *downstreamConn, params []string) error {
690 if len(params) != 1 {
691 return fmt.Errorf("expected exactly one argument")
692 }
693 username := params[0]
694
695 u := dc.srv.getUser(username)
696 if u == nil {
697 return fmt.Errorf("unknown username %q", username)
698 }
699
700 u.stop()
701
[421]702 if err := dc.srv.db.DeleteUser(dc.user.ID); err != nil {
[379]703 return fmt.Errorf("failed to delete user: %v", err)
704 }
705
706 sendServicePRIVMSG(dc, fmt.Sprintf("deleted user %q", username))
707 return nil
708}
[436]709
710type channelFlagSet struct {
711 *flag.FlagSet
712 RelayDetached, ReattachOn, DetachAfter, DetachOn *string
713}
714
715func newChannelFlagSet() *channelFlagSet {
716 fs := &channelFlagSet{FlagSet: newFlagSet()}
717 fs.Var(stringPtrFlag{&fs.RelayDetached}, "relay-detached", "")
718 fs.Var(stringPtrFlag{&fs.ReattachOn}, "reattach-on", "")
719 fs.Var(stringPtrFlag{&fs.DetachAfter}, "detach-after", "")
720 fs.Var(stringPtrFlag{&fs.DetachOn}, "detach-on", "")
721 return fs
722}
723
724func (fs *channelFlagSet) update(channel *Channel) error {
725 if fs.RelayDetached != nil {
726 filter, err := parseFilter(*fs.RelayDetached)
727 if err != nil {
728 return err
729 }
730 channel.RelayDetached = filter
731 }
732 if fs.ReattachOn != nil {
733 filter, err := parseFilter(*fs.ReattachOn)
734 if err != nil {
735 return err
736 }
737 channel.ReattachOn = filter
738 }
739 if fs.DetachAfter != nil {
740 dur, err := time.ParseDuration(*fs.DetachAfter)
741 if err != nil || dur < 0 {
742 return fmt.Errorf("unknown duration for -detach-after %q (duration format: 0, 300s, 22h30m, ...)", *fs.DetachAfter)
743 }
744 channel.DetachAfter = dur
745 }
746 if fs.DetachOn != nil {
747 filter, err := parseFilter(*fs.DetachOn)
748 if err != nil {
749 return err
750 }
751 channel.DetachOn = filter
752 }
753 return nil
754}
755
756func handleServiceChannelUpdate(dc *downstreamConn, params []string) error {
757 if len(params) < 1 {
758 return fmt.Errorf("expected at least one argument")
759 }
760 name := params[0]
761
762 fs := newChannelFlagSet()
763 if err := fs.Parse(params[1:]); err != nil {
764 return err
765 }
766
767 uc, upstreamName, err := dc.unmarshalEntity(name)
768 if err != nil {
769 return fmt.Errorf("unknown channel %q", name)
770 }
771
[478]772 ch := uc.network.channels.Value(upstreamName)
773 if ch == nil {
[436]774 return fmt.Errorf("unknown channel %q", name)
775 }
776
777 if err := fs.update(ch); err != nil {
778 return err
779 }
780
781 uc.updateChannelAutoDetach(upstreamName)
782
783 if err := dc.srv.db.StoreChannel(uc.network.ID, ch); err != nil {
784 return fmt.Errorf("failed to update channel: %v", err)
785 }
786
787 sendServicePRIVMSG(dc, fmt.Sprintf("updated channel %q", name))
788 return nil
789}
Note: See TracBrowser for help on using the repository browser.