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 string
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() == "youtube.com" || u.Hostname() == "youtu.be" {
51 // TODO finish this
52 output += "[Youtube] yeah you definitely posted a youtube link\n"
53 } else if len(u.Hostname()) > 0 {
54 desc, err := getDescriptionFromURL(value)
55 if err != nil {
56 log.Printf("Failed to get title from http URL")
57 fmt.Println(err)
58 continue
59 }
60 output += fmt.Sprintf("[URL] %s (%s)\n", desc, u.Hostname())
61 }
62 }
63
64 if len(output) > 0 {
65 return output, nil
66 } else {
67 return "", NoReply // We need to NoReply so we don't consume all messages.
68 }
69}
70
71// the three funcs below are taken from:
72// https://siongui.github.io/2016/05/10/go-get-html-title-via-net-html/
73func isTitleElement(n *html.Node) bool {
74 return n.Type == html.ElementNode && n.Data == "title"
75}
76
77func traverse(n *html.Node) (string, bool) {
78 if isTitleElement(n) {
79 return n.FirstChild.Data, true
80 }
81
82 for c := n.FirstChild; c != nil; c = c.NextSibling {
83 result, ok := traverse(c)
84 if ok {
85 return result, ok
86 }
87 }
88
89 return "", false
90}
91
92func getHtmlTitle(r io.Reader) (string, bool) {
93 doc, err := html.Parse(r)
94 if err != nil {
95 return "", false
96 }
97
98 return traverse(doc)
99}
100
101// yoinkies from
102// https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
103func byteCountSI(b int64) string {
104 const unit = 1000
105 if b < unit {
106 return fmt.Sprintf("%d B", b)
107 }
108 div, exp := int64(unit), 0
109 for n := b / unit; n >= unit; n /= unit {
110 div *= unit
111 exp++
112 }
113 return fmt.Sprintf("%.1f %cB",
114 float64(b)/float64(div), "kMGTPE"[exp])
115}
116
117// provides a basic, short description of whatever is inside
118// a posted URL.
119func getDescriptionFromURL(url string) (string, error) {
120 resp, err := http.Get(url)
121
122 if err != nil {
123 return "", err
124 }
125
126 defer resp.Body.Close()
127
128 mime := resp.Header.Get("content-type")
129
130 switch mime {
131 case "image/jpeg":
132 return fmt.Sprintf("JPEG image, size: %s", byteCountSI(resp.ContentLength)), nil
133 case "image/png":
134 return fmt.Sprintf("PNG image, size: %s", byteCountSI(resp.ContentLength)), nil
135 default:
136 output, ok := getHtmlTitle(resp.Body)
137
138 if !ok {
139 return "", errors.New("Failed to find <title> in html")
140 }
141
142 return output, nil
143 }
144}