// Copyright (C) 2021 nytpu // SPDX-License-Identifier: AGPL-3.0-or-later // For more license details, see LICENSE or . // This file incorporates code from makeworld's amfora: // https://github.com/makeworld-the-better-one/amfora/blob/master/subscriptions/structs.go // Original source is Copyright 2020 makeworld and is licensed under the // terms of the GNU General Public License Version 3.0. // https://www.gnu.org/licenses/gpl-3.0-standalone.html package core import ( "reflect" "sync" "time" "github.com/mmcdole/gofeed" ) // FullData is used for both reading from json and for storing the global feeds // map. contains read mutexes so concurrency can be used. type FullData struct { Feeds map[string]*Feed `json:"feeds,omitempty"` Pages map[string]*Page `json:"pages,omitempty"` FeedsMu *sync.RWMutex `json:"-"` PagesMu *sync.RWMutex `json:"-"` } // Feed holds all the necessary fields when storing a feed type Feed struct { Title string `json:"title,omitempty"` Link string `json:"link,omitempty"` // link to the human-readable page for the feed. same as FeedLink for gemini feeds. FeedLink string `json:"feedlink,omitempty"` // link to the feed itself. Updated time.Time `json:"updated,omitempty"` Items []*Item `json:"items,omitempty"` } // Item is a single item from a subscripton type Item struct { Title string `json:"title,omitempty"` // if empty, use Link as title Content string `json:"content,omitempty"` Link string `json:"link,omitempty"` Published time.Time `json:"updated,omitempty"` } // Page holds all the necessary fields when storing a page being watched for // changes. type Page struct { Title string `json:"title,omitempty"` Link string `json:"link,omitempty"` Updated time.Time `json:"updated,omitempty"` Hash string `json:"hash,omitempty"` } // gofeedConvert converts a gofeed.Feed into a Feed struct func GofeedConvert(f *gofeed.Feed, title string) (*Feed, error) { new := &Feed{} if title != "" { new.Title = title } else { new.Title = f.Title } if f.UpdatedParsed != nil { new.Updated = *f.UpdatedParsed } else if f.PublishedParsed != nil { new.Updated = *f.PublishedParsed } new.Link = f.Link new.FeedLink = f.FeedLink for _, v := range f.Items { item := &Item{} item.Title = v.Title item.Content = v.Content if v.UpdatedParsed != nil { item.Published = *v.UpdatedParsed } else if v.PublishedParsed != nil { item.Published = *v.PublishedParsed } item.Link = v.Link new.Items = append(new.Items, item) } return new, nil } // InsertFeed will insert or update a core.Feed in a core.FullData func (d *FullData) InsertFeed(new *Feed, remote string) { d.FeedsMu.Lock() oldFeed, ok := d.Feeds[remote] if !ok || !reflect.DeepEqual(new, oldFeed) { // Feeds are different, or there was never an old one if oldFeed != nil && oldFeed.Title != "" { new.Title = oldFeed.Title } d.Feeds[remote] = new d.FeedsMu.Unlock() } else { d.FeedsMu.Unlock() } } // InsertPage will insert or update a core.Page in a core.FullData func (d *FullData) InsertPage(new *Page, remote string) { d.PagesMu.Lock() oldPage, ok := d.Pages[remote] if !ok || d.Pages[remote].Hash != new.Hash { // pages are different, or there was never an old one if oldPage != nil && new.Title == "" && oldPage.Title != "" { new.Title = oldPage.Title } d.Pages[remote] = new d.PagesMu.Unlock() } else { d.PagesMu.Unlock() } } // Implement sort.Interface for Feed // Len returns the length of Items. func (f *Feed) Len() int { return len(f.Items) } // Less compares Published of Items[i], Items[k] and returns true if Items[i] is // before Items[j]. func (f *Feed) Less(i, j int) bool { return f.Items[i].Published.Before(f.Items[j].Published) } // Swap swaps Items[i] and Items[j]. func (f *Feed) Swap(i, j int) { f.Items[i], f.Items[j] = f.Items[j], f.Items[i] } // Lock locks both feed and page mutexes. func (j *FullData) Lock() { j.FeedsMu.Lock() j.PagesMu.Lock() } // Unlock unlocks both feed and page mutexes. func (j *FullData) Unlock() { j.FeedsMu.Unlock() j.PagesMu.Unlock() } // RLock read-locks both feed and page mutexes. func (j *FullData) RLock() { j.FeedsMu.RLock() j.PagesMu.RLock() } // RUnlock read-unlocks both feed and page mutexes. func (j *FullData) RUnlock() { j.FeedsMu.RUnlock() j.PagesMu.RUnlock() } // PageEntry is a single item on a subscriptions page. It is used for both feeds // and pages. type PageEntry struct { Prefix string // whatever goes before the post title to identify the feed. Title string // article/entry title URL string Published time.Time } // PageEntries is reverse chronological list of entries used when generating subs. type PageEntries struct { Entries []*PageEntry } // Implement sort.Interface for PageEntries // Len returns the length of Entries func (e *PageEntries) Len() int { return len(e.Entries) } // Less compares the Published if Entries[i], Entries[j] and returns true if // Entries[i] is /after/ Entries[j] (for reverse chronological sorting) func (e *PageEntries) Less(i, j int) bool { return e.Entries[i].Published.After(e.Entries[j].Published) } // Swap swaps Entries[i] and Entries[j] func (e *PageEntries) Swap(i, j int) { e.Entries[i], e.Entries[j] = e.Entries[j], e.Entries[i] }