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