fix: rewrite useCVMClient to query relays directly instead of NIP-44 round-trip
The encrypted CVM request/response cycle (kind 21000/21001) was too unreliable in the browser — timeouts on every call. Replace with direct Nostr relay queries using nostr.group() which is already proven to work in the app. Same filtering logic as the server. No dependency on CVM server being up for data queries. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
c672066c2c
commit
3a4e4ead05
1 changed files with 335 additions and 149 deletions
|
|
@ -1,47 +1,23 @@
|
|||
/**
|
||||
* useCVMClient
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
* Browser-side client for the SkateSpots CVM server running on LNVPS.
|
||||
* Queries Nostr relays directly from the browser — same logic as the CVM
|
||||
* server but without the NIP-44 encrypted round-trip (which was unreliable).
|
||||
*
|
||||
* Calls the MCP tools exposed by the server via a direct Nostr-encrypted
|
||||
* request/response cycle (NIP-44 gift wrap style), without bundling the
|
||||
* full @contextvm/sdk (which has Node.js dependencies incompatible with
|
||||
* browser bundling).
|
||||
*
|
||||
* Protocol (simplified MCP over Nostr):
|
||||
* 1. Generate ephemeral Nostr keypair for this session
|
||||
* 2. Encrypt the MCP JSON-RPC request with NIP-44 to the server pubkey
|
||||
* 3. Publish as kind 21000 (ContextVM request event) to relays
|
||||
* 4. Subscribe for a response event tagged with our ephemeral pubkey
|
||||
* 5. Decrypt the response and parse the MCP result
|
||||
*
|
||||
* Server pubkey: e3564bb877adb0c99a7e4973ccb22e516d7d9eca0027d0bd62a04174ed670530
|
||||
* This gives instant results with no dependency on the CVM server being up.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
generateSecretKey,
|
||||
getPublicKey,
|
||||
finalizeEvent,
|
||||
Relay,
|
||||
} from 'nostr-tools';
|
||||
import { nip44 } from 'nostr-tools';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
|
||||
// ─── CVM Server Config ───────────────────────────────────────────────────────
|
||||
// ─── Relay list ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const CVM_SERVER_PUBKEY =
|
||||
'e3564bb877adb0c99a7e4973ccb22e516d7d9eca0027d0bd62a04174ed670530';
|
||||
|
||||
const CVM_RELAYS = [
|
||||
const DATA_RELAYS = [
|
||||
'wss://goskatespots.nostr1.com',
|
||||
'wss://relay.ditto.pub',
|
||||
'wss://relay.damus.io',
|
||||
];
|
||||
|
||||
// ContextVM uses kind 21000 for client→server requests (ephemeral)
|
||||
const CVM_REQUEST_KIND = 21000;
|
||||
const CVM_RESPONSE_KIND = 21001;
|
||||
|
||||
// ─── Tool result types ───────────────────────────────────────────────────────
|
||||
|
||||
export interface SpotResult {
|
||||
|
|
@ -49,6 +25,7 @@ export interface SpotResult {
|
|||
description: string;
|
||||
city?: string;
|
||||
country?: string;
|
||||
location?: string;
|
||||
difficulty?: string;
|
||||
features: string[];
|
||||
geohash?: string;
|
||||
|
|
@ -108,118 +85,41 @@ export interface CommunityStats {
|
|||
queried_at: string;
|
||||
}
|
||||
|
||||
// ─── Core: send a CVM tool call via Nostr ────────────────────────────────────
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async function callCVMTool(
|
||||
toolName: string,
|
||||
toolArgs: Record<string, unknown>,
|
||||
timeoutMs = 15000
|
||||
): Promise<unknown> {
|
||||
// Generate ephemeral keypair for this request
|
||||
const sk = generateSecretKey();
|
||||
const pk = getPublicKey(sk);
|
||||
|
||||
// Build the MCP JSON-RPC request
|
||||
const requestId = Math.floor(Math.random() * 1000000);
|
||||
const mcpRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: requestId,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: toolName,
|
||||
arguments: toolArgs,
|
||||
},
|
||||
};
|
||||
|
||||
// Encrypt request to server using NIP-44
|
||||
const conversationKey = nip44.v2.utils.getConversationKey(sk, CVM_SERVER_PUBKEY);
|
||||
const encryptedContent = nip44.v2.encrypt(
|
||||
JSON.stringify(mcpRequest),
|
||||
conversationKey
|
||||
);
|
||||
|
||||
// Build and sign the request event
|
||||
const signedEvent = finalizeEvent({
|
||||
kind: CVM_REQUEST_KIND,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['p', CVM_SERVER_PUBKEY]],
|
||||
content: encryptedContent,
|
||||
}, sk);
|
||||
|
||||
// Use raw Relay connections (nostr-tools v2 compatible)
|
||||
// Try each relay in order, race for the first response
|
||||
return new Promise((resolve, reject) => {
|
||||
const relays: InstanceType<typeof Relay>[] = [];
|
||||
let settled = false;
|
||||
|
||||
const cleanup = () => {
|
||||
relays.forEach(r => { try { r.close(); } catch { /* ignore */ } });
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
cleanup();
|
||||
reject(new Error(`CVM tool call timed out after ${timeoutMs}ms`));
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
const since = Math.floor(Date.now() / 1000) - 5;
|
||||
|
||||
CVM_RELAYS.forEach(async (url) => {
|
||||
try {
|
||||
const relay = await Relay.connect(url);
|
||||
relays.push(relay);
|
||||
|
||||
// Subscribe for response BEFORE publishing
|
||||
const sub = relay.subscribe(
|
||||
[{ kinds: [CVM_RESPONSE_KIND], '#p': [pk], since }],
|
||||
{
|
||||
onevent(responseEvent) {
|
||||
if (settled) return;
|
||||
try {
|
||||
const convKey = nip44.v2.utils.getConversationKey(sk, responseEvent.pubkey);
|
||||
const decrypted = nip44.v2.decrypt(responseEvent.content, convKey);
|
||||
const mcpResponse = JSON.parse(decrypted);
|
||||
if (mcpResponse.id !== requestId) return;
|
||||
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
sub.close();
|
||||
cleanup();
|
||||
|
||||
if (mcpResponse.error) {
|
||||
reject(new Error(mcpResponse.error.message ?? 'CVM error'));
|
||||
} else {
|
||||
resolve(mcpResponse.result);
|
||||
}
|
||||
} catch {
|
||||
// Not for us — ignore
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Now publish the request
|
||||
await relay.publish(signedEvent);
|
||||
} catch {
|
||||
// This relay failed — others may still succeed
|
||||
}
|
||||
});
|
||||
});
|
||||
function getTag(event: { tags: string[][] }, name: string): string | undefined {
|
||||
return event.tags.find((t) => t[0] === name)?.[1];
|
||||
}
|
||||
|
||||
// Parse JSON from MCP tool result content array
|
||||
function parseToolResult<T>(result: unknown): T {
|
||||
const r = result as { content?: Array<{ type: string; text?: string }> };
|
||||
const text = r?.content?.[0]?.text;
|
||||
if (!text) throw new Error('Empty tool result');
|
||||
return JSON.parse(text) as T;
|
||||
function getTags(event: { tags: string[][] }, name: string): string[] {
|
||||
return event.tags
|
||||
.filter((t) => t[0] === name)
|
||||
.map((t) => t[1])
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function deduplicateAddressable(events: { pubkey: string; kind: number; tags: string[][]; created_at: number }[]) {
|
||||
const seen = new Map<string, typeof events[0]>();
|
||||
for (const e of events) {
|
||||
const d = getTag(e, 'd') ?? '';
|
||||
const key = `${e.pubkey}:${e.kind}:${d}`;
|
||||
const existing = seen.get(key);
|
||||
if (!existing || e.created_at > existing.created_at) {
|
||||
seen.set(key, e);
|
||||
}
|
||||
}
|
||||
return Array.from(seen.values());
|
||||
}
|
||||
|
||||
function buildNaddr(event: { kind: number; pubkey: string; tags: string[][] }): string {
|
||||
return `${event.kind}:${event.pubkey}:${getTag(event, 'd') ?? ''}`;
|
||||
}
|
||||
|
||||
// ─── The hook ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function useCVMClient() {
|
||||
const { nostr } = useNostr();
|
||||
|
||||
const findSpots = useCallback(
|
||||
async (params: {
|
||||
city?: string;
|
||||
|
|
@ -229,10 +129,100 @@ export function useCVMClient() {
|
|||
search?: string;
|
||||
limit?: number;
|
||||
}): Promise<{ count: number; spots: SpotResult[] }> => {
|
||||
const result = await callCVMTool('find_spots', params as Record<string, unknown>);
|
||||
return parseToolResult(result);
|
||||
const signal = AbortSignal.any([AbortSignal.timeout(10000)]);
|
||||
const relayGroup = nostr.group(DATA_RELAYS);
|
||||
|
||||
const raw = await relayGroup.query(
|
||||
[{ kinds: [30818], limit: 500 }],
|
||||
{ signal }
|
||||
);
|
||||
|
||||
let spots = deduplicateAddressable(raw as typeof raw & { tags: string[][] }[]);
|
||||
|
||||
// Only valid skate spots
|
||||
spots = spots.filter((e) =>
|
||||
e.tags.some((t) => t[0] === 't' && t[1] === 'skatespot')
|
||||
);
|
||||
|
||||
if (params.difficulty && params.difficulty !== 'all') {
|
||||
spots = spots.filter(
|
||||
(e) => getTag(e, 'difficulty')?.toLowerCase() === params.difficulty
|
||||
);
|
||||
}
|
||||
|
||||
if (params.city) {
|
||||
const q = params.city.toLowerCase();
|
||||
spots = spots.filter(
|
||||
(e) =>
|
||||
(getTag(e, 'city') ?? '').toLowerCase().includes(q) ||
|
||||
(getTag(e, 'location') ?? '').toLowerCase().includes(q) ||
|
||||
(getTag(e, 'title') ?? '').toLowerCase().includes(q) ||
|
||||
(e as unknown as { content: string }).content.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
if (params.country) {
|
||||
const q = params.country.toLowerCase();
|
||||
spots = spots.filter(
|
||||
(e) =>
|
||||
(getTag(e, 'country') ?? '').toLowerCase().includes(q) ||
|
||||
(getTag(e, 'location') ?? '').toLowerCase().includes(q) ||
|
||||
(e as unknown as { content: string }).content.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
if (params.features && params.features.length > 0) {
|
||||
spots = spots.filter((e) => {
|
||||
const spotFeatures = [
|
||||
...getTags(e, 'feature'),
|
||||
...getTags(e, 't'),
|
||||
].map((f) => f.toLowerCase());
|
||||
const content = (e as unknown as { content: string }).content.toLowerCase();
|
||||
return params.features!.some(
|
||||
(f) =>
|
||||
spotFeatures.some((sf) => sf.includes(f.toLowerCase())) ||
|
||||
content.includes(f.toLowerCase())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (params.search) {
|
||||
const q = params.search.toLowerCase();
|
||||
spots = spots.filter(
|
||||
(e) =>
|
||||
(getTag(e, 'title') ?? '').toLowerCase().includes(q) ||
|
||||
(getTag(e, 'name') ?? '').toLowerCase().includes(q) ||
|
||||
(e as unknown as { content: string }).content.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
spots.sort((a, b) => b.created_at - a.created_at);
|
||||
spots = spots.slice(0, params.limit ?? 10);
|
||||
|
||||
const result: SpotResult[] = spots.map((e) => {
|
||||
const ev = e as unknown as { content: string; pubkey: string; kind: number; tags: string[][]; created_at: number };
|
||||
const imeta = ev.tags.find((t) => t[0] === 'imeta');
|
||||
const imetaUrl = imeta?.find((p) => p.startsWith('url '))?.replace('url ', '');
|
||||
return {
|
||||
name: getTag(ev, 'title') ?? getTag(ev, 'name') ?? 'Unnamed Spot',
|
||||
description: ev.content.slice(0, 300),
|
||||
city: getTag(ev, 'city'),
|
||||
country: getTag(ev, 'country'),
|
||||
location: getTag(ev, 'location'),
|
||||
difficulty: getTag(ev, 'difficulty'),
|
||||
features: getTags(ev, 't').filter((t) => t !== 'skatespot'),
|
||||
geohash: getTag(ev, 'g'),
|
||||
image: getTag(ev, 'image') ?? imetaUrl,
|
||||
author_pubkey: ev.pubkey,
|
||||
naddr: buildNaddr(ev),
|
||||
url: `https://skate-spots.org/spot/${ev.pubkey}/${getTag(ev, 'd') ?? ''}`,
|
||||
created_at: new Date(ev.created_at * 1000).toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
return { count: result.length, spots: result };
|
||||
},
|
||||
[]
|
||||
[nostr]
|
||||
);
|
||||
|
||||
const findEvents = useCallback(
|
||||
|
|
@ -244,10 +234,73 @@ export function useCVMClient() {
|
|||
to_date?: string;
|
||||
limit?: number;
|
||||
}): Promise<{ count: number; events: EventResult[] }> => {
|
||||
const result = await callCVMTool('find_events', params as Record<string, unknown>);
|
||||
return parseToolResult(result);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const signal = AbortSignal.any([AbortSignal.timeout(10000)]);
|
||||
const relayGroup = nostr.group(DATA_RELAYS);
|
||||
|
||||
const raw = await relayGroup.query(
|
||||
[{ kinds: [31923], limit: 200, since: now }],
|
||||
{ signal }
|
||||
);
|
||||
|
||||
let events = deduplicateAddressable(raw as typeof raw & { tags: string[][] }[]);
|
||||
|
||||
if (params.city) {
|
||||
const q = params.city.toLowerCase();
|
||||
events = events.filter(
|
||||
(e) =>
|
||||
(getTag(e, 'location') ?? '').toLowerCase().includes(q) ||
|
||||
(getTag(e, 'city') ?? '').toLowerCase().includes(q) ||
|
||||
(e as unknown as { content: string }).content.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
if (params.event_type) {
|
||||
const q = params.event_type.toLowerCase();
|
||||
events = events.filter(
|
||||
(e) =>
|
||||
getTags(e, 't').some((t) => t.toLowerCase().includes(q)) ||
|
||||
(getTag(e, 'title') ?? '').toLowerCase().includes(q) ||
|
||||
(e as unknown as { content: string }).content.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
if (params.to_date) {
|
||||
const toTs = Math.floor(new Date(params.to_date).getTime() / 1000);
|
||||
events = events.filter((e) => {
|
||||
const start = parseInt(getTag(e, 'start') ?? '0');
|
||||
return !start || start <= toTs;
|
||||
});
|
||||
}
|
||||
|
||||
events.sort((a, b) => {
|
||||
const startA = parseInt(getTag(a, 'start') ?? '0');
|
||||
const startB = parseInt(getTag(b, 'start') ?? '0');
|
||||
return startA - startB;
|
||||
});
|
||||
events = events.slice(0, params.limit ?? 10);
|
||||
|
||||
const result: EventResult[] = events.map((e) => {
|
||||
const ev = e as unknown as { content: string; pubkey: string; kind: number; tags: string[][]; created_at: number };
|
||||
const startTag = getTag(ev, 'start');
|
||||
const endTag = getTag(ev, 'end');
|
||||
return {
|
||||
title: getTag(ev, 'title') ?? 'Untitled Event',
|
||||
description: ev.content.slice(0, 300),
|
||||
location: getTag(ev, 'location'),
|
||||
start: startTag ? new Date(parseInt(startTag) * 1000).toISOString() : undefined,
|
||||
end: endTag ? new Date(parseInt(endTag) * 1000).toISOString() : undefined,
|
||||
type: getTags(ev, 't'),
|
||||
image: getTag(ev, 'image'),
|
||||
author_pubkey: ev.pubkey,
|
||||
naddr: buildNaddr(ev),
|
||||
url: `https://skate-spots.org/event/${ev.pubkey}/${getTag(ev, 'd') ?? ''}`,
|
||||
};
|
||||
});
|
||||
|
||||
return { count: result.length, events: result };
|
||||
},
|
||||
[]
|
||||
[nostr]
|
||||
);
|
||||
|
||||
const findCoaches = useCallback(
|
||||
|
|
@ -257,10 +310,64 @@ export function useCVMClient() {
|
|||
location?: string;
|
||||
limit?: number;
|
||||
}): Promise<{ count: number; profiles: CoachResult[] }> => {
|
||||
const result = await callCVMTool('find_coaches', params as Record<string, unknown>);
|
||||
return parseToolResult(result);
|
||||
const kindMap: Record<string, number[]> = {
|
||||
coach: [35001],
|
||||
filmer: [35002],
|
||||
editor: [35003],
|
||||
any: [35001, 35002, 35003],
|
||||
};
|
||||
const kinds = kindMap[params.role ?? 'any'];
|
||||
const signal = AbortSignal.any([AbortSignal.timeout(10000)]);
|
||||
const relayGroup = nostr.group(DATA_RELAYS);
|
||||
|
||||
const raw = await relayGroup.query([{ kinds, limit: 200 }], { signal });
|
||||
let profiles = deduplicateAddressable(raw as typeof raw & { tags: string[][] }[]);
|
||||
|
||||
if (params.skills && params.skills.length > 0) {
|
||||
profiles = profiles.filter((e) => {
|
||||
const profileSkills = getTags(e, 'skill').map((s) => s.toLowerCase());
|
||||
const bio = (e as unknown as { content: string }).content.toLowerCase();
|
||||
return params.skills!.some(
|
||||
(s) =>
|
||||
profileSkills.some((ps) => ps.includes(s.toLowerCase())) ||
|
||||
bio.includes(s.toLowerCase())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (params.location) {
|
||||
const q = params.location.toLowerCase();
|
||||
profiles = profiles.filter(
|
||||
(e) =>
|
||||
(getTag(e, 'location') ?? '').toLowerCase().includes(q) ||
|
||||
(getTag(e, 'city') ?? '').toLowerCase().includes(q) ||
|
||||
(e as unknown as { content: string }).content.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
profiles = profiles.slice(0, params.limit ?? 10);
|
||||
|
||||
const roleLabel = (kind: number): 'coach' | 'filmer' | 'editor' =>
|
||||
kind === 35001 ? 'coach' : kind === 35002 ? 'filmer' : 'editor';
|
||||
|
||||
const result: CoachResult[] = profiles.map((e) => {
|
||||
const ev = e as unknown as { content: string; pubkey: string; kind: number; tags: string[][]; created_at: number };
|
||||
return {
|
||||
role: roleLabel(ev.kind),
|
||||
pubkey: ev.pubkey,
|
||||
bio: ev.content.slice(0, 300),
|
||||
location: getTag(ev, 'location'),
|
||||
skills: getTags(ev, 'skill'),
|
||||
experience: getTag(ev, 'experience'),
|
||||
lightning_address: getTag(ev, 'lud16') ?? getTag(ev, 'contact'),
|
||||
naddr: buildNaddr(ev),
|
||||
url: `https://skate-spots.org/coach/${ev.pubkey}/${getTag(ev, 'd') ?? ''}`,
|
||||
};
|
||||
});
|
||||
|
||||
return { count: result.length, profiles: result };
|
||||
},
|
||||
[]
|
||||
[nostr]
|
||||
);
|
||||
|
||||
const findGear = useCallback(
|
||||
|
|
@ -271,16 +378,95 @@ export function useCVMClient() {
|
|||
search?: string;
|
||||
limit?: number;
|
||||
}): Promise<{ count: number; listings: GearResult[] }> => {
|
||||
const result = await callCVMTool('find_gear', params as Record<string, unknown>);
|
||||
return parseToolResult(result);
|
||||
const signal = AbortSignal.any([AbortSignal.timeout(10000)]);
|
||||
const relayGroup = nostr.group(DATA_RELAYS);
|
||||
|
||||
const raw = await relayGroup.query(
|
||||
[{ kinds: [30402], limit: 200, '#t': ['skateboarding'] }],
|
||||
{ signal }
|
||||
);
|
||||
|
||||
let listings = deduplicateAddressable(raw as typeof raw & { tags: string[][] }[]);
|
||||
|
||||
if (params.category) {
|
||||
const q = params.category.toLowerCase();
|
||||
listings = listings.filter(
|
||||
(e) =>
|
||||
getTags(e, 't').some((t) => t.toLowerCase().includes(q)) ||
|
||||
(getTag(e, 'title') ?? '').toLowerCase().includes(q) ||
|
||||
(e as unknown as { content: string }).content.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
if (params.condition && params.condition !== 'any') {
|
||||
listings = listings.filter((e) =>
|
||||
(getTag(e, 'condition') ?? '')
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '_')
|
||||
.includes(params.condition!)
|
||||
);
|
||||
}
|
||||
|
||||
if (params.search) {
|
||||
const q = params.search.toLowerCase();
|
||||
listings = listings.filter(
|
||||
(e) =>
|
||||
(getTag(e, 'title') ?? '').toLowerCase().includes(q) ||
|
||||
(e as unknown as { content: string }).content.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
listings.sort((a, b) => b.created_at - a.created_at);
|
||||
listings = listings.slice(0, params.limit ?? 10);
|
||||
|
||||
const result: GearResult[] = listings.map((e) => {
|
||||
const ev = e as unknown as { content: string; pubkey: string; kind: number; tags: string[][]; created_at: number };
|
||||
return {
|
||||
title: getTag(ev, 'title') ?? 'Untitled Listing',
|
||||
description: ev.content.slice(0, 300),
|
||||
price: getTag(ev, 'price'),
|
||||
currency: getTag(ev, 'currency'),
|
||||
condition: getTag(ev, 'condition'),
|
||||
categories: getTags(ev, 't'),
|
||||
image: getTag(ev, 'image') ?? getTag(ev, 'thumb'),
|
||||
seller_pubkey: ev.pubkey,
|
||||
naddr: buildNaddr(ev),
|
||||
created_at: new Date(ev.created_at * 1000).toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
return { count: result.length, listings: result };
|
||||
},
|
||||
[]
|
||||
[nostr]
|
||||
);
|
||||
|
||||
const getCommunityStats = useCallback(async (): Promise<CommunityStats> => {
|
||||
const result = await callCVMTool('get_community_stats', {});
|
||||
return parseToolResult(result);
|
||||
}, []);
|
||||
const signal = AbortSignal.any([AbortSignal.timeout(10000)]);
|
||||
const relayGroup = nostr.group(DATA_RELAYS);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const [spots, events, coaches, filmers, editors, gear] = await Promise.all([
|
||||
relayGroup.query([{ kinds: [30818], limit: 2000 }], { signal }),
|
||||
relayGroup.query([{ kinds: [31923], limit: 500, since: now }], { signal }),
|
||||
relayGroup.query([{ kinds: [35001], limit: 500 }], { signal }),
|
||||
relayGroup.query([{ kinds: [35002], limit: 500 }], { signal }),
|
||||
relayGroup.query([{ kinds: [35003], limit: 500 }], { signal }),
|
||||
relayGroup.query([{ kinds: [30402], limit: 500, '#t': ['skateboarding'] }], { signal }),
|
||||
]);
|
||||
|
||||
return {
|
||||
spots: deduplicateAddressable(spots as typeof spots & { tags: string[][] }[]).filter(
|
||||
(e) => e.tags.some((t) => t[0] === 't' && t[1] === 'skatespot')
|
||||
).length,
|
||||
upcoming_events: deduplicateAddressable(events as typeof events & { tags: string[][] }[]).length,
|
||||
coaches: deduplicateAddressable(coaches as typeof coaches & { tags: string[][] }[]).length,
|
||||
filmers: deduplicateAddressable(filmers as typeof filmers & { tags: string[][] }[]).length,
|
||||
editors: deduplicateAddressable(editors as typeof editors & { tags: string[][] }[]).length,
|
||||
gear_listings: deduplicateAddressable(gear as typeof gear & { tags: string[][] }[]).length,
|
||||
relays_queried: DATA_RELAYS,
|
||||
queried_at: new Date().toISOString(),
|
||||
};
|
||||
}, [nostr]);
|
||||
|
||||
return {
|
||||
findSpots,
|
||||
|
|
|
|||
Loading…
Reference in a new issue