From 06e32c334034b87e67ca8e9bf65eccb33746ccd9 Mon Sep 17 00:00:00 2001
From: Alexandre Morignot <erdnaxeli@cervoi.se>
Date: Wed, 5 Oct 2016 00:43:05 +0200
Subject: [PATCH] Site

---
 bot/bot.go                   |  34 +++++++++++---
 bot/collection.go            |  29 +++++++++---
 bot/factory.go               |  16 ++++---
 bot/post.go                  |   4 +-
 main.go                      |  12 ++++-
 {bot => site}/content.go     |   2 +-
 site/reader.go               |   5 ++
 site/utils.go                |  40 ++++++++++++++++
 site/youtube.go              |  54 ++++++++++++++++++++++
 tests/bot_test.go            |  87 +++++++++++++++++++++++++++++------
 tests/dummy.go               |   5 +-
 tests/mock.go                |  28 +++++++++--
 tests/site_test.go           |  50 ++++++++++++++++++++
 transport/irc/.events.go.swp | Bin 0 -> 12288 bytes
 transport/irc/.print.go.swp  | Bin 0 -> 12288 bytes
 transport/irc/events.go      |   8 ++--
 transport/irc/print.go       |   6 +--
 17 files changed, 329 insertions(+), 51 deletions(-)
 rename {bot => site}/content.go (96%)
 create mode 100644 site/reader.go
 create mode 100644 site/utils.go
 create mode 100644 site/youtube.go
 create mode 100644 tests/site_test.go
 create mode 100644 transport/irc/.events.go.swp
 create mode 100644 transport/irc/.print.go.swp

diff --git a/bot/bot.go b/bot/bot.go
index 7d55774..c378019 100644
--- a/bot/bot.go
+++ b/bot/bot.go
@@ -2,31 +2,53 @@ package bot
 
 import (
 	"log"
+	"regexp"
 	"sync"
 
 	"github.com/mvdan/xurls"
+
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 type Bot interface {
 	Collection
 
-	ParseLine(author string, line string, contents chan Content) error
+	ParseLine(author string, line string, contents chan *site.Content) error
 }
 
 type PlayBot struct {
 	Collection
 }
 
-func NewPlayBot(source string, db Db) *PlayBot {
+func NewPlayBot(source string, db Db, readers []site.Reader) *PlayBot {
 	return &PlayBot{
 		Collection: &PlayBotCollection{
-			Source: source,
-			Db:     db,
+			Source:  source,
+			Db:      db,
+			Readers: readers,
 		},
 	}
 }
 
-func (pb *PlayBot) ParseLine(author string, line string, contents chan Content) error {
+func (*PlayBot) ExtractTags(line string) []string {
+	tagsSeen := make(map[string]bool)
+	var tags []string
+	re := regexp.MustCompile(`(?:^| )#([a-zA-Z0-9_]+)`)
+
+	for _, match := range re.FindAllStringSubmatch(line, -1) {
+		if tagsSeen[match[1]] {
+			continue
+		}
+
+		tagsSeen[match[1]] = true
+		tags = append(tags, match[1])
+	}
+
+	return tags
+}
+
+func (pb *PlayBot) ParseLine(author string, line string, contents chan *site.Content) error {
+	tags := pb.ExtractTags(line)
 	urls := xurls.Strict.FindAllString(line, -1)
 	var wg sync.WaitGroup
 
@@ -35,7 +57,7 @@ func (pb *PlayBot) ParseLine(author string, line string, contents chan Content)
 		go func(url string) {
 			defer wg.Done()
 
-			content, err := pb.Add(url)
+			content, err := pb.Add(url, tags)
 			if err != nil {
 				log.Print(err)
 			} else {
diff --git a/bot/collection.go b/bot/collection.go
index 38d09d6..49d22cc 100644
--- a/bot/collection.go
+++ b/bot/collection.go
@@ -2,23 +2,38 @@ package bot
 
 import (
 	"log"
+
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 type Collection interface {
-	Add(url string) (Content, error)
+	Add(url string, tags []string) (*site.Content, error)
 	Get()
 }
 
 type PlayBotCollection struct {
-	Source string
-	Db     Db
+	Source  string
+	Db      Db
+	Readers []site.Reader
 }
 
-func (pb *PlayBotCollection) Add(url string) (Content, error) {
-	log.Printf("%s: Add() %s", pb.Source, url)
-	content := Content{Title: url}
+func (pb *PlayBotCollection) Add(url string, tags []string) (*site.Content, error) {
+	log.Printf("%s: Add() %s, %q", pb.Source, url, tags)
+
+	var content *site.Content
+	for _, reader := range pb.Readers {
+		var err error
+		content, err = reader.Read(url)
+		if err != nil {
+			return nil, err
+		}
+		if content != nil {
+			break
+		}
+	}
+
 	pb.Db.Create(content)
-	return Content{}, nil
+	return content, nil
 }
 
 func (*PlayBotCollection) Get() {
diff --git a/bot/factory.go b/bot/factory.go
index dbdf0c6..9db1936 100644
--- a/bot/factory.go
+++ b/bot/factory.go
@@ -2,28 +2,32 @@ package bot
 
 import (
 	"log"
+
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 type Factory interface {
 	GetBot(string) Bot
 }
 
-type playBotFactory struct{}
+type playBotFactory struct {
+	readers []site.Reader
+}
 
-func NewPlayBotFactory(dbParams DbParams) *playBotFactory {
-	return &playBotFactory{}
+func NewPlayBotFactory(dbParams DbParams, readers []site.Reader) *playBotFactory {
+	return &playBotFactory{readers}
 }
 
-func (*playBotFactory) GetBot(source string) Bot {
+func (f *playBotFactory) GetBot(source string) Bot {
 	// give DB as arg
 
 	db := &DummyDb{}
-	return NewPlayBot(source, db)
+	return NewPlayBot(source, db, f.readers)
 }
 
 type DummyDb struct{}
 
 func (db *DummyDb) Create(value interface{}) Db {
-	log.Printf("Create(): %s", value)
+	log.Printf("Create(): %q", value)
 	return db
 }
diff --git a/bot/post.go b/bot/post.go
index 096ad4c..0a37648 100644
--- a/bot/post.go
+++ b/bot/post.go
@@ -2,6 +2,8 @@ package bot
 
 import (
 	"time"
+
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 type Post struct {
@@ -9,7 +11,7 @@ type Post struct {
 	Date    time.Time
 	Author  string `gorm:"column:sender_irc"`
 	Source  string `gorm:"column:chan"`
-	Content Content
+	Content site.Content
 	Tag     Tag
 }
 
diff --git a/main.go b/main.go
index 2cd35ab..a7368b0 100644
--- a/main.go
+++ b/main.go
@@ -8,6 +8,7 @@ import (
 	"gopkg.in/yaml.v2"
 
 	"git.iiens.net/morignot2011/playbot/bot"
+	"git.iiens.net/morignot2011/playbot/site"
 	"git.iiens.net/morignot2011/playbot/transport/irc"
 )
 
@@ -54,7 +55,16 @@ func main() {
 	var config config
 	err = yaml.Unmarshal(file, &config)
 	quit := make(map[string](chan bool))
-	factory := bot.NewPlayBotFactory(config.Db)
+
+	youtube, err := site.NewYoutube("AIzaSyD32EU9lb9cnNcEaSUBKCkKodk6rjl3HYc")
+	if err != nil {
+		log.Fatal(err)
+	}
+	readers := []site.Reader{
+		//&site.Youtube{},
+		youtube,
+	}
+	factory := bot.NewPlayBotFactory(config.Db, readers)
 
 	for name, config := range config.Transport {
 		c := startTransport(name, config, factory)
diff --git a/bot/content.go b/site/content.go
similarity index 96%
rename from bot/content.go
rename to site/content.go
index 18a44ce..96109b3 100644
--- a/bot/content.go
+++ b/site/content.go
@@ -1,4 +1,4 @@
-package bot
+package site
 
 type Content struct {
 	Author       string `gorm:"column:sender"`
diff --git a/site/reader.go b/site/reader.go
new file mode 100644
index 0000000..f7c4260
--- /dev/null
+++ b/site/reader.go
@@ -0,0 +1,5 @@
+package site
+
+type Reader interface {
+	Read(url string) (*Content, error)
+}
diff --git a/site/utils.go b/site/utils.go
new file mode 100644
index 0000000..aee25b1
--- /dev/null
+++ b/site/utils.go
@@ -0,0 +1,40 @@
+package site
+
+import (
+	"fmt"
+	"regexp"
+	"strconv"
+)
+
+type NotISO8601 struct {
+	s string
+}
+
+func (e NotISO8601) Error() string {
+	return fmt.Sprintf("'%s' is not in ISO 8601 format")
+}
+
+func ParseISO8601Duration(s string) (int, error) {
+	re := regexp.MustCompile(`^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$`)
+	match := re.FindStringSubmatch(s)
+
+	if len(match) == 0 {
+		return -1, NotISO8601{s}
+	}
+
+	var duration int
+	if match[1] != "" {
+		hours, _ := strconv.Atoi(match[1])
+		duration += 3600 * hours
+	}
+	if match[2] != "" {
+		minutes, _ := strconv.Atoi(match[2])
+		duration += 60 * minutes
+	}
+	if match[3] != "" {
+		seconds, _ := strconv.Atoi(match[3])
+		duration += seconds
+	}
+
+	return duration, nil
+}
diff --git a/site/youtube.go b/site/youtube.go
new file mode 100644
index 0000000..c872b47
--- /dev/null
+++ b/site/youtube.go
@@ -0,0 +1,54 @@
+package site
+
+import (
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+	"regexp"
+
+	"google.golang.org/api/googleapi/transport"
+	"google.golang.org/api/youtube/v3"
+)
+
+type Youtube struct {
+	service *youtube.Service
+}
+
+func NewYoutube(key string) (Reader, error) {
+	client := &http.Client{
+		Transport: &transport.APIKey{Key: key},
+	}
+
+	service, err := youtube.New(client)
+	return &Youtube{service: service}, err
+}
+
+func (yt Youtube) Read(url string) (*Content, error) {
+	log.Print(url)
+	re := regexp.MustCompile(`(?:^|[^!])https?://(?:www.youtube.com/watch\?[a-zA-Z0-9_=&-]*v=|youtu.be/)([a-zA-Z0-9_-]+)`)
+	match := re.FindStringSubmatch(url)
+	log.Print(match)
+	if len(match) == 0 {
+		return nil, nil
+	}
+
+	response, err := yt.service.Videos.List("snippet,contentDetails").Id(match[1]).Do()
+	if err != nil {
+		return nil, err
+	}
+
+	video := response.Items[0]
+	duration, err := ParseISO8601Duration(video.ContentDetails.Duration)
+	if err != nil {
+		return nil, errors.New(fmt.Sprintf("Invalid duration: %s", err))
+	}
+
+	return &Content{
+		Author:   video.Snippet.ChannelTitle,
+		Duration: duration + 1,
+		Source:   "youtube",
+		SourceId: video.Id,
+		Url:      "https://www.youtube.com/watch?v=" + video.Id,
+	}, nil
+}
diff --git a/tests/bot_test.go b/tests/bot_test.go
index 2c662a7..23fadc8 100644
--- a/tests/bot_test.go
+++ b/tests/bot_test.go
@@ -1,17 +1,18 @@
 package test
 
 import (
-    "reflect"
+	"reflect"
 	"sort"
 	"testing"
 
 	"git.iiens.net/morignot2011/playbot/bot"
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 func TestAdd(t *testing.T) {
 	db := &DummyDb{}
 	bot := bot.NewPlayBot("test", db)
-	content, err := bot.Add("http://www.youtube.com/watch?v=IiWapK6WQPg")
+	content, err := bot.Add("http://www.youtube.com/watch?v=IiWapK6WQPg", []string{"hardstle", "raw"})
 
 	if err != nil {
 		t.Fatalf("Receive error %s", err)
@@ -48,43 +49,99 @@ func TestAdd(t *testing.T) {
 	}
 }
 
+func TestExtractTags(t *testing.T) {
+	db := &DummyDb{}
+	bot := bot.NewPlayBot("test", db)
+
+	testCases := []struct {
+		msg  string
+		tags []string
+	}{
+		{
+			msg:  "hello #boy, wa#nna dance?",
+			tags: []string{"boy"},
+		},
+		{
+			msg:  "#I'm #Bond, James #Bond",
+			tags: []string{"I", "Bond"},
+		},
+		{
+			msg:  "last #test but not #least",
+			tags: []string{"test", "least"},
+		},
+	}
+
+	for _, c := range testCases {
+		tags := bot.ExtractTags(c.msg)
+		if !reflect.DeepEqual(tags, c.tags) {
+			t.Error("Expected %q but got %q", c.tags, tags)
+		}
+	}
+}
+
 func TestParseLine(t *testing.T) {
 	testCases := []struct {
 		msg      string
-		expected []string
+		expected []AddArgs
 	}{
 		{
-			msg:      "this is a http://perdu.com test",
-			expected: []string{"http://perdu.com"},
+			msg: "this is a http://perdu.com test",
+			expected: []AddArgs{
+				{
+					url:  "http://perdu.com",
+					tags: nil,
+				},
+			},
 		},
 		{
 			msg:      "nothing here",
-			expected: []string{},
+			expected: nil,
+		},
+		{
+			msg: "some http://giantbatfarts.com/ #with #tags",
+			expected: []AddArgs{
+				{
+					url:  "http://giantbatfarts.com/",
+					tags: []string{"tags", "with"},
+				},
+			},
 		},
 		{
-			msg:      "some http://url.com #with #tags",
-			expected: []string{"http://url.com"},
+			msg: "https://youtu.be/zpG4CZcTpRM?t=1m24s url: http://heeeeeeeey.com/ #oki",
+			expected: []AddArgs{
+				{
+					url:  "http://heeeeeeeey.com/",
+					tags: []string{"oki"},
+				},
+				{
+					url:  "https://youtu.be/zpG4CZcTpRM?t=1m24s",
+					tags: []string{"oki"},
+				},
+			},
 		},
 		{
-			msg:      "http://two.com urls: http://second.com",
-			expected: []string{"http://second.com", "http://two.com"},
+			msg: "fake tag https://translate.google.com/#en/ja/PlayBot",
+			expected: []AddArgs{
+				{
+					url:  "https://translate.google.com/#en/ja/PlayBot",
+					tags: nil,
+				},
+			},
 		},
 	}
 
 	for _, c := range testCases {
-		fc := &FakeCollection{
-            AddCalls: []string{},
-        }
+		fc := &FakeCollection{}
 		testBot := &bot.PlayBot{
 			Collection: fc,
 		}
-		contents := make(chan bot.Content)
+		contents := make(chan site.Content)
 
 		go testBot.ParseLine("me", c.msg, contents)
 		for range contents {
 		}
 
-		sort.Strings(fc.AddCalls)
+		sort.Sort(ByUrl(fc.AddCalls))
 		if !reflect.DeepEqual(fc.AddCalls, c.expected) {
 			t.Errorf("Expected %q but got %q", c.expected, fc.AddCalls)
 		}
diff --git a/tests/dummy.go b/tests/dummy.go
index 527d9fb..2bc53d5 100644
--- a/tests/dummy.go
+++ b/tests/dummy.go
@@ -2,13 +2,14 @@ package test
 
 import (
 	"git.iiens.net/morignot2011/playbot/bot"
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 type DummyDb struct {
-	createCalls []bot.Content
+	createCalls []site.Content
 }
 
 func (db *DummyDb) Create(value interface{}) bot.Db {
-	db.createCalls = append(db.createCalls, value.(bot.Content))
+	db.createCalls = append(db.createCalls, value.(site.Content))
 	return db
 }
diff --git a/tests/mock.go b/tests/mock.go
index 6333ad5..f044181 100644
--- a/tests/mock.go
+++ b/tests/mock.go
@@ -1,17 +1,35 @@
 package test
 
 import (
-	"git.iiens.net/morignot2011/playbot/bot"
+	"sort"
+
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 type FakeCollection struct {
-	AddCalls []string
+	AddCalls []AddArgs
+}
+
+type AddArgs struct {
+	url  string
+	tags []string
 }
 
-func (c *FakeCollection) Add(url string) (bot.Content, error) {
-	c.AddCalls = append(c.AddCalls, url)
+type ByUrl []AddArgs
+
+func (a ByUrl) Len() int           { return len(a) }
+func (a ByUrl) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a ByUrl) Less(i, j int) bool { return a[i].url < a[j].url }
+
+func (c *FakeCollection) Add(url string, tags []string) (site.Content, error) {
+	sort.Strings(tags)
+
+	c.AddCalls = append(c.AddCalls, AddArgs{
+		url:  url,
+		tags: tags,
+	})
 
-	return bot.Content{}, nil
+	return site.Content{}, nil
 }
 
 func (*FakeCollection) Get() {
diff --git a/tests/site_test.go b/tests/site_test.go
new file mode 100644
index 0000000..57ea278
--- /dev/null
+++ b/tests/site_test.go
@@ -0,0 +1,50 @@
+package test
+
+import (
+	"testing"
+
+	"git.iiens.net/morignot2011/playbot/site"
+)
+
+func TestParseISO8601Duration(t *testing.T) {
+	testCases := []struct {
+		s string
+		i int
+	}{
+		{
+			s: "PT5H",
+			i: 18000,
+		},
+		{
+			s: "PT0S",
+			i: 0,
+		},
+		{
+			s: "P5H3M1S",
+			i: -1,
+		},
+		{
+			s: "PT5H3m1S",
+			i: -1,
+		},
+		{
+			s: "PT5H3M1S",
+			i: 18181,
+		},
+		{
+			s: "PT98M1234S",
+			i: 7114,
+		},
+		{
+			s: "",
+			i: -1,
+		},
+	}
+
+	for _, c := range testCases {
+		d, _ := site.ParseISO8601Duration(c.s)
+		if d != c.i {
+			t.Errorf("Expected %d but got %d for '%s'", c.i, d, c.s)
+		}
+	}
+}
diff --git a/transport/irc/.events.go.swp b/transport/irc/.events.go.swp
new file mode 100644
index 0000000000000000000000000000000000000000..92779672c8ec1df79889d67ee8382586b7a777ee
GIT binary patch
literal 12288
zcmeI2L2DC16vwB6hlo|Fg4c0H*re%fQ!hpAp$bY%m8w+_rO0M?vK_LSiL;YpTUrl_
z;9c<xc=987)DPgrFQF&FUcC9AWD`(msK?4%^3O2y-o7{QH=Dzf*PMN{bcfH2S%&Kz
zW4~$#k9STjv#*XZX;U3PlezM?G1rz#Wj@9}H0_T9Mu|z|ohUO=VUs9RPADaHA+&T+
z$5@qV<K|`?ji{T)+fCylXJcJ-jdhX24RT9rSBT6EBhyF(h`?9^J=dC<pJZ3(8vUR8
z<=_%OKlO0zEo3ADM1Tko0U|&IhyW2F0z}~dBH+3c>^Vw0QI-{y<HTQMIi-OJ5CI}U
z1c(3;AOb{y2oM1xKm>>Y5jchf;*>Fcnz5-#Y##sr58wX}&NB8B`VM`DK0)uGx6m8t
z9yAS|gnpcX9r^&hhF(E0p%>5sGy_dTHRv1ie1Sef@1cEY4|)LIhZ@jD=mO;T)Zf=J
z<Vss60z`la5CI}U1c(3;AOb|-I1t!l6?`gAbgk0vrqK?~#$hrTtE|#8mX|YrZGodP
zIg`VMg6}d`$qa9b)z}ttMQIu4_!4(j(ysGPk<~}ORk)Uc3LmKxw`G`Y#+ug(<)kQ6
zBW2T@oj@aWvF;tT%r{sC!TfqZ{G!c@^#&G8^T@Ck+f2H!%33|0@X+zvl1<iz-PBc%
zBDWC8*N41luJ&nly&rWj_cc^~U7+H`)1Gqt`V4orCl8C6s=|kpiIeFO1AF2qPb{Ws
z8001wiRlD&zP?d7oI<v{2MWmk2LZmoF(2GG0(2`*oUz;Dwsbd)3j<%#sJA@uQT*1#
zvQfIv8Yl;$S4@^kD^~rXx5B_X@GAxOP!ffgKlvx41)p+o_)e5Q{`e2!{7*oTD>y<^
zynjmYzFt9%-1~3#nx0$K%6qL=F40~OOO)hjt#<)a9p69CL&mys(vI=a!peRFroA~6

literal 0
HcmV?d00001

diff --git a/transport/irc/.print.go.swp b/transport/irc/.print.go.swp
new file mode 100644
index 0000000000000000000000000000000000000000..853d46928d8b839cbf92af3320f486514dd72286
GIT binary patch
literal 12288
zcmeI&%TC)s6b9gvN-U@fRP+Hl7$G^d*a_W4Y%3N)f`SC?+Hoe<2s7i3C%qvqZ^H|K
zcoCj}?%0w&92>ifEUYeD^pEswlFXSC%_n6Mc?O-&{cX_;I>h5O(agKam-ClfbmNGm
z9qQ<ZOjWVgFutjwOdHngrPDtR!`KX?Utwax+{R&|T%eTHd7z~WkBn7`Hg3Js?S@$z
z{Tvt<IveRcGuDNQH)K|67bK=`j2QwDXcic|(OPegKCE}kpZc=5B;G6@H1EMG1Rwwb
z2tWV=5P$##An>dNT-Ks9KGBQnR14MInwqP(m>>WF2tWV=5P$##AOHafKmY;|_zwl5
zA<@M=(F3P={$Kz9pYV%&&K>7B=azHLx#C=MzH|0CpEx_5x11KI=<|wa3<y8~0uX=z
z1Rwwb2tWV=5P-lxByd8rsY!y};zIez_cpCHc0;V>-g_afZPRG1W8pio+PCq3eFwcQ
z$_waavCm~isj+QwOf=Q6>?=}}a?h{aGd9{u?V9_3d@Ay*VzHVlCxhzK@>|+8>;1*0
d`C^|(T+2uFNF5dT=Y>y{Me$*jNWoX>=@Bb7kWc^s

literal 0
HcmV?d00001

diff --git a/transport/irc/events.go b/transport/irc/events.go
index 4292c1d..6457110 100644
--- a/transport/irc/events.go
+++ b/transport/irc/events.go
@@ -1,9 +1,9 @@
 package irc
 
 import (
-	"git.iiens.net/morignot2011/playbot/bot"
-
 	irc "github.com/fluffle/goirc/client"
+
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
 func (t *IrcTransport) connected(conn *irc.Conn, line *irc.Line) {
@@ -12,7 +12,7 @@ func (t *IrcTransport) connected(conn *irc.Conn, line *irc.Line) {
 		conn.Join(channel)
 		bot := t.botFactory.GetBot("irc.iiens.net")
 		t.bots[channel] = bot
-		bot.Add("lol.com")
+		bot.Add("lol.com", []string{})
 	}
 }
 
@@ -24,7 +24,7 @@ func (t *IrcTransport) privmsg(conn *irc.Conn, line *irc.Line) {
 	channel := line.Target()
 	msg := line.Args[1]
 	b := t.bots[channel]
-	contents := make(chan bot.Content)
+	contents := make(chan *site.Content)
 
 	go b.ParseLine(line.Nick, msg, contents)
 
diff --git a/transport/irc/print.go b/transport/irc/print.go
index 2c85e53..e7cc912 100644
--- a/transport/irc/print.go
+++ b/transport/irc/print.go
@@ -3,11 +3,11 @@ package irc
 import (
 	"log"
 
-	"git.iiens.net/morignot2011/playbot/bot"
+	"git.iiens.net/morignot2011/playbot/site"
 )
 
-func (t *IrcTransport) printContent(content bot.Content) {
-	log.Printf("Print lol")
+func (t *IrcTransport) printContent(content *site.Content) {
+	log.Printf("Print lol: %q", content)
 }
 
 func (t *IrcTransport) printError(err error) {
-- 
GitLab