2021-10-23 02:00:44 +00:00
|
|
|
package history
|
|
|
|
|
|
|
|
import (
|
2021-10-25 21:51:36 +00:00
|
|
|
"errors"
|
2021-10-23 02:00:44 +00:00
|
|
|
"fmt"
|
2021-10-25 21:51:36 +00:00
|
|
|
"net/url"
|
2021-10-23 02:00:44 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
|
|
|
|
|
|
|
"github.com/gorilla/feeds"
|
|
|
|
)
|
|
|
|
|
2021-10-27 02:45:38 +00:00
|
|
|
const changeGroupMaxSize = 100
|
|
|
|
|
2021-10-25 21:51:36 +00:00
|
|
|
func recentChangesFeed(opts FeedOptions) *feeds.Feed {
|
2021-10-23 02:00:44 +00:00
|
|
|
feed := &feeds.Feed{
|
2021-10-26 22:34:11 +00:00
|
|
|
Title: cfg.WikiName + " (recent changes)",
|
2021-10-23 02:00:44 +00:00
|
|
|
Link: &feeds.Link{Href: cfg.URL},
|
2021-10-27 02:45:38 +00:00
|
|
|
Description: fmt.Sprintf("List of %d recent changes on the wiki", changeGroupMaxSize),
|
2021-10-23 02:00:44 +00:00
|
|
|
Updated: time.Now(),
|
|
|
|
}
|
2021-10-27 02:45:38 +00:00
|
|
|
revs := newRecentChangesStream()
|
2021-10-25 21:51:36 +00:00
|
|
|
groups := opts.grouping.Group(revs)
|
2021-10-23 02:00:44 +00:00
|
|
|
for _, grp := range groups {
|
2021-10-25 21:51:36 +00:00
|
|
|
item := grp.feedItem(opts)
|
2021-10-23 02:00:44 +00:00
|
|
|
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()
|
2021-10-23 02:00:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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()
|
2021-10-23 02:00:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
|
2021-10-27 02:45:38 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2021-10-27 02:45:38 +00:00
|
|
|
// 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.
|
2021-10-27 02:45:38 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-10-27 02:45:38 +00:00
|
|
|
currTime := rev.Time
|
|
|
|
currGroup := newRevisionGroup(rev)
|
|
|
|
for {
|
|
|
|
rev, done := nextRev()
|
|
|
|
if done {
|
|
|
|
return append(res, currGroup)
|
|
|
|
}
|
|
|
|
|
2021-10-26 22:34:11 +00:00
|
|
|
if currTime.Sub(rev.Time) < period && currGroup[0].Username == rev.Username {
|
|
|
|
currGroup.addRevision(rev)
|
2021-10-25 21:51:36 +00:00
|
|
|
} else {
|
2021-10-26 22:34:11 +00:00
|
|
|
res = append(res, currGroup)
|
2021-10-27 02:45:38 +00:00
|
|
|
if len(res) == changeGroupMaxSize {
|
|
|
|
return res
|
|
|
|
}
|
2021-10-26 22:34:11 +00:00
|
|
|
currGroup = newRevisionGroup(rev)
|
2021-10-25 21:51:36 +00:00
|
|
|
}
|
|
|
|
currTime = rev.Time
|
|
|
|
}
|
2021-10-23 02:00:44 +00:00
|
|
|
}
|
|
|
|
|
2021-10-25 21:51:36 +00:00
|
|
|
func (grp revisionGroup) feedItem(opts FeedOptions) feeds.Item {
|
2021-10-23 02:00:44 +00:00
|
|
|
return feeds.Item{
|
2021-10-26 22:34:11 +00:00
|
|
|
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),
|
2021-10-23 02:00:44 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-10-26 22:34:11 +00:00
|
|
|
author := grp[0].Username
|
2021-10-23 02:00:44 +00:00
|
|
|
if len(grp) == 1 {
|
2021-10-26 22:34:11 +00:00
|
|
|
return fmt.Sprintf("%s by %s", message, author)
|
2021-10-23 02:00:44 +00:00
|
|
|
} else {
|
2021-10-26 22:34:11 +00:00
|
|
|
return fmt.Sprintf("%d edits by %s (%s, ...)", len(grp), author, message)
|
2021-10-23 02:00:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-25 21:51:36 +00:00
|
|
|
func (grp revisionGroup) descriptionForFeed(order FeedGroupOrder) string {
|
2021-10-23 02:00:44 +00:00
|
|
|
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())
|
|
|
|
}
|
2021-10-23 02:00:44 +00:00
|
|
|
}
|
|
|
|
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 {
|
2021-10-27 02:45:38 +00:00
|
|
|
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{}
|
|
|
|
|
2021-10-27 02:45:38 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-10-27 02:45:38 +00:00
|
|
|
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") {
|
2021-10-27 02:45:38 +00:00
|
|
|
case "", "old-to-new":
|
2021-10-26 22:34:11 +00:00
|
|
|
return OldToNew, nil
|
|
|
|
case "new-to-old":
|
2021-10-25 21:51:36 +00:00
|
|
|
return NewToOld, nil
|
|
|
|
}
|
|
|
|
return 0, errors.New("unknown order")
|
|
|
|
}
|