package main
import (
"context"
"flag"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
"git.sr.ht/~handlerug/girc"
_ "github.com/mattn/go-sqlite3"
"mvdan.cc/xurls/v2"
"git.sr.ht/~handlerug/handlebot/database"
"git.sr.ht/~handlerug/handlebot/urlpreview"
"git.sr.ht/~handlerug/handlebot/weather"
"git.sr.ht/~handlerug/handlebot/wolframalpha"
"git.sr.ht/~handlerug/handlebot/youtube"
)
const BotName = "handlebot"
const BotVersion = "0.0.6"
const ForgeLink = "https://sr.ht/~handlerug/handlebot"
const UserAgent = BotName + "/" + BotVersion
var urlRegexp = xurls.Strict()
type userAgentTransport struct {
inner http.RoundTripper
userAgent string
}
func (u *userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", u.userAgent)
}
return u.inner.RoundTrip(req)
}
func main() {
cfgPath := flag.String("config", "/etc/wormy.json", "path to the config file")
flag.Parse()
cfg := parseConfig(*cfgPath)
log.Println("handlebot starting")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
db, err := database.NewSqliteDB(cfg.Database)
if err != nil {
log.Fatal(err)
}
version, newVersion, err := db.Migrate(ctx)
if err != nil {
log.Fatal("error migrating: ", err)
} else if newVersion != version {
log.Printf("migrated from version %d to %d", version, newVersion)
}
cancel()
var debugWriter io.Writer
if val, ok := os.LookupEnv("TRACE"); val != "" && ok {
debugWriter = os.Stdout
}
var (
previewer *urlpreview.Previewer
forecaster *weather.Forecaster
waClient *wolframalpha.Client
ytClient *youtube.Client
)
httpClient := &http.Client{
Transport: &userAgentTransport{
inner: http.DefaultTransport,
userAgent: UserAgent,
},
}
previewer = &urlpreview.Previewer{
HTTPClient: httpClient,
UAOverrides: cfg.UAOverrides,
}
if cfg.APIKeys.OpenWeatherMap != "" && cfg.APIKeys.MapQuest != "" {
forecaster = &weather.Forecaster{
Credentials: weather.APICredentials{
OpenWeatherMap: cfg.APIKeys.OpenWeatherMap,
MapQuest: cfg.APIKeys.MapQuest,
},
HTTPClient: httpClient,
}
}
if cfg.APIKeys.WolframAlpha != "" {
waClient = &wolframalpha.Client{
AppID: cfg.APIKeys.WolframAlpha,
HTTPClient: httpClient,
}
}
if cfg.APIKeys.YouTube != "" {
ytClient = &youtube.Client{
APIKey: cfg.APIKeys.YouTube,
HTTPClient: httpClient,
}
}
client := girc.New(girc.Config{
Server: cfg.IRC.Server,
ServerPass: cfg.IRC.Password,
SSL: cfg.IRC.TLS,
Port: cfg.IRC.Port,
Nick: cfg.IRC.Nick,
Name: cfg.IRC.Name,
User: cfg.IRC.User,
Debug: debugWriter,
RecoverFunc: girc.DefaultRecoverHandler,
AllowFlood: true,
})
h := NewHandlers(HandlersOpts{
Client: client,
ServerHost: cfg.IRC.Server,
CmdPrefix: cfg.CommandPrefix,
})
h.Add("weather", []string{"w"}, HandleWeather, `
Prints current weather. If location is omitted, the previous location is used.
Forecasts are not implemented (i.e. only the current weather is shown).
Examples: ".w amsterdam", ".w" (uses "amsterdam")
`)
h.Add("jisho", nil, handleJisho, `
Runs a Jisho.org search.
Examples: ".jisho himitsu", ".jisho 秘密"
`)
h.Add("wolframalpha", []string{"wa"}, handleWolframAlpha, `
Queries Wolfram|Alpha.
Examples: ".wa 2+2", ".wa circumference of the moon"
`)
h.Add("bots", nil, handleBots, "")
ctx = context.Background()
ctx = database.Context(ctx, db)
ctx = weather.Context(ctx, forecaster)
ctx = wolframalpha.Context(ctx, waClient)
ctx = youtube.Context(ctx, ytClient)
client.Handlers.Add(girc.CONNECTED, func(c *girc.Client, e girc.Event) {
log.Println("connected to IRC")
if cfg.Channel != "" {
c.Cmd.Join(cfg.Channel)
}
if len(cfg.Channels) > 0 {
for _, channel := range cfg.Channels {
c.Cmd.Join(channel)
}
}
})
client.Handlers.AddBg(girc.PRIVMSG, func(c *girc.Client, e girc.Event) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// Normal command dispatching
if h.Handle(ctx, e) {
return
}
// Ensure the message is not a command
if strings.HasPrefix(e.Last(), cfg.CommandPrefix) {
return
}
// In case no command matched the message, find URLs and parse them
matches := urlRegexp.FindAllString(e.Last(), -1)
var wg sync.WaitGroup
for _, match := range matches {
u, err := url.Parse(match)
if err != nil {
continue
}
wg.Add(1)
go func() {
defer wg.Done()
resp, err := previewer.Preview(ctx, u)
if err != nil {
log.Printf("error handling %q: %s", u.String(), err.Error())
} else if resp != "" {
c.Cmd.Replyf(e, "┗━ %s", ellipsize(resp, 430))
}
}()
}
wg.Wait()
})
for {
if err := client.Connect(); err != nil {
log.Printf("error connecting to IRC: %s", err)
log.Println("reconnecting in 30 seconds...")
time.Sleep(30 * time.Second)
} else {
log.Println("stopped")
return
}
}
}