|
@ -15,25 +15,75 @@ import ( |
|
|
|
|
|
|
|
|
const minPollingIntervalSeconds = 60 // 1 min (News feeds can be genuinely spammy)
|
|
|
const minPollingIntervalSeconds = 60 // 1 min (News feeds can be genuinely spammy)
|
|
|
|
|
|
|
|
|
type feedPoller struct{} |
|
|
|
|
|
|
|
|
type rssBotService struct { |
|
|
|
|
|
types.DefaultService |
|
|
|
|
|
id string |
|
|
|
|
|
serviceUserID string |
|
|
|
|
|
Feeds map[string]struct { // feed_url => { }
|
|
|
|
|
|
PollIntervalMins int `json:"poll_interval_mins"` |
|
|
|
|
|
Rooms []string `json:"rooms"` |
|
|
|
|
|
NextPollTimestampSecs int64 // Internal: When we should poll again
|
|
|
|
|
|
FeedUpdatedTimestampSecs int64 // Internal: The last time the feed was updated
|
|
|
|
|
|
} `json:"feeds"` |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (s *rssBotService) ServiceUserID() string { return s.serviceUserID } |
|
|
|
|
|
func (s *rssBotService) ServiceID() string { return s.id } |
|
|
|
|
|
func (s *rssBotService) ServiceType() string { return "rssbot" } |
|
|
|
|
|
|
|
|
|
|
|
// Register will check the liveness of each RSS feed given. If all feeds check out okay, no error is returned.
|
|
|
|
|
|
func (s *rssBotService) Register(oldService types.Service, client *matrix.Client) error { |
|
|
|
|
|
if len(s.Feeds) == 0 { |
|
|
|
|
|
// this is an error UNLESS the old service had some feeds in which case they are deleting us :(
|
|
|
|
|
|
var numOldFeeds int |
|
|
|
|
|
oldFeedService, ok := oldService.(*rssBotService) |
|
|
|
|
|
if !ok { |
|
|
|
|
|
log.WithField("service", oldService).Error("Old service isn't a rssBotService") |
|
|
|
|
|
} else { |
|
|
|
|
|
numOldFeeds = len(oldFeedService.Feeds) |
|
|
|
|
|
} |
|
|
|
|
|
if numOldFeeds == 0 { |
|
|
|
|
|
return errors.New("An RSS feed must be specified.") |
|
|
|
|
|
} |
|
|
|
|
|
return nil |
|
|
|
|
|
} |
|
|
|
|
|
// Make sure we can parse the feed
|
|
|
|
|
|
for feedURL, feedInfo := range s.Feeds { |
|
|
|
|
|
fp := gofeed.NewParser() |
|
|
|
|
|
if _, err := fp.ParseURL(feedURL); err != nil { |
|
|
|
|
|
return fmt.Errorf("Failed to read URL %s: %s", feedURL, err.Error()) |
|
|
|
|
|
} |
|
|
|
|
|
if len(feedInfo.Rooms) == 0 { |
|
|
|
|
|
return fmt.Errorf("Feed %s has no rooms to send updates to", feedURL) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return nil |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
func (p *feedPoller) IntervalSecs() int64 { return 10 } |
|
|
|
|
|
func (p *feedPoller) OnPoll(s types.Service, cli *matrix.Client) { |
|
|
|
|
|
|
|
|
func (s *rssBotService) PostRegister(oldService types.Service) { |
|
|
|
|
|
if len(s.Feeds) == 0 { // bye-bye :(
|
|
|
|
|
|
logger := log.WithFields(log.Fields{ |
|
|
|
|
|
"service_id": s.ServiceID(), |
|
|
|
|
|
"service_type": s.ServiceType(), |
|
|
|
|
|
}) |
|
|
|
|
|
logger.Info("Deleting service: No feeds remaining.") |
|
|
|
|
|
polling.StopPolling(s) |
|
|
|
|
|
if err := database.GetServiceDB().DeleteService(s.ServiceID()); err != nil { |
|
|
|
|
|
logger.WithError(err).Error("Failed to delete service") |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (s *rssBotService) OnPoll(cli *matrix.Client) time.Time { |
|
|
logger := log.WithFields(log.Fields{ |
|
|
logger := log.WithFields(log.Fields{ |
|
|
"service_id": s.ServiceID(), |
|
|
"service_id": s.ServiceID(), |
|
|
"service_type": s.ServiceType(), |
|
|
"service_type": s.ServiceType(), |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
frService, ok := s.(*rssBotService) |
|
|
|
|
|
if !ok { |
|
|
|
|
|
logger.Error("RSSBot: OnPoll called without a Feed Service instance") |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
now := time.Now().Unix() // Second resolution
|
|
|
now := time.Now().Unix() // Second resolution
|
|
|
|
|
|
|
|
|
// Work out which feeds should be polled
|
|
|
// Work out which feeds should be polled
|
|
|
var pollFeeds []string |
|
|
var pollFeeds []string |
|
|
for u, feedInfo := range frService.Feeds { |
|
|
|
|
|
|
|
|
for u, feedInfo := range s.Feeds { |
|
|
if feedInfo.NextPollTimestampSecs == 0 || now >= feedInfo.NextPollTimestampSecs { |
|
|
if feedInfo.NextPollTimestampSecs == 0 || now >= feedInfo.NextPollTimestampSecs { |
|
|
// re-query this feed
|
|
|
// re-query this feed
|
|
|
pollFeeds = append(pollFeeds, u) |
|
|
pollFeeds = append(pollFeeds, u) |
|
@ -41,12 +91,12 @@ func (p *feedPoller) OnPoll(s types.Service, cli *matrix.Client) { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if len(pollFeeds) == 0 { |
|
|
if len(pollFeeds) == 0 { |
|
|
return |
|
|
|
|
|
|
|
|
return s.nextTimestamp() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Query each feed and send new items to subscribed rooms
|
|
|
// Query each feed and send new items to subscribed rooms
|
|
|
for _, u := range pollFeeds { |
|
|
for _, u := range pollFeeds { |
|
|
feed, items, err := p.queryFeed(frService, u) |
|
|
|
|
|
|
|
|
feed, items, err := s.queryFeed(u) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
logger.WithField("feed_url", u).WithError(err).Error("Failed to query feed") |
|
|
logger.WithField("feed_url", u).WithError(err).Error("Failed to query feed") |
|
|
continue |
|
|
continue |
|
@ -54,7 +104,7 @@ func (p *feedPoller) OnPoll(s types.Service, cli *matrix.Client) { |
|
|
// Loop backwards since [0] is the most recent and we want to send in chronological order
|
|
|
// Loop backwards since [0] is the most recent and we want to send in chronological order
|
|
|
for i := len(items) - 1; i >= 0; i-- { |
|
|
for i := len(items) - 1; i >= 0; i-- { |
|
|
item := items[i] |
|
|
item := items[i] |
|
|
if err := p.sendToRooms(frService, cli, u, feed, item); err != nil { |
|
|
|
|
|
|
|
|
if err := s.sendToRooms(cli, u, feed, item); err != nil { |
|
|
logger.WithFields(log.Fields{ |
|
|
logger.WithFields(log.Fields{ |
|
|
"feed_url": u, |
|
|
"feed_url": u, |
|
|
log.ErrorKey: err, |
|
|
log.ErrorKey: err, |
|
@ -65,13 +115,26 @@ func (p *feedPoller) OnPoll(s types.Service, cli *matrix.Client) { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Persist the service to save the next poll times
|
|
|
// Persist the service to save the next poll times
|
|
|
if _, err := database.GetServiceDB().StoreService(frService); err != nil { |
|
|
|
|
|
|
|
|
if _, err := database.GetServiceDB().StoreService(s); err != nil { |
|
|
logger.WithError(err).Error("Failed to persist next poll times for service") |
|
|
logger.WithError(err).Error("Failed to persist next poll times for service") |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return s.nextTimestamp() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (s *rssBotService) nextTimestamp() time.Time { |
|
|
|
|
|
// return the earliest next poll ts
|
|
|
|
|
|
var earliestNextTs int64 |
|
|
|
|
|
for _, feedInfo := range s.Feeds { |
|
|
|
|
|
if earliestNextTs == 0 || feedInfo.NextPollTimestampSecs < earliestNextTs { |
|
|
|
|
|
earliestNextTs = feedInfo.NextPollTimestampSecs |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return time.Unix(earliestNextTs, 0) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Query the given feed, update relevant timestamps and return NEW items
|
|
|
// Query the given feed, update relevant timestamps and return NEW items
|
|
|
func (p *feedPoller) queryFeed(s *rssBotService, feedURL string) (*gofeed.Feed, []gofeed.Item, error) { |
|
|
|
|
|
|
|
|
func (s *rssBotService) queryFeed(feedURL string) (*gofeed.Feed, []gofeed.Item, error) { |
|
|
log.WithField("feed_url", feedURL).Info("Querying feed") |
|
|
log.WithField("feed_url", feedURL).Info("Querying feed") |
|
|
var items []gofeed.Item |
|
|
var items []gofeed.Item |
|
|
fp := gofeed.NewParser() |
|
|
fp := gofeed.NewParser() |
|
@ -115,11 +178,11 @@ func (p *feedPoller) queryFeed(s *rssBotService, feedURL string) (*gofeed.Feed, |
|
|
// TODO: Handle the 'sy' Syndication extension to control update interval.
|
|
|
// TODO: Handle the 'sy' Syndication extension to control update interval.
|
|
|
// See http://www.feedforall.com/syndication.htm and http://web.resource.org/rss/1.0/modules/syndication/
|
|
|
// See http://www.feedforall.com/syndication.htm and http://web.resource.org/rss/1.0/modules/syndication/
|
|
|
|
|
|
|
|
|
p.updateFeedInfo(s, feedURL, nextPollTsSec, feedLastUpdatedTs) |
|
|
|
|
|
|
|
|
s.updateFeedInfo(feedURL, nextPollTsSec, feedLastUpdatedTs) |
|
|
return feed, items, nil |
|
|
return feed, items, nil |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func (p *feedPoller) updateFeedInfo(s *rssBotService, feedURL string, nextPollTs, feedUpdatedTs int64) { |
|
|
|
|
|
|
|
|
func (s *rssBotService) updateFeedInfo(feedURL string, nextPollTs, feedUpdatedTs int64) { |
|
|
for u := range s.Feeds { |
|
|
for u := range s.Feeds { |
|
|
if u != feedURL { |
|
|
if u != feedURL { |
|
|
continue |
|
|
continue |
|
@ -131,7 +194,7 @@ func (p *feedPoller) updateFeedInfo(s *rssBotService, feedURL string, nextPollTs |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func (p *feedPoller) sendToRooms(s *rssBotService, cli *matrix.Client, feedURL string, feed *gofeed.Feed, item gofeed.Item) error { |
|
|
|
|
|
|
|
|
func (s *rssBotService) sendToRooms(cli *matrix.Client, feedURL string, feed *gofeed.Feed, item gofeed.Item) error { |
|
|
logger := log.WithField("feed_url", feedURL).WithField("title", item.Title) |
|
|
logger := log.WithField("feed_url", feedURL).WithField("title", item.Title) |
|
|
logger.Info("New feed item") |
|
|
logger.Info("New feed item") |
|
|
for _, roomID := range s.Feeds[feedURL].Rooms { |
|
|
for _, roomID := range s.Feeds[feedURL].Rooms { |
|
@ -150,66 +213,6 @@ func itemToHTML(feed *gofeed.Feed, item gofeed.Item) matrix.HTMLMessage { |
|
|
)) |
|
|
)) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
type rssBotService struct { |
|
|
|
|
|
types.DefaultService |
|
|
|
|
|
id string |
|
|
|
|
|
serviceUserID string |
|
|
|
|
|
Feeds map[string]struct { // feed_url => { }
|
|
|
|
|
|
PollIntervalMins int `json:"poll_interval_mins"` |
|
|
|
|
|
Rooms []string `json:"rooms"` |
|
|
|
|
|
NextPollTimestampSecs int64 // Internal: When we should poll again
|
|
|
|
|
|
FeedUpdatedTimestampSecs int64 // Internal: The last time the feed was updated
|
|
|
|
|
|
} `json:"feeds"` |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (s *rssBotService) ServiceUserID() string { return s.serviceUserID } |
|
|
|
|
|
func (s *rssBotService) ServiceID() string { return s.id } |
|
|
|
|
|
func (s *rssBotService) ServiceType() string { return "rssbot" } |
|
|
|
|
|
func (s *rssBotService) Poller() types.Poller { return &feedPoller{} } |
|
|
|
|
|
|
|
|
|
|
|
// Register will check the liveness of each RSS feed given. If all feeds check out okay, no error is returned.
|
|
|
|
|
|
func (s *rssBotService) Register(oldService types.Service, client *matrix.Client) error { |
|
|
|
|
|
if len(s.Feeds) == 0 { |
|
|
|
|
|
// this is an error UNLESS the old service had some feeds in which case they are deleting us :(
|
|
|
|
|
|
var numOldFeeds int |
|
|
|
|
|
oldFeedService, ok := oldService.(*rssBotService) |
|
|
|
|
|
if !ok { |
|
|
|
|
|
log.WithField("service", oldService).Error("Old service isn't a rssBotService") |
|
|
|
|
|
} else { |
|
|
|
|
|
numOldFeeds = len(oldFeedService.Feeds) |
|
|
|
|
|
} |
|
|
|
|
|
if numOldFeeds == 0 { |
|
|
|
|
|
return errors.New("An RSS feed must be specified.") |
|
|
|
|
|
} |
|
|
|
|
|
return nil |
|
|
|
|
|
} |
|
|
|
|
|
// Make sure we can parse the feed
|
|
|
|
|
|
for feedURL, feedInfo := range s.Feeds { |
|
|
|
|
|
fp := gofeed.NewParser() |
|
|
|
|
|
if _, err := fp.ParseURL(feedURL); err != nil { |
|
|
|
|
|
return fmt.Errorf("Failed to read URL %s: %s", feedURL, err.Error()) |
|
|
|
|
|
} |
|
|
|
|
|
if len(feedInfo.Rooms) == 0 { |
|
|
|
|
|
return fmt.Errorf("Feed %s has no rooms to send updates to", feedURL) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return nil |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (s *rssBotService) PostRegister(oldService types.Service) { |
|
|
|
|
|
if len(s.Feeds) == 0 { // bye-bye :(
|
|
|
|
|
|
logger := log.WithFields(log.Fields{ |
|
|
|
|
|
"service_id": s.ServiceID(), |
|
|
|
|
|
"service_type": s.ServiceType(), |
|
|
|
|
|
}) |
|
|
|
|
|
logger.Info("Deleting service: No feeds remaining.") |
|
|
|
|
|
polling.StopPolling(s) |
|
|
|
|
|
if err := database.GetServiceDB().DeleteService(s.ServiceID()); err != nil { |
|
|
|
|
|
logger.WithError(err).Error("Failed to delete service") |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func init() { |
|
|
func init() { |
|
|
types.RegisterService(func(serviceID, serviceUserID, webhookEndpointURL string) types.Service { |
|
|
types.RegisterService(func(serviceID, serviceUserID, webhookEndpointURL string) types.Service { |
|
|
r := &rssBotService{ |
|
|
r := &rssBotService{ |
|
|