1+ import { useQuery } from "@tanstack/react-query" ;
2+ import { useNostr } from "@/hooks/useNostr" ;
3+ import { KINDS } from "@/lib/nostr-kinds" ;
4+ import type { NostrEvent } from "@nostrify/nostrify" ;
5+
6+ interface GroupDeletionRequest {
7+ deletionEvent : NostrEvent ;
8+ groupId : string ;
9+ isValid : boolean ;
10+ reason ?: string ;
11+ }
12+
13+ /**
14+ * Hook to fetch and validate deletion requests for groups
15+ * Returns a map of groupId -> deletion request info
16+ */
17+ export function useGroupDeletionRequests ( groupIds : string [ ] ) {
18+ const { nostr } = useNostr ( ) ;
19+
20+ return useQuery ( {
21+ queryKey : [ "group-deletion-requests" , groupIds . sort ( ) ] ,
22+ queryFn : async ( c ) => {
23+ if ( groupIds . length === 0 ) {
24+ return new Map < string , GroupDeletionRequest > ( ) ;
25+ }
26+
27+ try {
28+ const signal = AbortSignal . any ( [ c . signal , AbortSignal . timeout ( 5000 ) ] ) ;
29+
30+ // Query for kind 5 deletion events that reference groups via 'a' tags or 'e' tags
31+ // We need to extract event IDs from group IDs for 'e' tag filtering
32+ const groupEventIds : string [ ] = [ ] ;
33+
34+ // For each group ID, we need to query for the actual group event to get its event ID
35+ // This is needed for 'e' tag compatibility
36+ for ( const groupId of groupIds ) {
37+ const parts = groupId . split ( ":" ) ;
38+ if ( parts . length === 3 && parts [ 0 ] === KINDS . GROUP . toString ( ) ) {
39+ const [ , pubkey , identifier ] = parts ;
40+ try {
41+ const groupEvents = await nostr . query ( [ {
42+ kinds : [ KINDS . GROUP ] ,
43+ authors : [ pubkey ] ,
44+ "#d" : [ identifier ] ,
45+ limit : 1
46+ } ] , { signal : AbortSignal . timeout ( 2000 ) } ) ;
47+
48+ if ( groupEvents . length > 0 ) {
49+ groupEventIds . push ( groupEvents [ 0 ] . id ) ;
50+ }
51+ } catch ( error ) {
52+ // If we can't fetch the group event, skip it for 'e' tag filtering
53+ console . warn ( "Could not fetch group event for deletion filtering:" , groupId , error ) ;
54+ }
55+ }
56+ }
57+
58+ // Query for deletion events using both 'a' tags and 'e' tags
59+ const deletionEvents = await nostr . query ( [
60+ {
61+ kinds : [ KINDS . DELETION ] ,
62+ "#a" : groupIds ,
63+ limit : 500
64+ } ,
65+ ...( groupEventIds . length > 0 ? [ {
66+ kinds : [ KINDS . DELETION ] ,
67+ "#e" : groupEventIds ,
68+ limit : 500
69+ } ] : [ ] )
70+ ] , { signal } ) ;
71+
72+ const deletionMap = new Map < string , GroupDeletionRequest > ( ) ;
73+
74+ // Process each deletion event
75+ for ( const deletionEvent of deletionEvents ) {
76+ // Find the 'a' tags that reference groups (kind 34550)
77+ const aTags = deletionEvent . tags . filter ( tag =>
78+ tag [ 0 ] === "a" && tag [ 1 ] && tag [ 1 ] . startsWith ( `${ KINDS . GROUP } :` )
79+ ) ;
80+
81+ // Process 'a' tags (addressable event references)
82+ for ( const aTag of aTags ) {
83+ const groupId = aTag [ 1 ] ;
84+
85+ // Parse the group ID to get the owner's pubkey
86+ const parts = groupId . split ( ":" ) ;
87+ if ( parts . length !== 3 || parts [ 0 ] !== KINDS . GROUP . toString ( ) ) {
88+ continue ;
89+ }
90+
91+ const [ , groupOwnerPubkey ] = parts ;
92+
93+ // Validate that the deletion request is from the group owner
94+ const isValid = deletionEvent . pubkey === groupOwnerPubkey ;
95+
96+ // Only store the most recent valid deletion request for each group
97+ const existing = deletionMap . get ( groupId ) ;
98+ if ( ! existing || ( isValid && deletionEvent . created_at > existing . deletionEvent . created_at ) ) {
99+ deletionMap . set ( groupId , {
100+ deletionEvent,
101+ groupId,
102+ isValid,
103+ reason : deletionEvent . content || undefined
104+ } ) ;
105+ }
106+ }
107+
108+ // Also check 'e' tags for event ID references
109+ // This provides compatibility with relays that primarily use 'e' tags for deletions
110+ const eTags = deletionEvent . tags . filter ( tag => tag [ 0 ] === "e" && tag [ 1 ] ) ;
111+
112+ if ( eTags . length > 0 ) {
113+ // For 'e' tag processing, we need to be more careful since we need to match
114+ // event IDs to group IDs. We'll query for the referenced events to validate them.
115+ for ( const eTag of eTags ) {
116+ const eventId = eTag [ 1 ] ;
117+
118+ try {
119+ // Query for the referenced event to see if it's a group event
120+ const referencedEvents = await nostr . query ( [ {
121+ ids : [ eventId ] ,
122+ kinds : [ KINDS . GROUP ] ,
123+ limit : 1
124+ } ] , { signal : AbortSignal . timeout ( 2000 ) } ) ;
125+
126+ if ( referencedEvents . length > 0 ) {
127+ const groupEvent = referencedEvents [ 0 ] ;
128+ const dTag = groupEvent . tags . find ( tag => tag [ 0 ] === "d" ) ;
129+
130+ if ( dTag ) {
131+ const groupId = `${ KINDS . GROUP } :${ groupEvent . pubkey } :${ dTag [ 1 ] } ` ;
132+
133+ // Check if this group is in our target list
134+ if ( groupIds . includes ( groupId ) ) {
135+ // Validate that the deletion request is from the group owner
136+ const isValid = deletionEvent . pubkey === groupEvent . pubkey ;
137+
138+ // Only store the most recent valid deletion request for each group
139+ const existing = deletionMap . get ( groupId ) ;
140+ if ( ! existing || ( isValid && deletionEvent . created_at > existing . deletionEvent . created_at ) ) {
141+ deletionMap . set ( groupId , {
142+ deletionEvent,
143+ groupId,
144+ isValid,
145+ reason : deletionEvent . content || undefined
146+ } ) ;
147+ }
148+ }
149+ }
150+ }
151+ } catch ( error ) {
152+ // If we can't fetch the referenced event, skip this 'e' tag
153+ console . warn ( "Could not fetch referenced event for deletion validation:" , eventId , error ) ;
154+ continue ;
155+ }
156+ }
157+ }
158+ }
159+
160+ return deletionMap ;
161+ } catch ( error ) {
162+ console . error ( "Error fetching group deletion requests:" , error ) ;
163+ return new Map < string , GroupDeletionRequest > ( ) ;
164+ }
165+ } ,
166+ enabled : groupIds . length > 0 ,
167+ staleTime : 30000 , // 30 seconds
168+ gcTime : 300000 , // 5 minutes
169+ } ) ;
170+ }
171+
172+ /**
173+ * Hook to check if a specific group has been deleted
174+ */
175+ export function useIsGroupDeleted ( groupId : string | undefined ) {
176+ const { data : deletionRequests } = useGroupDeletionRequests (
177+ groupId ? [ groupId ] : [ ]
178+ ) ;
179+
180+ if ( ! groupId || ! deletionRequests ) {
181+ return {
182+ isDeleted : false ,
183+ deletionRequest : undefined ,
184+ isLoading : false
185+ } ;
186+ }
187+
188+ const deletionRequest = deletionRequests . get ( groupId ) ;
189+
190+ return {
191+ isDeleted : deletionRequest ?. isValid || false ,
192+ deletionRequest : deletionRequest ?. isValid ? deletionRequest : undefined ,
193+ isLoading : false
194+ } ;
195+ }
0 commit comments