Monorepo for Tangled
0
fork

Configure Feed

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

appview/db,models: add tables for vouches

Signed-off-by: oppiliappan <me@oppi.li>

+426
+10
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 + kind text not null default 'vouch', 99 + reason text, 100 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 101 + primary key (did, subject_did), 102 + check (did <> subject_did), 103 + check (kind in ('vouch', 'denounce')) 104 + ); 95 105 create table if not exists issues ( 96 106 id integer primary key autoincrement, 97 107 owner_did text not null,
+330
appview/db/vouch.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "fmt" 6 + "log" 7 + "strings" 8 + "time" 9 + 10 + "tangled.org/core/appview/models" 11 + "tangled.org/core/orm" 12 + ) 13 + 14 + func AddVouch(e Execer, vouch *models.Vouch) error { 15 + query := `insert or replace into vouches (did, subject_did, kind, reason) values (?, ?, ?, ?)` 16 + _, err := e.Exec(query, vouch.Did, vouch.SubjectDid, vouch.Kind, vouch.Reason) 17 + return err 18 + } 19 + 20 + func GetVouch(e Execer, did, subjectDid string) (*models.Vouch, error) { 21 + vouches, err := GetVouches(e, 0, orm.FilterEq("did", did), orm.FilterEq("subject_did", subjectDid)) 22 + if err != nil { 23 + return nil, err 24 + } 25 + if len(vouches) == 0 { 26 + return nil, sql.ErrNoRows 27 + } 28 + return &vouches[0], nil 29 + } 30 + 31 + func GetVouches(e Execer, limit int, filters ...orm.Filter) ([]models.Vouch, error) { 32 + var vouches []models.Vouch 33 + 34 + var conditions []string 35 + var args []any 36 + for _, filter := range filters { 37 + conditions = append(conditions, filter.Condition()) 38 + args = append(args, filter.Arg()...) 39 + } 40 + 41 + whereClause := "" 42 + if conditions != nil { 43 + whereClause = " where " + strings.Join(conditions, " and ") 44 + } 45 + limitClause := "" 46 + if limit > 0 { 47 + limitClause = " limit ?" 48 + args = append(args, limit) 49 + } 50 + 51 + query := fmt.Sprintf( 52 + `select did, subject_did, kind, reason, created_at 53 + from vouches 54 + %s 55 + order by created_at desc 56 + %s 57 + `, whereClause, limitClause) 58 + 59 + rows, err := e.Query(query, args...) 60 + if err != nil { 61 + return nil, err 62 + } 63 + defer rows.Close() 64 + 65 + for rows.Next() { 66 + var vouch models.Vouch 67 + var createdAt string 68 + var reason sql.NullString 69 + err := rows.Scan( 70 + &vouch.Did, 71 + &vouch.SubjectDid, 72 + &vouch.Kind, 73 + &reason, 74 + &createdAt, 75 + ) 76 + if err != nil { 77 + return nil, err 78 + } 79 + createdAtTime, err := time.Parse(time.RFC3339, createdAt) 80 + if err != nil { 81 + log.Println("unable to determine created at time") 82 + vouch.CreatedAt = time.Now() 83 + } else { 84 + vouch.CreatedAt = createdAtTime 85 + } 86 + if reason.Valid { 87 + vouch.Reason = &reason.String 88 + } 89 + vouches = append(vouches, vouch) 90 + } 91 + return vouches, nil 92 + } 93 + 94 + func DeleteVouch(e Execer, did, subjectDid string) error { 95 + _, err := e.Exec(`delete from vouches where did = ? and subject_did = ?`, did, subjectDid) 96 + return err 97 + } 98 + 99 + func DeleteVouchByRkey(e Execer, did, rkey string) error { 100 + _, err := e.Exec(`delete from vouches where did = ? and subject_did = ?`, did, rkey) 101 + return err 102 + } 103 + 104 + func GetVouchStats(e Execer, did string) (models.VouchStats, error) { 105 + var vouches, denounces int64 106 + err := e.QueryRow( 107 + `SELECT 108 + COUNT(CASE WHEN kind = 'vouch' THEN 1 END) AS vouches, 109 + COUNT(CASE WHEN kind = 'denounce' THEN 1 END) AS denounces 110 + FROM vouches 111 + WHERE subject_did = ?`, did).Scan(&vouches, &denounces) 112 + if err != nil { 113 + return models.VouchStats{}, err 114 + } 115 + return models.VouchStats{ 116 + Vouches: vouches, 117 + Denounces: denounces, 118 + }, nil 119 + } 120 + 121 + func GetVouchStatsBatch(e Execer, dids []string) (map[string]models.VouchStats, error) { 122 + if len(dids) == 0 { 123 + return nil, nil 124 + } 125 + 126 + placeholders := make([]string, len(dids)) 127 + args := make([]any, len(dids)) 128 + for i, did := range dids { 129 + placeholders[i] = "?" 130 + args[i] = did 131 + } 132 + placeholderStr := strings.Join(placeholders, ",") 133 + 134 + query := fmt.Sprintf(` 135 + select 136 + subject_did, 137 + count(case when kind = 'vouch' then 1 end) as vouches, 138 + count(case when kind = 'denounce' then 1 end) as denounces 139 + from vouches 140 + where subject_did in (%s) 141 + group by subject_did 142 + `, placeholderStr) 143 + 144 + result := make(map[string]models.VouchStats) 145 + 146 + rows, err := e.Query(query, args...) 147 + if err != nil { 148 + return nil, err 149 + } 150 + defer rows.Close() 151 + 152 + for rows.Next() { 153 + var did string 154 + var vouches, denounces int64 155 + if err := rows.Scan(&did, &vouches, &denounces); err != nil { 156 + return nil, err 157 + } 158 + result[did] = models.VouchStats{ 159 + Vouches: vouches, 160 + Denounces: denounces, 161 + } 162 + } 163 + 164 + for _, did := range dids { 165 + if _, exists := result[did]; !exists { 166 + result[did] = models.VouchStats{ 167 + Vouches: 0, 168 + Denounces: 0, 169 + } 170 + } 171 + } 172 + 173 + return result, nil 174 + } 175 + 176 + func GetVouchesGiven(e Execer, did string) ([]models.Vouch, error) { 177 + return GetVouches(e, 0, orm.FilterEq("did", did)) 178 + } 179 + 180 + func GetVouchesReceived(e Execer, did string) ([]models.Vouch, error) { 181 + return GetVouches(e, 0, orm.FilterEq("subject_did", did)) 182 + } 183 + 184 + // GetNetworkVouchesForSubject returns vouches for subjectDid from people that viewerDid follows or vouches for 185 + func GetNetworkVouchesForSubject(e Execer, viewerDid, subjectDid string, limit int) ([]models.Vouch, error) { 186 + query := ` 187 + select distinct v.did, v.subject_did, v.kind, v.reason, v.created_at 188 + from vouches v 189 + where v.subject_did = ? 190 + and v.kind = 'vouch' 191 + and v.did in ( 192 + select subject_did from follows where user_did = ? 193 + union 194 + select subject_did from vouches where did = ? and kind = 'vouch' 195 + ) 196 + order by v.created_at desc 197 + ` 198 + args := []any{subjectDid, viewerDid, viewerDid} 199 + 200 + if limit > 0 { 201 + query += " limit ?" 202 + args = append(args, limit) 203 + } 204 + 205 + rows, err := e.Query(query, args...) 206 + if err != nil { 207 + return nil, err 208 + } 209 + defer rows.Close() 210 + 211 + var vouches []models.Vouch 212 + for rows.Next() { 213 + var vouch models.Vouch 214 + var createdAt string 215 + var reason sql.NullString 216 + err := rows.Scan( 217 + &vouch.Did, 218 + &vouch.SubjectDid, 219 + &vouch.Kind, 220 + &reason, 221 + &createdAt, 222 + ) 223 + if err != nil { 224 + return nil, err 225 + } 226 + createdAtTime, err := time.Parse(time.RFC3339, createdAt) 227 + if err != nil { 228 + log.Println("unable to determine created at time") 229 + vouch.CreatedAt = time.Now() 230 + } else { 231 + vouch.CreatedAt = createdAtTime 232 + } 233 + if reason.Valid { 234 + vouch.Reason = &reason.String 235 + } 236 + vouches = append(vouches, vouch) 237 + } 238 + return vouches, nil 239 + } 240 + 241 + // GetNetworkDenouncesForSubject returns denounces for subjectDid from people that viewerDid follows or vouches for 242 + func GetNetworkDenouncesForSubject(e Execer, viewerDid, subjectDid string, limit int) ([]models.Vouch, error) { 243 + query := ` 244 + select distinct v.did, v.subject_did, v.kind, v.reason, v.created_at 245 + from vouches v 246 + where v.subject_did = ? 247 + and v.kind = 'denounce' 248 + and v.did in ( 249 + select subject_did from follows where user_did = ? 250 + union 251 + select subject_did from vouches where did = ? and kind = 'vouch' 252 + ) 253 + order by v.created_at desc 254 + ` 255 + args := []any{subjectDid, viewerDid, viewerDid} 256 + 257 + if limit > 0 { 258 + query += " limit ?" 259 + args = append(args, limit) 260 + } 261 + 262 + rows, err := e.Query(query, args...) 263 + if err != nil { 264 + return nil, err 265 + } 266 + defer rows.Close() 267 + 268 + var vouches []models.Vouch 269 + for rows.Next() { 270 + var vouch models.Vouch 271 + var createdAt string 272 + var reason sql.NullString 273 + err := rows.Scan( 274 + &vouch.Did, 275 + &vouch.SubjectDid, 276 + &vouch.Kind, 277 + &reason, 278 + &createdAt, 279 + ) 280 + if err != nil { 281 + return nil, err 282 + } 283 + createdAtTime, err := time.Parse(time.RFC3339, createdAt) 284 + if err != nil { 285 + log.Println("unable to determine created at time") 286 + vouch.CreatedAt = time.Now() 287 + } else { 288 + vouch.CreatedAt = createdAtTime 289 + } 290 + if reason.Valid { 291 + vouch.Reason = &reason.String 292 + } 293 + vouches = append(vouches, vouch) 294 + } 295 + return vouches, nil 296 + } 297 + 298 + // CountNetworkVouchesForSubject returns count of vouches for subjectDid from viewerDid's network 299 + func CountNetworkVouchesForSubject(e Execer, viewerDid, subjectDid string) (int64, error) { 300 + var count int64 301 + err := e.QueryRow(` 302 + select count(distinct v.did) 303 + from vouches v 304 + where v.subject_did = ? 305 + and v.kind = 'vouch' 306 + and v.did in ( 307 + select subject_did from follows where user_did = ? 308 + union 309 + select subject_did from vouches where did = ? and kind = 'vouch' 310 + ) 311 + `, subjectDid, viewerDid, viewerDid).Scan(&count) 312 + return count, err 313 + } 314 + 315 + // CountNetworkDenouncesForSubject returns count of denounces for subjectDid from viewerDid's network 316 + func CountNetworkDenouncesForSubject(e Execer, viewerDid, subjectDid string) (int64, error) { 317 + var count int64 318 + err := e.QueryRow(` 319 + select count(distinct v.did) 320 + from vouches v 321 + where v.subject_did = ? 322 + and v.kind = 'denounce' 323 + and v.did in ( 324 + select subject_did from follows where user_did = ? 325 + union 326 + select subject_did from vouches where did = ? and kind = 'vouch' 327 + ) 328 + `, subjectDid, viewerDid, viewerDid).Scan(&count) 329 + return count, err 330 + }
+70
appview/ingester.go
··· 65 65 switch e.Commit.Collection { 66 66 case tangled.GraphFollowNSID: 67 67 err = i.ingestFollow(e) 68 + case tangled.GraphVouchNSID: 69 + err = i.ingestVouch(ctx, e) 68 70 case tangled.FeedStarNSID: 69 71 err = i.ingestStar(e) 70 72 case tangled.PublicKeyNSID: ··· 198 200 199 201 if err != nil { 200 202 return fmt.Errorf("failed to %s follow record: %w", e.Commit.Operation, err) 203 + } 204 + 205 + return nil 206 + } 207 + 208 + func (i *Ingester) ingestVouch(ctx context.Context, e *jmodels.Event) error { 209 + var err error 210 + did := e.Did 211 + 212 + l := i.Logger.With("handler", "ingestVouch") 213 + l = l.With("nsid", e.Commit.Collection) 214 + 215 + switch e.Commit.Operation { 216 + case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 217 + raw := json.RawMessage(e.Commit.Record) 218 + record := tangled.GraphVouch{} 219 + err = json.Unmarshal(raw, &record) 220 + if err != nil { 221 + l.Error("invalid record", "err", err) 222 + return err 223 + } 224 + 225 + // rkey is the subject_did being vouched for/denounced 226 + subjectDID := e.Commit.RKey 227 + 228 + _, err = syntax.ParseDID(subjectDID) 229 + if err != nil { 230 + l.Error("invalid subject_did in rkey", "err", err, "rkey", subjectDID) 231 + return fmt.Errorf("invalid subject_did: %w", err) 232 + } 233 + 234 + if did == subjectDID { 235 + l.Warn("attempted self-vouch", "did", did) 236 + return fmt.Errorf("cannot vouch for self") 237 + } 238 + 239 + subjectId, err := i.IdResolver.ResolveIdent(ctx, subjectDID) 240 + if err != nil { 241 + return err 242 + } 243 + 244 + if subjectId.Handle.IsInvalidHandle() { 245 + return err 246 + } 247 + 248 + kind := "vouch" 249 + if record.Kind != nil { 250 + kind = *record.Kind 251 + } 252 + 253 + if kind != "vouch" && kind != "denounce" { 254 + l.Error("invalid kind", "kind", kind) 255 + return fmt.Errorf("invalid kind: %s", kind) 256 + } 257 + 258 + err = db.AddVouch(i.Db, &models.Vouch{ 259 + Did: did, 260 + SubjectDid: subjectDID, 261 + Kind: kind, 262 + Reason: record.Reason, 263 + }) 264 + 265 + case jmodels.CommitOperationDelete: 266 + err = db.DeleteVouchByRkey(i.Db, did, e.Commit.RKey) 267 + } 268 + 269 + if err != nil { 270 + return fmt.Errorf("failed to %s vouch record: %w", e.Commit.Operation, err) 201 271 } 202 272 203 273 return nil
+16
appview/models/vouch.go
··· 1 + package models 2 + 3 + import "time" 4 + 5 + type Vouch struct { 6 + Did string 7 + SubjectDid string 8 + Kind string 9 + Reason *string 10 + CreatedAt time.Time 11 + } 12 + 13 + type VouchStats struct { 14 + Vouches int64 15 + Denounces int64 16 + }