From b6ae678335c55b88d690177b470ca440b66fd3b3 Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Sun, 14 Jun 2020 21:51:53 +0100 Subject: feat(clbot): Add IRC support to the IRC bot. Change-Id: I183488824882750c46e7216b98ab48e1d8f48096 Reviewed-on: https://cl.tvl.fyi/c/depot/+/343 Reviewed-by: eta Reviewed-by: tazjin --- fun/clbot/clbot.go | 164 +++++++++++++++++++++++++++++++++++++++++++------- fun/clbot/default.nix | 1 + fun/clbot/go.mod | 3 +- fun/clbot/go.sum | 16 ++++- 4 files changed, 158 insertions(+), 26 deletions(-) (limited to 'fun') diff --git a/fun/clbot/clbot.go b/fun/clbot/clbot.go index 7fa12f2c3b..b7bfa0d961 100644 --- a/fun/clbot/clbot.go +++ b/fun/clbot/clbot.go @@ -2,16 +2,21 @@ package main import ( "context" + "crypto/tls" "flag" + "fmt" "io/ioutil" "os" "os/signal" + "strings" "time" + "code.tvl.fyi/fun/clbot/backoffutil" "code.tvl.fyi/fun/clbot/gerrit" - "github.com/davecgh/go-spew/spew" + "code.tvl.fyi/fun/clbot/gerrit/gerritevents" log "github.com/golang/glog" "golang.org/x/crypto/ssh" + "gopkg.in/irc.v3" ) var ( @@ -19,9 +24,17 @@ var ( gerritSSHHostKey = flag.String("gerrit_ssh_pubkey", "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIUNYBYPCCBNDFSd0BuCR+8kgeuJ7IA5S2nTNQmkQUYNyXK+ot5os7rHtCk96+grd5+J8jFCuFBWisUe8h8NC0Q=", "Gerrit SSH public key") gerritSSHTimeout = flag.Duration("gerrit_tcp_timeout", 5*time.Second, "Gerrit SSH TCP connect timeout") - required = flag.NewFlagSet("required flags", flag.ExitOnError) - gerritAuthUsername = required.String("gerrit_ssh_auth_username", "", "Gerrit SSH username") - gerritAuthKeyPath = required.String("gerrit_ssh_auth_key", "", "Gerrit SSH private key path") + gerritAuthUsername = flag.String("gerrit_ssh_auth_username", "", "Gerrit SSH username") + gerritAuthKeyPath = flag.String("gerrit_ssh_auth_key", "", "Gerrit SSH private key path") + + ircServer = flag.String("irc_server", "chat.freenode.net:7000", "IRC server to connect to") + ircNick = flag.String("irc_nick", "clbot", "Nick to use when connecting to IRC") + ircUser = flag.String("irc_user", "clbot", "User string to use for IRC") + ircName = flag.String("irc_name", "clbot", "Name string to use for IRC") + ircChannel = flag.String("irc_channel", "##tvl", "Channel to send messages to") + ircPassword = flag.String("irc_pass", "", "Password to use for IRC") + ircSendLimit = flag.Duration("irc_send_limit", 100*time.Millisecond, "Delay between messages") + ircSendBurst = flag.Int("irc_send_burst", 10, "Number of messages which can be sent in a burst") ) func mustFixedHostKey(f string) ssh.HostKeyCallback { @@ -44,33 +57,106 @@ func mustPrivateKey(p string) ssh.AuthMethod { return ssh.PublicKeys(pk) } -func checkRequired(fs *flag.FlagSet) { - missing := map[string]bool{} - fs.VisitAll(func(f *flag.Flag) { missing[f.Name] = true }) - fs.Visit(func(f *flag.Flag) { delete(missing, f.Name) }) - for f := range missing { - log.Errorf("flag %q was unset but is required", f) - } - if len(missing) > 0 { - os.Exit(1) - } -} - var shutdownFuncs []func() func callOnShutdown(f func()) { shutdownFuncs = append(shutdownFuncs, f) } +const ignoreBotChar = "\xe2\x80\x8b" + +func runIRC(ctx context.Context, ircCfg irc.ClientConfig, sendMsg <-chan string) { + bo := backoffutil.NewDefaultBackOff() + ircCfg.Handler = irc.HandlerFunc(func(c *irc.Client, m *irc.Message) { + if m.Command == "NOTICE" && m.Prefix.Name == "NickServ" && strings.Contains(m.Trailing(), "dentified") { + // We're probably identified now, go join the channel. + c.Writef("JOIN %s", *ircChannel) + } + }) + for { + timer := time.NewTimer(bo.NextBackOff()) + select { + case <-ctx.Done(): + timer.Stop() + return + case <-timer.C: + break + } + + (func() { + connectedStart := time.Now() + ircConn, err := tls.Dial("tcp", *ircServer, nil) + if err != nil { + log.Errorf("connecting to IRC at tcp/%s: %v", *ircServer, err) + return + } + ircClient := irc.NewClient(ircConn, ircCfg) + ircClientCtx, cancel := context.WithCancel(ctx) + defer cancel() + go func() { + for { + select { + case <-ircClientCtx.Done(): + return + case msg := <-sendMsg: + log.Infof("sending message %q to %v", msg, *ircChannel) + ircClient.Writef("PRIVMSG %s :%s%s", *ircChannel, ignoreBotChar, msg) + } + } + }() + log.Infof("connecting to IRC on tcp/%s", *ircServer) + if err := ircClient.RunContext(ircClientCtx); err != nil { + connectedEnd := time.Now() + connectedFor := connectedEnd.Sub(connectedStart) + if connectedFor > 60*time.Second { + bo.Reset() + } + log.Errorf("IRC RunContext: %v", err) + return + } + })() + } +} + +func username(p gerritevents.PatchSet) string { + options := []string{ + p.Uploader.Username, + p.Uploader.Name, + p.Uploader.Email, + } + for _, opt := range options { + if opt != "" { + return opt + } + } + return "UNKNOWN USER" +} + +func patchSetURL(c gerritevents.Change, p gerritevents.PatchSet) string { + return fmt.Sprintf("https://cl.tvl.fyi/%d", c.Number) +} + func main() { flag.Parse() - checkRequired(required) + failed := false + if *gerritAuthUsername == "" { + log.Errorf("gerrit_ssh_auth_username must be set") + failed = true + } + if *gerritAuthKeyPath == "" { + log.Errorf("gerrit_ssh_auth_key must be set") + failed = true + } + if failed { + os.Exit(2) + } shutdownCh := make(chan os.Signal) signal.Notify(shutdownCh, os.Interrupt) go func() { <-shutdownCh - for n := len(shutdownFuncs); n >= 0; n-- { + signal.Reset(os.Interrupt) + for n := len(shutdownFuncs) - 1; n >= 0; n-- { shutdownFuncs[n]() } }() @@ -87,15 +173,47 @@ func main() { gw, err := gerrit.New(ctx, "tcp", *gerritAddr, cfg) if err != nil { - log.Errorf("gerrit.New(%q): %v", *gerritAddr, err) + log.Exitf("gerrit.New(%q): %v", *gerritAddr, err) } callOnShutdown(func() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() gw.Close(ctx) }) - for e := range gw.Events() { - log.Infof("hello: %v", spew.Sdump(e)) - } + sendMsgChan := make(chan string, 5) + go func() { + for e := range gw.Events() { + var parsedMsg string + switch e := e.(type) { + case *gerritevents.PatchSetCreated: + if e.Change.Project != "depot" || e.Change.Branch != "master" || e.PatchSet.Number != 1 { + continue + } + parsedMsg = fmt.Sprintf("CL/%d: %q proposed by %s - %s", e.Change.Number, e.Change.Subject, username(e.PatchSet), patchSetURL(e.Change, e.PatchSet)) + case *gerritevents.ChangeMerged: + if e.Change.Project != "depot" || e.Change.Branch != "master" { + continue + } + parsedMsg = fmt.Sprintf("CL/%d: %q submitted by %s - %s", e.Change.Number, e.Change.Subject, username(e.PatchSet), patchSetURL(e.Change, e.PatchSet)) + } + if parsedMsg != "" { + sendMsgChan <- parsedMsg + } + } + }() + + ircCtx, ircCancel := context.WithCancel(ctx) + callOnShutdown(ircCancel) + go runIRC(ircCtx, irc.ClientConfig{ + Nick: *ircNick, + User: *ircUser, + Name: *ircName, + Pass: *ircPassword, + + SendLimit: *ircSendLimit, + SendBurst: *ircSendBurst, + }, sendMsgChan) + + <-ctx.Done() } diff --git a/fun/clbot/default.nix b/fun/clbot/default.nix index 7a255f027a..e6b9c2fb9b 100644 --- a/fun/clbot/default.nix +++ b/fun/clbot/default.nix @@ -14,5 +14,6 @@ depot.nix.buildGo.program { gopkgs."github.com".davecgh.go-spew.spew.gopkg gopkgs."github.com".golang.glog.gopkg gopkgs."golang.org".x.crypto.ssh.gopkg + gopkgs."gopkg.in"."irc.v3".gopkg ]; } diff --git a/fun/clbot/go.mod b/fun/clbot/go.mod index f954c752cb..f1e366ba9d 100644 --- a/fun/clbot/go.mod +++ b/fun/clbot/go.mod @@ -4,8 +4,9 @@ go 1.14 require ( github.com/cenkalti/backoff/v4 v4.0.2 - github.com/davecgh/go-spew v1.1.1 + github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/google/go-cmp v0.4.1 golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 + gopkg.in/irc.v3 v3.1.3 ) diff --git a/fun/clbot/go.sum b/fun/clbot/go.sum index 0c7fb33cb9..d5434526e8 100644 --- a/fun/clbot/go.sum +++ b/fun/clbot/go.sum @@ -1,13 +1,17 @@ -github.com/cenkalti/backoff v1.1.0 h1:QnvVp8ikKCDWOsFheytRCoYWYPO/ObCTBGxT19Hc+yE= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs= github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -16,4 +20,12 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/irc.v3 v3.1.3 h1:yeTiJ365882L8h4AnBKYfesD92y5R5ZhGiylu9DfcPY= +gopkg.in/irc.v3 v3.1.3/go.mod h1:shO2gz8+PVeS+4E6GAny88Z0YVVQSxQghdrMVGQsR9s= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -- cgit 1.4.1