source: code/trunk/service.go@ 525

Last change on this file since 525 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
Line 
1package soju
2
3import (
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"
16 "flag"
17 "fmt"
18 "io/ioutil"
19 "math/big"
20 "sort"
21 "strings"
22 "time"
23
24 "github.com/google/shlex"
25 "golang.org/x/crypto/bcrypt"
26 "gopkg.in/irc.v3"
27)
28
29const serviceNick = "BouncerServ"
30const serviceNickCM = "bouncerserv"
31const serviceRealname = "soju bouncer service"
32
33var servicePrefix = &irc.Prefix{
34 Name: serviceNick,
35 User: serviceNick,
36 Host: serviceNick,
37}
38
39type serviceCommandSet map[string]*serviceCommand
40
41type serviceCommand struct {
42 usage string
43 desc string
44 handle func(dc *downstreamConn, params []string) error
45 children serviceCommandSet
46 admin bool
47}
48
49func sendServiceNOTICE(dc *downstreamConn, text string) {
50 dc.SendMessage(&irc.Message{
51 Prefix: servicePrefix,
52 Command: "NOTICE",
53 Params: []string{dc.nick, text},
54 })
55}
56
57func sendServicePRIVMSG(dc *downstreamConn, text string) {
58 dc.SendMessage(&irc.Message{
59 Prefix: servicePrefix,
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
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))
75 return
76 }
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 }
81
82 if cmd.handle == nil {
83 if len(cmd.children) > 0 {
84 var l []string
85 appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
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
96 if err := cmd.handle(dc, params); err != nil {
97 sendServicePRIVMSG(dc, fmt.Sprintf("error: %v", err))
98 }
99}
100
101func (cmds serviceCommandSet) Get(params []string) (*serviceCommand, []string, error) {
102 if len(params) == 0 {
103 return nil, nil, fmt.Errorf("no command specified")
104 }
105
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
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
140var serviceCommands serviceCommandSet
141
142func init() {
143 serviceCommands = serviceCommandSet{
144 "help": {
145 usage: "[command]",
146 desc: "print help message",
147 handle: handleServiceHelp,
148 },
149 "network": {
150 children: serviceCommandSet{
151 "create": {
152 usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-connect-command command]...",
153 desc: "add a new network",
154 handle: handleServiceNetworkCreate,
155 },
156 "status": {
157 desc: "show a list of saved networks and their current status",
158 handle: handleServiceNetworkStatus,
159 },
160 "update": {
161 usage: "<name> [-addr addr] [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-connect-command command]...",
162 desc: "update a network",
163 handle: handleServiceNetworkUpdate,
164 },
165 "delete": {
166 usage: "<name>",
167 desc: "delete a network",
168 handle: handleServiceNetworkDelete,
169 },
170 },
171 },
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 },
186 "sasl": {
187 children: serviceCommandSet{
188 "set-plain": {
189 usage: "<network name> <username> <password>",
190 desc: "set SASL PLAIN credentials",
191 handle: handleServiceSASLSetPlain,
192 },
193 "reset": {
194 usage: "<network name>",
195 desc: "disable SASL authentication and remove stored credentials",
196 handle: handleServiceSASLReset,
197 },
198 },
199 },
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 },
208 "delete": {
209 usage: "<username>",
210 desc: "delete a user",
211 handle: handleUserDelete,
212 admin: true,
213 },
214 },
215 admin: true,
216 },
217 "change-password": {
218 usage: "<new password>",
219 desc: "change your password",
220 handle: handlePasswordChange,
221 },
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 },
231 }
232}
233
234func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, admin bool, l *[]string) {
235 for _, name := range cmds.Names() {
236 cmd := cmds[name]
237 if cmd.admin && !admin {
238 continue
239 }
240 words := append(prefix, name)
241 if len(cmd.children) == 0 {
242 s := strings.Join(words, " ")
243 *l = append(*l, s)
244 } else {
245 appendServiceCommandSetHelp(cmd.children, words, admin, l)
246 }
247 }
248}
249
250func handleServiceHelp(dc *downstreamConn, params []string) error {
251 if len(params) > 0 {
252 cmd, rest, err := serviceCommands.Get(params)
253 if err != nil {
254 return err
255 }
256 words := params[:len(params)-len(rest)]
257
258 if len(cmd.children) > 0 {
259 var l []string
260 appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
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)
270 }
271 } else {
272 var l []string
273 appendServiceCommandSetHelp(serviceCommands, nil, dc.user.Admin, &l)
274 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
275 }
276 return nil
277}
278
279func newFlagSet() *flag.FlagSet {
280 fs := flag.NewFlagSet("", flag.ContinueOnError)
281 fs.SetOutput(ioutil.Discard)
282 return fs
283}
284
285type stringSliceFlag []string
286
287func (v *stringSliceFlag) String() string {
288 return fmt.Sprint([]string(*v))
289}
290
291func (v *stringSliceFlag) Set(s string) error {
292 *v = append(*v, s)
293 return nil
294}
295
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 {
304 if f.ptr == nil || *f.ptr == nil {
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
318 ConnectCommands []string
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 {
338 case "ircs", "irc+insecure", "unix":
339 default:
340 return fmt.Errorf("unknown scheme %q (supported schemes: ircs, irc+insecure, unix)", scheme)
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
376func handleServiceNetworkCreate(dc *downstreamConn, params []string) error {
377 fs := newNetworkFlagSet()
378 if err := fs.Parse(params); err != nil {
379 return err
380 }
381 if fs.Addr == nil {
382 return fmt.Errorf("flag -addr is required")
383 }
384
385 record := &Network{
386 Addr: *fs.Addr,
387 Nick: dc.nick,
388 }
389 if err := fs.update(record); err != nil {
390 return err
391 }
392
393 network, err := dc.user.createNetwork(record)
394 if err != nil {
395 return fmt.Errorf("could not create network: %v", err)
396 }
397
398 sendServicePRIVMSG(dc, fmt.Sprintf("created network %q", network.GetName()))
399 return nil
400}
401
402func handleServiceNetworkStatus(dc *downstreamConn, params []string) error {
403 dc.user.forEachNetwork(func(net *network) {
404 var statuses []string
405 var details string
406 if uc := net.conn; uc != nil {
407 if dc.nick != uc.nick {
408 statuses = append(statuses, "connected as "+uc.nick)
409 } else {
410 statuses = append(statuses, "connected")
411 }
412 details = fmt.Sprintf("%v channels", uc.channels.Len())
413 } else {
414 statuses = append(statuses, "disconnected")
415 if net.lastError != nil {
416 details = net.lastError.Error()
417 }
418 }
419
420 if net == dc.network {
421 statuses = append(statuses, "current")
422 }
423
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, ", "))
430 if details != "" {
431 s += ": " + details
432 }
433 sendServicePRIVMSG(dc, s)
434 })
435 return nil
436}
437
438func handleServiceNetworkUpdate(dc *downstreamConn, params []string) error {
439 if len(params) < 1 {
440 return fmt.Errorf("expected at least one argument")
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
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}
484
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
561 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
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
592func handleServiceSASLSetPlain(dc *downstreamConn, params []string) error {
593 if len(params) != 3 {
594 return fmt.Errorf("expected exactly 3 arguments")
595 }
596
597 net := dc.user.getNetwork(params[0])
598 if net == nil {
599 return fmt.Errorf("unknown network %q", params[0])
600 }
601
602 net.SASL.Plain.Username = params[1]
603 net.SASL.Plain.Password = params[2]
604 net.SASL.Mechanism = "PLAIN"
605
606 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
607 return err
608 }
609
610 sendServicePRIVMSG(dc, "credentials saved")
611 return nil
612}
613
614func handleServiceSASLReset(dc *downstreamConn, params []string) error {
615 if len(params) != 1 {
616 return fmt.Errorf("expected exactly one argument")
617 }
618
619 net := dc.user.getNetwork(params[0])
620 if net == nil {
621 return fmt.Errorf("unknown network %q", params[0])
622 }
623
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 = ""
629
630 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
631 return err
632 }
633
634 sendServicePRIVMSG(dc, "credentials reset")
635 return nil
636}
637
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}
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}
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
702 if err := dc.srv.db.DeleteUser(u.ID); err != nil {
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}
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
772 ch := uc.network.channels.Value(upstreamName)
773 if ch == nil {
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.