📜 feeds is a multi-protocol / multi-platform feed aggregation service that produces twtxt feeds for consumption by twtxt clients. https://feeds.twtxt.net
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

224 lines
4.7 KiB

package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/divan/num2words"
"github.com/dustin/go-humanize"
"github.com/robfig/cron"
log "github.com/sirupsen/logrus"
)
// JobSpec ...
type JobSpec struct {
Schedule string
Factory JobFactory
}
func NewJobSpec(schedule string, factory JobFactory) JobSpec {
return JobSpec{schedule, factory}
}
var (
Jobs map[string]JobSpec
StartupJobs map[string]JobSpec
)
func init() {
Jobs = map[string]JobSpec{
"RotateFeeds": NewJobSpec("@hourly", NewRotateFeedsJob),
"UpdateFeeds": NewJobSpec("@every 5m", NewUpdateFeedsJob),
"TikTokBot": NewJobSpec("0 0,30 * * * *", NewTikTokJob),
}
StartupJobs = map[string]JobSpec{
"RotateFeeds": Jobs["RotateFeeds"],
}
}
type JobFactory func(conf *Config) cron.Job
type RotateFeedsJob struct {
conf *Config
}
func NewRotateFeedsJob(conf *Config) cron.Job {
return &RotateFeedsJob{conf: conf}
}
func (job *RotateFeedsJob) Run() {
conf := job.conf
files, err := WalkMatch(conf.DataDir, "*.txt")
if err != nil {
log.WithError(err).Error("error reading feeds directory")
return
}
for _, file := range files {
stat, err := os.Stat(file)
if err != nil {
log.WithError(err).Error("error getting feed size")
continue
}
if stat.Size() > conf.MaxFeedSize {
log.Infof(
"rotating %s with size %s > %s",
BaseWithoutExt(file),
humanize.Bytes(uint64(stat.Size())),
humanize.Bytes(uint64(conf.MaxFeedSize)),
)
if err := RotateFile(file); err != nil {
log.WithError(err).Error("error rotating feed")
}
}
}
}
type UpdateFeedsJob struct {
conf *Config
}
func NewUpdateFeedsJob(conf *Config) cron.Job {
return &UpdateFeedsJob{conf: conf}
}
func (job *UpdateFeedsJob) Run() {
conf := job.conf
for name, feed := range conf.Feeds {
u, err := ParseURI(feed.URI)
if err != nil {
log.WithError(err).Errorf("error parsing feed %s: %s", name, feed.URI)
} else {
switch u.Type {
case "rss", "http", "https":
if err := UpdateRSSFeed(conf, name, feed.URI); err != nil {
log.WithError(err).Errorf("error updating rss feed %s: %s", name, feed.URI)
}
case "twitter":
if err := UpdateTwitterFeed(conf, name, u.Config); err != nil {
log.WithError(err).Errorf("error updating twitter feed %s: %s", name, feed.URI)
}
default:
log.Warnf("error unknown feed type %s: %s", name, feed.URI)
}
}
}
}
type TikTokJob struct {
conf *Config
name string
url string
symbols map[int]string
}
func NewTikTokJob(conf *Config) cron.Job {
symbols := map[int]string{
0: "🕛", 30: "🕧",
100: "🕐", 130: "🕜",
200: "🕑", 230: "🕝",
300: "🕒", 330: "🕞",
400: "🕓", 430: "🕟",
500: "🕔", 530: "🕠",
600: "🕕", 630: "🕡",
700: "🕖", 730: "🕢",
800: "🕗", 830: "🕣",
900: "🕘", 930: "🕤",
1000: "🕙", 1030: "🕥",
1100: "🕚", 1130: "🕦",
1200: "🕛", 1230: "🕧",
}
name := "tiktok"
url := fmt.Sprintf("@<%s %s>", name, URLForFeed(conf, name))
feed := &Feed{
Name: name,
Description: fmt.Sprintf(
"I am @%s an automated feed that twts every 30m with the current time (UTC)",
name,
),
}
fn := filepath.Join(conf.DataDir, fmt.Sprintf("%s.png", feed.Name))
if Exists(fn) && feed.Avatar == "" {
feed.Avatar = fmt.Sprintf("%s/%s/avatar.png", conf.BaseURL, feed.Name)
if avatarHash, err := FastHashFile(fn); err == nil {
feed.Avatar += "#" + avatarHash
} else {
log.WithError(err).Warnf("error updating avatar hash for %s", feed.Name)
}
}
conf.Feeds[name] = feed
return &TikTokJob{
conf: conf,
name: name,
url: url,
symbols: symbols,
}
}
func (job *TikTokJob) Run() {
conf := job.conf
fn := filepath.Join(conf.DataDir, fmt.Sprintf("%s.txt", job.name))
f, err := os.OpenFile(fn, os.O_APPEND|os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
log.WithError(err).Error("error opening file for writing")
return
}
defer f.Close()
now := time.Now().UTC()
hour := now.Hour() % 12
min := now.Minute()
var key int
if hour == 0 {
key = hour + min
} else {
key = (hour * 100) + min
}
sym := job.symbols[key]
var clock string
if hour == 0 {
clock = "twelve"
} else {
clock = num2words.Convert(hour)
}
if min == 0 {
clock += " o'clock"
} else if min == 30 {
clock += " thirty"
} else {
clock = fmt.Sprintf("%s past %s", num2words.Convert(min), clock)
}
if now.Hour() < 6 {
clock += " in the morning 😴"
} else if now.Hour() < 12 {
clock += " 🌞"
} else if now.Hour() < 18 {
clock += " in the afternoon 🌅"
} else {
clock += " in the evening 🌛"
}
if err := AppendTwt(f, fmt.Sprintf("%s The time is now %s", sym, clock)); err != nil {
log.WithError(err).Error("error writing @tiktok feed")
}
}