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