This repository has no description
0

Configure Feed

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

core / appview / db / vouch.go
9.0 kB 360 lines
1package db 2 3import ( 4 "database/sql" 5 "fmt" 6 "log" 7 "strings" 8 "time" 9 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 "github.com/ipfs/go-cid" 12 "tangled.org/core/appview/models" 13 "tangled.org/core/appview/pagination" 14 "tangled.org/core/orm" 15) 16 17func AddVouch(e Execer, vouch *models.Vouch) error { 18 query := `insert or replace into vouches (did, subject_did, cid, kind, reason) values (?, ?, ?, ?, ?)` 19 _, err := e.Exec(query, vouch.Did, vouch.SubjectDid, vouch.Cid.String(), vouch.Kind, vouch.Reason) 20 return err 21} 22 23func GetVouch(e Execer, did, subjectDid string) (*models.Vouch, error) { 24 vouches, err := GetVouches(e, pagination.Page{Limit: 1}, 25 orm.FilterEq("did", did), 26 orm.FilterEq("subject_did", subjectDid), 27 ) 28 if err != nil { 29 return nil, err 30 } 31 if len(vouches) == 0 { 32 return nil, sql.ErrNoRows 33 } 34 return &vouches[0], nil 35} 36 37func GetVouches(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Vouch, error) { 38 var conditions []string 39 var args []any 40 for _, filter := range filters { 41 conditions = append(conditions, filter.Condition()) 42 args = append(args, filter.Arg()...) 43 } 44 45 whereClause := "" 46 if len(conditions) > 0 { 47 whereClause = "where " + strings.Join(conditions, " and ") 48 } 49 50 pageClause := "" 51 if page.Limit > 0 { 52 pageClause = fmt.Sprintf("limit %d offset %d", page.Limit, page.Offset) 53 } 54 55 query := fmt.Sprintf( 56 `select did, subject_did, cid, kind, reason, created_at 57 from vouches 58 %s 59 order by created_at desc 60 %s`, 61 whereClause, pageClause) 62 63 rows, err := e.Query(query, args...) 64 if err != nil { 65 return nil, err 66 } 67 defer rows.Close() 68 69 var vouches []models.Vouch 70 for rows.Next() { 71 var v models.Vouch 72 var cidStr string 73 var createdAt string 74 var reason sql.NullString 75 76 if err := rows.Scan(&v.Did, &v.SubjectDid, &cidStr, &v.Kind, &reason, &createdAt); err != nil { 77 log.Println("error scanning vouch:", err) 78 continue 79 } 80 81 v.Cid, err = cid.Parse(cidStr) 82 if err != nil { 83 log.Println("unable to parse CID:", err) 84 continue 85 } 86 87 t, err := time.Parse(time.RFC3339, createdAt) 88 if err != nil { 89 log.Println("unable to determine created at time") 90 v.CreatedAt = time.Now() 91 } else { 92 v.CreatedAt = t 93 } 94 95 if reason.Valid { 96 v.Reason = &reason.String 97 } 98 99 vouches = append(vouches, v) 100 } 101 return vouches, nil 102} 103 104func DeleteVouch(e Execer, did, subjectDid string) error { 105 _, err := e.Exec(`delete from vouches where did = ? and subject_did = ?`, did, subjectDid) 106 return err 107} 108 109func DeleteVouchByRkey(e Execer, did, rkey string) error { 110 _, err := e.Exec(`delete from vouches where did = ? and subject_did = ?`, did, rkey) 111 return err 112} 113 114func GetNetworkVouchTimeline(e Execer, viewerDid, profileDid string, page pagination.Page) ([]models.Vouch, error) { 115 pageClause := "" 116 if page.Limit > 0 { 117 pageClause = fmt.Sprintf("limit %d offset %d", page.Limit, page.Offset) 118 } 119 120 query := fmt.Sprintf( 121 `select did, subject_did, cid, kind, reason, created_at 122 from vouches 123 where ( 124 subject_did = ? and did in (select subject_did from vouches where did = ? and kind = 'vouch') 125 ) or ( 126 did = ? and subject_did in (select subject_did from vouches where did = ? and kind = 'vouch') 127 ) 128 order by created_at desc 129 %s`, 130 pageClause) 131 132 rows, err := e.Query(query, profileDid, viewerDid, profileDid, viewerDid) 133 if err != nil { 134 return nil, err 135 } 136 defer rows.Close() 137 138 var vouches []models.Vouch 139 for rows.Next() { 140 var v models.Vouch 141 var cidStr string 142 var createdAt string 143 var reason sql.NullString 144 145 if err := rows.Scan(&v.Did, &v.SubjectDid, &cidStr, &v.Kind, &reason, &createdAt); err != nil { 146 log.Println("error scanning vouch:", err) 147 continue 148 } 149 150 v.Cid, err = cid.Parse(cidStr) 151 if err != nil { 152 log.Println("unable to parse CID:", err) 153 continue 154 } 155 156 t, err := time.Parse(time.RFC3339, createdAt) 157 if err != nil { 158 log.Println("unable to determine created at time") 159 v.CreatedAt = time.Now() 160 } else { 161 v.CreatedAt = t 162 } 163 164 if reason.Valid { 165 v.Reason = &reason.String 166 } 167 168 vouches = append(vouches, v) 169 } 170 return vouches, nil 171} 172 173func GetVouchRelationshipsBatch(e Execer, viewerDid syntax.DID, subjectDids []syntax.DID) (map[syntax.DID]*models.VouchRelationship, error) { 174 if viewerDid == "" { 175 return nil, fmt.Errorf("viewerDid cannot be empty") 176 } 177 178 result := make(map[syntax.DID]*models.VouchRelationship) 179 for _, subjectDid := range subjectDids { 180 result[subjectDid] = &models.VouchRelationship{ 181 ViewerDid: viewerDid, 182 SubjectDid: subjectDid, 183 NetworkVouches: []models.Vouch{}, 184 } 185 } 186 187 if len(subjectDids) == 0 { 188 return result, nil 189 } 190 191 directVouches, err := GetVouches(e, pagination.Page{}, 192 orm.FilterEq("did", viewerDid), 193 orm.FilterIn("subject_did", subjectDids), 194 ) 195 if err != nil { 196 return nil, err 197 } 198 for _, v := range directVouches { 199 if rel, ok := result[v.SubjectDid]; ok { 200 rel.NetworkVouches = append(rel.NetworkVouches, v) 201 } 202 } 203 204 networkVouches, err := GetVouches(e, pagination.Page{}, 205 orm.FilterEq("did", viewerDid), 206 orm.FilterEq("kind", string(models.VouchKindVouch)), 207 ) 208 if err != nil { 209 return nil, err 210 } 211 212 network := make([]syntax.DID, 0, len(networkVouches)) 213 for _, v := range networkVouches { 214 network = append(network, v.SubjectDid) 215 } 216 217 if len(network) > 0 { 218 networkToSubject, err := GetVouches(e, pagination.Page{}, 219 orm.FilterIn("subject_did", subjectDids), 220 orm.FilterIn("did", network), 221 ) 222 if err != nil { 223 return nil, err 224 } 225 for _, v := range networkToSubject { 226 if rel, ok := result[v.SubjectDid]; ok { 227 rel.NetworkVouches = append(rel.NetworkVouches, v) 228 } 229 } 230 } 231 232 return result, nil 233} 234 235func GetVouchRelationship(e Execer, viewerDid, subjectDid syntax.DID) (*models.VouchRelationship, error) { 236 batch, err := GetVouchRelationshipsBatch(e, viewerDid, []syntax.DID{subjectDid}) 237 if err != nil { 238 return nil, err 239 } 240 return batch[subjectDid], nil 241} 242 243// priority: 244// 1. collaborator invites sent 245// 2. knot member invites sent 246// 3. PR authors on FOO's repositories 247// 4. issue authors on FOO's repositories 248// 5. PR comment authors on FOO's repositories 249// 6. issue comment authors on FOO's repositories 250// 7. users FOO recently followed 251// 8. owners of repositories FOO recently starred 252func GetVouchSuggestions(e Execer, did string, limit int) ([]models.VouchSuggestion, error) { 253 query := ` 254 select did, reason from ( 255 select subject_did as did, 1 as priority, created, 256 'You invited this user to collaborate on a repository' as reason 257 from collaborators 258 where collaborators.did = ? 259 and subject_did != ? 260 261 union all 262 263 select subject as did, 2 as priority, created, 264 'You invited this user to your knot' as reason 265 from spindle_members 266 where spindle_members.did = ? 267 and subject != ? 268 269 union all 270 271 select p.owner_did as did, 3 as priority, p.created, 272 'This user opened a pull request on your repository' as reason 273 from pulls p 274 join repos r on r.at_uri = p.repo_at 275 where r.did = ? 276 and p.owner_did != ? 277 278 union all 279 280 select i.did as did, 4 as priority, i.created, 281 'This user opened an issue on your repository' as reason 282 from issues i 283 join repos r on r.at_uri = i.repo_at 284 where r.did = ? 285 and i.did != ? 286 287 union all 288 289 select pc.owner_did as did, 5 as priority, pc.created, 290 'This user commented on a pull request on your repository' as reason 291 from pull_comments pc 292 join repos r on r.at_uri = pc.repo_at 293 where r.did = ? 294 and pc.owner_did != ? 295 296 union all 297 298 select ic.did as did, 6 as priority, ic.created, 299 'This user commented on an issue on your repository' as reason 300 from issue_comments ic 301 join issues i on i.at_uri = ic.issue_at 302 join repos r on r.at_uri = i.repo_at 303 where r.did = ? 304 and ic.did != ? 305 306 union all 307 308 select f.subject_did as did, 7 as priority, f.followed_at as created, 309 'You recently followed this user' as reason 310 from follows f 311 where f.user_did = ? 312 and f.subject_did != ? 313 314 union all 315 316 select r.did as did, 8 as priority, s.created, 317 'You recently starred a repository by this user' as reason 318 from stars s 319 join repos r on r.at_uri = s.subject_at 320 where s.did = ? 321 and r.did != ? 322 ) 323 where did not in ( 324 select subject_did from vouches where vouches.did = ? 325 ) 326 group by did 327 order by min(priority) asc, max(created) desc 328 limit ? 329 ` 330 331 args := []any{ 332 did, did, // collaborators 333 did, did, // spindle_members 334 did, did, // pulls 335 did, did, // issues 336 did, did, // pull_comments 337 did, did, // issue_comments 338 did, did, // follows 339 did, did, // stars 340 did, // vouches exclusion 341 limit, 342 } 343 344 rows, err := e.Query(query, args...) 345 if err != nil { 346 return nil, fmt.Errorf("GetVouchSuggestions: %w", err) 347 } 348 defer rows.Close() 349 350 var suggestions []models.VouchSuggestion 351 for rows.Next() { 352 var s models.VouchSuggestion 353 if err := rows.Scan(&s.Did, &s.Reason); err != nil { 354 log.Println("error scanning vouch suggestion:", err) 355 continue 356 } 357 suggestions = append(suggestions, s) 358 } 359 return suggestions, nil 360}