this repo has no description
1package db 2 3import ( 4 "database/sql" 5 "fmt" 6 "log" 7 "time" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "tangled.sh/tangled.sh/core/api/tangled" 11) 12 13type RepoEvent struct { 14 Repo *Repo 15 Source *Repo 16} 17 18type ProfileTimeline struct { 19 ByMonth []ByMonth 20} 21 22type ByMonth struct { 23 RepoEvents []RepoEvent 24 IssueEvents IssueEvents 25 PullEvents PullEvents 26} 27 28func (b ByMonth) IsEmpty() bool { 29 return len(b.RepoEvents) == 0 && 30 len(b.IssueEvents.Items) == 0 && 31 len(b.PullEvents.Items) == 0 32} 33 34type IssueEvents struct { 35 Items []*Issue 36} 37 38type IssueEventStats struct { 39 Open int 40 Closed int 41} 42 43func (i IssueEvents) Stats() IssueEventStats { 44 var open, closed int 45 for _, issue := range i.Items { 46 if issue.Open { 47 open += 1 48 } else { 49 closed += 1 50 } 51 } 52 53 return IssueEventStats{ 54 Open: open, 55 Closed: closed, 56 } 57} 58 59type PullEvents struct { 60 Items []*Pull 61} 62 63func (p PullEvents) Stats() PullEventStats { 64 var open, merged, closed int 65 for _, pull := range p.Items { 66 switch pull.State { 67 case PullOpen: 68 open += 1 69 case PullMerged: 70 merged += 1 71 case PullClosed: 72 closed += 1 73 } 74 } 75 76 return PullEventStats{ 77 Open: open, 78 Merged: merged, 79 Closed: closed, 80 } 81} 82 83type PullEventStats struct { 84 Closed int 85 Open int 86 Merged int 87} 88 89const TimeframeMonths = 7 90 91func MakeProfileTimeline(e Execer, forDid string) (*ProfileTimeline, error) { 92 timeline := ProfileTimeline{ 93 ByMonth: make([]ByMonth, TimeframeMonths), 94 } 95 currentMonth := time.Now().Month() 96 timeframe := fmt.Sprintf("-%d months", TimeframeMonths) 97 98 pulls, err := GetPullsByOwnerDid(e, forDid, timeframe) 99 if err != nil { 100 return nil, fmt.Errorf("error getting pulls by owner did: %w", err) 101 } 102 103 // group pulls by month 104 for _, pull := range pulls { 105 pullMonth := pull.Created.Month() 106 107 if currentMonth-pullMonth >= TimeframeMonths { 108 // shouldn't happen; but times are weird 109 continue 110 } 111 112 idx := currentMonth - pullMonth 113 items := &timeline.ByMonth[idx].PullEvents.Items 114 115 *items = append(*items, &pull) 116 } 117 118 issues, err := GetIssuesByOwnerDid(e, forDid, timeframe) 119 if err != nil { 120 return nil, fmt.Errorf("error getting issues by owner did: %w", err) 121 } 122 123 for _, issue := range issues { 124 issueMonth := issue.Created.Month() 125 126 if currentMonth-issueMonth >= TimeframeMonths { 127 // shouldn't happen; but times are weird 128 continue 129 } 130 131 idx := currentMonth - issueMonth 132 items := &timeline.ByMonth[idx].IssueEvents.Items 133 134 *items = append(*items, &issue) 135 } 136 137 repos, err := GetAllReposByDid(e, forDid) 138 if err != nil { 139 return nil, fmt.Errorf("error getting all repos by did: %w", err) 140 } 141 142 for _, repo := range repos { 143 // TODO: get this in the original query; requires COALESCE because nullable 144 var sourceRepo *Repo 145 if repo.Source != "" { 146 sourceRepo, err = GetRepoByAtUri(e, repo.Source) 147 if err != nil { 148 return nil, err 149 } 150 } 151 152 repoMonth := repo.Created.Month() 153 154 if currentMonth-repoMonth >= TimeframeMonths { 155 // shouldn't happen; but times are weird 156 continue 157 } 158 159 idx := currentMonth - repoMonth 160 161 items := &timeline.ByMonth[idx].RepoEvents 162 *items = append(*items, RepoEvent{ 163 Repo: &repo, 164 Source: sourceRepo, 165 }) 166 } 167 168 return &timeline, nil 169} 170 171type Profile struct { 172 // ids 173 ID int 174 Did string 175 176 // data 177 Description string 178 IncludeBluesky bool 179 Location string 180 Links [5]string 181 Stats [2]VanityStat 182 PinnedRepos [6]syntax.ATURI 183} 184 185func (p Profile) IsLinksEmpty() bool { 186 for _, l := range p.Links { 187 if l != "" { 188 return false 189 } 190 } 191 return true 192} 193 194func (p Profile) IsStatsEmpty() bool { 195 for _, s := range p.Stats { 196 if s.Kind != "" { 197 return false 198 } 199 } 200 return true 201} 202 203func (p Profile) IsPinnedReposEmpty() bool { 204 for _, r := range p.PinnedRepos { 205 if r != "" { 206 return false 207 } 208 } 209 return true 210} 211 212type VanityStatKind string 213 214const ( 215 VanityStatMergedPRCount VanityStatKind = "merged-pull-request-count" 216 VanityStatClosedPRCount VanityStatKind = "closed-pull-request-count" 217 VanityStatOpenPRCount VanityStatKind = "open-pull-request-count" 218 VanityStatOpenIssueCount VanityStatKind = "open-issue-count" 219 VanityStatClosedIssueCount VanityStatKind = "closed-issue-count" 220 VanityStatRepositoryCount VanityStatKind = "repository-count" 221) 222 223func (v VanityStatKind) String() string { 224 switch v { 225 case VanityStatMergedPRCount: 226 return "Merged PRs" 227 case VanityStatClosedPRCount: 228 return "Closed PRs" 229 case VanityStatOpenPRCount: 230 return "Open PRs" 231 case VanityStatOpenIssueCount: 232 return "Open Issues" 233 case VanityStatClosedIssueCount: 234 return "Closed Issues" 235 case VanityStatRepositoryCount: 236 return "Repositories" 237 } 238 return "" 239} 240 241type VanityStat struct { 242 Kind VanityStatKind 243 Value uint64 244} 245 246func (p *Profile) ProfileAt() syntax.ATURI { 247 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.Did, tangled.ActorProfileNSID, "self")) 248} 249 250func UpsertProfile(tx *sql.Tx, profile *Profile) error { 251 defer tx.Rollback() 252 253 // update links 254 _, err := tx.Exec(`delete from profile_links where did = ?`, profile.Did) 255 if err != nil { 256 return err 257 } 258 // update vanity stats 259 _, err = tx.Exec(`delete from profile_stats where did = ?`, profile.Did) 260 if err != nil { 261 return err 262 } 263 264 // update pinned repos 265 _, err = tx.Exec(`delete from profile_pinned_repositories where did = ?`, profile.Did) 266 if err != nil { 267 return err 268 } 269 270 includeBskyValue := 0 271 if profile.IncludeBluesky { 272 includeBskyValue = 1 273 } 274 275 _, err = tx.Exec( 276 `insert or replace into profile ( 277 did, 278 description, 279 include_bluesky, 280 location 281 ) 282 values (?, ?, ?, ?)`, 283 profile.Did, 284 profile.Description, 285 includeBskyValue, 286 profile.Location, 287 ) 288 289 if err != nil { 290 log.Println("profile", "err", err) 291 return err 292 } 293 294 for _, link := range profile.Links { 295 if link == "" { 296 continue 297 } 298 299 _, err := tx.Exec( 300 `insert into profile_links (did, link) values (?, ?)`, 301 profile.Did, 302 link, 303 ) 304 305 if err != nil { 306 log.Println("profile_links", "err", err) 307 return err 308 } 309 } 310 311 for _, v := range profile.Stats { 312 if v.Kind == "" { 313 continue 314 } 315 316 _, err := tx.Exec( 317 `insert into profile_stats (did, kind) values (?, ?)`, 318 profile.Did, 319 v.Kind, 320 ) 321 322 if err != nil { 323 log.Println("profile_stats", "err", err) 324 return err 325 } 326 } 327 328 for _, pin := range profile.PinnedRepos { 329 if pin == "" { 330 continue 331 } 332 333 _, err := tx.Exec( 334 `insert into profile_pinned_repositories (did, at_uri) values (?, ?)`, 335 profile.Did, 336 pin, 337 ) 338 339 if err != nil { 340 log.Println("profile_pinned_repositories") 341 return err 342 } 343 } 344 345 return tx.Commit() 346} 347 348func GetProfile(e Execer, did string) (*Profile, error) { 349 var profile Profile 350 profile.Did = did 351 352 includeBluesky := 0 353 err := e.QueryRow( 354 `select description, include_bluesky, location from profile where did = ?`, 355 did, 356 ).Scan(&profile.Description, &includeBluesky, &profile.Location) 357 if err == sql.ErrNoRows { 358 profile := Profile{} 359 profile.Did = did 360 return &profile, nil 361 } 362 363 if err != nil { 364 return nil, err 365 } 366 367 if includeBluesky != 0 { 368 profile.IncludeBluesky = true 369 } 370 371 rows, err := e.Query(`select link from profile_links where did = ?`, did) 372 if err != nil { 373 return nil, err 374 } 375 defer rows.Close() 376 i := 0 377 for rows.Next() { 378 if err := rows.Scan(&profile.Links[i]); err != nil { 379 return nil, err 380 } 381 i++ 382 } 383 384 rows, err = e.Query(`select kind from profile_stats where did = ?`, did) 385 if err != nil { 386 return nil, err 387 } 388 defer rows.Close() 389 i = 0 390 for rows.Next() { 391 if err := rows.Scan(&profile.Stats[i].Kind); err != nil { 392 return nil, err 393 } 394 value, err := GetVanityStat(e, profile.Did, profile.Stats[i].Kind) 395 if err != nil { 396 return nil, err 397 } 398 profile.Stats[i].Value = value 399 i++ 400 } 401 402 rows, err = e.Query(`select at_uri from profile_pinned_repositories where did = ?`, did) 403 if err != nil { 404 return nil, err 405 } 406 defer rows.Close() 407 i = 0 408 for rows.Next() { 409 if err := rows.Scan(&profile.PinnedRepos[i]); err != nil { 410 return nil, err 411 } 412 i++ 413 } 414 415 return &profile, nil 416} 417 418func GetVanityStat(e Execer, did string, stat VanityStatKind) (uint64, error) { 419 query := "" 420 var args []any 421 switch stat { 422 case VanityStatMergedPRCount: 423 query = `select count(id) from pulls where owner_did = ? and state = ?` 424 args = append(args, did, PullMerged) 425 case VanityStatClosedPRCount: 426 query = `select count(id) from pulls where owner_did = ? and state = ?` 427 args = append(args, did, PullClosed) 428 case VanityStatOpenPRCount: 429 query = `select count(id) from pulls where owner_did = ? and state = ?` 430 args = append(args, did, PullOpen) 431 case VanityStatOpenIssueCount: 432 query = `select count(id) from issues where owner_did = ? and open = 1` 433 args = append(args, did) 434 case VanityStatClosedIssueCount: 435 query = `select count(id) from issues where owner_did = ? and open = 0` 436 args = append(args, did) 437 case VanityStatRepositoryCount: 438 query = `select count(id) from repos where did = ?` 439 args = append(args, did) 440 } 441 442 var result uint64 443 err := e.QueryRow(query, args...).Scan(&result) 444 if err != nil { 445 return 0, err 446 } 447 448 return result, nil 449}