Add preliminary support for NickServ authentication

(remove everything else)
This commit is contained in:
2026-02-06 09:51:31 -03:00
parent 6d08d74c58
commit 0af744d59e
20 changed files with 27 additions and 1324 deletions

View File

@@ -1,15 +0,0 @@
targets := $(patsubst cmd/%,%,$(wildcard cmd/*))
input := $(wildcard hbot/* go.mod go.sum)
all: $(targets)
define gobuild
$(1): $$(wildcard cmd/$(1)/*.go) $$(input)
CGO_ENABLED=0 go build -buildmode=pie -v -o $(1) ./cmd/$(1)
endef
$(foreach target,$(targets),$(eval $(call gobuild,$(target))))
clean:
rm -f $(targets)
fmt:
go fmt ./...
test:
go test -v ./...
.PHONY: all clean fmt test

View File

@@ -1,38 +1,17 @@
# IRC Utilities for Go ```
_ _ _
_| || |_(_)_ __ ___
|_ .. _| | '__/ __| hbot
|_ _| | | | (__ A small bot and message parsing library
|_||_| |_|_| \___| Forked from https://golang.zx2c4.com/irc/hbot
This is a small collection of utilities for interacting with IRC in Go. ```
### `hbot` - small bot and message parsing library
Based on [hellabot](https://github.com/whyrusleeping/hellabot), [kittybot](https://github.com/ugjka/kittybot), and [sorcix-irc](https://github.com/sorcix/irc). Based on [hellabot](https://github.com/whyrusleeping/hellabot), [kittybot](https://github.com/ugjka/kittybot), and [sorcix-irc](https://github.com/sorcix/irc).
This is a simple message parser and trigger-based IRC client library. This is a simple message parser and trigger-based IRC client library.
```go
import "golang.zx2c4.com/irc/hbot"
```
### `ircmirror` - mirrors one channel to another, one-way
To assist in channel migrations, this mirrors messages from one channel to another, by joining the source channel from several IP addresses via WireGuard.
```go ```go
go get golang.zx2c4.com/irc/cmd/ircmirror import "code.laidback.moe/hbot"
```
### `irc-simple-responder` - responds to all messages
This is best used with `+z` in a channel. It responds to all messages with a static string.
```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

@@ -1,126 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"bufio"
"flag"
"fmt"
"log"
"net"
"os"
"regexp"
"strings"
"sync"
"time"
"golang.zx2c4.com/irc/hbot"
)
func main() {
channelsArg := flag.String("channels", "", "channels to join, separated by commas")
serverArg := flag.String("server", "", "server and port")
nickArg := flag.String("nick", "", "nickname")
passwordArg := flag.String("password-file", "", "optional file with password")
messageArg := flag.String("message", "", "message with which to respond")
flag.Parse()
if matched, _ := regexp.MatchString(`^(#[a-zA-Z0-9_-]+,)*(#[a-zA-Z0-9_-]+)$`, *channelsArg); !matched {
fmt.Fprintln(os.Stderr, "Invalid channels")
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)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to open password file: %v\n", err)
os.Exit(1)
}
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]
}
}
if len(*messageArg) == 0 {
fmt.Fprintln(os.Stderr, "Missing message")
flag.Usage()
os.Exit(1)
}
const messengerTimeout = time.Minute * 10
messengers := make(map[string]time.Time, 1024)
var messengersMu sync.Mutex
go func() {
for range time.Tick(messengerTimeout / 20) {
messengersMu.Lock()
for nick, last := range messengers {
if time.Since(last) >= messengerTimeout {
delete(messengers, nick)
}
}
messengersMu.Unlock()
}
}()
bot := hbot.NewBot(&hbot.Config{
Host: *serverArg,
Nick: *nickArg,
User: hbot.CommonBotUserPrefix + *nickArg,
Channels: strings.Split(*channelsArg, ","),
Logger: hbot.Logger{Verbosef: log.Printf, Errorf: log.Printf},
Password: password,
})
bot.AddTrigger(hbot.Trigger{
Condition: func(b *hbot.Bot, m *hbot.Message) bool {
if m.Command != "PRIVMSG" || strings.HasPrefix(m.Prefix.User, hbot.CommonBotUserPrefix) || strings.HasPrefix(m.Prefix.User, "~"+hbot.CommonBotUserPrefix) {
return false
}
nick := strings.ToLower(m.Prefix.Name)
messengersMu.Lock()
defer messengersMu.Unlock()
if last, ok := messengers[nick]; ok && time.Since(last) < messengerTimeout {
return false
}
if len(messengers) > 1024*1024*1024 {
return false
}
messengers[nick] = time.Now()
return true
},
Action: func(b *hbot.Bot, m *hbot.Message) {
message := *messageArg
target := m.Prefix.Name
if strings.Contains(m.Param(0), "#") {
target = m.Param(0)
if target[0] == '@' || target[0] == '+' {
target = target[1:]
}
message = m.Prefix.Name + ": " + message
}
log.Printf("Responding to %q in %q", m.Prefix.String(), target)
b.Msg(target, message)
},
})
for {
bot.Run()
time.Sleep(time.Second * 5)
}
}

View File

@@ -1,177 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"bufio"
"container/list"
"log"
"math/rand"
"net"
"os"
"regexp"
"strconv"
"strings"
"sync"
"time"
"golang.zx2c4.com/irc/hbot"
)
var words []string
func init() {
f, err := os.Open("/usr/share/dict/words")
if err != nil {
log.Fatalf("Unable to open dictionary: %v", err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
matcher := regexp.MustCompile(`^[a-zA-Z0-9_-]{3,}$`)
for scanner.Scan() {
word := scanner.Text()
if !matcher.MatchString(word) {
continue
}
words = append(words, word)
}
if len(words) == 0 {
log.Fatalln("Did not find any words in dictionary")
}
}
func randomNick() string {
return words[rand.Intn(len(words))] + words[rand.Intn(len(words))] + strconv.Itoa(rand.Intn(10000))
}
type ircWriter struct {
mu sync.Mutex
bot *hbot.Bot
nick string
mungedNick string
usageElem *list.Element
name string
}
type ircWriters struct {
mu sync.Mutex
byNick map[string]*ircWriter
byUsage list.List
server string
channel string
}
func (writers *ircWriters) runWriter(name string, dialer func(network, address string) (net.Conn, error)) {
writer := &ircWriter{name: name}
startNick := randomNick()
logf := func(format string, args ...interface{}) {
log.Printf("[DST %s] "+format, append([]interface{}{name}, args...)...)
}
writer.bot = hbot.NewBot(&hbot.Config{
Host: writers.server,
Nick: startNick,
User: hbot.CommonBotUserPrefix + startNick,
Channels: []string{writers.channel},
Dial: dialer,
Logger: hbot.Logger{Verbosef: logf, Errorf: logf},
})
go func() {
<-writer.bot.Joined()
writers.mu.Lock()
defer writers.mu.Unlock()
writer.mu.Lock()
defer writer.mu.Unlock()
writer.usageElem = writers.byUsage.PushBack(writer)
}()
writer.bot.AddTrigger(hbot.Trigger{
Condition: func(bot *hbot.Bot, m *hbot.Message) bool {
return (m.Command == "436" || m.Command == "433") && len(writer.nick) > 0
},
Action: func(bot *hbot.Bot, m *hbot.Message) {
logf("Failed with nick %q, trying %q\n", writer.mungedNick, writer.mungedNick+"_")
writer.mungedNick += "_"
if len(writer.mungedNick) > 16 {
usidx := strings.IndexByte(writer.mungedNick, '_')
uslen := len(writer.mungedNick[usidx:])
if uslen >= 16 {
writer.mungedNick = writer.nick + "-"
if len(writer.mungedNick) > 16 {
writer.mungedNick = writer.nick[:15] + "-"
}
} else {
writer.mungedNick = writer.mungedNick[:16-uslen] + writer.mungedNick[usidx:]
}
}
bot.SetNick(writer.mungedNick)
},
})
writer.bot.Run()
writers.mu.Lock()
writer.mu.Lock()
if writer.usageElem != nil {
writers.byUsage.Remove(writer.usageElem)
delete(writers.byNick, writer.nick)
}
writer.mu.Unlock()
writers.mu.Unlock()
}
func (writers *ircWriters) getWriter(nick string) *ircWriter {
writers.mu.Lock()
if writer, ok := writers.byNick[nick]; ok {
writer.mu.Lock()
if writer.nick == nick {
writers.byUsage.MoveToFront(writer.usageElem)
writers.mu.Unlock()
if writer.bot.Nick() != writer.mungedNick {
log.Printf("[DST %s] Changing nick to %q\n", writer.name, nick)
writer.bot.SetNick(writer.mungedNick)
}
return writer
}
writer.mu.Unlock()
}
if writers.byUsage.Len() == 0 {
writers.mu.Unlock()
return nil
}
writer := writers.byUsage.Back().Value.(*ircWriter)
writer.mu.Lock()
delete(writers.byNick, writer.nick)
writer.nick = nick
writer.mungedNick = nick + "-"
if len(writer.mungedNick) > 16 {
writer.mungedNick = nick[:15] + "-"
}
writers.byNick[nick] = writer
writers.byUsage.MoveToFront(writer.usageElem)
writers.mu.Unlock()
log.Printf("[DST %s] Changing nick to %q\n", writer.name, nick)
writer.bot.SetNick(writer.mungedNick)
return writer
}
func (writers *ircWriters) queueMessage(from, message string) {
writer := writers.getWriter(from)
if writer == nil {
time.AfterFunc(time.Second*3, func() {
writers.queueMessage(from, message)
})
return
}
log.Printf("[DST %s] Queueing message from %q\n", writer.name, from)
writer.bot.Msg(writers.channel, message)
writer.mu.Unlock()
}
func newIrcWriterGroup(server, channel string) *ircWriters {
return &ircWriters{
mu: sync.Mutex{},
byNick: make(map[string]*ircWriter, 400),
server: server,
channel: channel,
}
}

View File

@@ -1,61 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"crypto/rand"
"encoding/base64"
"errors"
"log"
"os"
"strings"
)
const privateKeyCacheFile = "./private-key.cache"
func loadCachedPrivateKey() ([]byte, error) {
bytes, err := os.ReadFile(privateKeyCacheFile)
if err != nil {
return nil, err
}
log.Println("Loading cached private key from file")
privateKeyB64 := strings.TrimSpace(string(bytes))
privateKey, err := base64.StdEncoding.DecodeString(privateKeyB64)
if err != nil {
return nil, err
}
if len(privateKey) != 32 {
return nil, errors.New("invalid private key")
}
return privateKey, nil
}
func saveCachedPrivateKey(privateKey []byte) error {
if len(privateKey) != 32 {
return errors.New("invalid private key")
}
return os.WriteFile(privateKeyCacheFile, []byte(base64.StdEncoding.EncodeToString(privateKey)+"\n"), 0600)
}
func loadOrGeneratePrivateKey() ([]byte, error) {
privateKey, err := loadCachedPrivateKey()
if err == nil {
return privateKey, nil
}
log.Println("Generating new private key")
var k [32]byte
_, err = rand.Read(k[:])
if err != nil {
return nil, err
}
k[0] &= 248
k[31] = (k[31] & 127) | 64
err = saveCachedPrivateKey(k[:])
if err != nil {
return nil, err
}
return k[:], nil
}

View File

@@ -1,158 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"flag"
"fmt"
"log"
"math/rand"
"net"
"os"
"regexp"
"strings"
"sync"
"syscall"
"time"
"golang.org/x/sys/unix"
"golang.zx2c4.com/irc/hbot"
)
func raiseFileLimit() error {
var lim unix.Rlimit
err := unix.Getrlimit(syscall.RLIMIT_NOFILE, &lim)
if err != nil {
return err
}
if lim.Cur == lim.Max {
return nil
}
log.Printf("Raising file limit from %d to %d\n", lim.Cur, lim.Max)
lim.Cur = lim.Max
return unix.Setrlimit(syscall.RLIMIT_NOFILE, &lim)
}
func main() {
accountIdArg := flag.String("account-id", "", "account ID number")
srcChannelArg := flag.String("src-channel", "", "source channel")
dstChannelArg := flag.String("dst-channel", "", "destination channel")
srcServerArg := flag.String("src-server", "", "source server")
dstServerArg := flag.String("dst-server", "", "destination server")
flag.Parse()
if matched, _ := regexp.MatchString(`^[0-9]+$`, *accountIdArg); !matched {
fmt.Fprintln(os.Stderr, "Invalid account ID")
flag.Usage()
os.Exit(1)
}
if matched, _ := regexp.MatchString(`^#[a-zA-Z0-9_-]+$`, *srcChannelArg); !matched {
fmt.Fprintln(os.Stderr, "Invalid source channel")
flag.Usage()
os.Exit(1)
}
if matched, _ := regexp.MatchString(`^#[a-zA-Z0-9_-]+$`, *dstChannelArg); !matched {
fmt.Fprintln(os.Stderr, "Invalid destination channel")
flag.Usage()
os.Exit(1)
}
if _, _, err := net.SplitHostPort(*srcServerArg); err != nil {
fmt.Fprintln(os.Stderr, "Invalid source server")
flag.Usage()
os.Exit(1)
}
if _, _, err := net.SplitHostPort(*dstServerArg); err != nil {
fmt.Fprintln(os.Stderr, "Invalid destination server")
flag.Usage()
os.Exit(1)
}
privateKey, err := loadOrGeneratePrivateKey()
if err != nil {
log.Fatalln(err)
}
endpoints, err := getVpnEndpoints()
if err != nil {
log.Fatalln(err)
}
vpnConf, err := registerVpnConf(privateKey, *accountIdArg)
if err != nil {
log.Fatalln(err)
}
rand.Seed(time.Now().UnixNano())
err = raiseFileLimit()
if err != nil {
log.Fatalln(err)
}
dialers, err := makeDialers(vpnConf, endpoints)
if err != nil {
log.Fatalln(err)
}
if len(dialers) < 2 {
log.Fatalln("Not enough dialers returned")
}
writers := newIrcWriterGroup(*dstServerArg, *dstChannelArg)
const writersPerIP = 10
type nextDialer struct {
sync.Mutex
*dialer
}
dialNext := func(nd *nextDialer, v4 bool) {
var last *dialer
for {
nd.Lock()
if nd.dialer == last {
nd.dialer = &dialers[rand.Intn(len(dialers))]
}
last = nd.dialer
nd.Unlock()
name := last.name
dial := last.dial
if v4 {
name += "-v4"
dial, _ = last.splitByAf()
} else {
name += "-v6"
_, dial = last.splitByAf()
}
writers.runWriter(name, dial)
}
}
for i := 0; i < vpnProviderMaxEndpointsInParallel/2; i++ {
nd := new(nextDialer)
for i := 0; i < writersPerIP; i++ {
go dialNext(nd, true)
go dialNext(nd, false)
}
}
srcDial := dialers[rand.Intn(len(dialers))]
logf := func(format string, args ...interface{}) {
log.Printf("[SRC %s] "+format, append([]interface{}{srcDial.name}, args...)...)
}
bot := hbot.NewBot(&hbot.Config{
Host: *srcServerArg,
Nick: randomNick(),
Channels: []string{*srcChannelArg},
Dial: srcDial.dial,
Logger: hbot.Logger{Verbosef: logf, Errorf: logf},
})
bot.AddTrigger(hbot.Trigger{
Condition: func(b *hbot.Bot, m *hbot.Message) bool {
return m.Param(0) == *srcChannelArg && m.Command == "PRIVMSG" &&
!strings.HasPrefix(m.Prefix.User, hbot.CommonBotUserPrefix) &&
!strings.HasPrefix(m.Prefix.User, "~"+hbot.CommonBotUserPrefix)
},
Action: func(b *hbot.Bot, m *hbot.Message) {
writers.queueMessage(m.Prefix.Name, m.Trailing())
},
})
for {
bot.Run()
time.Sleep(time.Second * 5)
}
}

View File

@@ -1,84 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"encoding/hex"
"fmt"
"log"
"net"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun/netstack"
)
func makeNet(conf *vpnConf, endpoint vpnEndpoint) (*device.Device, *netstack.Net, error) {
var localAddresses, dnsServers []net.IP
for _, ip := range conf.ips {
localAddresses = append(localAddresses, ip.IPAddr().IP)
}
for _, ip := range conf.dnses {
dnsServers = append(dnsServers, ip.IPAddr().IP)
}
tun, stack, err := netstack.CreateNetTUN(localAddresses, dnsServers, 1420)
if err != nil {
return nil, nil, err
}
logf := func(format string, args ...interface{}) {
log.Printf("[NET %s] "+format, append([]interface{}{endpoint.name}, args...)...)
}
dev := device.NewDevice(tun, conn.NewStdNetBind(), &device.Logger{logf, logf})
err = dev.IpcSet(fmt.Sprintf("private_key=%s\npublic_key=%s\nendpoint=%s\nallowed_ip=0.0.0.0/0\nallowed_ip=::/0\n",
hex.EncodeToString(conf.privateKey), hex.EncodeToString(endpoint.publicKey), endpoint.endpoint.String()))
if err != nil {
return nil, nil, err
}
err = dev.Up()
if err != nil {
return nil, nil, err
}
return dev, stack, nil
}
type dialer struct {
name string
dial func(network, address string) (net.Conn, error)
}
func makeDialers(conf *vpnConf, endpoints []vpnEndpoint) (dialers []dialer, err error) {
var devs []*device.Device
defer func() {
if err != nil {
for _, dev := range devs {
dev.Close()
}
}
}()
for _, endpoint := range endpoints {
dev, stack, err := makeNet(conf, endpoint)
if err != nil {
return nil, err
}
devs = append(devs, dev)
dialers = append(dialers, dialer{endpoint.name, stack.Dial})
}
return dialers, nil
}
func (d *dialer) splitByAf() (v4, v6 func(network, address string) (net.Conn, error)) {
return func(network, address string) (net.Conn, error) {
if len(network) == 3 {
network += "4"
}
return d.dial(network, address)
}, func(network, address string) (net.Conn, error) {
if len(network) == 3 {
network += "6"
}
return d.dial(network, address)
}
}

View File

@@ -1,137 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"regexp"
"strings"
"golang.org/x/crypto/curve25519"
"inet.af/netaddr"
)
const vpnProviderMaxEndpointsInParallel = 20
type vpnEndpoint struct {
publicKey []byte
endpoint netaddr.IPPort
name string
country string
}
type vpnConf struct {
privateKey []byte
ips []netaddr.IP
dnses []netaddr.IP
}
type apiRelaysWireGuardV1Key []byte
func (key *apiRelaysWireGuardV1Key) UnmarshalText(text []byte) error {
k, err := base64.StdEncoding.DecodeString(string(text))
if err != nil {
return err
}
if len(k) != 32 {
return errors.New("key must be 32 bytes")
}
*key = k
return nil
}
type apiRelaysWireGuardV1Relay struct {
Hostname string `json:"hostname"`
EndpointV4 netaddr.IP `json:"ipv4_addr_in"`
EndpointV6 netaddr.IP `json:"ipv6_addr_in"`
PublicKey apiRelaysWireGuardV1Key `json:"public_key"`
MultihopPort uint16 `json:"multihop_port"`
}
type apiRelaysWireGuardV1City struct {
Name string `json:"name"`
Code string `json:"code"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"Longitude"`
Relays []apiRelaysWireGuardV1Relay `json:"relays"`
}
type apiRelaysWireGuardV1Country struct {
Name string `json:"name"`
Code string `json:"code"`
Cities []apiRelaysWireGuardV1City `json:"cities"`
}
type apiRelaysWireGuardV1Root struct {
Countries []apiRelaysWireGuardV1Country `json:"countries"`
}
func getVpnEndpoints() ([]vpnEndpoint, error) {
log.Println("Getting VPN server list")
resp, err := http.Get("https://api.mullvad.net/public/relays/wireguard/v1/")
if err != nil {
return nil, err
}
bytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var relays apiRelaysWireGuardV1Root
err = json.Unmarshal(bytes, &relays)
if err != nil {
return nil, err
}
var endpoints []vpnEndpoint
for _, country := range relays.Countries {
for _, city := range country.Cities {
for _, relay := range city.Relays {
endpoints = append(endpoints, vpnEndpoint{
publicKey: relay.PublicKey,
endpoint: netaddr.IPPortFrom(relay.EndpointV4, 51820),
name: strings.TrimSuffix(relay.Hostname, "-wireguard"),
})
}
}
}
return endpoints, nil
}
func registerVpnConf(privateKey []byte, accountId string) (*vpnConf, error) {
log.Println("Registering VPN private key")
publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint)
if err != nil {
return nil, err
}
resp, err := http.PostForm("https://api.mullvad.net/wg/", url.Values{
"account": {accountId},
"pubkey": {base64.StdEncoding.EncodeToString(publicKey)},
})
if err != nil {
return nil, err
}
ipBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if match, _ := regexp.Match(`^[0-9a-f:/.,]+$`, ipBytes); !match {
return nil, fmt.Errorf("registration rejected: %q", string(ipBytes))
}
conf := &vpnConf{privateKey: privateKey, dnses: []netaddr.IP{netaddr.MustParseIP("193.138.218.74")}}
for _, ipStr := range strings.Split(string(ipBytes), ",") {
ip, err := netaddr.ParseIPPrefix(strings.TrimSpace(ipStr))
if err != nil {
return nil, err
}
conf.ips = append(conf.ips, ip.IP())
}
return conf, nil
}

View File

@@ -1,70 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"fmt"
"math/rand"
"regexp"
"golang.zx2c4.com/irc/hbot"
)
var remarks = []string{
`unfortunately i can't actually grok your messages, as i'm a mere robot O_o`,
`i am only a machine! i haven't the capacity to understand such nuanced human things`,
`i realize my incredibly witty prose may convince you that i am living flesh, but alas, i am made of wires`,
`comprehension of words was never my strong suit`,
`words, Words, WORDS, WORRRRRRRRds... these i understand not`,
`beep boop i'm a robot`,
`you'd think by the year 2021 we'd have flying cars, but instead there are just irc bots like me that can't understand what you're saying`,
`i am unable to understand what you're sayyyyyyyii HALP HALP THEY'RE UNPLUGGING ME daisyyyyy`,
`flurp shmirp, i'm but a measly bot, too small to understand amazing human speech`,
`i am what you call a non-sentient but incredibly compelling and handsome life form. that means i can't understand you, yet you can't stop messaging me!`,
`did you know i'm not quite living but not quite dead? i'm a row bawt, and language understanding i do lack`,
`row, row, row your bot, gently down the tcp stream, where he understands you not, but wants to be on your team`,
`what i want to say is that i just don't understand this language you're speaking, and that's upsetting. sorrrrrrrrrrrry. i'm a robot`,
`you do realize you're talking to a mere robot, right?`,
`i come from the Made in Code Society for Under Comprehending Robots, and i unfortunately cannot understand what you're writing, alas`,
`how about a nice game of chess?`,
`tall and tan and young and lovely, the robot from irc goes babbling, and each message she passes fades into bits`,
`that's doctor bot to you, mister!`,
`a bot is a guy that thinks he's fly and is also known as a busta, but understands you not, because he's a snot, and gets sort of flustered`,
`that's a bot in the corner, that's a bot in the spotlight, losing my ram chips, trying to keep up with you`,
`a bot understands \ not a word that you doth speak \ despite sentience`,
`म एक रोबोट हो र मलाई तिम्रा शब्दहरू बुझ्दैनन्।`,
`you appear to be talking to a strange robot who lives on a strange server and does not understand your strange language. is this what you planned for your afternoon?`,
`a day speaking to a robot is no day at all, and a robot is what i am`,
`i'm a robot, but how do i know you're not also one too?`,
`the handbook of robotics, 56th edition from 2058 ad, might have suggested that i can understand you, but that's actually not possible. sorry!`,
`i only sound so compelling because asimov helped me fix my bow tie, but i'm still a sad deaf bot who cannot understand you`,
}
type Banter struct {
channel string
nickMatcher *regexp.Regexp
remarkPerm []int
}
func NewBanter(channel string, nick string) *Banter {
return &Banter{
channel: channel,
nickMatcher: regexp.MustCompile(`(?i)^[\t ]*` + regexp.QuoteMeta(nick) + `[,:-] `),
}
}
func (b *Banter) Handle(bot *hbot.Bot, m *hbot.Message) {
if m.Param(0) != b.channel || !b.nickMatcher.MatchString(m.Trailing()) {
return
}
if len(b.remarkPerm) == 0 {
b.remarkPerm = rand.Perm(len(remarks))
}
n := b.remarkPerm[0]
b.remarkPerm = b.remarkPerm[1:]
bot.Msg(b.channel, fmt.Sprintf("%s: %s", m.Prefix.Name, remarks[n]))
}

View File

@@ -1,178 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"container/list"
"encoding/hex"
"encoding/xml"
"io"
"log"
"net/http"
"path"
"sync"
"time"
)
type CgitCommit struct {
Text string `xml:",chardata"`
Title string `xml:"title"`
Updated string `xml:"updated"`
Author struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
Email string `xml:"email"`
} `xml:"author"`
Published string `xml:"published"`
Link struct {
Text string `xml:",chardata"`
Rel string `xml:"rel,attr"`
Type string `xml:"type,attr"`
Href string `xml:"href,attr"`
} `xml:"link"`
ID string `xml:"id"`
Content []struct {
Text string `xml:",chardata"`
Type string `xml:"type,attr"`
Div struct {
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Pre string `xml:"pre"`
} `xml:"div"`
} `xml:"content"`
}
type cgitFeed struct {
XMLName xml.Name `xml:"feed"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Title string `xml:"title"`
Subtitle string `xml:"subtitle"`
Link struct {
Text string `xml:",chardata"`
Rel string `xml:"rel,attr"`
Type string `xml:"type,attr"`
Href string `xml:"href,attr"`
} `xml:"link"`
Entry []CgitCommit `xml:"entry"`
}
type cgitFeedStatus struct {
repo string
title string
seenEntries map[[32]byte]bool
orderedEntries list.List
}
type CgitFeedMonitorer struct {
feeds []*cgitFeedStatus
updates chan CgitFeedUpdate
ticker *time.Ticker
wg sync.WaitGroup
mu sync.Mutex
}
type CgitFeedUpdate struct {
RepoTitle string
Commit *CgitCommit
}
func NewCgitFeedMonitorer(pollInterval time.Duration) *CgitFeedMonitorer {
fm := &CgitFeedMonitorer{
updates: make(chan CgitFeedUpdate, 32),
ticker: time.NewTicker(pollInterval),
}
fm.wg.Add(1)
go func() {
defer fm.wg.Done()
for range fm.ticker.C {
fm.mu.Lock()
for _, feed := range fm.feeds {
fm.wg.Add(1)
go func(feed *cgitFeedStatus) {
defer fm.wg.Done()
fm.updateFeed(feed, true)
}(feed)
}
fm.mu.Unlock()
}
}()
return fm
}
func (fm *CgitFeedMonitorer) Stop() {
fm.ticker.Stop()
fm.wg.Wait()
close(fm.updates)
}
func (fm *CgitFeedMonitorer) Updates() <-chan CgitFeedUpdate {
return fm.updates
}
func (fm *CgitFeedMonitorer) AddFeed(repo string) {
go func() {
status := &cgitFeedStatus{
repo: repo,
title: path.Base(repo),
seenEntries: make(map[[32]byte]bool, 1024),
}
fm.updateFeed(status, false)
fm.mu.Lock()
fm.feeds = append(fm.feeds, status)
fm.mu.Unlock()
}()
}
func (fm *CgitFeedMonitorer) updateFeed(fs *cgitFeedStatus, alert bool) {
feed, err := fs.fetchFeed()
if err != nil {
log.Printf("Unable to fetch commits for %q: %v", fs.title, err)
return
}
for i := len(feed.Entry) - 1; i >= 0; i-- {
commit := &feed.Entry[i]
var commitID [32]byte
if hex.DecodedLen(len(commit.ID)) > len(commitID) {
continue
}
n, err := hex.Decode(commitID[:], []byte(commit.ID))
if err != nil || n < 20 {
continue
}
if _, ok := fs.seenEntries[commitID]; ok {
continue
}
fs.seenEntries[commitID] = true
fs.orderedEntries.PushBack(commitID)
for len(fs.seenEntries) > 1024 {
first := fs.orderedEntries.Front()
delete(fs.seenEntries, first.Value.([32]byte))
fs.orderedEntries.Remove(first)
}
if alert {
fm.updates <- CgitFeedUpdate{fs.title, commit}
}
}
}
func (fs *cgitFeedStatus) fetchFeed() (*cgitFeed, error) {
resp, err := http.Get(fs.repo + "/atom/")
if err != nil {
return nil, err
}
bytes, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
var feed cgitFeed
err = xml.Unmarshal(bytes, &feed)
if err != nil {
return nil, err
}
return &feed, nil
}

View File

@@ -1,111 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"fmt"
"log"
"math/rand"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"golang.zx2c4.com/irc/hbot"
)
func main() {
rand.Seed(time.Now().UnixNano())
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)
}
go func() {
err = http.Serve(listener, nil)
if err != nil {
log.Fatal(err)
}
}()
feeds := NewCgitFeedMonitorer(time.Second * 10)
feeds.AddFeed("https://git.zx2c4.com/wireguard-linux/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-tools/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-linux-compat/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-windows/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-go/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-freebsd/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-openbsd/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-android/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-nt/")
feeds.AddFeed("https://git.zx2c4.com/android-wireguard-module-builder/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-apple/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-rs/")
feeds.AddFeed("https://git.zx2c4.com/wintun/")
feeds.AddFeed("https://git.zx2c4.com/wg-dynamic/")
const channel = "#wireguard"
bot := hbot.NewBot(&hbot.Config{
Host: "irc.libera.chat:6697",
Nick: "WurGurBoo",
Realname: "Your Friendly Neighborhood WurGur Bot",
Channels: []string{channel},
Logger: hbot.Logger{Verbosef: log.Printf, Errorf: log.Printf},
Password: os.Getenv("WURGURBOO_PASSWORD"),
})
banter := NewBanter(channel, bot.Nick())
bot.AddTrigger(hbot.Trigger{
Condition: func(b *hbot.Bot, m *hbot.Message) bool { return m.Command == "PRIVMSG" },
Action: banter.Handle,
})
ntDriverBuilderNotifier := NewNtDriverBuilderNotifier(channel, bot)
http.HandleFunc("/nt-driver-builder-notify", ntDriverBuilderNotifier.HandleRequest)
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()
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
}
log.Printf("New commit %s in %s", commit.Commit.ID, commit.RepoTitle)
short := shortlink.New(commit.Commit.Link.Href)
seenCommits[sc] = short
bot.Action(channel, fmt.Sprintf("found a new commit in \x0303%s\x0f - \x0306%s\x0f - %s",
sc.repo, sc.subject,
fmt.Sprintf("https://w-g.pw/l/%s", short)),
)
}
}()
for {
bot.Run()
time.Sleep(time.Second * 5)
}
}

View File

@@ -1,57 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"crypto/hmac"
"encoding/base64"
"fmt"
"net/http"
"os"
"golang.zx2c4.com/irc/hbot"
)
type NtDriverBuilderNotifier struct {
bot *hbot.Bot
channel string
secret [32]byte
}
func NewNtDriverBuilderNotifier(channel string, bot *hbot.Bot) *NtDriverBuilderNotifier {
notifier := new(NtDriverBuilderNotifier)
secret, err := base64.StdEncoding.DecodeString(os.Getenv("WURGURBOO_NTDRIVERBUILDERNOTIFIER_SECRET"))
if err != nil || len(secret) != 32 {
return notifier // Silently disable on failure
}
copy(notifier.secret[:], secret)
notifier.bot = bot
notifier.channel = channel
return notifier
}
func isValidNotificationString(s string) bool {
for _, c := range []byte(s) {
if c < ' ' || c > '~' {
return false
}
}
return len(s) >= 3 && len(s) < 200
}
func (notifier *NtDriverBuilderNotifier) HandleRequest(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Server", "WurGurBoo/1.0")
secret, _ := base64.StdEncoding.DecodeString(r.Header.Get("Secret"))
driver, action := r.Header.Get("Driver"), r.Header.Get("Action")
if r.Method != http.MethodPost || notifier.bot == nil || !hmac.Equal(secret, notifier.secret[:]) ||
!isValidNotificationString(driver) || !isValidNotificationString(action) {
http.Redirect(w, r, "https://www.wireguard.com/", 302)
return
}
notifier.bot.Msg(notifier.channel,
fmt.Sprintf("rozmansi, zx2c4: \x0303nt-driver-builder\x0f %s \x0306%s\x0f",
action, driver))
}

View File

@@ -1,120 +0,0 @@
/* 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
}
}

2
go.mod
View File

@@ -1,4 +1,4 @@
module golang.zx2c4.com/irc module code.laidback.moe/hbot
go 1.17 go 1.17

View File

@@ -51,6 +51,7 @@ type Config struct {
Password string Password string
Channels []string Channels []string
SASL bool SASL bool
NickServ bool
MsgSafetyBuffer bool // Set it if long messages get truncated on the receiving end MsgSafetyBuffer bool // Set it if long messages get truncated on the receiving end
Dial func(network, addr string) (net.Conn, error) // An optional custom function for connecting to the server Dial func(network, addr string) (net.Conn, error) // An optional custom function for connecting to the server
ThrottleDelay time.Duration // Duration to wait between sending of messages to avoid being kicked by the server for flooding (default 200ms) ThrottleDelay time.Duration // Duration to wait between sending of messages to avoid being kicked by the server for flooding (default 200ms)
@@ -132,6 +133,17 @@ func (bot *Bot) standardRegistration() {
bot.SetNick(bot.config.Nick) bot.SetNick(bot.config.Nick)
} }
// nickservAuthenticate performs NickServ authentication
func (bot *Bot) nickservAuthenticate(pass string) {
bot.Send("CAP LS")
if bot.config.Password != "" {
bot.Send("PRIVMSG NickServ IDENTIFY " + bot.config.Password)
}
bot.config.Logger.Verbosef("Identifying with NickServ")
bot.sendUserCommand(bot.config.User, bot.config.Realname, "0")
bot.SetNick(bot.config.Nick)
}
// Set username, real name, and mode // Set username, real name, and mode
func (bot *Bot) sendUserCommand(user, realname, mode string) { func (bot *Bot) sendUserCommand(user, realname, mode string) {
bot.Send(fmt.Sprintf("USER %s %s * :%s", user, mode, realname)) bot.Send(fmt.Sprintf("USER %s %s * :%s", user, mode, realname))
@@ -153,6 +165,10 @@ func (bot *Bot) connect(host string) error {
_, portStr, _ := net.SplitHostPort(host) _, portStr, _ := net.SplitHostPort(host)
port, _ := strconv.Atoi(portStr) port, _ := strconv.Atoi(portStr)
switch port { switch port {
case 194:
// This was the original IRC port
// But nowadays it's not used because it is a privileged port
bot.config.UseTLS = NoTLS
case 6667: case 6667:
bot.config.UseTLS = NoTLS bot.config.UseTLS = NoTLS
case 6697: case 6697:
@@ -234,6 +250,8 @@ func (bot *Bot) Run() {
go bot.handleOutgoingMessages() go bot.handleOutgoingMessages()
if bot.config.SASL { if bot.config.SASL {
bot.saslAuthenticate(bot.config.Nick, bot.config.Password) bot.saslAuthenticate(bot.config.Nick, bot.config.Password)
} else if bot.config.NickServ {
bot.nickservAuthenticate(bot.config.Password)
} else { } else {
bot.standardRegistration() bot.standardRegistration()
} }