source: code/trunk/service.go@ 509

Last change on this file since 509 was 508, checked in by ecs, 4 years ago

handleUserDelete: delete the correct user

Prior to this, we deleted the user issuing the deletion rather than the
user which should've been deleted.

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
[508]702 if err := dc.srv.db.DeleteUser(u.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.