feat(clbot): Add IRC support to the IRC bot.
Change-Id: I183488824882750c46e7216b98ab48e1d8f48096 Reviewed-on: https://cl.tvl.fyi/c/depot/+/343 Reviewed-by: eta <eta@theta.eu.org> Reviewed-by: tazjin <mail@tazj.in>
This commit is contained in:
parent
098bbb4d67
commit
b6ae678335
5 changed files with 170 additions and 26 deletions
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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=
|
||||
|
|
12
third_party/gopkgs/gopkg.in/irc.v3/default.nix
vendored
Normal file
12
third_party/gopkgs/gopkg.in/irc.v3/default.nix
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{ depot, ... }:
|
||||
|
||||
depot.buildGo.external {
|
||||
path = "gopkg.in/irc.v3";
|
||||
|
||||
src = depot.third_party.fetchFromGitHub {
|
||||
owner = "go-irc";
|
||||
repo = "irc";
|
||||
rev = "21a5301d6035ea204b2a7bb522a7b4598e5f6b28";
|
||||
sha256 = "1pi5y73pr4prhw5bvmp4babiw02nndizgmpksdgrrg28l9f2wm0n";
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue