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