+492
Diff
round #0
+11
appview/db/db.go
+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
+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
+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
+
}