Monorepo for Tangled tangled.org
757
fork

Configure Feed

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

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

authored by

oppiliappan and committed by
Tangled
2015bbf1 37b3af7e

+494
+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,
+360
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/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 + 17 + func 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 + 23 + func 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 + 37 + func 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 + 104 + func 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 + 109 + func 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 + 114 + func 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 + 173 + func 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 + 235 + func 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 252 + func 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 + }
+123
appview/models/vouch.go
··· 1 + package models 2 + 3 + import ( 4 + "fmt" 5 + "time" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + "github.com/ipfs/go-cid" 9 + ) 10 + 11 + type VouchSuggestion struct { 12 + Did syntax.DID 13 + Reason string 14 + VouchRelationship *VouchRelationship 15 + } 16 + 17 + type VouchKind string 18 + 19 + const ( 20 + VouchKindVouch VouchKind = "vouch" 21 + VouchKindDenounce VouchKind = "denounce" 22 + ) 23 + 24 + func ParseVouchKind(v string) (VouchKind, error) { 25 + switch v { 26 + case "vouch": 27 + return VouchKindVouch, nil 28 + case "denounce": 29 + return VouchKindDenounce, nil 30 + default: 31 + return VouchKindVouch, fmt.Errorf("invalid vouch kind: %s", v) 32 + } 33 + } 34 + 35 + type Vouch struct { 36 + Did syntax.DID 37 + SubjectDid syntax.DID 38 + Cid cid.Cid 39 + Kind VouchKind 40 + Reason *string 41 + CreatedAt time.Time 42 + } 43 + 44 + func (v Vouch) IsVouch() bool { 45 + return v.Kind == VouchKindVouch 46 + } 47 + 48 + func (v Vouch) IsDenounce() bool { 49 + return v.Kind == VouchKindDenounce 50 + } 51 + 52 + type VouchStats struct { 53 + Vouches int64 54 + Denounces int64 55 + } 56 + 57 + type VouchRelationship struct { 58 + ViewerDid syntax.DID 59 + SubjectDid syntax.DID 60 + 61 + NetworkVouches []Vouch 62 + } 63 + 64 + func (vr *VouchRelationship) IsDirectVouch() bool { 65 + for _, v := range vr.NetworkVouches { 66 + if v.Did == vr.ViewerDid && v.SubjectDid == vr.SubjectDid && v.Kind == VouchKindVouch { 67 + return true 68 + } 69 + } 70 + return false 71 + } 72 + 73 + func (vr *VouchRelationship) IsDirectDenounce() bool { 74 + for _, v := range vr.NetworkVouches { 75 + if v.Did == vr.ViewerDid && v.SubjectDid == vr.SubjectDid && v.Kind == VouchKindDenounce { 76 + return true 77 + } 78 + } 79 + return false 80 + } 81 + 82 + func (vr *VouchRelationship) IndirectVouches() []Vouch { 83 + var indirectVouches []Vouch 84 + for _, v := range vr.NetworkVouches { 85 + if v.Did != vr.ViewerDid { 86 + indirectVouches = append(indirectVouches, v) 87 + } 88 + } 89 + return indirectVouches 90 + } 91 + 92 + func (vr *VouchRelationship) IsEmpty() bool { 93 + return len(vr.NetworkVouches) == 0 94 + } 95 + 96 + func (vr *VouchRelationship) GetDirectVouch() *Vouch { 97 + for _, v := range vr.NetworkVouches { 98 + if v.Did == vr.ViewerDid && v.SubjectDid == vr.SubjectDid { 99 + return &v 100 + } 101 + } 102 + return nil 103 + } 104 + 105 + func (vr *VouchRelationship) VouchStrength() int { 106 + count := 0 107 + for _, v := range vr.NetworkVouches { 108 + if v.Did != vr.ViewerDid && v.Kind == VouchKindVouch { 109 + count++ 110 + } 111 + } 112 + return count 113 + } 114 + 115 + func (vr *VouchRelationship) DenounceStrength() int { 116 + count := 0 117 + for _, v := range vr.NetworkVouches { 118 + if v.Did != vr.ViewerDid && v.Kind == VouchKindDenounce { 119 + count++ 120 + } 121 + } 122 + return count 123 + }