Blob


1 package main
3 import (
4 "crypto/tls"
5 "flag"
6 "fmt"
7 "io"
8 "io/ioutil"
9 "log"
10 "net/http"
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 )
33 func matrix2gemini(conn *irc.Conn, line *irc.Line) {
34 matches := msgRe.FindAllString(line.Text(), -1)
36 // it's not a good idea to defer inside a loop, but we know
37 // len(matches) is small (usually just 1). Morover, I like
38 // living in danger!
40 for _, link := range matches {
41 resp, err := http.Get(link)
42 if err != nil {
43 conn.Privmsg(
44 channel,
45 fmt.Sprintf("failed to download %q: %s", link, err),
46 )
47 continue
48 }
49 defer resp.Body.Close()
51 ext := path.Ext(link)
52 tmpfile, err := ioutil.TempFile(*matrixOutDir, "message-*"+ext)
53 if err != nil {
54 conn.Privmsg(channel, fmt.Sprintf("failed to tmpfile: %s", err))
55 return
56 }
57 defer tmpfile.Close()
59 io.Copy(tmpfile, resp.Body)
61 conn.Privmsg(
62 channel,
63 fmt.Sprintf(
64 "better: %s/%s",
65 *baseurl,
66 filepath.Base(tmpfile.Name()),
67 ),
68 )
69 }
70 }
72 func messageTooLong(conn *irc.Conn, line *irc.Line) {
73 matches := tooLongRe.FindStringSubmatch(line.Text())
74 if len(matches) != 2 {
75 return
76 }
78 url := matches[1]
80 resp, err := http.Get(url)
81 if err != nil {
82 conn.Privmsg(
83 channel,
84 fmt.Sprintf("failed to download %q: %s", url, err),
85 )
86 return
87 }
88 defer resp.Body.Close()
90 sb := &strings.Builder{}
91 if _, err := io.Copy(sb, resp.Body); err != nil {
92 conn.Privmsg(
93 channel,
94 fmt.Sprintf("failed to read body of %q: %s", url, err),
95 )
96 return
97 }
99 conn.Privmsg(channel, fmt.Sprintf("%s ha detto:", line.Nick))
100 for _, line := range strings.Split(sb.String(), "\n") {
101 conn.Privmsg(
102 channel,
103 line,
108 func stringifyNode(node *html.Node) string {
109 s := ""
111 if node.Type == html.TextNode {
112 return node.Data
115 for child := node.FirstChild; child != nil; child = child.NextSibling {
116 s += stringifyNode(child)
119 return s
122 func pagetitle(conn *irc.Conn, line *irc.Line) {
123 log.Println("text is", line.Text())
124 matches := httplink.FindAllString(line.Text(), -1)
126 for _, link := range matches {
127 log.Println("fetching", link, "...")
129 resp, err := http.Get(link)
130 if err != nil {
131 continue
133 defer resp.Body.Close()
135 doc, err := html.Parse(resp.Body)
136 if err != nil {
137 continue
140 sel := cascadia.MustCompile("head > title")
141 n := cascadia.Query(doc, sel)
143 if n == nil {
144 continue
147 title := stringifyNode(n)
148 if len(title) > 50 {
149 title = title[:50]
151 conn.Privmsg(channel, stringifyNode(n))
155 func dostuff(conn *irc.Conn, line *irc.Line) {
156 matrix2gemini(conn, line)
157 messageTooLong(conn, line)
158 pagetitle(conn, line)
159 // ...
162 func main() {
163 flag.Parse()
165 cfg := irc.NewConfig("gemitbot")
166 cfg.SSL = true
167 cfg.SSLConfig = &tls.Config{ServerName: "irc.libera.chat"}
168 cfg.Server = "irc.libera.chat:7000"
169 cfg.NewNick = func(n string) string { return n + "^" }
171 c := irc.Client(cfg)
173 c.HandleFunc(irc.CONNECTED, func(conn *irc.Conn, line *irc.Line) {
174 log.Println("connected, joining", channel)
175 conn.Join(channel)
176 })
178 c.HandleFunc(irc.PRIVMSG, dostuff)
179 c.HandleFunc(irc.ACTION, dostuff)
181 quit := make(chan bool)
183 c.HandleFunc(irc.DISCONNECTED, func(conn *irc.Conn, line *irc.Line) {
184 quit <- true
185 })
187 if err := c.Connect(); err != nil {
188 log.Fatalln("connection error:", err)
191 <-quit