forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package db
2
3import (
4 "sort"
5 "time"
6
7 "github.com/bluesky-social/indigo/atproto/syntax"
8)
9
10type TimelineEvent struct {
11 *Repo
12 *Follow
13 *Star
14
15 EventAt time.Time
16
17 // optional: populate only if Repo is a fork
18 Source *Repo
19
20 // optional: populate only if event is Follow
21 *Profile
22 *FollowStats
23 *FollowStatus
24
25 // optional: populate only if event is Repo
26 IsStarred bool
27 StarCount int64
28}
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, limit int, loggedInUserDid string) ([]TimelineEvent, error) {
33 var events []TimelineEvent
34
35 repos, err := getTimelineRepos(e, limit, loggedInUserDid)
36 if err != nil {
37 return nil, err
38 }
39
40 stars, err := getTimelineStars(e, limit, loggedInUserDid)
41 if err != nil {
42 return nil, err
43 }
44
45 follows, err := getTimelineFollows(e, limit, loggedInUserDid)
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 fetchStarStatuses(e Execer, loggedInUserDid string, repos []Repo) (map[string]bool, error) {
67 if loggedInUserDid == "" {
68 return nil, nil
69 }
70
71 var repoAts []syntax.ATURI
72 for _, r := range repos {
73 repoAts = append(repoAts, r.RepoAt())
74 }
75
76 return GetStarStatuses(e, loggedInUserDid, repoAts)
77}
78
79func getRepoStarInfo(repo *Repo, starStatuses map[string]bool) (bool, int64) {
80 var isStarred bool
81 if starStatuses != nil {
82 isStarred = starStatuses[repo.RepoAt().String()]
83 }
84
85 var starCount int64
86 if repo.RepoStats != nil {
87 starCount = int64(repo.RepoStats.StarCount)
88 }
89
90 return isStarred, starCount
91}
92
93func getTimelineRepos(e Execer, limit int, loggedInUserDid string) ([]TimelineEvent, error) {
94 repos, err := GetRepos(e, limit)
95 if err != nil {
96 return nil, err
97 }
98
99 // fetch all source repos
100 var args []string
101 for _, r := range repos {
102 if r.Source != "" {
103 args = append(args, r.Source)
104 }
105 }
106
107 var origRepos []Repo
108 if args != nil {
109 origRepos, err = GetRepos(e, 0, FilterIn("at_uri", args))
110 }
111 if err != nil {
112 return nil, err
113 }
114
115 uriToRepo := make(map[string]Repo)
116 for _, r := range origRepos {
117 uriToRepo[r.RepoAt().String()] = r
118 }
119
120 starStatuses, err := fetchStarStatuses(e, loggedInUserDid, repos)
121 if err != nil {
122 return nil, err
123 }
124
125 var events []TimelineEvent
126 for _, r := range repos {
127 var source *Repo
128 if r.Source != "" {
129 if origRepo, ok := uriToRepo[r.Source]; ok {
130 source = &origRepo
131 }
132 }
133
134 isStarred, starCount := getRepoStarInfo(&r, starStatuses)
135
136 events = append(events, TimelineEvent{
137 Repo: &r,
138 EventAt: r.Created,
139 Source: source,
140 IsStarred: isStarred,
141 StarCount: starCount,
142 })
143 }
144
145 return events, nil
146}
147
148func getTimelineStars(e Execer, limit int, loggedInUserDid string) ([]TimelineEvent, error) {
149 stars, err := GetStars(e, limit)
150 if err != nil {
151 return nil, err
152 }
153
154 // filter star records without a repo
155 n := 0
156 for _, s := range stars {
157 if s.Repo != nil {
158 stars[n] = s
159 n++
160 }
161 }
162 stars = stars[:n]
163
164 var repos []Repo
165 for _, s := range stars {
166 repos = append(repos, *s.Repo)
167 }
168
169 starStatuses, err := fetchStarStatuses(e, loggedInUserDid, repos)
170 if err != nil {
171 return nil, err
172 }
173
174 var events []TimelineEvent
175 for _, s := range stars {
176 isStarred, starCount := getRepoStarInfo(s.Repo, starStatuses)
177
178 events = append(events, TimelineEvent{
179 Star: &s,
180 EventAt: s.Created,
181 IsStarred: isStarred,
182 StarCount: starCount,
183 })
184 }
185
186 return events, nil
187}
188
189func getTimelineFollows(e Execer, limit int, loggedInUserDid string) ([]TimelineEvent, error) {
190 follows, err := GetFollows(e, limit)
191 if err != nil {
192 return nil, err
193 }
194
195 var subjects []string
196 for _, f := range follows {
197 subjects = append(subjects, f.SubjectDid)
198 }
199
200 if subjects == nil {
201 return nil, nil
202 }
203
204 profiles, err := GetProfiles(e, FilterIn("did", subjects))
205 if err != nil {
206 return nil, err
207 }
208
209 followStatMap, err := GetFollowerFollowingCounts(e, subjects)
210 if err != nil {
211 return nil, err
212 }
213
214 var followStatuses map[string]FollowStatus
215 if loggedInUserDid != "" {
216 followStatuses, err = GetFollowStatuses(e, loggedInUserDid, subjects)
217 if err != nil {
218 return nil, err
219 }
220 }
221
222 var events []TimelineEvent
223 for _, f := range follows {
224 profile, _ := profiles[f.SubjectDid]
225 followStatMap, _ := followStatMap[f.SubjectDid]
226
227 followStatus := IsNotFollowing
228 if followStatuses != nil {
229 followStatus = followStatuses[f.SubjectDid]
230 }
231
232 events = append(events, TimelineEvent{
233 Follow: &f,
234 Profile: profile,
235 FollowStats: &followStatMap,
236 FollowStatus: &followStatus,
237 EventAt: f.FollowedAt,
238 })
239 }
240
241 return events, nil
242}