diff --git a/main.go b/main.go index 6d8f86d..f542e30 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "github.com/velour/catbase/plugins/fact" "github.com/velour/catbase/plugins/leftpad" "github.com/velour/catbase/plugins/reminder" + "github.com/velour/catbase/plugins/rss" "github.com/velour/catbase/plugins/talker" "github.com/velour/catbase/plugins/your" "github.com/velour/catbase/plugins/zork" @@ -57,6 +58,7 @@ func main() { b.AddHandler("reminder", reminder.New(b)) b.AddHandler("babbler", babbler.New(b)) b.AddHandler("zork", zork.New(b)) + b.AddHandler("rss", rss.New(b)) // catches anything left, will always return true b.AddHandler("factoid", fact.New(b)) diff --git a/plugins/rss/rss.go b/plugins/rss/rss.go new file mode 100644 index 0000000..bc7bee5 --- /dev/null +++ b/plugins/rss/rss.go @@ -0,0 +1,119 @@ +package rss + +import ( + "fmt" + "strings" + "time" + + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" + + "github.com/mmcdole/gofeed" +) + +type RSSPlugin struct { + Bot bot.Bot + cache map[string]*cacheItem + shelfLife time.Duration + maxLines int +} + +type cacheItem struct { + key string + data []string + currentLine int + expiration time.Time +} + +func (c *cacheItem) getCurrentPage(maxLines int) string { + if len(c.data) <= maxLines { + return strings.Join(c.data, "\n") + } + + start := c.currentLine + end := start + maxLines + if end > len(c.data) { + end = len(c.data) + } + + page := strings.Join(c.data[start:end], "\n") + + if end - start == maxLines { + c.currentLine = end + } else { + c.currentLine = maxLines-(end-start) + page += "\n" + page += strings.Join(c.data[0:c.currentLine], "\n") + } + + return page +} + +func New(bot bot.Bot) *RSSPlugin { + return &RSSPlugin{ + Bot: bot, + cache: map[string]*cacheItem{}, + shelfLife: time.Minute * 20, + maxLines: 5, + } +} + +func (p *RSSPlugin) Message(message msg.Message) bool { + tokens := strings.Fields(message.Body) + numTokens := len(tokens) + + if numTokens == 2 && strings.ToLower(tokens[0]) == "rss" { + if item, ok := p.cache[strings.ToLower(tokens[1])]; ok && time.Now().Before(item.expiration) { + p.Bot.SendMessage(message.Channel, item.getCurrentPage(p.maxLines)) + return true + } else { + fp := gofeed.NewParser() + feed, err := fp.ParseURL(tokens[1]) + if err != nil { + p.Bot.SendMessage(message.Channel, fmt.Sprintf("RSS error: %s", err.Error())) + return true + } + item := &cacheItem{ + key: strings.ToLower(tokens[1]), + data: []string{feed.Title}, + expiration: time.Now().Add(p.shelfLife), + currentLine: 0, + } + + for _, feedItem := range feed.Items { + item.data = append(item.data, feedItem.Title) + } + + p.cache[strings.ToLower(tokens[1])] = item + + p.Bot.SendMessage(message.Channel, item.getCurrentPage(p.maxLines)) + return true + } + } + + return false +} + +func (p *RSSPlugin) LoadData() { + // This bot has no data to load +} + +// Help responds to help requests. Every plugin must implement a help function. +func (p *RSSPlugin) Help(channel string, parts []string) { + p.Bot.SendMessage(channel, "try '!rss http://rss.cnn.com/rss/edition.rss'") +} + +// Empty event handler because this plugin does not do anything on event recv +func (p *RSSPlugin) Event(kind string, message msg.Message) bool { + return false +} + +// Handler for bot's own messages +func (p *RSSPlugin) BotMessage(message msg.Message) bool { + return false +} + +// Register any web URLs desired +func (p *RSSPlugin) RegisterWeb() *string { + return nil +} diff --git a/plugins/rss/rss_test.go b/plugins/rss/rss_test.go new file mode 100644 index 0000000..7c20fd1 --- /dev/null +++ b/plugins/rss/rss_test.go @@ -0,0 +1,58 @@ +package rss + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" + "github.com/velour/catbase/bot/user" +) + +func makeMessage(payload string) msg.Message { + isCmd := strings.HasPrefix(payload, "!") + if isCmd { + payload = payload[1:] + } + return msg.Message{ + User: &user.User{Name: "tester"}, + Channel: "test", + Body: payload, + Command: isCmd, + } +} + +func TestRSS(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + res := c.Message(makeMessage("!rss http://rss.cnn.com/rss/edition.rss")) + assert.Len(t, mb.Messages, 1) + assert.True(t, res) +} + +func TestRSSPaging(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + for i := 0; i < 20; i++ { + res := c.Message(makeMessage("!rss http://rss.cnn.com/rss/edition.rss")) + assert.True(t, res) + } + + assert.Len(t, mb.Messages, 20) + + for i := 0; i < len(mb.Messages); i++ { + if i > 0 && strings.Contains(mb.Messages[i], "CNN.com - RSS Channel - Intl Homepage - News") { + fmt.Println("----------------") + fmt.Println(mb.Messages[i]) + fmt.Println("----------------") + break + } + fmt.Println("----------------") + fmt.Println(mb.Messages[i]) + fmt.Println("----------------") + } +}