From aeb33b00a3f7d77715795afd3b6eb9c797563acd Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Mon, 31 May 2021 18:54:05 +0200 Subject: [PATCH] wurgurboo: convert into systemd service with short links Signed-off-by: Jason A. Donenfeld --- README.md | 8 +++ cmd/wurgurboo/main.go | 93 ++++++++++++++-------------- cmd/wurgurboo/shortlink.go | 120 +++++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 47 deletions(-) create mode 100644 cmd/wurgurboo/shortlink.go diff --git a/README.md b/README.md index 2561abc..51cd34f 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,11 @@ This is best used with `+z` in a channel. It responds to all messages with a sta ```go go get golang.zx2c4.com/irc/cmd/irc-simple-responder ``` + +### `wurgurboo` - the "WurGurBoo" bot for `#wireguard` + +This polls for commits and does various things in the `#wireguard` channel. It's meant to be run as a [`go-web-service`](https://git.zx2c4.com/go-web-services/). + +```go +go get golang.zx2c4.com/irc/cmd/wurgurboo +``` diff --git a/cmd/wurgurboo/main.go b/cmd/wurgurboo/main.go index 6363784..999b84e 100644 --- a/cmd/wurgurboo/main.go +++ b/cmd/wurgurboo/main.go @@ -6,56 +6,32 @@ package main import ( - "bufio" - "flag" "fmt" "log" "net" + "net/http" "os" - "regexp" + "os/signal" + "path/filepath" + "syscall" "time" "golang.zx2c4.com/irc/hbot" ) func main() { - channelArg := flag.String("channel", "", "channel to join") - serverArg := flag.String("server", "", "server and port") - nickArg := flag.String("nick", "", "nickname") - passwordArg := flag.String("password-file", "", "optional file with password") - flag.Parse() - if matched, _ := regexp.MatchString(`^#[a-zA-Z0-9_-]+$`, *channelArg); !matched { - fmt.Fprintln(os.Stderr, "Invalid channel") - flag.Usage() - os.Exit(1) + shortlink := NewShortLink(filepath.Join(os.Getenv("STATE_DIRECTORY"), "links.txt")) + http.HandleFunc("/l/", shortlink.HandleRequest) + listener, err := net.FileListener(os.Stdin) + if err != nil { + log.Fatal(err) } - if _, _, err := net.SplitHostPort(*serverArg); err != nil { - fmt.Fprintln(os.Stderr, "Invalid server") - flag.Usage() - os.Exit(1) - } - if matched, _ := regexp.MatchString(`^[a-zA-Z0-9\[\]_-]{1,16}$`, *nickArg); !matched { - fmt.Fprintln(os.Stderr, "Invalid nick") - flag.Usage() - os.Exit(1) - } - password := "" - if len(*passwordArg) > 0 { - f, err := os.Open(*passwordArg) + go func() { + err = http.Serve(listener, nil) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to open password file: %v\n", err) - os.Exit(1) + log.Fatal(err) } - password, err = bufio.NewReader(f).ReadString('\n') - f.Close() - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to read password file: %v\n", err) - os.Exit(1) - } - if len(password) > 0 && password[len(password)-1] == '\n' { - password = password[:len(password)-1] - } - } + }() feeds := NewCgitFeedMonitorer(time.Second * 10) feeds.AddFeed("https://git.zx2c4.com/wireguard-linux/") @@ -72,25 +48,48 @@ func main() { feeds.AddFeed("https://git.zx2c4.com/wintun/") feeds.AddFeed("https://git.zx2c4.com/wg-dynamic/") + const channel = "#wireguard" bot := hbot.NewBot(&hbot.Config{ - Host: *serverArg, - Nick: *nickArg, + Host: "irc.libera.chat:6697", + Nick: "WurGurBoo", Realname: "Your Friendly Neighborhood WurGur Bot", - Channels: []string{*channelArg}, + Channels: []string{channel}, Logger: hbot.Logger{Verbosef: log.Printf, Errorf: log.Printf}, - Password: password, + Password: os.Getenv("WURGURBOO_PASSWORD"), }) - urlShortener := regexp.MustCompile(`(.*\?id=[a-f0-9]{8})([a-f0-9]+)$`) + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + go func() { + for range c { + bot.Close() + shortlink.SaveToDisk() + os.Exit(0) + } + }() + + type seenCommit struct { + repo string + subject string + } + seenCommits := make(map[seenCommit]string, 4096) + go func() { for commit := range feeds.Updates() { <-bot.Joined() - log.Printf("New commit %s in %s", commit.Commit.ID, commit.RepoTitle) - url := commit.Commit.Link.Href - if matches := urlShortener.FindStringSubmatch(url); len(matches) == 3 { - url = matches[1] + sc := seenCommit{commit.RepoTitle, commit.Commit.Title} + if short, ok := seenCommits[sc]; ok { + log.Printf("Updated commit %s in %s", commit.Commit.ID, commit.RepoTitle) + shortlink.Set(short, commit.Commit.Link.Href, true) + continue } - bot.Msg(*channelArg, fmt.Sprintf("\x01ACTION found a new commit in \x0303%s\x0f - \x0306%s\x0f - %s\x01", commit.RepoTitle, commit.Commit.Title, url)) + log.Printf("New commit %s in %s", commit.Commit.ID, commit.RepoTitle) + short := shortlink.New(commit.Commit.Link.Href) + seenCommits[sc] = short + bot.Msg(channel, fmt.Sprintf("\x01ACTION found a new commit in \x0303%s\x0f - \x0306%s\x0f - %s\x01", + sc.repo, sc.subject, + fmt.Sprintf("https://w-g.pw/l/%s", short)), + ) } }() diff --git a/cmd/wurgurboo/shortlink.go b/cmd/wurgurboo/shortlink.go new file mode 100644 index 0000000..af15064 --- /dev/null +++ b/cmd/wurgurboo/shortlink.go @@ -0,0 +1,120 @@ +/* SPDX-License-Identifier: GPL-2.0 + * + * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved. + */ + +package main + +import ( + "bufio" + "crypto/rand" + "encoding/base64" + "fmt" + "log" + "net/http" + "os" + "path" + "strings" + "sync" + "time" +) + +type ShortLink struct { + mu sync.RWMutex + diskCachePath string + links map[string]string + dirty bool + cacher *time.Timer +} + +func NewShortLink(diskCachePath string) *ShortLink { + sl := &ShortLink{ + diskCachePath: diskCachePath, + links: make(map[string]string, 4096), + } + f, err := os.Open(sl.diskCachePath) + if err == nil { + defer f.Close() + scanner := bufio.NewScanner(f) + for scanner.Scan() { + parts := strings.SplitN(scanner.Text(), "\t", 2) + if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 { + continue + } + sl.links[parts[0]] = parts[1] + } + } + return sl +} + +func (sl *ShortLink) Set(k, v string, overwrite bool) bool { + sl.mu.Lock() + defer sl.mu.Unlock() + if wantV, ok := sl.links[k]; ok && (!overwrite || wantV == v) { + return false + } + sl.links[k] = v + if !sl.dirty { + sl.dirty = true + time.AfterFunc(time.Second*10, sl.SaveToDisk) + } + return true +} + +func (sl *ShortLink) New(v string) string { + for { + var bytes [3]byte + _, err := rand.Read(bytes[:]) + if err != nil { + log.Panicf("RNG is broken: %v", err) + } + k := base64.RawURLEncoding.EncodeToString(bytes[:]) + sl.mu.RLock() + _, exists := sl.links[k] + sl.mu.RUnlock() + if !exists && sl.Set(k, v, false) { + return k + } + } +} + +func (sl *ShortLink) HandleRequest(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Server", "WurGurBoo/1.0") + target := "https://www.wireguard.com/" + wantL, key := path.Split(r.URL.Path) + if wantL == "/l/" { + sl.mu.RLock() + v, ok := sl.links[key] + sl.mu.RUnlock() + if ok { + target = v + } + } + http.Redirect(w, r, target, 302) +} + +func (sl *ShortLink) SaveToDisk() { + sl.mu.Lock() + dirty := sl.dirty + sl.dirty = false + sl.mu.Unlock() + if !dirty { + return + } + f, err := os.Create(sl.diskCachePath + ".tmp") + if err != nil { + log.Printf("Unable to backup short link db: %v", err) + return + } + defer f.Close() + sl.mu.RLock() + for k, v := range sl.links { + fmt.Fprintf(f, "%s\t%s\n", k, v) + } + sl.mu.RUnlock() + err = os.Rename(f.Name(), sl.diskCachePath) + if err != nil { + log.Printf("Unable to backup short link db: %v", err) + return + } +}