diff --git a/README.md b/README.md index 7955288..ecc18f8 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ ## Supported NIPs - [x] **NIP-01**: Basic protocol flow + - [x] Regular Events + - [x] Replaceable Events (kinds 10000-19999) + - [x] Ephemeral Events (kinds 20000-29999) + - [x] Addressable Events (kinds 30000-39999) - [x] **NIP-02**: Contact List and Petnames - `kind: 3`: delete past contact lists as soon as the relay receives a new one - [x] **NIP-04**: Encrypted Direct Message @@ -36,8 +40,8 @@ - not planned - [x] **NIP-28** Public Chat - `kind: 41`: handled similar to `kind 0` metadata events -- [ ] **NIP-33**: Parameterized Replaceable Events - - todo +- [x] **NIP-33**: Addressable Events (moved to NIP-01) + - ✅ Implemented as part of NIP-01 addressable events - [ ] **NIP-40**: Expiration Timestamp - todo - [x] **NIP-42**: Authentication of clients to relays diff --git a/crud.py b/crud.py index a084c3a..82fb056 100644 --- a/crud.py +++ b/crud.py @@ -193,9 +193,20 @@ async def mark_events_deleted(relay_id: str, nostr_filter: NostrFilter): async def delete_events(relay_id: str, nostr_filter: NostrFilter): if nostr_filter.is_empty(): return None - _, where, values = nostr_filter.to_sql_components(relay_id) + inner_joins, where, values = nostr_filter.to_sql_components(relay_id) - query = f"DELETE from nostrrelay.events WHERE {' AND '.join(where)}" + if inner_joins: + # Use subquery for DELETE operations with JOINs + subquery = f""" + SELECT nostrrelay.events.id FROM nostrrelay.events + {" ".join(inner_joins)} + WHERE {" AND ".join(where)} + """ + query = f"DELETE FROM nostrrelay.events WHERE id IN ({subquery})" + else: + # Simple DELETE without JOINs + query = f"DELETE FROM events WHERE {' AND '.join(where)}" + await db.execute(query, values) # todo: delete tags diff --git a/relay/client_connection.py b/relay/client_connection.py index dbe459e..7c0fdde 100644 --- a/relay/client_connection.py +++ b/relay/client_connection.py @@ -166,6 +166,19 @@ class NostrClientConnection: self.relay_id, NostrFilter(kinds=[e.kind], authors=[e.pubkey], until=e.created_at), ) + if e.is_addressable_event: + # Extract 'd' tag value for addressable replacement (NIP-01) + d_tag_value = next((t[1] for t in e.tags if t[0] == "d"), None) + + if d_tag_value: + deletion_filter = NostrFilter( + kinds=[e.kind], + authors=[e.pubkey], + **{"#d": [d_tag_value]}, + until=e.created_at + ) + + await delete_events(self.relay_id, deletion_filter) if not e.is_ephemeral_event: await create_event(e) await self._broadcast_event(e) diff --git a/relay/event.py b/relay/event.py index 5e9e923..44d34ae 100644 --- a/relay/event.py +++ b/relay/event.py @@ -71,6 +71,10 @@ class NostrEvent(BaseModel): def is_ephemeral_event(self) -> bool: return self.kind >= 20000 and self.kind < 30000 + @property + def is_addressable_event(self) -> bool: + return self.kind >= 30000 and self.kind < 40000 + def check_signature(self): event_id = self.event_id if self.id != event_id: diff --git a/relay/filter.py b/relay/filter.py index 1160cc9..835b893 100644 --- a/relay/filter.py +++ b/relay/filter.py @@ -8,6 +8,7 @@ from .event import NostrEvent class NostrFilter(BaseModel): e: list[str] = Field(default=[], alias="#e") p: list[str] = Field(default=[], alias="#p") + d: list[str] = Field(default=[], alias="#d") ids: list[str] = [] authors: list[str] = [] kinds: list[int] = [] @@ -30,9 +31,12 @@ class NostrFilter(BaseModel): if self.until and self.until > 0 and e.created_at > self.until: return False - found_e_tag = self.tag_in_list(e.tags, "e") - found_p_tag = self.tag_in_list(e.tags, "p") - if not found_e_tag or not found_p_tag: + # Check tag filters - only fail if filter is specified and no match found + if not self.tag_in_list(e.tags, "e"): + return False + if not self.tag_in_list(e.tags, "p"): + return False + if not self.tag_in_list(e.tags, "d"): return False return True @@ -87,6 +91,14 @@ class NostrFilter(BaseModel): ) where.append(f" p_tags.value in ({p_s}) AND p_tags.name = 'p'") + if len(self.d): + d_s = ",".join([f"'{d}'" for d in self.d]) + d_join = "INNER JOIN nostrrelay.event_tags d_tags ON nostrrelay.events.id = d_tags.event_id" + d_where = f" d_tags.value in ({d_s}) AND d_tags.name = 'd'" + + inner_joins.append(d_join) + where.append(d_where) + if len(self.ids) != 0: ids = ",".join([f"'{_id}'" for _id in self.ids]) where.append(f"id IN ({ids})")