-
Notifications
You must be signed in to change notification settings - Fork 0
/
subscription.go
196 lines (165 loc) · 4.93 KB
/
subscription.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
// Package subscription handles the state of a subscription to a given feed
package subscription
import (
"encoding/hex"
"sort"
"time"
"github.com/vmihailenco/msgpack"
"github.com/pkg/errors"
"github.com/satori/go.uuid"
"github.com/LegoRemix/cmdfeed/rss"
)
// State represents the current state of a subscription to a feed
type State interface {
EntryList() []Entry
UniqueID() uuid.UUID
}
type impl struct {
Entries []Entry `msgpack:"entries"`
Feed rss.State `msgpack:"feed"`
Opts Options `msgpack:"options"`
UUID uuid.UUID `msgpack:"uuid"`
}
// Options lets one control exactly how a subscription state is managed
type Options struct {
// IncludeRemovedEntries controls whether we keep entries not in the current FeedState in the Subscription
IncludeRemovedEntries bool `msgpack:"include_removed_entries,omitempty"`
}
// Entry represents a single entry in a subscription feed, it has slightly different semantics from rss.Item
type Entry struct {
Title string `msgpack:"title,omitempty"`
Description string `msgpack:"description,omitempty"`
Content string `msgpack:"content,omitempty"`
Link string `msgpack:"link,omitempty"`
Updated time.Time `msgpack:"updated,omitempty"`
Published time.Time `msgpack:"published,omitempty"`
GUID string `msgpack:"guid,omitempty"`
Categories []string `msgpack:"categories,omitempty"`
ImageTitle string `msgpack:"image_title,omitempty"`
ImageURL string `msgpack:"image_url, omitempty"`
AuthorName string `msgpack:"author_name,omitempty"`
AuthorEmail string `msgpack:"author_email,omitempty"`
}
// StateWithOptions returns a new copy of the state with the given options
func (s *impl) StateWithOptions(opt Options) State {
return &impl{
Entries: s.Entries,
Feed: s.Feed,
Opts: opt,
UUID: s.UUID,
}
}
// EntryList returns the list of entries in this sub State
func (s *impl) EntryList() []Entry {
return s.Entries
}
// returns a UUID for this st
func (s *impl) UniqueID() uuid.UUID {
return s.UUID
}
// ID creates a unique ID for the entry
func (e Entry) ID() (string, error) {
if e.GUID != "" {
return e.GUID, nil
}
hashed, err := msgpack.Marshal(e)
if err != nil {
return "", err
}
return hex.EncodeToString(hashed), nil
}
// feedStateToEntries extracts all of the Entrys from a FeedState
func feedStateToEntries(rssState rss.State) []Entry {
items := rssState.Feed().Items
result := make([]Entry, 0, len(items))
for _, item := range items {
// grab a valid update time for this sub entry
updated := rssState.FetchTime()
if item.Updated != nil {
// if we have a valid update time, we hash that this so we can easily see updates
updated = item.Updated.UTC()
}
// grab a valid publish time for this sub entry
published := rssState.FetchTime()
if item.Published != nil {
published = *item.Published
}
entry := Entry{
Title: item.Title,
Description: item.Description,
Content: item.Content,
Link: item.Link,
Updated: updated,
Published: published,
Categories: item.Categories,
GUID: item.GUID,
}
if item.Author != nil {
entry.AuthorName = item.Author.Name
entry.AuthorEmail = item.Author.Email
}
if item.Image != nil {
entry.ImageURL = item.Image.URL
entry.ImageTitle = item.Image.Title
}
result = append(result, entry)
}
return result
}
// mergeEntries merges two lists of entries and then sorts by updateTime
func mergeEntries(left, right []Entry) ([]Entry, error) {
// deduplicate the two entry lists
entrySet := make(map[string]Entry)
for _, lst := range [][]Entry{left, right} {
for _, entry := range lst {
id, err := entry.ID()
if err != nil {
return nil, err
}
entrySet[id] = entry
}
}
// unpack the set back into an array
result := make([]Entry, 0, len(entrySet))
for _, v := range entrySet {
result = append(result, v)
}
// sort the entries by updated time
sort.Slice(result, func(i, j int) bool { return result[i].Updated.Before(result[j].Updated) })
return result, nil
}
// NewState creates a new subscription.State instance against a given URL
func NewState(url string, opt Options) (State, error) {
feedState, err := rss.NewState(url)
if err != nil {
return nil, err
}
entries := feedStateToEntries(feedState)
return &impl{
Feed: feedState,
Entries: entries,
Opts: opt,
UUID: uuid.NewV4(),
}, nil
}
// Update creates a newly State of the subscription with updated entries
func (s *impl) Update() (State, error) {
updated, err := s.Feed.UpdatedState()
if err != nil {
return nil, err
}
//create a newly updated list of entries
newEntries := feedStateToEntries(updated)
if s.Opts.IncludeRemovedEntries {
newEntries, err = mergeEntries(newEntries, s.Entries)
if err != nil {
return nil, errors.Wrap(err, "failed to merge subscription entries")
}
}
return &impl{
Feed: updated,
Entries: newEntries,
Opts: s.Opts,
UUID: s.UUID,
}, nil
}