wurgurboo: convert into systemd service with short links

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld
2021-05-31 18:54:05 +02:00
parent b52ec14d2d
commit aeb33b00a3
3 changed files with 174 additions and 47 deletions

View File

@@ -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
```

View File

@@ -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)
}
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)
shortlink := NewShortLink(filepath.Join(os.Getenv("STATE_DIRECTORY"), "links.txt"))
http.HandleFunc("/l/", shortlink.HandleRequest)
listener, err := net.FileListener(os.Stdin)
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()
go func() {
err = http.Serve(listener, nil)
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]
}
log.Fatal(err)
}
}()
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)),
)
}
}()

120
cmd/wurgurboo/shortlink.go Normal file
View File

@@ -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
}
}