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