Monorepo for Tangled tangled.org
788
fork

Configure Feed

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

appview/{db,models}: add table and models for vouching #18

open opened by oppi.li targeting master from op/uxqspoupnxkp
Labels

None yet.

assignee

None yet.

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

History

3 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
appview/{db,models}: add table and models for vouching
merge conflicts detected
expand
  • api/tangled/cbor_gen.go:9735
  • cmd/cborgen/cborgen.go:24
expand 0 comments
1 commit
expand
appview/{db,models}: add table and models for vouching
expand 0 comments
oppi.li submitted #0
1 commit
expand
appview/{db,models}: add table and models for vouching
expand 0 comments