plugins/link_handler.go (view raw)
1package plugins
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "log"
8 "net/http"
9 "net/url"
10 "strings"
11
12 "golang.org/x/net/html"
13 "gopkg.in/irc.v3"
14)
15
16// This plugin is an example of how to make something that will
17// respond (or just have read access to) every message that comes in.
18// The plugins.go file has a special case for handling an 'empty' Triggers string.
19// on such a case, it will simply run Execute on every message that it sees.
20func init() {
21 Register(LinkHandler{})
22}
23
24type LinkHandler struct{}
25
26func (LinkHandler) Triggers() []string {
27 return []string{""}
28}
29
30func (LinkHandler) Execute(m *irc.Message) (string, error) {
31 // The message starts with a '.', so we ignore it.
32 if strings.HasPrefix(m.Params[1], ".") {
33 return "", NoReply
34 }
35
36 var output strings.Builder
37
38 // in PRIVMSG's case, the second (first, if counting from 0) parameter
39 // is the string that contains the complete message.
40 for _, value := range strings.Split(m.Params[1], " ") {
41 u, err := url.Parse(value)
42
43 if err != nil {
44 continue
45 }
46
47 // just a test check for the time being.
48 // this if statement block will be used for content that is
49 // non-generic. I.e it belongs to a specific website, like
50 // stackoverflow or youtube.
51 if u.Hostname() == "www.youtube.com" || u.Hostname() == "youtube.com" || u.Hostname() == "youtu.be" {
52 // TODO finish this
53 yt, err := YoutubeDescriptionFromUrl(u)
54 if err != nil {
55 return "", err
56 }
57 output.WriteString(yt)
58 output.WriteByte('\n')
59 } else if len(u.Hostname()) > 0 {
60 desc, err := getDescriptionFromURL(value)
61 if err != nil {
62 log.Printf("Failed to get title from http URL")
63 fmt.Println(err)
64 continue
65 }
66 output.WriteString(fmt.Sprintf("[URL] %s (%s)\n", desc, u.Hostname()))
67 }
68 }
69
70 if output.Len() > 0 {
71 return output.String(), nil
72 } else {
73 return "", NoReply // We need to NoReply so we don't consume all messages.
74 }
75}
76
77// the three funcs below are taken from:
78// https://siongui.github.io/2016/05/10/go-get-html-title-via-net-html/
79func isTitleElement(n *html.Node) bool {
80 return n.Type == html.ElementNode && n.Data == "title"
81}
82
83func traverse(n *html.Node) (string, bool) {
84 if isTitleElement(n) {
85 return n.FirstChild.Data, true
86 }
87
88 for c := n.FirstChild; c != nil; c = c.NextSibling {
89 result, ok := traverse(c)
90 if ok {
91 return result, ok
92 }
93 }
94
95 return "", false
96}
97
98func getHtmlTitle(r io.Reader) (string, bool) {
99 doc, err := html.Parse(r)
100 if err != nil {
101 return "", false
102 }
103
104 return traverse(doc)
105}
106
107// yoinkies from
108// https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
109func byteCountSI(b int64) string {
110 const unit = 1000
111 if b < unit {
112 return fmt.Sprintf("%d B", b)
113 }
114 div, exp := int64(unit), 0
115 for n := b / unit; n >= unit; n /= unit {
116 div *= unit
117 exp++
118 }
119 return fmt.Sprintf("%.1f %cB",
120 float64(b)/float64(div), "kMGTPE"[exp])
121}
122
123// provides a basic, short description of whatever is inside
124// a posted URL.
125func getDescriptionFromURL(url string) (string, error) {
126 resp, err := http.Get(url)
127
128 if err != nil {
129 return "", err
130 }
131
132 defer resp.Body.Close()
133
134 mime := resp.Header.Get("content-type")
135
136 switch mime {
137 case "image/jpeg":
138 return fmt.Sprintf("JPEG image, size: %s", byteCountSI(resp.ContentLength)), nil
139 case "image/png":
140 return fmt.Sprintf("PNG image, size: %s", byteCountSI(resp.ContentLength)), nil
141 default:
142 output, ok := getHtmlTitle(resp.Body)
143
144 if !ok {
145 return "", errors.New("Failed to find <title> in html")
146 }
147
148 return output, nil
149 }
150}