1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2024-10-30 11:46:16 +00:00
mycorrhiza/history/feed.go

217 lines
5.3 KiB
Go
Raw Normal View History

package history
import (
2021-10-25 21:51:36 +00:00
"errors"
"fmt"
2021-10-25 21:51:36 +00:00
"net/url"
"strings"
"time"
"github.com/bouncepaw/mycorrhiza/cfg"
"github.com/gorilla/feeds"
)
const changeGroupMaxSize = 100
2021-10-25 21:51:36 +00:00
func recentChangesFeed(opts FeedOptions) *feeds.Feed {
feed := &feeds.Feed{
Title: cfg.WikiName + " (recent changes)",
Link: &feeds.Link{Href: cfg.URL},
Description: fmt.Sprintf("List of %d recent changes on the wiki", changeGroupMaxSize),
Updated: time.Now(),
}
revs := newRecentChangesStream()
2021-10-25 21:51:36 +00:00
groups := opts.grouping.Group(revs)
for _, grp := range groups {
2021-10-25 21:51:36 +00:00
item := grp.feedItem(opts)
feed.Add(&item)
}
return feed
}
// RecentChangesRSS creates recent changes feed in RSS format.
2021-10-25 21:51:36 +00:00
func RecentChangesRSS(opts FeedOptions) (string, error) {
return recentChangesFeed(opts).ToRss()
}
// RecentChangesAtom creates recent changes feed in Atom format.
2021-10-25 21:51:36 +00:00
func RecentChangesAtom(opts FeedOptions) (string, error) {
return recentChangesFeed(opts).ToAtom()
}
// RecentChangesJSON creates recent changes feed in JSON format.
2021-10-25 21:51:36 +00:00
func RecentChangesJSON(opts FeedOptions) (string, error) {
return recentChangesFeed(opts).ToJSON()
}
// revisionGroup is a slice of revisions, ordered most recent first.
2021-10-25 21:51:36 +00:00
type revisionGroup []Revision
func newRevisionGroup(rev Revision) revisionGroup {
return revisionGroup([]Revision{rev})
}
func (grp *revisionGroup) addRevision(rev Revision) {
*grp = append(*grp, rev)
}
func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) {
var (
currentYear int
currentMonth time.Month
)
for _, rev := range revs {
if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear {
currentYear = rev.Time.Year()
currentMonth = rev.Time.Month()
res = append(res, newRevisionGroup(rev))
} else {
res[len(res)-1].addRevision(rev)
}
}
return res
}
// groupRevisionsByPeriodFromNow groups close-together revisions and returns the first changeGroupMaxSize (30) groups.
2021-10-25 21:51:36 +00:00
// If two revisions happened within period of each other, they are put in the same group.
func groupRevisionsByPeriod(revs recentChangesStream, period time.Duration) (res []revisionGroup) {
nextRev := revs.iterator()
rev, empty := nextRev()
if empty {
2021-10-25 21:51:36 +00:00
return res
}
currTime := rev.Time
currGroup := newRevisionGroup(rev)
for {
rev, done := nextRev()
if done {
return append(res, currGroup)
}
if currTime.Sub(rev.Time) < period && currGroup[0].Username == rev.Username {
currGroup.addRevision(rev)
2021-10-25 21:51:36 +00:00
} else {
res = append(res, currGroup)
if len(res) == changeGroupMaxSize {
return res
}
currGroup = newRevisionGroup(rev)
2021-10-25 21:51:36 +00:00
}
currTime = rev.Time
}
}
2021-10-25 21:51:36 +00:00
func (grp revisionGroup) feedItem(opts FeedOptions) feeds.Item {
return feeds.Item{
Title: grp.title(opts.groupOrder),
// groups for feeds should have the same author for all revisions
Author: &feeds.Author{Name: grp[0].Username},
2021-10-25 21:51:36 +00:00
Id: grp[len(grp)-1].Hash,
Description: grp.descriptionForFeed(opts.groupOrder),
Created: grp[len(grp)-1].Time, // earliest revision
Updated: grp[0].Time, // latest revision
Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()},
}
}
2021-10-25 21:51:36 +00:00
func (grp revisionGroup) title(order FeedGroupOrder) string {
var message string
switch order {
case NewToOld:
message = grp[0].Message
case OldToNew:
message = grp[len(grp)-1].Message
}
author := grp[0].Username
if len(grp) == 1 {
return fmt.Sprintf("%s by %s", message, author)
} else {
return fmt.Sprintf("%d edits by %s (%s, ...)", len(grp), author, message)
}
}
2021-10-25 21:51:36 +00:00
func (grp revisionGroup) descriptionForFeed(order FeedGroupOrder) string {
builder := strings.Builder{}
2021-10-25 21:51:36 +00:00
switch order {
case NewToOld:
for _, rev := range grp {
builder.WriteString(rev.descriptionForFeed())
}
case OldToNew:
for i := len(grp) - 1; i >= 0; i-- {
builder.WriteString(grp[i].descriptionForFeed())
}
}
return builder.String()
}
2021-10-25 21:51:36 +00:00
type FeedOptions struct {
grouping FeedGrouping
groupOrder FeedGroupOrder
}
func ParseFeedOptions(query url.Values) (FeedOptions, error) {
grouping, err := parseFeedGrouping(query)
if err != nil {
return FeedOptions{}, err
}
groupOrder, err := parseFeedGroupOrder(query)
if err != nil {
return FeedOptions{}, err
}
return FeedOptions{grouping, groupOrder}, nil
}
type FeedGrouping interface {
Group(recentChangesStream) []revisionGroup
2021-10-25 21:51:36 +00:00
}
func parseFeedGrouping(query url.Values) (FeedGrouping, error) {
if query.Get("period") == "" {
return NormalFeedGrouping{}, nil
} else {
period, err := time.ParseDuration(query.Get("period"))
if err != nil {
return nil, err
}
return PeriodFeedGrouping{Period: period}, nil
}
}
type NormalFeedGrouping struct{}
func (NormalFeedGrouping) Group(revs recentChangesStream) (res []revisionGroup) {
for _, rev := range revs.next(changeGroupMaxSize) {
2021-10-25 21:51:36 +00:00
res = append(res, newRevisionGroup(rev))
}
return res
}
type PeriodFeedGrouping struct {
Period time.Duration
}
func (g PeriodFeedGrouping) Group(revs recentChangesStream) (res []revisionGroup) {
2021-10-25 21:51:36 +00:00
return groupRevisionsByPeriod(revs, g.Period)
}
type FeedGroupOrder int
const (
NewToOld FeedGroupOrder = iota
OldToNew FeedGroupOrder = iota
)
func parseFeedGroupOrder(query url.Values) (FeedGroupOrder, error) {
switch query.Get("order") {
case "", "old-to-new":
return OldToNew, nil
case "new-to-old":
2021-10-25 21:51:36 +00:00
return NewToOld, nil
}
return 0, errors.New("unknown order")
}