diff --git a/helpers.go b/helpers.go index c0bd969..4fb6cb0 100644 --- a/helpers.go +++ b/helpers.go @@ -154,3 +154,49 @@ func extractEventID(jsonStr string) string { // get 64 characters of the id return jsonStr[start : start+64] } + +func extractDTag(jsonStr string) string { + // look for ["d", pattern + start := strings.Index(jsonStr, `["d"`) + if start == -1 { + return "" + } + + // move to the next quote + offset := strings.IndexRune(jsonStr[start+4:], '"') + start += 4 + offset + 1 + + // find the ending quote + end := strings.IndexRune(jsonStr[start:], '"') + if end == -1 { + return "" + } + + // get the contents + return jsonStr[start : start+end] +} + +func extractTimestamp(jsonStr string) Timestamp { + // look for "created_at": pattern + start := strings.Index(jsonStr, `"created_at"`) + if start == -1 { + return 0 + } + + // move to the next number + offset := strings.IndexAny(jsonStr[start+12:], "9876543210") + if offset == -1 { + return 0 + } + start += 12 + offset + + // find the end + end := strings.IndexAny(jsonStr[start:], ",} ") + if end == -1 { + return 0 + } + + // get the contents + ts, _ := strconv.ParseInt(jsonStr[start:start+end], 10, 64) + return Timestamp(ts) +} diff --git a/helpers_test.go b/helpers_test.go index 1bf6fc0..e7872f5 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -43,4 +43,26 @@ func TestSubIdExtract(t *testing.T) { } } +func TestDTagExtract(t *testing.T) { + { + data := `["EVENT", "xxz" ,{"kind":30023,"id":"6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16","pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["d", "balalaika"]["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638"}]` + require.Equal(t, "balalaika", extractDTag(data)) + } + { + data := `{"kind":1,"pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638","id": "6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16" }` + require.Equal(t, "", extractDTag(data)) + } +} + +func TestTimestampExtract(t *testing.T) { + { + data := `["EVENT", "xxz" ,{"kind":30023,"id":"6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16","pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at": 1736909072 ,"tags":[["d", "balalaika"]["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638"}]` + require.Equal(t, Timestamp(1736909072), extractTimestamp(data)) + } + { + data := `{"kind":1,"pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638","id": "6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16" ,"created_at":01736909054}` + require.Equal(t, Timestamp(1736909054), extractTimestamp(data)) + } +} + func ptr[S any](s S) *S { return &s } diff --git a/relay.go b/relay.go index d02d340..e38c190 100644 --- a/relay.go +++ b/relay.go @@ -231,9 +231,13 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error // if this is an "EVENT" we will have this preparser logic that should speed things up a little // as we skip handling duplicate events subid := extractSubID(message) - subscription, ok := r.Subscriptions.Load(subIdToSerial(subid)) - if ok && subscription.checkDuplicate != nil { - if subscription.checkDuplicate(extractEventID(message[10+len(subid):]), r.URL) { + sub, ok := r.Subscriptions.Load(subIdToSerial(subid)) + if ok && sub.checkDuplicate != nil { + if sub.checkDuplicate(extractEventID(message[10+len(subid):]), r.URL) { + continue + } + } else if sub.checkDuplicateReplaceable != nil { + if sub.checkDuplicateReplaceable(extractDTag(message), extractTimestamp(message)) { continue } } @@ -437,6 +441,8 @@ func (r *Relay) PrepareSubscription(ctx context.Context, filters Filters, opts . label = string(o) case WithCheckDuplicate: sub.checkDuplicate = o + case WithCheckDuplicateReplaceable: + sub.checkDuplicateReplaceable = o } } diff --git a/subscription.go b/subscription.go index e6d7e1c..5c8de42 100644 --- a/subscription.go +++ b/subscription.go @@ -33,10 +33,14 @@ type Subscription struct { // Context will be .Done() when the subscription ends Context context.Context - // if it is not nil, CheckDuplicate will be called for every event received + // if it is not nil, checkDuplicate will be called for every event received // if it returns true that event will not be processed further. checkDuplicate func(id string, relay string) bool + // if it is not nil, checkDuplicateReplaceable will be called for every event received + // if it returns true that event will not be processed further. + checkDuplicateReplaceable func(d string, ts Timestamp) bool + match func(*Event) bool // this will be either Filters.Match or Filters.MatchIgnoringTimestampConstraints live atomic.Bool eosed atomic.Bool @@ -63,9 +67,15 @@ type WithCheckDuplicate func(id, relay string) bool func (_ WithCheckDuplicate) IsSubscriptionOption() {} +// WithCheckDuplicateReplaceable sets checkDuplicateReplaceable on the subscription +type WithCheckDuplicateReplaceable func(d string, ts Timestamp) bool + +func (_ WithCheckDuplicateReplaceable) IsSubscriptionOption() {} + var ( _ SubscriptionOption = (WithLabel)("") _ SubscriptionOption = (WithCheckDuplicate)(nil) + _ SubscriptionOption = (WithCheckDuplicateReplaceable)(nil) ) func (sub *Subscription) start() {