goldsmith/plugins/syndicate/syndicate.go
2024-03-03 18:09:21 -08:00

320 lines
6.2 KiB
Go

package syndicate
import (
"bytes"
"fmt"
"net/url"
"sort"
"sync"
"time"
"git.foosoft.net/alex/goldsmith"
"github.com/gorilla/feeds"
)
type feed struct {
config FeedConfig
items itemList
lock sync.Mutex
}
type item struct {
title string
authorName string
authorEmail string
description string
id string
updated time.Time
created time.Time
content string
url string
}
type itemList []item
func (self itemList) Len() int {
return len(self)
}
func (self itemList) Less(i, j int) bool {
if self[i].created != self[j].created {
return self[i].created.Before(self[j].created)
}
if self[i].updated != self[j].updated {
return self[i].updated.Before(self[j].updated)
}
if self[i].url != self[j].url {
return self[i].url < self[j].url
}
return false
}
func (self itemList) Swap(i, j int) {
self[i], self[j] = self[j], self[i]
}
type FeedConfig struct {
AtomPath string
JsonPath string
RssPath string
Title string
Url string
Description string
AuthorName string
AuthorEmail string
Updated time.Time
Created time.Time
Id string
Subtitle string
Copyright string
ImageUrl string
ImageTitle string
ImageWidth int
ImageHeight int
ItemConfig ItemConfig
}
type ItemConfig struct {
TitleKey string
AuthorNameKey string
AuthorEmailKey string
DescriptionKey string
IdKey string
UpdatedKey string
CreatedKey string
ContentKey string
ContentFromFile bool
}
// Syndicate chainable context.
type Syndicate struct {
baseUrl string
feedNameKey string
feeds map[string]*feed
lock sync.Mutex
}
// New creates a new instance of the Syndicate plugin
func New(baseUrl, feedNameKey string) *Syndicate {
return &Syndicate{
baseUrl: baseUrl,
feedNameKey: feedNameKey,
feeds: make(map[string]*feed),
}
}
func (*Syndicate) Name() string {
return "syndicate"
}
func (self *Syndicate) WithFeed(name string, config FeedConfig) *Syndicate {
self.feeds[name] = &feed{config: config}
return self
}
func (self *Syndicate) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
defer context.DispatchFile(inputFile)
getString := func(key string) string {
if len(key) == 0 {
return ""
}
prop, ok := inputFile.Prop(key)
if !ok {
return ""
}
result, ok := prop.(string)
if !ok {
return ""
}
return result
}
getDate := func(key string) time.Time {
if len(key) == 0 {
return time.Time{}
}
prop, ok := inputFile.Prop(key)
if !ok {
return time.Time{}
}
result, ok := prop.(time.Time)
if !ok {
return time.Time{}
}
return result
}
feedName := getString(self.feedNameKey)
if len(feedName) == 0 {
return nil
}
self.lock.Lock()
feed, ok := self.feeds[feedName]
self.lock.Unlock()
if !ok {
return fmt.Errorf("feed %s has is not configured", feedName)
}
baseUrl, err := url.Parse(self.baseUrl)
if err != nil {
return err
}
currUrl, err := url.Parse(inputFile.Path())
if err != nil {
return err
}
item := item{
title: getString(feed.config.ItemConfig.TitleKey),
authorName: getString(feed.config.ItemConfig.AuthorNameKey),
authorEmail: getString(feed.config.ItemConfig.AuthorEmailKey),
description: getString(feed.config.ItemConfig.DescriptionKey),
id: getString(feed.config.ItemConfig.IdKey),
updated: getDate(feed.config.ItemConfig.UpdatedKey),
created: getDate(feed.config.ItemConfig.CreatedKey),
content: getString(feed.config.ItemConfig.ContentKey),
url: baseUrl.ResolveReference(currUrl).String(),
}
if len(item.content) == 0 && feed.config.ItemConfig.ContentFromFile {
var buff bytes.Buffer
if _, err := inputFile.WriteTo(&buff); err != nil {
return err
}
item.content = string(buff.Bytes())
}
if len(item.id) == 0 {
item.id = item.url
}
feed.lock.Lock()
feed.items = append(feed.items, item)
feed.lock.Unlock()
return nil
}
func (self *feed) output(context *goldsmith.Context) error {
feed := feeds.Feed{
Title: self.config.Title,
Link: &feeds.Link{Href: self.config.Url},
Description: self.config.Description,
Updated: self.config.Updated,
Created: self.config.Created,
Id: self.config.Id,
Subtitle: self.config.Subtitle,
Copyright: self.config.Copyright,
}
if len(self.config.AuthorName) > 0 || len(self.config.AuthorEmail) > 0 {
feed.Author = &feeds.Author{
Name: self.config.AuthorName,
Email: self.config.AuthorEmail,
}
}
if len(self.config.ImageUrl) > 0 {
feed.Image = &feeds.Image{
Url: self.config.ImageUrl,
Title: self.config.ImageTitle,
Width: self.config.ImageWidth,
Height: self.config.ImageHeight,
}
}
sort.Sort(self.items)
for _, item := range self.items {
feedItem := feeds.Item{
Title: item.title,
Description: item.description,
Id: item.id,
Updated: item.updated,
Created: item.created,
Content: item.content,
Link: &feeds.Link{Href: item.url},
}
if len(item.authorName) > 0 || len(item.authorEmail) > 0 {
feedItem.Author = &feeds.Author{
Name: item.authorName,
Email: item.authorEmail,
}
}
feed.Items = append(feed.Items, &feedItem)
}
if len(self.config.AtomPath) > 0 {
var buff bytes.Buffer
if err := feed.WriteAtom(&buff); err != nil {
return err
}
file, err := context.CreateFileFromReader(self.config.AtomPath, bytes.NewReader(buff.Bytes()))
if err != nil {
return err
}
context.DispatchFile(file)
}
if len(self.config.RssPath) > 0 {
var buff bytes.Buffer
if err := feed.WriteRss(&buff); err != nil {
return err
}
file, err := context.CreateFileFromReader(self.config.RssPath, bytes.NewReader(buff.Bytes()))
if err != nil {
return err
}
context.DispatchFile(file)
}
if len(self.config.JsonPath) > 0 {
var buff bytes.Buffer
if err := feed.WriteJSON(&buff); err != nil {
return err
}
file, err := context.CreateFileFromReader(self.config.JsonPath, bytes.NewReader(buff.Bytes()))
if err != nil {
return err
}
context.DispatchFile(file)
}
return nil
}
func (self *Syndicate) Finalize(context *goldsmith.Context) error {
for _, feed := range self.feeds {
feed.output(context)
}
return nil
}