Monorepo for Tangled tangled.org
6

Configure Feed

Select the types of activity you want to include in your feed.

1package db 2 3import ( 4 "database/sql" 5 "fmt" 6 "strings" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 "tangled.org/core/api/tangled" 10 "tangled.org/core/appview/models" 11 "tangled.org/core/orm" 12) 13 14// ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs. 15// It will ignore missing refLinks. 16func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 17 var ( 18 issueRefs []models.ReferenceLink 19 pullRefs []models.ReferenceLink 20 ) 21 for _, ref := range refLinks { 22 switch ref.Kind { 23 case models.RefKindIssue: 24 issueRefs = append(issueRefs, ref) 25 case models.RefKindPull: 26 pullRefs = append(pullRefs, ref) 27 } 28 } 29 issueUris, err := findIssueReferences(e, issueRefs) 30 if err != nil { 31 return nil, fmt.Errorf("find issue references: %w", err) 32 } 33 pullUris, err := findPullReferences(e, pullRefs) 34 if err != nil { 35 return nil, fmt.Errorf("find pull references: %w", err) 36 } 37 38 return append(issueUris, pullUris...), nil 39} 40 41func findIssueReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 42 if len(refLinks) == 0 { 43 return nil, nil 44 } 45 vals := make([]string, len(refLinks)) 46 args := make([]any, 0, len(refLinks)*4) 47 for i, ref := range refLinks { 48 vals[i] = "(?, ?, ?, ?)" 49 args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentRkey) 50 } 51 query := fmt.Sprintf( 52 `with input(owner_did, name, issue_id, comment_id) as ( 53 values %s 54 ) 55 select 56 i.at_uri, c.at_uri 57 from input inp 58 join repos r 59 on r.did = inp.owner_did 60 and r.name = inp.name 61 join issues i 62 on i.repo_did = r.repo_did 63 and i.issue_id = inp.issue_id 64 left join comments c 65 on inp.comment_id is not null 66 and c.subject_uri = i.at_uri 67 and c.id = inp.comment_id 68 `, 69 strings.Join(vals, ","), 70 ) 71 rows, err := e.Query(query, args...) 72 if err != nil { 73 return nil, err 74 } 75 defer rows.Close() 76 77 var uris []syntax.ATURI 78 79 for rows.Next() { 80 // Scan rows 81 var issueUri string 82 var commentUri sql.NullString 83 var uri syntax.ATURI 84 if err := rows.Scan(&issueUri, &commentUri); err != nil { 85 return nil, err 86 } 87 if commentUri.Valid { 88 uri = syntax.ATURI(commentUri.String) 89 } else { 90 uri = syntax.ATURI(issueUri) 91 } 92 uris = append(uris, uri) 93 } 94 if err := rows.Err(); err != nil { 95 return nil, fmt.Errorf("iterate rows: %w", err) 96 } 97 98 return uris, nil 99} 100 101func findPullReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 102 if len(refLinks) == 0 { 103 return nil, nil 104 } 105 vals := make([]string, len(refLinks)) 106 args := make([]any, 0, len(refLinks)*4) 107 for i, ref := range refLinks { 108 vals[i] = "(?, ?, ?, ?)" 109 args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentRkey) 110 } 111 query := fmt.Sprintf( 112 `with input(owner_did, name, pull_id, comment_id) as ( 113 values %s 114 ) 115 select 116 p.owner_did, p.rkey, c.at_uri 117 from input inp 118 join repos r 119 on r.did = inp.owner_did 120 and r.name = inp.name 121 join pulls p 122 on p.repo_did = r.repo_did 123 and p.pull_id = inp.pull_id 124 left join comments c 125 on inp.comment_id is not null 126 and c.subject_uri = ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) 127 and c.id = inp.comment_id 128 `, 129 strings.Join(vals, ","), 130 ) 131 rows, err := e.Query(query, args...) 132 if err != nil { 133 return nil, err 134 } 135 defer rows.Close() 136 137 var uris []syntax.ATURI 138 139 for rows.Next() { 140 // Scan rows 141 var pullOwner, pullRkey string 142 var commentUri sql.NullString 143 var uri syntax.ATURI 144 if err := rows.Scan(&pullOwner, &pullRkey, &commentUri); err != nil { 145 return nil, err 146 } 147 if commentUri.Valid { 148 // no-op 149 uri = syntax.ATURI(commentUri.String) 150 } else { 151 uri = syntax.ATURI(fmt.Sprintf( 152 "at://%s/%s/%s", 153 pullOwner, 154 tangled.RepoPullNSID, 155 pullRkey, 156 )) 157 } 158 uris = append(uris, uri) 159 } 160 return uris, nil 161} 162 163func putReferences(tx *sql.Tx, fromAt syntax.ATURI, references []syntax.ATURI) error { 164 err := deleteReferences(tx, fromAt) 165 if err != nil { 166 return fmt.Errorf("delete old reference_links: %w", err) 167 } 168 if len(references) == 0 { 169 return nil 170 } 171 172 values := make([]string, 0, len(references)) 173 args := make([]any, 0, len(references)*2) 174 for _, ref := range references { 175 values = append(values, "(?, ?)") 176 args = append(args, fromAt, ref) 177 } 178 _, err = tx.Exec( 179 fmt.Sprintf( 180 `insert into reference_links (from_at, to_at) 181 values %s`, 182 strings.Join(values, ","), 183 ), 184 args..., 185 ) 186 if err != nil { 187 return fmt.Errorf("insert new reference_links: %w", err) 188 } 189 return nil 190} 191 192func deleteReferences(tx *sql.Tx, fromAt syntax.ATURI) error { 193 _, err := tx.Exec(`delete from reference_links where from_at = ?`, fromAt) 194 return err 195} 196 197func GetReferencesAll(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]syntax.ATURI, error) { 198 var ( 199 conditions []string 200 args []any 201 ) 202 for _, filter := range filters { 203 conditions = append(conditions, filter.Condition()) 204 args = append(args, filter.Arg()...) 205 } 206 207 whereClause := "" 208 if conditions != nil { 209 whereClause = " where " + strings.Join(conditions, " and ") 210 } 211 212 rows, err := e.Query( 213 fmt.Sprintf( 214 `select from_at, to_at from reference_links %s`, 215 whereClause, 216 ), 217 args..., 218 ) 219 if err != nil { 220 return nil, fmt.Errorf("query reference_links: %w", err) 221 } 222 defer rows.Close() 223 224 result := make(map[syntax.ATURI][]syntax.ATURI) 225 226 for rows.Next() { 227 var from, to syntax.ATURI 228 if err := rows.Scan(&from, &to); err != nil { 229 return nil, fmt.Errorf("scan row: %w", err) 230 } 231 232 result[from] = append(result[from], to) 233 } 234 if err := rows.Err(); err != nil { 235 return nil, fmt.Errorf("iterate rows: %w", err) 236 } 237 238 return result, nil 239} 240 241func GetBacklinks(e Execer, target syntax.ATURI) ([]models.RichReferenceLink, error) { 242 rows, err := e.Query( 243 `select from_at from reference_links 244 where to_at = ? and from_at <> to_at`, 245 target, 246 ) 247 if err != nil { 248 return nil, fmt.Errorf("query backlinks: %w", err) 249 } 250 defer rows.Close() 251 252 var ( 253 backlinks []models.RichReferenceLink 254 backlinksMap = make(map[string][]syntax.ATURI) 255 ) 256 for rows.Next() { 257 var from syntax.ATURI 258 if err := rows.Scan(&from); err != nil { 259 return nil, fmt.Errorf("scan row: %w", err) 260 } 261 nsid := from.Collection().String() 262 backlinksMap[nsid] = append(backlinksMap[nsid], from) 263 } 264 if err := rows.Err(); err != nil { 265 return nil, fmt.Errorf("iterate rows: %w", err) 266 } 267 268 var ls []models.RichReferenceLink 269 ls, err = getIssueBacklinks(e, backlinksMap[tangled.RepoIssueNSID]) 270 if err != nil { 271 return nil, fmt.Errorf("get issue backlinks: %w", err) 272 } 273 backlinks = append(backlinks, ls...) 274 ls, err = getPullBacklinks(e, backlinksMap[tangled.RepoPullNSID]) 275 if err != nil { 276 return nil, fmt.Errorf("get pull backlinks: %w", err) 277 } 278 backlinks = append(backlinks, ls...) 279 switch target.Collection() { 280 case tangled.RepoIssueNSID: 281 ls, err = getIssueCommentBacklinks(e, target, backlinksMap[tangled.FeedCommentNSID]) 282 if err != nil { 283 return nil, fmt.Errorf("get issue_comment backlinks: %w", err) 284 } 285 backlinks = append(backlinks, ls...) 286 case tangled.RepoPullNSID: 287 ls, err = getPullCommentBacklinks(e, target, backlinksMap[tangled.FeedCommentNSID]) 288 if err != nil { 289 return nil, fmt.Errorf("get pull_comment backlinks: %w", err) 290 } 291 backlinks = append(backlinks, ls...) 292 } 293 294 return backlinks, nil 295} 296 297func getIssueBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 298 if len(aturis) == 0 { 299 return nil, nil 300 } 301 vals := make([]string, len(aturis)) 302 args := make([]any, 0, len(aturis)*2) 303 for i, aturi := range aturis { 304 vals[i] = "(?, ?)" 305 did := aturi.Authority().String() 306 rkey := aturi.RecordKey().String() 307 args = append(args, did, rkey) 308 } 309 rows, err := e.Query( 310 fmt.Sprintf( 311 `select r.did, r.name, i.issue_id, i.title, i.open 312 from issues i 313 join repos r 314 on r.repo_did = i.repo_did 315 where (i.did, i.rkey) in (%s)`, 316 strings.Join(vals, ","), 317 ), 318 args..., 319 ) 320 if err != nil { 321 return nil, err 322 } 323 defer rows.Close() 324 var refLinks []models.RichReferenceLink 325 for rows.Next() { 326 var l models.RichReferenceLink 327 l.Kind = models.RefKindIssue 328 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil { 329 return nil, err 330 } 331 refLinks = append(refLinks, l) 332 } 333 if err := rows.Err(); err != nil { 334 return nil, fmt.Errorf("iterate rows: %w", err) 335 } 336 return refLinks, nil 337} 338 339func getIssueCommentBacklinks(e Execer, target syntax.ATURI, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 340 if len(aturis) == 0 { 341 return nil, nil 342 } 343 filter := orm.FilterIn("c.at_uri", aturis) 344 exclude := orm.FilterNotEq("i.at_uri", target) 345 rows, err := e.Query( 346 fmt.Sprintf( 347 `select r.did, r.name, i.issue_id, c.rkey, i.title, i.open 348 from comments c 349 join issues i 350 on i.at_uri = c.subject_uri 351 join repos r 352 on r.repo_did = i.repo_did 353 where %s and %s`, 354 filter.Condition(), 355 exclude.Condition(), 356 ), 357 append(filter.Arg(), exclude.Arg()...)..., 358 ) 359 if err != nil { 360 return nil, err 361 } 362 defer rows.Close() 363 var refLinks []models.RichReferenceLink 364 for rows.Next() { 365 var l models.RichReferenceLink 366 l.Kind = models.RefKindIssue 367 l.CommentRkey = new(syntax.RecordKey) 368 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentRkey, &l.Title, &l.State); err != nil { 369 return nil, err 370 } 371 refLinks = append(refLinks, l) 372 } 373 if err := rows.Err(); err != nil { 374 return nil, fmt.Errorf("iterate rows: %w", err) 375 } 376 return refLinks, nil 377} 378 379func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 380 if len(aturis) == 0 { 381 return nil, nil 382 } 383 vals := make([]string, len(aturis)) 384 args := make([]any, 0, len(aturis)*2) 385 for i, aturi := range aturis { 386 vals[i] = "(?, ?)" 387 did := aturi.Authority().String() 388 rkey := aturi.RecordKey().String() 389 args = append(args, did, rkey) 390 } 391 rows, err := e.Query( 392 fmt.Sprintf( 393 `select r.did, r.name, p.pull_id, p.title, p.state 394 from pulls p 395 join repos r 396 on r.repo_did = p.repo_did 397 where (p.owner_did, p.rkey) in (%s)`, 398 strings.Join(vals, ","), 399 ), 400 args..., 401 ) 402 if err != nil { 403 return nil, err 404 } 405 defer rows.Close() 406 var refLinks []models.RichReferenceLink 407 for rows.Next() { 408 var l models.RichReferenceLink 409 l.Kind = models.RefKindPull 410 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil { 411 return nil, err 412 } 413 refLinks = append(refLinks, l) 414 } 415 if err := rows.Err(); err != nil { 416 return nil, fmt.Errorf("iterate rows: %w", err) 417 } 418 return refLinks, nil 419} 420 421func getPullCommentBacklinks(e Execer, target syntax.ATURI, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 422 if len(aturis) == 0 { 423 return nil, nil 424 } 425 filter := orm.FilterIn("c.at_uri", aturis) 426 exclude := orm.FilterNotEq("p.at_uri", target) 427 rows, err := e.Query( 428 fmt.Sprintf( 429 `select r.did, r.name, p.pull_id, c.rkey, p.title, p.state 430 from repos r 431 join pulls p 432 on r.repo_did = p.repo_did 433 join comments c 434 on ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) = c.subject_uri 435 where %s and %s`, 436 filter.Condition(), 437 exclude.Condition(), 438 ), 439 append(filter.Arg(), exclude.Arg()...)..., 440 ) 441 if err != nil { 442 return nil, err 443 } 444 defer rows.Close() 445 var refLinks []models.RichReferenceLink 446 for rows.Next() { 447 var l models.RichReferenceLink 448 l.Kind = models.RefKindPull 449 l.CommentRkey = new(syntax.RecordKey) 450 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentRkey, &l.Title, &l.State); err != nil { 451 return nil, err 452 } 453 refLinks = append(refLinks, l) 454 } 455 if err := rows.Err(); err != nil { 456 return nil, fmt.Errorf("iterate rows: %w", err) 457 } 458 return refLinks, nil 459}