func containsAny(item string, filterWords []string) bool {
	itemWords := strings.FieldsFunc(item, func(c rune) bool {
		return !unicode.IsLetter(c) && !unicode.IsNumber(c)
	})
	for _, itemWord := range itemWords {
		for _, filterWord := range filterWords {
			if filterWord == itemWord {
				return true
			}
		}
	}
	return false
}
containsAny takes a string and an array of words and returns whether any of the words
in the list are contained in the string. The words in the string are considered to be
separated by any non-alphanumeric character.
package rssbot
import (
	"errors"
	"fmt"
	"html"
	"net/http"
	"strconv"
	"strings"
	"time"
	"unicode"
	"github.com/die-net/lrucache"
	"github.com/gregjones/httpcache"
	"github.com/matrix-org/go-neb/database"
	"github.com/matrix-org/go-neb/polling"
	"github.com/matrix-org/go-neb/types"
	"github.com/mmcdole/gofeed"
	"github.com/prometheus/client_golang/prometheus"
	log "github.com/sirupsen/logrus"
	mevt "maunium.net/go/mautrix/event"
	"maunium.net/go/mautrix/id"
)
const ServiceType = "rssbot"
var cachingClient *http.Client
var (
	pollCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
		Name: "goneb_rss_polls_total",
		Help: "The number of feed polls from RSS services",
	}, []string{"http_status"})
)
const minPollingIntervalSeconds = 60 * 5  
type includeRules struct {
	Author []string `json:"author"`
	Title []string `json:"title"`
	Description []string `json:"description"`
}
type Service struct {
	types.DefaultService
	Feeds map[string]struct {
		PollIntervalMins int `json:"poll_interval_mins"`
		Rooms []id.RoomID `json:"rooms"`
		IsFailing bool `json:"is_failing"`
		FeedUpdatedTimestampSecs int64 `json:"last_updated_ts_secs"`
		MustInclude includeRules `json:"must_include"`
		MustNotInclude includeRules `json:"must_not_include"`
		NextPollTimestampSecs int64
		RecentGUIDs []string
	} `json:"feeds"`
}
func (s *Service) Register(oldService types.Service, client types.MatrixClient) error {
	if len(s.Feeds) == 0 {
		var numOldFeeds int
		oldFeedService, ok := oldService.(*Service)
		if !ok {
			log.WithField("service", oldService).Error("Old service isn't an rssbot.Service")
		} else {
			numOldFeeds = len(oldFeedService.Feeds)
		}
		if numOldFeeds == 0 {
			return errors.New("An RSS feed must be specified")
		}
		return nil
	}
	for feedURL, feedInfo := range s.Feeds {
		if _, err := readFeed(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)
		}
	}
	s.joinRooms(client)
	return nil
}
func (s *Service) joinRooms(client types.MatrixClient) {
	roomSet := make(map[id.RoomID]bool)
	for _, feedInfo := range s.Feeds {
		for _, roomID := range feedInfo.Rooms {
			roomSet[roomID] = true
		}
	}
	for roomID := range roomSet {
		if _, err := client.JoinRoom(roomID.String(), "", nil); err != nil {
			log.WithFields(log.Fields{
				log.ErrorKey: err,
				"room_id":    roomID,
			}).Error("Failed to join room")
		}
	}
}
func (s *Service) PostRegister(oldService types.Service) {
	if len(s.Feeds) == 0 {  
		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 *Service) OnPoll(cli types.MatrixClient) time.Time {
	logger := log.WithFields(log.Fields{
		"service_id":   s.ServiceID(),
		"service_type": s.ServiceType(),
	})
	now := time.Now().Unix()  
	var pollFeeds []string
	for u, feedInfo := range s.Feeds {
		if feedInfo.NextPollTimestampSecs == 0 || now >= feedInfo.NextPollTimestampSecs {
			pollFeeds = append(pollFeeds, u)
		}
	}
	if len(pollFeeds) == 0 {
		return s.nextTimestamp()
	}
	for _, u := range pollFeeds {
		feed, items, err := s.queryFeed(u)
		if err != nil {
			logger.WithField("feed_url", u).WithError(err).Error("Failed to query feed")
			incrementMetrics(u, err)
			continue
		}
		incrementMetrics(u, nil)
		logger.WithFields(log.Fields{
			"feed_url":   u,
			"feed_items": len(feed.Items),
			"new_items":  len(items),
		}).Info("Sending new items")
		for i := len(items) - 1; i >= 0; i-- {
			item := items[i]
			if err := s.sendToRooms(cli, u, feed, item); err != nil {
				logger.WithFields(log.Fields{
					"feed_url":   u,
					log.ErrorKey: err,
					"item":       item,
				}).Error("Failed to send item to room")
			}
		}
	}
	if _, err := database.GetServiceDB().StoreService(s); err != nil {
		logger.WithError(err).Error("Failed to persist next poll times for service")
	}
	return s.nextTimestamp()
}
func incrementMetrics(urlStr string, err error) {
	if err != nil {
		herr, ok := err.(gofeed.HTTPError)
		statusCode := 0  
		if ok {
			statusCode = herr.StatusCode
		}
		pollCounter.With(prometheus.Labels{"http_status": strconv.Itoa(statusCode)}).Inc()
	} else {
		pollCounter.With(prometheus.Labels{"http_status": "200"}).Inc()  
	}
}
func (s *Service) nextTimestamp() time.Time {
	var earliestNextTS int64
	for _, feedInfo := range s.Feeds {
		if earliestNextTS == 0 || feedInfo.NextPollTimestampSecs < earliestNextTS {
			earliestNextTS = feedInfo.NextPollTimestampSecs
		}
	}
	now := time.Now().Unix()
	if earliestNextTS <= now {
		earliestNextTS = now + 60
	}
	return time.Unix(earliestNextTS, 0)
}
func (s *Service) queryFeed(feedURL string) (*gofeed.Feed, []gofeed.Item, error) {
	log.WithField("feed_url", feedURL).Info("Querying feed")
	var items []gofeed.Item
	feed, err := readFeed(feedURL)
	if err == nil && len(feed.Items) == 0 {
		err = errors.New("feed has 0 items")
	}
	if err != nil {
		f := s.Feeds[feedURL]
		f.IsFailing = true
		s.Feeds[feedURL] = f
		return nil, items, err
	}
	ensureItemsHaveGUIDs(feed)
	if s.Feeds[feedURL].NextPollTimestampSecs != 0 {
		items = s.newItems(feedURL, feed.Items)
	}
	now := time.Now().Unix()  
	nextPollTSSec := now + minPollingIntervalSeconds
	if s.Feeds[feedURL].PollIntervalMins > int(minPollingIntervalSeconds/60) {
		nextPollTSSec = now + int64(s.Feeds[feedURL].PollIntervalMins*60)
	}
	f := s.Feeds[feedURL]
	maxGuids := 2 * len(feed.Items)
	if len(f.RecentGUIDs) > maxGuids {
		maxGuids = len(f.RecentGUIDs)  
	}
	if maxGuids > 10000 {
		maxGuids = 10000
	}
	lastSet := uniqueStrings(f.RecentGUIDs)  
	thisSet := uniqueGuids(feed.Items)       
	guids := append(thisSet, lastSet...)     
	guids = uniqueStrings(guids)
	if len(guids) > maxGuids {
		guids = guids[0:maxGuids]
	}
	f.NextPollTimestampSecs = nextPollTSSec
	f.FeedUpdatedTimestampSecs = now
	f.RecentGUIDs = guids
	f.IsFailing = false
	s.Feeds[feedURL] = f
	return feed, items, nil
}