-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathcuration.go
More file actions
265 lines (238 loc) · 7.52 KB
/
curation.go
File metadata and controls
265 lines (238 loc) · 7.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
package main
import (
"context"
"iter"
"unsafe"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/nip27"
"fiatjaf.com/nostr/nip70"
"fiatjaf.com/nostr/sdk"
"github.com/fiatjaf/pyramid/global"
"github.com/fiatjaf/pyramid/pyramid"
"github.com/mailru/easyjson"
)
var reactionKinds = []nostr.Kind{6, 7, 9321, 9735, 9802, 1, 1111}
func processReactions(ctx context.Context, event nostr.Event) {
totalMembers := pyramid.Members.Size()
if totalMembers <= 10 {
// makes no sense to have this in this case
return
}
// sum existing reactions for this target, a unique vote per member
popularVotes := make(map[string]map[nostr.PubKey]struct{})
bestVotes := make(map[string]map[nostr.PubKey]struct{})
for val, tagNames := range getTargets(event) {
for _, tagName := range tagNames {
for reaction := range global.IL.Main.QueryEvents(nostr.Filter{
Since: nostr.Now() - 60*60*24*7, /* a week ago */
Kinds: reactionKinds,
Tags: nostr.TagMap{tagName: []string{val}},
}, 1000) {
for target := range getTargets(reaction) {
if votes, ok := popularVotes[target]; ok {
votes[reaction.PubKey] = struct{}{}
} else {
popularVotes[target] = map[nostr.PubKey]struct{}{reaction.PubKey: {}}
}
if reaction.Kind != 1 && reaction.Kind != 1111 {
if votes, ok := bestVotes[target]; ok {
votes[reaction.PubKey] = struct{}{}
} else {
bestVotes[target] = map[nostr.PubKey]struct{}{reaction.PubKey: {}}
}
}
}
}
}
}
popularThreshold := min(2, (totalMembers*global.Settings.Popular.PercentThreshold)/100)
uppermostThreshold := min(3, (totalMembers*global.Settings.Uppermost.PercentThreshold)/100)
// for all events we meet the popular threshold for
for target, votes := range popularVotes {
if len(votes) < popularThreshold {
continue
}
// fetch
targetEvent := fetchEventBasedOnHintsWeHave(target)
if targetEvent == nil {
return
}
if nip70.IsProtected(*targetEvent) || nip70.HasEmbeddedProtected(*targetEvent) {
return
}
// add to the qualified layers
if votes, ok := bestVotes[target]; ok && len(votes) >= uppermostThreshold {
if err := global.IL.Uppermost.SaveEvent(*targetEvent); err != nil && err != eventstore.ErrDupEvent {
log.Warn().Err(err).Msg("failed to save to uppermost layer")
} else {
// if promoted to uppermost, delete from popular
if err := global.IL.Popular.DeleteEvent(targetEvent.ID); err != nil {
log.Warn().Err(err).Msg("failed to remove from popular layer after uppermost promotion")
}
}
} else {
// only save to popular if not saved to uppermost
if err := global.IL.Popular.SaveEvent(*targetEvent); err != nil && err != eventstore.ErrDupEvent {
log.Warn().Err(err).Msg("failed to save to popular layer")
}
}
}
}
// emits a tuple of (either an id or an address, ["a", "q"] or ["e", "q"])
func getTargets(reaction nostr.Event) iter.Seq2[string, []string] {
return func(yield func(string, []string) bool) {
// for zaps consider the zap request
if reaction.Kind == 9735 {
if desc := reaction.Tags.Find("description"); desc != nil {
if err := easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(desc[1]), len(desc[1])), &reaction); err != nil {
return
}
} else {
return
}
}
// ignore reactions that are obviously negative
if reaction.Content == "⚠️" || reaction.Content == "-" {
return
}
if eTag := reaction.Tags.Find("e"); eTag != nil {
if !yield(eTag[1], []string{"e", "q"}) {
return
}
}
if qTag := reaction.Tags.Find("q"); qTag != nil {
val := qTag[1]
if _, err := nostr.IDFromHex(val); err == nil {
if !yield(val, []string{"e", "q"}) {
return
}
} else if _, err := nostr.ParseAddrString(val); err == nil {
if !yield(val, []string{"a", "q"}) {
return
}
}
}
if aTag := reaction.Tags.Find("a"); aTag != nil {
if !yield(aTag[1], []string{"a", "q"}) {
return
}
}
}
}
func fetchEventBasedOnHintsWeHave(target string) *nostr.Event {
ctx := context.Background()
usedHints := make(map[string]struct{})
// try to parse as an address and fetch directly
if ptr, err := nostr.ParseAddrString(target); err == nil {
if evt, _, err := global.Nostr.FetchSpecificEvent(ctx, ptr, sdk.FetchSpecificEventParameters{
SkipLocalStore: false,
}); err == nil && evt != nil {
return evt
}
} else if id, err := nostr.IDFromHex(target); err == nil {
// try to fetch once from local storage or from some hardcoded relay
if evt, _, err := global.Nostr.FetchSpecificEvent(ctx, nostr.EventPointer{
ID: id,
}, sdk.FetchSpecificEventParameters{
SkipLocalStore: false,
}); err == nil && evt != nil {
return evt
}
}
// query for each tag type to try to get more relay hints
for _, tagName := range []string{"e", "E", "a", "A", "q"} {
for result := range global.IL.Main.QueryEvents(nostr.Filter{
Tags: nostr.TagMap{tagName: []string{target}},
}, 100) {
// find the corresponding tag
for _, tag := range result.Tags {
if len(tag) < 2 || tag[0] != tagName || tag[1] != target {
continue
}
// check for relay hints in the tag
if len(tag) >= 3 {
hint := nostr.NormalizeURL(tag[2])
if _, used := usedHints[hint]; !used {
usedHints[hint] = struct{}{}
// try to fetch based on tag type
var ptr nostr.Pointer
if tagName == "a" || tagName == "A" {
if p, err := nostr.ParseAddrString(tag[1]); err == nil {
p.Relays = []string{hint}
ptr = p
}
} else {
if id, err := nostr.IDFromHex(tag[1]); err == nil {
ptr = nostr.EventPointer{ID: id, Relays: []string{hint}}
}
}
if ptr != nil {
if evt, _, err := global.Nostr.FetchSpecificEvent(ctx, ptr, sdk.FetchSpecificEventParameters{SkipLocalStore: true}); err == nil && evt != nil {
return evt
}
}
}
}
// check for author hint for non-address targets
if tagName != "a" && tagName != "A" {
hint := result.PubKey.Hex()
if _, used := usedHints[hint]; !used {
usedHints[hint] = struct{}{}
if id, err := nostr.IDFromHex(target); err == nil {
ptr := nostr.EventPointer{ID: id, Author: result.PubKey}
if evt, _, err := global.Nostr.FetchSpecificEvent(ctx, ptr, sdk.FetchSpecificEventParameters{
SkipLocalStore: true,
}); err == nil && evt != nil {
return evt
}
}
}
}
}
// parse content with nip27 for additional references
for blk := range nip27.Parse(result.Content) {
willUse := false
switch ptr := blk.Pointer.(type) {
case nostr.EventPointer:
if ptr.ID.Hex() != target {
continue
}
for _, relay := range ptr.Relays {
relay = nostr.NormalizeURL(relay)
if _, used := usedHints[relay]; !used {
willUse = true
usedHints[relay] = struct{}{}
}
}
if ptr.Author != nostr.ZeroPK {
hint := ptr.Author.Hex()
if _, used := usedHints[hint]; !used {
willUse = true
usedHints[hint] = struct{}{}
}
}
case nostr.EntityPointer:
if ptr.AsTagReference() != target {
continue
}
for _, relay := range ptr.Relays {
relay = nostr.NormalizeURL(relay)
if _, used := usedHints[relay]; !used {
willUse = true
usedHints[relay] = struct{}{}
}
}
default:
continue
}
if willUse {
if evt, _, err := global.Nostr.FetchSpecificEvent(ctx, blk.Pointer, sdk.FetchSpecificEventParameters{SkipLocalStore: true}); err == nil && evt != nil {
return evt
}
}
}
}
}
return nil
}