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