marvin

A simple and modular IRC bot

git clone https://git.8pit.net/marvin.git

  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}