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