7675d010c2
Adds three new skills extracted from mkstack's restructured AGENTS.md and trims the corresponding AGENTS.md sections to match. - nostr-security: XSS threat model, URL and CSS sanitization patterns, author filtering for trust-sensitive queries, NIP-72 moderation walkthrough, and a pre-merge checklist. The skill's references to sanitizeUrl and sanitizeCssString are pointed at Ditto's existing helpers in src/lib/sanitizeUrl.ts and src/lib/fontLoader.ts. - testing: Vitest + TestApp conventions, mocked browser APIs, and the project policy on when (not) to create new test files. - nip85-stats: reference documentation for NIP-85 Trusted Assertion stats (kinds 30382, 30383, 30384) including a ready-to-copy useNip85Stats hook for future use; not currently wired into Ditto. AGENTS.md changes: - Shrink the Nostr Security Model section from a verbose kinds-and-URLs walkthrough into a compact rule list plus a spoof-vs-authors example, with a pointer to the new skill. - Trim the Writing Tests section to the policy + skill pointer, moving the TestApp example and browser-API mocks into the skill. - Demote Loading States / Empty States from a top-level section to a subsection under CRITICAL Design Standards so the document's top-level headings describe domains, not presentation details. Net: AGENTS.md 1654 -> 1480 lines (~10%).
157 lines
4.7 KiB
TypeScript
157 lines
4.7 KiB
TypeScript
import { useNostr } from '@nostrify/react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useAppContext } from '@/hooks/useAppContext';
|
|
|
|
/** Engagement counts exposed by NIP-85 kind 30383 (events) and 30384 (addressable events). */
|
|
export interface Nip85EventStats {
|
|
commentCount: number;
|
|
repostCount: number;
|
|
reactionCount: number;
|
|
zapCount: number;
|
|
/** Zap amount in sats. */
|
|
zapAmount: number;
|
|
}
|
|
|
|
/** A subset of NIP-85 kind 30382 (user) stats — extend as needed. */
|
|
export interface Nip85UserStats {
|
|
followers: number;
|
|
postCount: number;
|
|
}
|
|
|
|
/**
|
|
* Read an integer tag value from a NIP-85 assertion event. Returns 0 when missing
|
|
* or unparseable, which mirrors the semantics of "no data" in NIP-85.
|
|
*/
|
|
function getIntTag(tags: string[][], tagName: string): number {
|
|
const tag = tags.find(([name]) => name === tagName);
|
|
if (!tag?.[1]) return 0;
|
|
const n = parseInt(tag[1], 10);
|
|
return Number.isFinite(n) ? n : 0;
|
|
}
|
|
|
|
/**
|
|
* Fetches NIP-85 event stats (kind 30383) from the configured stats pubkey.
|
|
* Returns `null` when no stats pubkey is configured or the provider has no
|
|
* assertion for this event.
|
|
*/
|
|
export function useNip85EventStats(eventId: string | undefined) {
|
|
const { nostr } = useNostr();
|
|
const { config } = useAppContext();
|
|
const statsPubkey = config.nip85StatsPubkey;
|
|
|
|
return useQuery<Nip85EventStats | null>({
|
|
queryKey: ['nip85-event-stats', eventId, statsPubkey],
|
|
queryFn: async ({ signal }) => {
|
|
if (!eventId || !statsPubkey) return null;
|
|
|
|
const combined = AbortSignal.any([signal, AbortSignal.timeout(2000)]);
|
|
|
|
try {
|
|
const events = await nostr.query(
|
|
[{ kinds: [30383], authors: [statsPubkey], '#d': [eventId], limit: 1 }],
|
|
{ signal: combined },
|
|
);
|
|
|
|
if (events.length === 0) return null;
|
|
|
|
const { tags } = events[0];
|
|
return {
|
|
commentCount: getIntTag(tags, 'comment_cnt'),
|
|
repostCount: getIntTag(tags, 'repost_cnt'),
|
|
reactionCount: getIntTag(tags, 'reaction_cnt'),
|
|
zapCount: getIntTag(tags, 'zap_cnt'),
|
|
zapAmount: getIntTag(tags, 'zap_amount'),
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
enabled: !!eventId && !!statsPubkey,
|
|
staleTime: 30 * 1000,
|
|
retry: false,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches NIP-85 user stats (kind 30382) from the configured stats pubkey.
|
|
* Returns `null` when no stats pubkey is configured or the provider has no
|
|
* assertion for this pubkey.
|
|
*/
|
|
export function useNip85UserStats(pubkey: string | undefined) {
|
|
const { nostr } = useNostr();
|
|
const { config } = useAppContext();
|
|
const statsPubkey = config.nip85StatsPubkey;
|
|
|
|
return useQuery<Nip85UserStats | null>({
|
|
queryKey: ['nip85-user-stats', pubkey, statsPubkey],
|
|
queryFn: async ({ signal }) => {
|
|
if (!pubkey || !statsPubkey) return null;
|
|
|
|
const combined = AbortSignal.any([signal, AbortSignal.timeout(2000)]);
|
|
|
|
try {
|
|
const events = await nostr.query(
|
|
[{ kinds: [30382], authors: [statsPubkey], '#d': [pubkey], limit: 1 }],
|
|
{ signal: combined },
|
|
);
|
|
|
|
if (events.length === 0) return null;
|
|
|
|
const { tags } = events[0];
|
|
return {
|
|
followers: getIntTag(tags, 'followers'),
|
|
postCount: getIntTag(tags, 'post_cnt'),
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
enabled: !!pubkey && !!statsPubkey,
|
|
staleTime: 60 * 1000,
|
|
retry: false,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches NIP-85 addressable event stats (kind 30384) from the configured
|
|
* stats pubkey. The `addr` argument is the full NIP-01 event address string,
|
|
* e.g. `30023:<pubkey>:<d-tag>`.
|
|
*/
|
|
export function useNip85AddrStats(addr: string | undefined) {
|
|
const { nostr } = useNostr();
|
|
const { config } = useAppContext();
|
|
const statsPubkey = config.nip85StatsPubkey;
|
|
|
|
return useQuery<Nip85EventStats | null>({
|
|
queryKey: ['nip85-addr-stats', addr, statsPubkey],
|
|
queryFn: async ({ signal }) => {
|
|
if (!addr || !statsPubkey) return null;
|
|
|
|
const combined = AbortSignal.any([signal, AbortSignal.timeout(2000)]);
|
|
|
|
try {
|
|
const events = await nostr.query(
|
|
[{ kinds: [30384], authors: [statsPubkey], '#d': [addr], limit: 1 }],
|
|
{ signal: combined },
|
|
);
|
|
|
|
if (events.length === 0) return null;
|
|
|
|
const { tags } = events[0];
|
|
return {
|
|
commentCount: getIntTag(tags, 'comment_cnt'),
|
|
repostCount: getIntTag(tags, 'repost_cnt'),
|
|
reactionCount: getIntTag(tags, 'reaction_cnt'),
|
|
zapCount: getIntTag(tags, 'zap_cnt'),
|
|
zapAmount: getIntTag(tags, 'zap_amount'),
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
enabled: !!addr && !!statsPubkey,
|
|
staleTime: 30 * 1000,
|
|
retry: false,
|
|
});
|
|
}
|