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}