package main import ( "crypto/tls" "flag" "fmt" "io" "log" "net/http" "os" "path" "path/filepath" "regexp" "strings" "github.com/andybalholm/cascadia" irc "github.com/fluffle/goirc/client" "golang.org/x/net/html" ) var ( baseurl = flag.String("baseurl", "gemini://m2i.omarpolo.com", "base url") matrixOutDir = flag.String("matrix-out", "", "matrix out directory") msgRe = regexp.MustCompile(`https://.*/[^\s]+\.(txt|png|jpg|jpeg|gif)`) channel = "#gemini-it" tooLongRe = regexp.MustCompile(`full message at (https://libera.ems.host/.*)[)]`) httplink = regexp.MustCompile(`https?://[^\s)]+`) gemlink = regexp.MustCompile(`gemini://[^\s)]+`) ) func matrix2gemini(conn *irc.Conn, line *irc.Line) { matches := msgRe.FindAllString(line.Text(), -1) // it's not a good idea to defer inside a loop, but we know // len(matches) is small (usually just 1). Morover, I like // living in danger! for _, link := range matches { resp, err := http.Get(link) if err != nil { conn.Privmsg( channel, fmt.Sprintf("failed to download %q: %s", link, err), ) continue } defer resp.Body.Close() ext := path.Ext(link) tmpfile, err := os.CreateTemp(*matrixOutDir, "message-*"+ext) if err != nil { conn.Privmsg(channel, fmt.Sprintf("failed to tmpfile: %s", err)) return } defer tmpfile.Close() os.Chmod(tmpfile.Name(), 0644) io.Copy(tmpfile, resp.Body) conn.Privmsg( channel, fmt.Sprintf( "better: %s/%s", *baseurl, filepath.Base(tmpfile.Name()), ), ) } } func messageTooLong(conn *irc.Conn, line *irc.Line) { matches := tooLongRe.FindStringSubmatch(line.Text()) if len(matches) != 2 { return } url := matches[1] resp, err := http.Get(url) if err != nil { conn.Privmsg( channel, fmt.Sprintf("failed to download %q: %s", url, err), ) return } defer resp.Body.Close() sb := &strings.Builder{} if _, err := io.Copy(sb, resp.Body); err != nil { conn.Privmsg( channel, fmt.Sprintf("failed to read body of %q: %s", url, err), ) return } conn.Privmsg(channel, fmt.Sprintf("%s ha detto:", line.Nick)) for _, line := range strings.Split(sb.String(), "\n") { conn.Privmsg( channel, line, ) } } func stringifyNode(node *html.Node) string { s := "" if node.Type == html.TextNode { return node.Data } for child := node.FirstChild; child != nil; child = child.NextSibling { s += stringifyNode(child) } return s } func wwwpagetitle(conn *irc.Conn, line *irc.Line) { matches := httplink.FindAllString(line.Text(), -1) for _, link := range matches { log.Println("fetching", link, "...") resp, err := http.Get(link) if err != nil { continue } defer resp.Body.Close() doc, err := html.Parse(resp.Body) if err != nil { continue } sel := cascadia.MustCompile("head > title") n := cascadia.Query(doc, sel) if n == nil { continue } title := stringifyNode(n) if len(title) > 50 { title = title[:50] } conn.Privmsg(channel, stringifyNode(n)) } } func gempagetitle(conn *irc.Conn, line *irc.Line) { matches := gemlink.FindAllString(line.Text(), -1) for _, link := range matches { log.Println("fetching", link, "...") title, err := geminiTitle(link) if err != nil { continue } conn.Privmsg(channel, title) } } func pong(conn *irc.Conn, line *irc.Line) { if line.Text() != ",ping" { return } conn.Privmsg( channel, "pong :)", ) } func dostuff(conn *irc.Conn, line *irc.Line) { matrix2gemini(conn, line) messageTooLong(conn, line) wwwpagetitle(conn, line) gempagetitle(conn, line) pong(conn, line) // ... } func main() { flag.Parse() cfg := irc.NewConfig("gemitbot") cfg.SSL = true cfg.SSLConfig = &tls.Config{ServerName: "irc.libera.chat"} cfg.Server = "irc.libera.chat:7000" cfg.NewNick = func(n string) string { return n + "^" } c := irc.Client(cfg) c.HandleFunc(irc.CONNECTED, func(conn *irc.Conn, line *irc.Line) { log.Println("connected, joining", channel) conn.Join(channel) }) c.HandleFunc(irc.PRIVMSG, dostuff) c.HandleFunc(irc.ACTION, dostuff) quit := make(chan bool) c.HandleFunc(irc.DISCONNECTED, func(conn *irc.Conn, line *irc.Line) { quit <- true }) if err := c.Connect(); err != nil { log.Fatalln("connection error:", err) } <-quit }