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"
)