all repos — paprika @ eb2f22649405dd676a6c6c3cf1eedfa325fa8bef

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