Blob


1 package main
3 import (
4 "crypto/tls"
5 "flag"
6 "fmt"
7 "io"
8 "log"
9 "net/http"
10 "os"
11 "path"
12 "path/filepath"
13 "regexp"
14 "strings"
16 "github.com/andybalholm/cascadia"
17 irc "github.com/fluffle/goirc/client"
18 "golang.org/x/net/html"
19 )
21 var (
22 baseurl = flag.String("baseurl", "gemini://m2i.omarpolo.com", "base url")
23 matrixOutDir = flag.String("matrix-out", "", "matrix out directory")
25 msgRe = regexp.MustCompile(`https://.*/[^\s]+\.(txt|png|jpg|jpeg|gif)`)
26 channel = "#gemini-it"
28 tooLongRe = regexp.MustCompile(`full message at (https://libera.ems.host/.*)[)]`)
30 httplink = regexp.MustCompile(`https?://[^\s)]+`)
31 gemlink = regexp.MustCompile(`gemini://[^\s)]+`)
32 )
34 func matrix2gemini(conn *irc.Conn, line *irc.Line) {
35 matches := msgRe.FindAllString(line.Text(), -1)
37 // it's not a good idea to defer inside a loop, but we know
38 // len(matches) is small (usually just 1). Morover, I like
39 // living in danger!
41 for _, link := range matches {
42 resp, err := http.Get(link)
43 if err != nil {
44 conn.Privmsg(
45 channel,
46 fmt.Sprintf("failed to download %q: %s", link, err),
47 )
48 continue
49 }
50 defer resp.Body.Close()
52 ext := path.Ext(link)
53 tmpfile, err := os.CreateTemp(*matrixOutDir, "message-*"+ext)
54 if err != nil {
55 conn.Privmsg(channel, fmt.Sprintf("failed to tmpfile: %s", err))
56 return
57 }
58 defer tmpfile.Close()
60 os.Chmod(tmpfile.Name(), 0644)
62 io.Copy(tmpfile, resp.Body)
64 conn.Privmsg(
65 channel,
66 fmt.Sprintf(
67 "better: %s/%s",
68 *baseurl,
69 filepath.Base(tmpfile.Name()),
70 ),
71 )
72 }
73 }
75 func messageTooLong(conn *irc.Conn, line *irc.Line) {
76 matches := tooLongRe.FindStringSubmatch(line.Text())
77 if len(matches) != 2 {
78 return
79 }
81 url := matches[1]
83 resp, err := http.Get(url)
84 if err != nil {
85 conn.Privmsg(
86 channel,
87 fmt.Sprintf("failed to download %q: %s", url, err),
88 )
89 return
90 }
91 defer resp.Body.Close()
93 sb := &strings.Builder{}
94 if _, err := io.Copy(sb, resp.Body); err != nil {
95 conn.Privmsg(
96 channel,
97 fmt.Sprintf("failed to read body of %q: %s", url, err),
98 )
99 return
102 conn.Privmsg(channel, fmt.Sprintf("%s ha detto:", line.Nick))
103 for _, line := range strings.Split(sb.String(), "\n") {
104 conn.Privmsg(
105 channel,
106 line,
111 func stringifyNode(node *html.Node) string {
112 s := ""
114 if node.Type == html.TextNode {
115 return node.Data
118 for child := node.FirstChild; child != nil; child = child.NextSibling {
119 s += stringifyNode(child)
122 return s
125 func wwwpagetitle(conn *irc.Conn, line *irc.Line) {
126 matches := httplink.FindAllString(line.Text(), -1)
128 for _, link := range matches {
129 log.Println("fetching", link, "...")
131 resp, err := http.Get(link)
132 if err != nil {
133 continue
135 defer resp.Body.Close()
137 doc, err := html.Parse(resp.Body)
138 if err != nil {
139 continue
142 sel := cascadia.MustCompile("head > title")
143 n := cascadia.Query(doc, sel)
145 if n == nil {
146 continue
149 title := stringifyNode(n)
150 if len(title) > 50 {
151 title = title[:50]
153 conn.Privmsg(channel, stringifyNode(n))
157 func gempagetitle(conn *irc.Conn, line *irc.Line) {
158 matches := gemlink.FindAllString(line.Text(), -1)
160 for _, link := range matches {
161 log.Println("fetching", link, "...")
162 title, err := geminiTitle(link)
163 if err != nil {
164 continue
167 conn.Privmsg(channel, title)
171 func pong(conn *irc.Conn, line *irc.Line) {
172 if line.Text() != ",ping" {
173 return
175 conn.Privmsg(
176 channel,
177 "pong :)",
181 func dostuff(conn *irc.Conn, line *irc.Line) {
182 matrix2gemini(conn, line)
183 messageTooLong(conn, line)
184 wwwpagetitle(conn, line)
185 gempagetitle(conn, line)
186 pong(conn, line)
187 // ...
190 func main() {
191 flag.Parse()
193 cfg := irc.NewConfig("gemitbot")
194 cfg.SSL = true
195 cfg.SSLConfig = &tls.Config{ServerName: "irc.libera.chat"}
196 cfg.Server = "irc.libera.chat:7000"
197 cfg.NewNick = func(n string) string { return n + "^" }
199 c := irc.Client(cfg)
201 c.HandleFunc(irc.CONNECTED, func(conn *irc.Conn, line *irc.Line) {
202 log.Println("connected, joining", channel)
203 conn.Join(channel)
204 })
206 c.HandleFunc(irc.PRIVMSG, dostuff)
207 c.HandleFunc(irc.ACTION, dostuff)
209 quit := make(chan bool)
211 c.HandleFunc(irc.DISCONNECTED, func(conn *irc.Conn, line *irc.Line) {
212 quit <- true
213 })
215 if err := c.Connect(); err != nil {
216 log.Fatalln("connection error:", err)
219 <-quit