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}