Blob
Date:
Sat Jan 8 15:47:57 2022 UTC
Message:
fetch the title also for gemini URLs
package main
import (
"crypto/tls"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"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 := ioutil.TempFile(*matrixOutDir, "message-*"+ext)
if err != nil {
conn.Privmsg(channel, fmt.Sprintf("failed to tmpfile: %s", err))
return
}
defer tmpfile.Close()
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) {
log.Println("text is", line.Text())
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 dostuff(conn *irc.Conn, line *irc.Line) {
matrix2gemini(conn, line)
messageTooLong(conn, line)
wwwpagetitle(conn, line)
gempagetitle(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
}
Omar Polo