···9292 primary key (user_did, subject_did),
9393 check (user_did <> subject_did)
9494 );
9595+ create table if not exists vouches (
9696+ did text not null,
9797+ subject_did text not null,
9898+ kind text not null default 'vouch',
9999+ reason text,
100100+ created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
101101+ primary key (did, subject_did),
102102+ check (did <> subject_did),
103103+ check (kind in ('vouch', 'denounce'))
104104+ );
95105 create table if not exists issues (
96106 id integer primary key autoincrement,
97107 owner_did text not null,
+330
appview/db/vouch.go
···11+package db
22+33+import (
44+ "database/sql"
55+ "fmt"
66+ "log"
77+ "strings"
88+ "time"
99+1010+ "tangled.org/core/appview/models"
1111+ "tangled.org/core/orm"
1212+)
1313+1414+func AddVouch(e Execer, vouch *models.Vouch) error {
1515+ query := `insert or replace into vouches (did, subject_did, kind, reason) values (?, ?, ?, ?)`
1616+ _, err := e.Exec(query, vouch.Did, vouch.SubjectDid, vouch.Kind, vouch.Reason)
1717+ return err
1818+}
1919+2020+func GetVouch(e Execer, did, subjectDid string) (*models.Vouch, error) {
2121+ vouches, err := GetVouches(e, 0, orm.FilterEq("did", did), orm.FilterEq("subject_did", subjectDid))
2222+ if err != nil {
2323+ return nil, err
2424+ }
2525+ if len(vouches) == 0 {
2626+ return nil, sql.ErrNoRows
2727+ }
2828+ return &vouches[0], nil
2929+}
3030+3131+func GetVouches(e Execer, limit int, filters ...orm.Filter) ([]models.Vouch, error) {
3232+ var vouches []models.Vouch
3333+3434+ var conditions []string
3535+ var args []any
3636+ for _, filter := range filters {
3737+ conditions = append(conditions, filter.Condition())
3838+ args = append(args, filter.Arg()...)
3939+ }
4040+4141+ whereClause := ""
4242+ if conditions != nil {
4343+ whereClause = " where " + strings.Join(conditions, " and ")
4444+ }
4545+ limitClause := ""
4646+ if limit > 0 {
4747+ limitClause = " limit ?"
4848+ args = append(args, limit)
4949+ }
5050+5151+ query := fmt.Sprintf(
5252+ `select did, subject_did, kind, reason, created_at
5353+ from vouches
5454+ %s
5555+ order by created_at desc
5656+ %s
5757+ `, whereClause, limitClause)
5858+5959+ rows, err := e.Query(query, args...)
6060+ if err != nil {
6161+ return nil, err
6262+ }
6363+ defer rows.Close()
6464+6565+ for rows.Next() {
6666+ var vouch models.Vouch
6767+ var createdAt string
6868+ var reason sql.NullString
6969+ err := rows.Scan(
7070+ &vouch.Did,
7171+ &vouch.SubjectDid,
7272+ &vouch.Kind,
7373+ &reason,
7474+ &createdAt,
7575+ )
7676+ if err != nil {
7777+ return nil, err
7878+ }
7979+ createdAtTime, err := time.Parse(time.RFC3339, createdAt)
8080+ if err != nil {
8181+ log.Println("unable to determine created at time")
8282+ vouch.CreatedAt = time.Now()
8383+ } else {
8484+ vouch.CreatedAt = createdAtTime
8585+ }
8686+ if reason.Valid {
8787+ vouch.Reason = &reason.String
8888+ }
8989+ vouches = append(vouches, vouch)
9090+ }
9191+ return vouches, nil
9292+}
9393+9494+func DeleteVouch(e Execer, did, subjectDid string) error {
9595+ _, err := e.Exec(`delete from vouches where did = ? and subject_did = ?`, did, subjectDid)
9696+ return err
9797+}
9898+9999+func DeleteVouchByRkey(e Execer, did, rkey string) error {
100100+ _, err := e.Exec(`delete from vouches where did = ? and subject_did = ?`, did, rkey)
101101+ return err
102102+}
103103+104104+func GetVouchStats(e Execer, did string) (models.VouchStats, error) {
105105+ var vouches, denounces int64
106106+ err := e.QueryRow(
107107+ `SELECT
108108+ COUNT(CASE WHEN kind = 'vouch' THEN 1 END) AS vouches,
109109+ COUNT(CASE WHEN kind = 'denounce' THEN 1 END) AS denounces
110110+ FROM vouches
111111+ WHERE subject_did = ?`, did).Scan(&vouches, &denounces)
112112+ if err != nil {
113113+ return models.VouchStats{}, err
114114+ }
115115+ return models.VouchStats{
116116+ Vouches: vouches,
117117+ Denounces: denounces,
118118+ }, nil
119119+}
120120+121121+func GetVouchStatsBatch(e Execer, dids []string) (map[string]models.VouchStats, error) {
122122+ if len(dids) == 0 {
123123+ return nil, nil
124124+ }
125125+126126+ placeholders := make([]string, len(dids))
127127+ args := make([]any, len(dids))
128128+ for i, did := range dids {
129129+ placeholders[i] = "?"
130130+ args[i] = did
131131+ }
132132+ placeholderStr := strings.Join(placeholders, ",")
133133+134134+ query := fmt.Sprintf(`
135135+ select
136136+ subject_did,
137137+ count(case when kind = 'vouch' then 1 end) as vouches,
138138+ count(case when kind = 'denounce' then 1 end) as denounces
139139+ from vouches
140140+ where subject_did in (%s)
141141+ group by subject_did
142142+ `, placeholderStr)
143143+144144+ result := make(map[string]models.VouchStats)
145145+146146+ rows, err := e.Query(query, args...)
147147+ if err != nil {
148148+ return nil, err
149149+ }
150150+ defer rows.Close()
151151+152152+ for rows.Next() {
153153+ var did string
154154+ var vouches, denounces int64
155155+ if err := rows.Scan(&did, &vouches, &denounces); err != nil {
156156+ return nil, err
157157+ }
158158+ result[did] = models.VouchStats{
159159+ Vouches: vouches,
160160+ Denounces: denounces,
161161+ }
162162+ }
163163+164164+ for _, did := range dids {
165165+ if _, exists := result[did]; !exists {
166166+ result[did] = models.VouchStats{
167167+ Vouches: 0,
168168+ Denounces: 0,
169169+ }
170170+ }
171171+ }
172172+173173+ return result, nil
174174+}
175175+176176+func GetVouchesGiven(e Execer, did string) ([]models.Vouch, error) {
177177+ return GetVouches(e, 0, orm.FilterEq("did", did))
178178+}
179179+180180+func GetVouchesReceived(e Execer, did string) ([]models.Vouch, error) {
181181+ return GetVouches(e, 0, orm.FilterEq("subject_did", did))
182182+}
183183+184184+// GetNetworkVouchesForSubject returns vouches for subjectDid from people that viewerDid follows or vouches for
185185+func GetNetworkVouchesForSubject(e Execer, viewerDid, subjectDid string, limit int) ([]models.Vouch, error) {
186186+ query := `
187187+ select distinct v.did, v.subject_did, v.kind, v.reason, v.created_at
188188+ from vouches v
189189+ where v.subject_did = ?
190190+ and v.kind = 'vouch'
191191+ and v.did in (
192192+ select subject_did from follows where user_did = ?
193193+ union
194194+ select subject_did from vouches where did = ? and kind = 'vouch'
195195+ )
196196+ order by v.created_at desc
197197+ `
198198+ args := []any{subjectDid, viewerDid, viewerDid}
199199+200200+ if limit > 0 {
201201+ query += " limit ?"
202202+ args = append(args, limit)
203203+ }
204204+205205+ rows, err := e.Query(query, args...)
206206+ if err != nil {
207207+ return nil, err
208208+ }
209209+ defer rows.Close()
210210+211211+ var vouches []models.Vouch
212212+ for rows.Next() {
213213+ var vouch models.Vouch
214214+ var createdAt string
215215+ var reason sql.NullString
216216+ err := rows.Scan(
217217+ &vouch.Did,
218218+ &vouch.SubjectDid,
219219+ &vouch.Kind,
220220+ &reason,
221221+ &createdAt,
222222+ )
223223+ if err != nil {
224224+ return nil, err
225225+ }
226226+ createdAtTime, err := time.Parse(time.RFC3339, createdAt)
227227+ if err != nil {
228228+ log.Println("unable to determine created at time")
229229+ vouch.CreatedAt = time.Now()
230230+ } else {
231231+ vouch.CreatedAt = createdAtTime
232232+ }
233233+ if reason.Valid {
234234+ vouch.Reason = &reason.String
235235+ }
236236+ vouches = append(vouches, vouch)
237237+ }
238238+ return vouches, nil
239239+}
240240+241241+// GetNetworkDenouncesForSubject returns denounces for subjectDid from people that viewerDid follows or vouches for
242242+func GetNetworkDenouncesForSubject(e Execer, viewerDid, subjectDid string, limit int) ([]models.Vouch, error) {
243243+ query := `
244244+ select distinct v.did, v.subject_did, v.kind, v.reason, v.created_at
245245+ from vouches v
246246+ where v.subject_did = ?
247247+ and v.kind = 'denounce'
248248+ and v.did in (
249249+ select subject_did from follows where user_did = ?
250250+ union
251251+ select subject_did from vouches where did = ? and kind = 'vouch'
252252+ )
253253+ order by v.created_at desc
254254+ `
255255+ args := []any{subjectDid, viewerDid, viewerDid}
256256+257257+ if limit > 0 {
258258+ query += " limit ?"
259259+ args = append(args, limit)
260260+ }
261261+262262+ rows, err := e.Query(query, args...)
263263+ if err != nil {
264264+ return nil, err
265265+ }
266266+ defer rows.Close()
267267+268268+ var vouches []models.Vouch
269269+ for rows.Next() {
270270+ var vouch models.Vouch
271271+ var createdAt string
272272+ var reason sql.NullString
273273+ err := rows.Scan(
274274+ &vouch.Did,
275275+ &vouch.SubjectDid,
276276+ &vouch.Kind,
277277+ &reason,
278278+ &createdAt,
279279+ )
280280+ if err != nil {
281281+ return nil, err
282282+ }
283283+ createdAtTime, err := time.Parse(time.RFC3339, createdAt)
284284+ if err != nil {
285285+ log.Println("unable to determine created at time")
286286+ vouch.CreatedAt = time.Now()
287287+ } else {
288288+ vouch.CreatedAt = createdAtTime
289289+ }
290290+ if reason.Valid {
291291+ vouch.Reason = &reason.String
292292+ }
293293+ vouches = append(vouches, vouch)
294294+ }
295295+ return vouches, nil
296296+}
297297+298298+// CountNetworkVouchesForSubject returns count of vouches for subjectDid from viewerDid's network
299299+func CountNetworkVouchesForSubject(e Execer, viewerDid, subjectDid string) (int64, error) {
300300+ var count int64
301301+ err := e.QueryRow(`
302302+ select count(distinct v.did)
303303+ from vouches v
304304+ where v.subject_did = ?
305305+ and v.kind = 'vouch'
306306+ and v.did in (
307307+ select subject_did from follows where user_did = ?
308308+ union
309309+ select subject_did from vouches where did = ? and kind = 'vouch'
310310+ )
311311+ `, subjectDid, viewerDid, viewerDid).Scan(&count)
312312+ return count, err
313313+}
314314+315315+// CountNetworkDenouncesForSubject returns count of denounces for subjectDid from viewerDid's network
316316+func CountNetworkDenouncesForSubject(e Execer, viewerDid, subjectDid string) (int64, error) {
317317+ var count int64
318318+ err := e.QueryRow(`
319319+ select count(distinct v.did)
320320+ from vouches v
321321+ where v.subject_did = ?
322322+ and v.kind = 'denounce'
323323+ and v.did in (
324324+ select subject_did from follows where user_did = ?
325325+ union
326326+ select subject_did from vouches where did = ? and kind = 'vouch'
327327+ )
328328+ `, subjectDid, viewerDid, viewerDid).Scan(&count)
329329+ return count, err
330330+}
+70
appview/ingester.go
···6565 switch e.Commit.Collection {
6666 case tangled.GraphFollowNSID:
6767 err = i.ingestFollow(e)
6868+ case tangled.GraphVouchNSID:
6969+ err = i.ingestVouch(ctx, e)
6870 case tangled.FeedStarNSID:
6971 err = i.ingestStar(e)
7072 case tangled.PublicKeyNSID:
···198200199201 if err != nil {
200202 return fmt.Errorf("failed to %s follow record: %w", e.Commit.Operation, err)
203203+ }
204204+205205+ return nil
206206+}
207207+208208+func (i *Ingester) ingestVouch(ctx context.Context, e *jmodels.Event) error {
209209+ var err error
210210+ did := e.Did
211211+212212+ l := i.Logger.With("handler", "ingestVouch")
213213+ l = l.With("nsid", e.Commit.Collection)
214214+215215+ switch e.Commit.Operation {
216216+ case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate:
217217+ raw := json.RawMessage(e.Commit.Record)
218218+ record := tangled.GraphVouch{}
219219+ err = json.Unmarshal(raw, &record)
220220+ if err != nil {
221221+ l.Error("invalid record", "err", err)
222222+ return err
223223+ }
224224+225225+ // rkey is the subject_did being vouched for/denounced
226226+ subjectDID := e.Commit.RKey
227227+228228+ _, err = syntax.ParseDID(subjectDID)
229229+ if err != nil {
230230+ l.Error("invalid subject_did in rkey", "err", err, "rkey", subjectDID)
231231+ return fmt.Errorf("invalid subject_did: %w", err)
232232+ }
233233+234234+ if did == subjectDID {
235235+ l.Warn("attempted self-vouch", "did", did)
236236+ return fmt.Errorf("cannot vouch for self")
237237+ }
238238+239239+ subjectId, err := i.IdResolver.ResolveIdent(ctx, subjectDID)
240240+ if err != nil {
241241+ return err
242242+ }
243243+244244+ if subjectId.Handle.IsInvalidHandle() {
245245+ return err
246246+ }
247247+248248+ kind := "vouch"
249249+ if record.Kind != nil {
250250+ kind = *record.Kind
251251+ }
252252+253253+ if kind != "vouch" && kind != "denounce" {
254254+ l.Error("invalid kind", "kind", kind)
255255+ return fmt.Errorf("invalid kind: %s", kind)
256256+ }
257257+258258+ err = db.AddVouch(i.Db, &models.Vouch{
259259+ Did: did,
260260+ SubjectDid: subjectDID,
261261+ Kind: kind,
262262+ Reason: record.Reason,
263263+ })
264264+265265+ case jmodels.CommitOperationDelete:
266266+ err = db.DeleteVouchByRkey(i.Db, did, e.Commit.RKey)
267267+ }
268268+269269+ if err != nil {
270270+ return fmt.Errorf("failed to %s vouch record: %w", e.Commit.Operation, err)
201271 }
202272203273 return nil