1package db
2
3import (
4 "sort"
5 "time"
6)
7
8type TimelineEvent struct {
9 *Repo
10 *Follow
11 *Star
12
13 EventAt time.Time
14
15 // optional: populate only if Repo is a fork
16 Source *Repo
17
18 // optional: populate only if event is Follow
19 *Profile
20 *FollowStats
21}
22
23const Limit = 50
24
25// TODO: this gathers heterogenous events from different sources and aggregates
26// them in code; if we did this entirely in sql, we could order and limit and paginate easily
27func MakeTimeline(e Execer) ([]TimelineEvent, error) {
28 var events []TimelineEvent
29
30 repos, err := getTimelineRepos(e)
31 if err != nil {
32 return nil, err
33 }
34
35 stars, err := getTimelineStars(e)
36 if err != nil {
37 return nil, err
38 }
39
40 follows, err := getTimelineFollows(e)
41 if err != nil {
42 return nil, err
43 }
44
45 events = append(events, repos...)
46 events = append(events, stars...)
47 events = append(events, follows...)
48
49 sort.Slice(events, func(i, j int) bool {
50 return events[i].EventAt.After(events[j].EventAt)
51 })
52
53 // Limit the slice to 100 events
54 if len(events) > Limit {
55 events = events[:Limit]
56 }
57
58 return events, nil
59}
60
61func getTimelineRepos(e Execer) ([]TimelineEvent, error) {
62 repos, err := GetRepos(e, Limit)
63 if err != nil {
64 return nil, err
65 }
66
67 // fetch all source repos
68 var args []string
69 for _, r := range repos {
70 if r.Source != "" {
71 args = append(args, r.Source)
72 }
73 }
74
75 var origRepos []Repo
76 if args != nil {
77 origRepos, err = GetRepos(e, 0, FilterIn("at_uri", args))
78 }
79 if err != nil {
80 return nil, err
81 }
82
83 uriToRepo := make(map[string]Repo)
84 for _, r := range origRepos {
85 uriToRepo[r.RepoAt().String()] = r
86 }
87
88 var events []TimelineEvent
89 for _, r := range repos {
90 var source *Repo
91 if r.Source != "" {
92 if origRepo, ok := uriToRepo[r.Source]; ok {
93 source = &origRepo
94 }
95 }
96
97 events = append(events, TimelineEvent{
98 Repo: &r,
99 EventAt: r.Created,
100 Source: source,
101 })
102 }
103
104 return events, nil
105}
106
107func getTimelineStars(e Execer) ([]TimelineEvent, error) {
108 stars, err := GetStars(e, Limit)
109 if err != nil {
110 return nil, err
111 }
112
113 // filter star records without a repo
114 n := 0
115 for _, s := range stars {
116 if s.Repo != nil {
117 stars[n] = s
118 n++
119 }
120 }
121 stars = stars[:n]
122
123 var events []TimelineEvent
124 for _, s := range stars {
125 events = append(events, TimelineEvent{
126 Star: &s,
127 EventAt: s.Created,
128 })
129 }
130
131 return events, nil
132}
133
134func getTimelineFollows(e Execer) ([]TimelineEvent, error) {
135 follows, err := GetFollows(e, Limit)
136 if err != nil {
137 return nil, err
138 }
139
140 var subjects []string
141 for _, f := range follows {
142 subjects = append(subjects, f.SubjectDid)
143 }
144
145 if subjects == nil {
146 return nil, nil
147 }
148
149 profiles, err := GetProfiles(e, FilterIn("did", subjects))
150 if err != nil {
151 return nil, err
152 }
153
154 followStatMap, err := GetFollowerFollowingCounts(e, subjects)
155 if err != nil {
156 return nil, err
157 }
158
159 var events []TimelineEvent
160 for _, f := range follows {
161 profile, _ := profiles[f.SubjectDid]
162 followStatMap, _ := followStatMap[f.SubjectDid]
163
164 events = append(events, TimelineEvent{
165 Follow: &f,
166 Profile: profile,
167 FollowStats: &followStatMap,
168 EventAt: f.FollowedAt,
169 })
170 }
171
172 return events, nil
173}