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 irc "github.com/fluffle/goirc/client"
17 "golang.org/x/net/html"
18 )
20 var (
21 baseurl = flag.String("baseurl", "gemini://m2i.omarpolo.com", "base url")
22 matrixOutDir = flag.String("matrix-out", "", "matrix out directory")
24 msgRe = regexp.MustCompile(`https://.*/[^\s]+\.(txt|png|jpg|jpeg|gif)`)
25 channel = "#gemini-it"
27 tooLongRe = regexp.MustCompile(`full message at (https://libera.ems.host/.*)[)]`)
29 httplink = regexp.MustCompile(`https?://[^\s]+`)
30 )
32 func matrix2gemini(conn *irc.Conn, line *irc.Line) {
33 matches := msgRe.FindAllString(line.Text(), -1)
35 // it's not a good idea to defer inside a loop, but we know
36 // len(matches) is small (usually just 1). Morover, I like
37 // living in danger!
39 for _, link := range matches {
40 resp, err := http.Get(link)
41 if err != nil {
42 conn.Privmsg(
43 channel,
44 fmt.Sprintf("failed to download %q: %s", link, err),
45 )
46 continue
47 }
48 defer resp.Body.Close()
50 ext := path.Ext(link)
51 tmpfile, err := ioutil.TempFile(*matrixOutDir, "message-*"+ext)
52 if err != nil {
53 conn.Privmsg(channel, fmt.Sprintf("failed to tmpfile: %s", err))
54 return
55 }
56 defer tmpfile.Close()
58 io.Copy(tmpfile, resp.Body)
60 conn.Privmsg(
61 channel,
62 fmt.Sprintf(
63 "better: %s/%s",
64 *baseurl,
65 filepath.Base(tmpfile.Name()),
66 ),
67 )
68 }
69 }
71 func messageTooLong(conn *irc.Conn, line *irc.Line) {
72 matches := tooLongRe.FindStringSubmatch(line.Text())
73 if len(matches) != 2 {
74 return
75 }
77 url := matches[1]
79 resp, err := http.Get(url)
80 if err != nil {
81 conn.Privmsg(
82 channel,
83 fmt.Sprintf("failed to download %q: %s", url, err),
84 )
85 return
86 }
87 defer resp.Body.Close()
89 sb := &strings.Builder{}
90 if _, err := io.Copy(sb, resp.Body); err != nil {
91 conn.Privmsg(
92 channel,
93 fmt.Sprintf("failed to read body of %q: %s", url, err),
94 )
95 return
96 }
98 conn.Privmsg(channel, fmt.Sprintf("%s ha detto:", line.Nick))
99 for _, line := range strings.Split(sb.String(), "\n") {
100 conn.Privmsg(
101 channel,
102 line,
107 func getPageTitle(node *html.Node) string {
108 for child := node.FirstChild; child != nil; child = child.NextSibling {
109 if child.Type == html.ElementNode && child.Data == "title" {
110 text := node.FirstChild.Data
111 return strings.Trim(text, " \t\n")
114 return ""
117 func getPageHead(node *html.Node) *html.Node {
118 if node.Type == html.ElementNode && node.Data == "head" {
119 return node
122 for child := node.FirstChild; child != nil; child = child.NextSibling {
123 if n := getPageHead(child); n != nil {
124 return n
127 return nil
130 func pagetitle(conn *irc.Conn, line *irc.Line) {
131 matches := httplink.FindAllString(line.Text(), -1)
133 for _, link := range matches {
134 resp, err := http.Get(link)
135 if err != nil {
136 continue
138 defer resp.Body.Close()
140 doc, err := html.Parse(resp.Body)
141 if err != nil {
142 continue
145 head := getPageHead(doc)
146 if head == nil {
147 continue
149 if title := getPageTitle(head); title != "" {
150 conn.Privmsg(channel, title)
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