all repos — paprika @ 4fb075344b513bb312cd5d28c9b869e113fc03a8

go rewrite of taigabot

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}