diff --git a/bot/bot.go b/bot/bot.go index 7d55774e81d57449fc62c8950ba6ddfe8dc3e8aa..c378019f83041db8598355ec3971f51ccd43ddc0 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 38d09d63dfd9957117a9c0d2155ce5371a87230a..49d22cc2326395fea0895f3792596f8e818b9d16 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 dbdf0c6d827c319a48b613593c6f8c5779a880bc..9db1936f2b55fe242fe91dc9d64635679ccde847 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 096ad4c0b3732d38cf65feb3f40fd7f7c5e0da8c..0a376483d59902ba0e302e5b9c60c05e013aa81e 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 2cd35abb8bad1b0aebe91fb9b8a541fa3a7def74..a7368b06a80e630d5faccf20055414ce0b4450c7 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 18a44ce7a112b190e2a708bdc67b378b9d1b5067..96109b349cb89845bce56bc0d3273d21f87dc64c 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 0000000000000000000000000000000000000000..f7c42605fcf24ffedaf7f44c6358315f8f60df26 --- /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 0000000000000000000000000000000000000000..aee25b137dbef3e5ee439261f884273bd5785722 --- /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 0000000000000000000000000000000000000000..c872b471ee28cf4a60dfe1436ed7439786b997bd --- /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 2c662a7c35805b664770d0e30c070ae2ab040c40..23fadc87b7b0cb588490750da112b02216cec523 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 527d9fb0cb8f2c5644fffc97d6097c98bf53323e..2bc53d58b2fa726ce6bddaf5d1d17574fb99a5a6 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 6333ad5c81688f641842fb47c29a7f95bb526803..f044181a2202e0f6aeb41326b480ca345e862004 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 0000000000000000000000000000000000000000..57ea278275a5983097545463c4f6f75e4ef25a08 --- /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 Binary files /dev/null and b/transport/irc/.events.go.swp differ diff --git a/transport/irc/.print.go.swp b/transport/irc/.print.go.swp new file mode 100644 index 0000000000000000000000000000000000000000..853d46928d8b839cbf92af3320f486514dd72286 Binary files /dev/null and b/transport/irc/.print.go.swp differ diff --git a/transport/irc/events.go b/transport/irc/events.go index 4292c1d730c1bf8df2fb8f9209717459007e0268..64571102f9508cfa9ea72c574ebd49eb9da1e5cb 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 2c85e53b563406ef3e651e60f44169507a427d4b..e7cc912d916a16916b54160c36ccd727712e8cad 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) {