diff --git a/bees/youtubebee/youtube.go b/bees/youtubebee/youtube.go new file mode 100644 index 00000000..7ca38a76 --- /dev/null +++ b/bees/youtubebee/youtube.go @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2014 Daniel 'grindhold' Brendle + * 2014-2017 Christian Muehlhaeuser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * Authors: + * Daniel 'grindhold' Brendle + * Christian Muehlhaeuser + * Mark Jung + */ + +// Package youtubebee is a Bee for tunneling Youtube push notifications. +package youtubebee + +import ( + "time" + "encoding/xml" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/muesli/beehive/bees" +) + +// YoutubeBee is a Bee for handling Youtube push notifications. +type YoutubeBee struct { + bees.Bee + + url string + + addr string + + eventChan chan bees.Event +} + +type Feed struct { + XMLName xml.Name `xml:"feed"` + Text string `xml:",chardata"` + Yt string `xml:"yt,attr"` + Xmlns string `xml:"xmlns,attr"` + Link []struct { + Text string `xml:",chardata"` + Rel string `xml:"rel,attr"` + Href string `xml:"href,attr"` + } `xml:"link"` + Title string `xml:"title"` + Updated string `xml:"updated"` + Entry struct { + Text string `xml:",chardata"` + ID string `xml:"id"` + VideoId string `xml:"videoId"` + ChannelId string `xml:"channelId"` + Title string `xml:"title"` + Link struct { + Text string `xml:",chardata"` + Rel string `xml:"rel,attr"` + Href string `xml:"href,attr"` + } `xml:"link"` + Author struct { + Text string `xml:",chardata"` + Name string `xml:"name"` + URI string `xml:"uri"` + } `xml:"author"` + Published string `xml:"published"` + Updated string `xml:"updated"` + } `xml:"entry"` +} + +// Run executes the Bee's event loop. +func (mod *YoutubeBee) Run(eventChan chan bees.Event) { + mod.eventChan = eventChan + subscriptionLink := "https://pubsubhubbub.appspot.com/subscribe" + channelURLTokens := strings.Split(mod.url, "/") + channelID := channelURLTokens[len(channelURLTokens)-1] + topic := "https://www.youtube.com/xml/feeds/videos.xml?channel_id=" + channelID + + srv := &http.Server{Addr: mod.addr, Handler: mod} + l, err := net.Listen("tcp", mod.addr) + if err != nil { + mod.LogErrorf("Can't listen on %s", mod.addr) + return + } + defer l.Close() + + go func() { + err := srv.Serve(l) + if err != nil { + mod.LogErrorf("Server error: %v", err) + } + // Go 1.8+: srv.Close() + // send POST to Google's pubsubhubbub to subscribe + // need to be in form-data format + data := url.Values{} + data.Set("hub.mode", "subscribe") + data.Set("hub.topic", topic) + data.Set("hub.callback", "mod.addr") + client := &http.Client{} + r, _ := http.NewRequest("POST", subscriptionLink, strings.NewReader(data.Encode())) // URL-encoded payload + r.Header.Add("Content-Type", "application/x-www-form-urlencoded") + r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) + + _, err = client.Do(r) + if err != nil { + mod.LogErrorf("Can't subscribe to youtube channel") + return + } + }() + + select { + case <-mod.SigChan: + return + } +} + +func (mod *YoutubeBee) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.Method == "POST" { + ev := bees.Event{ + Bee: mod.Name(), + } + var feed Feed + body, err := ioutil.ReadAll(req.Body) + if err != nil { + log.Printf("Error reading body: %v", err) + http.Error(w, "can't read body", http.StatusBadRequest) + } + xml.Unmarshal([]byte(body), &feed) + updatedTime, _ := time.Parse(time.RFC3339, feed.Entry.Updated) + publishedTime, _ := time.Parse(time.RFC3339, feed.Entry.Published) + diff := updatedTime.Sub(publishedTime) + // give the channel a minute window - for new videos, update and publish times aren't exact. + if diff.Minutes() <= 5 { + ev.Name = "new_video" + } else { + ev.Name = "change_video" + } + ev.Options.SetValue("channelUrl", "string", feed.Entry.Author.URI) + ev.Options.SetValue("vidUrl", "string", feed.Entry.Link.Href) + + mod.eventChan <- ev + } else if req.Method == "GET" { + challenge := req.URL.Query().Get("hub.challenge") + if challenge != "" { + fmt.Fprintf(w, challenge) + } + } +} + +// ReloadOptions parses the config options and initializes the Bee. +func (mod *YoutubeBee) ReloadOptions(options bees.BeeOptions) { + mod.SetOptions(options) + + options.Bind("address", &mod.addr) + options.Bind("channel", &mod.url) +} diff --git a/bees/youtubebee/youtubefactory.go b/bees/youtubebee/youtubefactory.go new file mode 100644 index 00000000..709eca74 --- /dev/null +++ b/bees/youtubebee/youtubefactory.go @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2014-2017 Christian Muehlhaeuser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * Authors: + * Christian Muehlhaeuser + * Mark Jung + */ + +package youtubebee + +import ( + "github.com/muesli/beehive/bees" +) + +// WebBeeFactory is a factory for WebBees. +type YoutubeBeeFactory struct { + bees.BeeFactory +} + +// New returns a new Bee instance configured with the supplied options. +func (factory *YoutubeBeeFactory) New(name, description string, options bees.BeeOptions) bees.BeeInterface { + bee := YoutubeBee{ + Bee: bees.NewBee(name, factory.ID(), description, options), + } + bee.ReloadOptions(options) + + return &bee +} + +// ID returns the ID of this Bee. +func (factory *YoutubeBeeFactory) ID() string { + return "youtubebee" +} + +// Name returns the name of this Bee. +func (factory *YoutubeBeeFactory) Name() string { + return "Youtube" +} + +// Description returns the description of this Bee. +func (factory *YoutubeBeeFactory) Description() string { + return "HTTP Server that listens to a Youtube channel" +} + +// Image returns the filename of an image for this Bee. +func (factory *YoutubeBeeFactory) Image() string { + return factory.ID() + ".png" +} + +// LogoColor returns the preferred logo background color (used by the admin interface). +func (factory *YoutubeBeeFactory) LogoColor() string { + return "#ff0000" +} + +// Options returns the options available to configure this Bee. +func (factory *YoutubeBeeFactory) Options() []bees.BeeOptionDescriptor { + opts := []bees.BeeOptionDescriptor{ + { + Name: "address", + Description: "Which addr to listen on, eg: 0.0.0.0:12345", + Type: "address", + Mandatory: true, + }, + { + Name: "channel", + Description: "What is the link of the channel you want to receive push notifications for?", + Type: "url", + Mandatory: true, + }, + } + return opts +} + +// Events describes the available events provided by this Bee. +func (factory *YoutubeBeeFactory) Events() []bees.EventDescriptor { + events := []bees.EventDescriptor{ + { + Namespace: factory.Name(), + Name: "new_video", + Description: "The channel posted a new video", + Options: []bees.PlaceholderDescriptor{ + { + Name: "channelUrl", + Description: "The url of the channel push notification was sent from", + Type: "url", + }, + { + Name: "vidUrl", + Description: "The url of the video relevant to the push notification", + Type: "url", + }, + }, + }, + { + Namespace: factory.Name(), + Name: "change_video", + Description: "The channel updated a video", + Options: []bees.PlaceholderDescriptor{ + { + Name: "channelUrl", + Description: "The url of the channel push notification was sent from", + Type: "url", + }, + { + Name: "vidUrl", + Description: "The url of the video relevant to the push notification", + Type: "url", + }, + }, + }, + } + return events +} + +func init() { + f := YoutubeBeeFactory{} + bees.RegisterFactory(&f) +} diff --git a/hives.go b/hives.go index 4734a6f7..1bfc23d2 100644 --- a/hives.go +++ b/hives.go @@ -65,4 +65,5 @@ import ( _ "github.com/muesli/beehive/bees/twiliobee" _ "github.com/muesli/beehive/bees/twitterbee" _ "github.com/muesli/beehive/bees/webbee" + _ "github.com/muesli/beehive/bees/youtubebee" )