this repo has no description
at icy/tolqpt 11 kB view raw
1package db 2 3import ( 4 "database/sql" 5 "fmt" 6 "log" 7 "net/url" 8 "slices" 9 "strings" 10 "time" 11 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 "tangled.org/core/appview/models" 14 "tangled.org/core/orm" 15) 16 17const TimeframeMonths = 7 18 19func MakeProfileTimeline(e Execer, forDid string) (*models.ProfileTimeline, error) { 20 timeline := models.ProfileTimeline{ 21 ByMonth: make([]models.ByMonth, TimeframeMonths), 22 } 23 currentMonth := time.Now().Month() 24 timeframe := fmt.Sprintf("-%d months", TimeframeMonths) 25 26 pulls, err := GetPullsByOwnerDid(e, forDid, timeframe) 27 if err != nil { 28 return nil, fmt.Errorf("error getting pulls by owner did: %w", err) 29 } 30 31 // group pulls by month 32 for _, pull := range pulls { 33 pullMonth := pull.Created.Month() 34 35 if currentMonth-pullMonth >= TimeframeMonths { 36 // shouldn't happen; but times are weird 37 continue 38 } 39 40 idx := currentMonth - pullMonth 41 items := &timeline.ByMonth[idx].PullEvents.Items 42 43 *items = append(*items, &pull) 44 } 45 46 issues, err := GetIssues( 47 e, 48 orm.FilterEq("did", forDid), 49 orm.FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)), 50 ) 51 if err != nil { 52 return nil, fmt.Errorf("error getting issues by owner did: %w", err) 53 } 54 55 for _, issue := range issues { 56 issueMonth := issue.Created.Month() 57 58 if currentMonth-issueMonth >= TimeframeMonths { 59 // shouldn't happen; but times are weird 60 continue 61 } 62 63 idx := currentMonth - issueMonth 64 items := &timeline.ByMonth[idx].IssueEvents.Items 65 66 *items = append(*items, &issue) 67 } 68 69 repos, err := GetRepos(e, 0, orm.FilterEq("did", forDid)) 70 if err != nil { 71 return nil, fmt.Errorf("error getting all repos by did: %w", err) 72 } 73 74 for _, repo := range repos { 75 // TODO: get this in the original query; requires COALESCE because nullable 76 var sourceRepo *models.Repo 77 if repo.Source != "" { 78 sourceRepo, err = GetRepoByAtUri(e, repo.Source) 79 if err != nil { 80 return nil, err 81 } 82 } 83 84 repoMonth := repo.Created.Month() 85 86 if currentMonth-repoMonth >= TimeframeMonths { 87 // shouldn't happen; but times are weird 88 continue 89 } 90 91 idx := currentMonth - repoMonth 92 93 items := &timeline.ByMonth[idx].RepoEvents 94 *items = append(*items, models.RepoEvent{ 95 Repo: &repo, 96 Source: sourceRepo, 97 }) 98 } 99 100 return &timeline, nil 101} 102 103func UpsertProfile(tx *sql.Tx, profile *models.Profile) error { 104 defer tx.Rollback() 105 106 // update links 107 _, err := tx.Exec(`delete from profile_links where did = ?`, profile.Did) 108 if err != nil { 109 return err 110 } 111 // update vanity stats 112 _, err = tx.Exec(`delete from profile_stats where did = ?`, profile.Did) 113 if err != nil { 114 return err 115 } 116 117 // update pinned repos 118 _, err = tx.Exec(`delete from profile_pinned_repositories where did = ?`, profile.Did) 119 if err != nil { 120 return err 121 } 122 123 includeBskyValue := 0 124 if profile.IncludeBluesky { 125 includeBskyValue = 1 126 } 127 128 _, err = tx.Exec( 129 `insert or replace into profile ( 130 did, 131 avatar, 132 description, 133 include_bluesky, 134 location, 135 pronouns 136 ) 137 values (?, ?, ?, ?, ?, ?)`, 138 profile.Did, 139 profile.Avatar, 140 profile.Description, 141 includeBskyValue, 142 profile.Location, 143 profile.Pronouns, 144 ) 145 146 if err != nil { 147 log.Println("profile", "err", err) 148 return err 149 } 150 151 for _, link := range profile.Links { 152 if link == "" { 153 continue 154 } 155 156 _, err := tx.Exec( 157 `insert into profile_links (did, link) values (?, ?)`, 158 profile.Did, 159 link, 160 ) 161 162 if err != nil { 163 log.Println("profile_links", "err", err) 164 return err 165 } 166 } 167 168 for _, v := range profile.Stats { 169 if v.Kind == "" { 170 continue 171 } 172 173 _, err := tx.Exec( 174 `insert into profile_stats (did, kind) values (?, ?)`, 175 profile.Did, 176 v.Kind, 177 ) 178 179 if err != nil { 180 log.Println("profile_stats", "err", err) 181 return err 182 } 183 } 184 185 for _, pin := range profile.PinnedRepos { 186 if pin == "" { 187 continue 188 } 189 190 _, err := tx.Exec( 191 `insert into profile_pinned_repositories (did, at_uri) values (?, ?)`, 192 profile.Did, 193 pin, 194 ) 195 196 if err != nil { 197 log.Println("profile_pinned_repositories", "err", err) 198 return err 199 } 200 } 201 202 return tx.Commit() 203} 204 205func GetProfiles(e Execer, filters ...orm.Filter) (map[string]*models.Profile, error) { 206 var conditions []string 207 var args []any 208 for _, filter := range filters { 209 conditions = append(conditions, filter.Condition()) 210 args = append(args, filter.Arg()...) 211 } 212 213 whereClause := "" 214 if conditions != nil { 215 whereClause = " where " + strings.Join(conditions, " and ") 216 } 217 218 profilesQuery := fmt.Sprintf( 219 `select 220 id, 221 did, 222 description, 223 include_bluesky, 224 location, 225 pronouns 226 from 227 profile 228 %s`, 229 whereClause, 230 ) 231 rows, err := e.Query(profilesQuery, args...) 232 if err != nil { 233 return nil, err 234 } 235 236 profileMap := make(map[string]*models.Profile) 237 for rows.Next() { 238 var profile models.Profile 239 var includeBluesky int 240 var pronouns sql.Null[string] 241 242 err = rows.Scan(&profile.ID, &profile.Did, &profile.Description, &includeBluesky, &profile.Location, &pronouns) 243 if err != nil { 244 return nil, err 245 } 246 247 if includeBluesky != 0 { 248 profile.IncludeBluesky = true 249 } 250 251 if pronouns.Valid { 252 profile.Pronouns = pronouns.V 253 } 254 255 profileMap[profile.Did] = &profile 256 } 257 if err = rows.Err(); err != nil { 258 return nil, err 259 } 260 261 // populate profile links 262 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(profileMap)), ", ") 263 args = make([]any, len(profileMap)) 264 i := 0 265 for did := range profileMap { 266 args[i] = did 267 i++ 268 } 269 270 linksQuery := fmt.Sprintf("select link, did from profile_links where did in (%s)", inClause) 271 rows, err = e.Query(linksQuery, args...) 272 if err != nil { 273 return nil, err 274 } 275 idxs := make(map[string]int) 276 for did := range profileMap { 277 idxs[did] = 0 278 } 279 for rows.Next() { 280 var link, did string 281 if err = rows.Scan(&link, &did); err != nil { 282 return nil, err 283 } 284 285 idx := idxs[did] 286 profileMap[did].Links[idx] = link 287 idxs[did] = idx + 1 288 } 289 290 pinsQuery := fmt.Sprintf("select at_uri, did from profile_pinned_repositories where did in (%s)", inClause) 291 rows, err = e.Query(pinsQuery, args...) 292 if err != nil { 293 return nil, err 294 } 295 idxs = make(map[string]int) 296 for did := range profileMap { 297 idxs[did] = 0 298 } 299 for rows.Next() { 300 var link syntax.ATURI 301 var did string 302 if err = rows.Scan(&link, &did); err != nil { 303 return nil, err 304 } 305 306 idx := idxs[did] 307 profileMap[did].PinnedRepos[idx] = link 308 idxs[did] = idx + 1 309 } 310 311 return profileMap, nil 312} 313 314func GetProfile(e Execer, did string) (*models.Profile, error) { 315 var profile models.Profile 316 var pronouns sql.Null[string] 317 var avatar sql.Null[string] 318 319 profile.Did = did 320 321 includeBluesky := 0 322 323 err := e.QueryRow( 324 `select avatar, description, include_bluesky, location, pronouns from profile where did = ?`, 325 did, 326 ).Scan(&avatar, &profile.Description, &includeBluesky, &profile.Location, &pronouns) 327 if err == sql.ErrNoRows { 328 profile := models.Profile{} 329 profile.Did = did 330 return &profile, nil 331 } 332 333 if err != nil { 334 return nil, err 335 } 336 337 if includeBluesky != 0 { 338 profile.IncludeBluesky = true 339 } 340 341 if pronouns.Valid { 342 profile.Pronouns = pronouns.V 343 } 344 345 if avatar.Valid { 346 profile.Avatar = avatar.V 347 } 348 349 rows, err := e.Query(`select link from profile_links where did = ?`, did) 350 if err != nil { 351 return nil, err 352 } 353 defer rows.Close() 354 i := 0 355 for rows.Next() { 356 if err := rows.Scan(&profile.Links[i]); err != nil { 357 return nil, err 358 } 359 i++ 360 } 361 362 rows, err = e.Query(`select kind from profile_stats where did = ?`, did) 363 if err != nil { 364 return nil, err 365 } 366 defer rows.Close() 367 i = 0 368 for rows.Next() { 369 if err := rows.Scan(&profile.Stats[i].Kind); err != nil { 370 return nil, err 371 } 372 value, err := GetVanityStat(e, profile.Did, profile.Stats[i].Kind) 373 if err != nil { 374 return nil, err 375 } 376 profile.Stats[i].Value = value 377 i++ 378 } 379 380 rows, err = e.Query(`select at_uri from profile_pinned_repositories where did = ?`, did) 381 if err != nil { 382 return nil, err 383 } 384 defer rows.Close() 385 i = 0 386 for rows.Next() { 387 if err := rows.Scan(&profile.PinnedRepos[i]); err != nil { 388 return nil, err 389 } 390 i++ 391 } 392 393 return &profile, nil 394} 395 396func GetVanityStat(e Execer, did string, stat models.VanityStatKind) (uint64, error) { 397 query := "" 398 var args []any 399 switch stat { 400 case models.VanityStatMergedPRCount: 401 query = `select count(id) from pulls where owner_did = ? and state = ?` 402 args = append(args, did, models.PullMerged) 403 case models.VanityStatClosedPRCount: 404 query = `select count(id) from pulls where owner_did = ? and state = ?` 405 args = append(args, did, models.PullClosed) 406 case models.VanityStatOpenPRCount: 407 query = `select count(id) from pulls where owner_did = ? and state = ?` 408 args = append(args, did, models.PullOpen) 409 case models.VanityStatOpenIssueCount: 410 query = `select count(id) from issues where did = ? and open = 1` 411 args = append(args, did) 412 case models.VanityStatClosedIssueCount: 413 query = `select count(id) from issues where did = ? and open = 0` 414 args = append(args, did) 415 case models.VanityStatRepositoryCount: 416 query = `select count(id) from repos where did = ?` 417 args = append(args, did) 418 } 419 420 var result uint64 421 err := e.QueryRow(query, args...).Scan(&result) 422 if err != nil { 423 return 0, err 424 } 425 426 return result, nil 427} 428 429func ValidateProfile(e Execer, profile *models.Profile) error { 430 // ensure description is not too long 431 if len(profile.Description) > 256 { 432 return fmt.Errorf("Entered bio is too long.") 433 } 434 435 // ensure description is not too long 436 if len(profile.Location) > 40 { 437 return fmt.Errorf("Entered location is too long.") 438 } 439 440 // ensure pronouns are not too long 441 if len(profile.Pronouns) > 40 { 442 return fmt.Errorf("Entered pronouns are too long.") 443 } 444 445 // ensure links are in order 446 err := validateLinks(profile) 447 if err != nil { 448 return err 449 } 450 451 // ensure all pinned repos are either own repos or collaborating repos 452 repos, err := GetRepos(e, 0, orm.FilterEq("did", profile.Did)) 453 if err != nil { 454 log.Printf("getting repos for %s: %s", profile.Did, err) 455 } 456 457 collaboratingRepos, err := CollaboratingIn(e, profile.Did) 458 if err != nil { 459 log.Printf("getting collaborating repos for %s: %s", profile.Did, err) 460 } 461 462 var validRepos []syntax.ATURI 463 for _, r := range repos { 464 validRepos = append(validRepos, r.RepoAt()) 465 } 466 for _, r := range collaboratingRepos { 467 validRepos = append(validRepos, r.RepoAt()) 468 } 469 470 for _, pinned := range profile.PinnedRepos { 471 if pinned == "" { 472 continue 473 } 474 if !slices.Contains(validRepos, pinned) { 475 return fmt.Errorf("Invalid pinned repo: `%s, does not belong to own or collaborating repos", pinned) 476 } 477 } 478 479 return nil 480} 481 482func validateLinks(profile *models.Profile) error { 483 for i, link := range profile.Links { 484 if link == "" { 485 continue 486 } 487 488 parsedURL, err := url.Parse(link) 489 if err != nil { 490 return fmt.Errorf("Invalid URL '%s': %v\n", link, err) 491 } 492 493 if parsedURL.Scheme == "" { 494 if strings.HasPrefix(link, "//") { 495 profile.Links[i] = "https:" + link 496 } else { 497 profile.Links[i] = "https://" + link 498 } 499 continue 500 } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { 501 return fmt.Errorf("Warning: URL '%s' has unusual scheme: %s\n", link, parsedURL.Scheme) 502 } 503 504 // catch relative paths 505 if parsedURL.Host == "" { 506 return fmt.Errorf("Warning: URL '%s' appears to be a relative path\n", link) 507 } 508 } 509 return nil 510}