1// This program is free software: you can redistribute it and/or modify
2// it under the terms of the GNU Affero General Public License as
3// published by the Free Software Foundation, either version 3 of the
4// License, or (at your option) any later version.
5//
6// This program is distributed in the hope that it will be useful, but
7// WITHOUT ANY WARRANTY; without even the implied warranty of
8// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9// Affero General Public License for more details.
10//
11// You should have received a copy of the GNU Affero General Public
12// License along with this program. If not, see <http://www.gnu.org/licenses/>.
13
14package url
15
16import (
17 "errors"
18 "fmt"
19 "github.com/nmeum/marvin/irc"
20 "github.com/nmeum/marvin/modules"
21 "golang.org/x/net/html"
22 "mime"
23 "net/http"
24 "regexp"
25 "strings"
26 "compress/zlib"
27)
28
29type Module struct {
30 regex *regexp.Regexp
31 RegexStr string `json:"regex"`
32}
33
34func Init(moduleSet *modules.ModuleSet) {
35 moduleSet.Register(new(Module))
36}
37
38func (m *Module) Name() string {
39 return "url"
40}
41
42func (m *Module) Help() string {
43 return "Displays information about posted URLs."
44}
45
46func (m *Module) Defaults() {
47 m.RegexStr = `(?i)\b((http|https)\://(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s` + "`" + `!()\[\]{};:'".,<>?«»“”‘’]))`
48}
49
50func (m *Module) Load(client *irc.Client) error {
51 regex, err := regexp.Compile(m.RegexStr)
52 if err != nil {
53 return err
54 }
55
56 m.regex = regex
57 client.CmdHook("privmsg", m.urlCmd)
58
59 return nil
60}
61
62func (m *Module) urlCmd(client *irc.Client, msg irc.Message) error {
63 url := m.regex.FindString(msg.Data)
64 if len(url) <= 0 {
65 return nil
66 }
67
68 resp, err := http.Head(url)
69 if err != nil {
70 return err
71 }
72 resp.Body.Close() // HEAD response doesn't have a body
73
74 info := m.infoString(resp)
75 if len(info) <= 0 {
76 return nil
77 }
78
79 return client.Write("NOTICE %s :%s", msg.Receiver, info)
80}
81
82func (m *Module) infoString(resp *http.Response) string {
83 var mtype string
84 var infos []string
85
86 ctype := resp.Header.Get("Content-Type")
87 if len(ctype) > 0 {
88 m, _, err := mime.ParseMediaType(ctype)
89 if err == nil {
90 mtype = m
91 infos = append(infos, fmt.Sprintf("Type: %s", mtype))
92 }
93 }
94
95 csize := resp.ContentLength
96 if csize >= 0 {
97 infos = append(infos, fmt.Sprintf("Size: %s", m.humanize(csize)))
98 }
99
100 if mtype == "text/html" {
101 title, err := m.extractTitle(resp.Request.URL.String())
102 if err == nil {
103 infos = append(infos, fmt.Sprintf("Title: %s", title))
104 }
105 }
106
107 info := strings.Join(infos, " | ")
108 if len(info) > 0 {
109 info = fmt.Sprintf("%s -- %s", strings.ToUpper(m.Name()), info)
110 }
111
112 return info
113}
114
115func (m *Module) extractTitle(url string) (title string, err error) {
116 resp, err := http.Get(url)
117 if err != nil {
118 return
119 }
120 defer resp.Body.Close()
121
122 var reader = resp.Body
123 if resp.Header.Get("Content-Encoding") == "deflate" {
124 readerZ, errZ := zlib.NewReader(resp.Body)
125 defer readerZ.Close()
126 if errZ == nil {
127 reader = readerZ
128 }
129 }
130
131 doc, err := html.Parse(reader)
132 if err != nil {
133 return
134 }
135
136 var parseFunc func(n *html.Node)
137 parseFunc = func(n *html.Node) {
138 if n.Type == html.ElementNode && n.Data == "title" {
139 child := n.FirstChild
140 if child != nil {
141 title = child.Data
142 } else {
143 return
144 }
145 }
146
147 for c := n.FirstChild; c != nil; c = c.NextSibling {
148 parseFunc(c)
149 }
150 }
151
152 parseFunc(doc)
153 if len(title) <= 0 {
154 err = errors.New("couldn't extract title")
155 return
156 }
157
158 return
159}
160
161func (m *Module) humanize(count int64) string {
162 switch {
163 case count > (1 << 40):
164 return fmt.Sprintf("%v TiB", count/(1<<40))
165 case count > (1 << 30):
166 return fmt.Sprintf("%v GiB", count/(1<<30))
167 case count > (1 << 20):
168 return fmt.Sprintf("%v MiB", count/(1<<20))
169 case count > (1 << 10):
170 return fmt.Sprintf("%v KiB", count/(1<<10))
171 default:
172 return fmt.Sprintf("%v B", count)
173 }
174}