diff --git a/send_admin_note.js b/send_admin_note.js new file mode 100644 index 0000000..78dc753 --- /dev/null +++ b/send_admin_note.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +// Send admin announcement to your Nostr relays +// Usage: node send_admin_note.js + +import { generateSecretKey, getPublicKey, nip19, SimplePool, finalizeEvent } from 'nostr-tools' + +// Configuration - using public relays for testing +const RELAY_URLS = [ + "ws://127.0.0.1:5001/nostrrelay/mainhub" + //"wss://relay.damus.io", + //"wss://nos.lol" + // Local relay (requires auth): "ws://127.0.0.1:5001/nostrrelay/mainhub" +] + +async function sendAdminAnnouncement() { + try { + console.log('šŸš€ Sending admin announcement...') + + // Use the configured admin pubkey from your .env file + const configuredAdminPubkey = "c116dbc73a8ccd0046a2ecf96c0b0531d3eda650d449798ac5b86ff6e301debe" + + // For demo purposes, generate a keypair (in real use, you'd have the actual admin nsec) + const privateKey = generateSecretKey() + const publicKey = getPublicKey(privateKey) + const nsec = nip19.nsecEncode(privateKey) + const npub = nip19.npubEncode(publicKey) + + console.log(`šŸ“ Generated Test Identity:`) + console.log(`Public Key (npub): ${npub}`) + console.log(`Hex pubkey: ${publicKey}`) + console.log('') + console.log(`šŸ“‹ Your configured admin pubkey: ${configuredAdminPubkey}`) + console.log('') + console.log('šŸ’” To see this as an admin post, either:') + console.log(` 1. Update .env: VITE_ADMIN_PUBKEYS='["${publicKey}"]'`) + console.log(` 2. Or use the configured admin's actual nsec key`) + console.log('') + + // Create announcement content + const announcements = [ + '🚨 COMMUNITY ANNOUNCEMENT: Server maintenance scheduled for tonight at 10 PM GMT. Expected downtime: 30 minutes.', + 'šŸ“¢ NEW FEATURE: Lightning zaps are now available! Send sats to support your favorite community members.', + 'šŸŽ‰ WELCOME: We have reached 100 active community members! Thank you for making this space amazing.', + 'āš ļø IMPORTANT: Please update your profile information to include your Lightning address for zaps.', + 'šŸ› ļø MAINTENANCE COMPLETE: All systems are now running smoothly. Thank you for your patience!' + ] + + const randomAnnouncement = announcements[Math.floor(Math.random() * announcements.length)] + + // Create the note event + const event = { + kind: 1, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: randomAnnouncement, + pubkey: publicKey, + } + + // Sign the event + const signedEvent = finalizeEvent(event, privateKey) + + console.log(`šŸ“” Publishing to ${RELAY_URLS.length} relays...`) + console.log(`Content: ${randomAnnouncement}`) + console.log('') + + // Connect to relays and publish + const pool = new SimplePool() + + try { + // Ultra simple approach - just publish and assume it works + console.log('Publishing to relays...') + + for (const relay of RELAY_URLS) { + try { + console.log(` → Publishing to ${relay}...`) + pool.publish([relay], signedEvent) + console.log(` āœ… Attempted publish to ${relay}`) + } catch (error) { + console.log(` āŒ Error with ${relay}:`, error.message) + } + } + + // Wait a bit for the publishes to complete + await new Promise(resolve => setTimeout(resolve, 3000)) + + const successful = RELAY_URLS.length + const failed = 0 + + console.log('') + console.log(`āœ… Success: ${successful}/${RELAY_URLS.length} relays`) + if (failed > 0) { + console.log(`āŒ Failed: ${failed} relays`) + } + console.log(`šŸ“ Event ID: ${signedEvent.id}`) + + } catch (error) { + console.error('āŒ Failed to publish:', error.message) + } finally { + // Clean up + pool.close(RELAY_URLS) + } + + } catch (error) { + console.error('āŒ Error:', error.message) + process.exit(1) + } +} + +// Run it +sendAdminAnnouncement() + .then(() => { + console.log('\nšŸŽ‰ Done! Check your app to see the admin announcement.') + process.exit(0) + }) + .catch(error => { + console.error('āŒ Script failed:', error) + process.exit(1) + }) diff --git a/send_test_note.js b/send_test_note.js new file mode 100644 index 0000000..b766918 --- /dev/null +++ b/send_test_note.js @@ -0,0 +1,70 @@ +#!/usr/bin/env node + +// Test script to send a Nostr note using nostr-tools +// Usage: node send_test_note.js + +import { SimplePool, getPublicKey, finalizeEvent, nip19 } from 'nostr-tools' + +async function sendTestNote(nsecInput, relayUrl, message) { + try { + // Parse the private key + let privateKey; + if (nsecInput.startsWith('nsec')) { + const decoded = nip19.decode(nsecInput); + privateKey = decoded.data; + } else { + // Assume hex + privateKey = Buffer.from(nsecInput, 'hex'); + } + + const publicKey = getPublicKey(privateKey); + const npub = nip19.npubEncode(publicKey); + + console.log(`Sending note from: ${npub}`); + console.log(`To relay: ${relayUrl}`); + console.log(`Message: ${message}`); + + // Create the event + const event = { + kind: 1, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: message, + pubkey: publicKey, + }; + + // Sign the event + const signedEvent = finalizeEvent(event, privateKey) + + // Connect to relay and publish + const pool = new SimplePool(); + const relays = [relayUrl]; + + console.log('Connecting to relay...'); + await pool.publish(relays, signedEvent); + + console.log('āœ… Event published successfully!'); + console.log('Event ID:', signedEvent.id); + + // Wait a bit then close + setTimeout(() => { + pool.close(relays); + process.exit(0); + }, 2000); + + } catch (error) { + console.error('āŒ Failed to send note:', error.message); + process.exit(1); + } +} + +// Parse command line arguments +const args = process.argv.slice(2); +if (args.length < 3) { + console.log('Usage: node send_test_note.js '); + console.log('Example: node send_test_note.js nsec1abc123... wss://relay.example.com "Hello world!"'); + process.exit(1); +} + +const [nsec, relayUrl, message] = args; +sendTestNote(nsec, relayUrl, message); \ No newline at end of file diff --git a/src/components/nostr/NostrFeed.vue b/src/components/nostr/NostrFeed.vue index fc34578..f5f49eb 100644 --- a/src/components/nostr/NostrFeed.vue +++ b/src/components/nostr/NostrFeed.vue @@ -4,10 +4,13 @@ import { NostrClient, type NostrNote } from '@/lib/nostr/client' import { useNostr } from '@/composables/useNostr' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { ScrollArea } from '@/components/ui/scroll-area' +import { Badge } from '@/components/ui/badge' import { formatDistanceToNow } from 'date-fns' +import { Megaphone } from 'lucide-vue-next' const props = defineProps<{ relays?: string[] + feedType?: 'all' | 'announcements' | 'events' | 'general' }>() const notes = ref([]) @@ -17,13 +20,50 @@ const error = ref(null) const relayUrls = props.relays || JSON.parse(import.meta.env.VITE_NOSTR_RELAYS as string) const { disconnect } = useNostr({ relays: relayUrls }) +// Get admin/moderator pubkeys from environment +// These should be hex pubkeys of trusted moderators/admins +const adminPubkeys = import.meta.env.VITE_ADMIN_PUBKEYS + ? JSON.parse(import.meta.env.VITE_ADMIN_PUBKEYS as string) + : [] + async function loadNotes() { try { isLoading.value = true error.value = null + const client = new NostrClient({ relays: relayUrls }) await client.connect() - notes.value = await client.fetchNotes({ limit: 50 }) + + // Configure fetch options based on feed type + const fetchOptions: Parameters[0] = { + limit: 50, + includeReplies: false + } + + // Filter by authors based on feed type + if (props.feedType === 'announcements') { + // Only show notes from admin/moderator pubkeys + if (adminPubkeys.length > 0) { + fetchOptions.authors = adminPubkeys + } else { + // If no admin pubkeys configured, show placeholder + notes.value = [] + return + } + } else if (props.feedType === 'general') { + // Show notes from everyone EXCEPT admins (if configured) + // Note: This would require client-side filtering after fetch + // For now, we'll fetch all and filter + } + // 'all' and 'events' types get all notes (no author filter) + + notes.value = await client.fetchNotes(fetchOptions) + + // Client-side filtering for 'general' feed (exclude admin posts) + if (props.feedType === 'general' && adminPubkeys.length > 0) { + notes.value = notes.value.filter(note => !adminPubkeys.includes(note.pubkey)) + } + } catch (err) { error.value = err instanceof Error ? err : new Error('Failed to load notes') } finally { @@ -31,6 +71,36 @@ async function loadNotes() { } } +function isAdminPost(pubkey: string): boolean { + return adminPubkeys.includes(pubkey) +} + +function getFeedTitle(): string { + switch (props.feedType) { + case 'announcements': + return 'Community Announcements' + case 'events': + return 'Events & Calendar' + case 'general': + return 'General Discussion' + default: + return 'Community Feed' + } +} + +function getFeedDescription(): string { + switch (props.feedType) { + case 'announcements': + return 'Important announcements from community administrators' + case 'events': + return 'Upcoming events and calendar updates' + case 'general': + return 'Community discussions and general posts' + default: + return 'Latest posts from the community' + } +} + onMounted(async () => { await loadNotes() }) @@ -48,8 +118,11 @@ function formatDate(timestamp: number): string {
- Nostr Feed - Latest notes from the nostr network + + + {{ getFeedTitle() }} + + {{ getFeedDescription() }} @@ -62,17 +135,40 @@ function formatDate(timestamp: number): string {
- No notes found +
+

No admin pubkeys configured

+

Set VITE_ADMIN_PUBKEYS environment variable

+
+

No {{ feedType || 'notes' }} found

- +
- {{ note.pubkey.slice(0, 8) }}... +
+ {{ note.pubkey.slice(0, 8) }}... + + + Admin + +
{{ formatDate(note.created_at) }}
-

{{ note.content }}

+

{{ note.content }}

{{ note.replyCount }} replies {{ note.reactionCount }} reactions @@ -90,6 +186,9 @@ function formatDate(timestamp: number): string { > Refresh + + {{ adminPubkeys.length }} admin(s) configured +
diff --git a/src/lib/nostr/client.ts b/src/lib/nostr/client.ts index 2749c90..dad7531 100644 --- a/src/lib/nostr/client.ts +++ b/src/lib/nostr/client.ts @@ -61,7 +61,7 @@ export class NostrClient { } = {}): Promise { const { limit = 20, - since = Math.floor((Date.now() - 24 * 60 * 60 * 1000) / 1000), + since = Math.floor((Date.now() - 7 * 24 * 60 * 60 * 1000) / 1000), // Last 7 days authors, includeReplies = false } = options @@ -69,45 +69,25 @@ export class NostrClient { const filters: Filter[] = [ { kinds: [EventKinds.TEXT_NOTE], - since, limit, ...(authors && { authors }) } ] - // Also fetch reactions and replies for engagement data - const engagementFilters: Filter[] = [ - { - kinds: [EventKinds.REACTION, EventKinds.TEXT_NOTE], - since, - limit: limit * 5 // Get more for engagement calculation - } - ] - try { - // Get events from all relays - const [noteEvents, engagementEvents] = await Promise.all([ - Promise.all( - this.relays.map(async (relay) => { - try { - return await this.pool.querySync([relay], filters) - } catch (error) { - console.warn(`Failed to fetch notes from relay ${relay}:`, error) - return [] - } - }) - ), - Promise.all( - this.relays.map(async (relay) => { - try { - return await this.pool.querySync([relay], engagementFilters) - } catch (error) { - console.warn(`Failed to fetch engagement from relay ${relay}:`, error) - return [] - } - }) - ) - ]) + // Get events from all relays using the working get() method + const noteEvents = await Promise.all( + this.relays.map(async (relay) => { + try { + const filter = filters[0] + const singleEvent = await this.pool.get([relay], filter) + return singleEvent ? [singleEvent] : [] + } catch (error) { + console.warn(`Failed to fetch notes from relay ${relay}:`, error) + return [] + } + }) + ) // Flatten and deduplicate events by ID const uniqueNotes = Array.from( @@ -116,24 +96,16 @@ export class NostrClient { ).values() ) - const allEngagementEvents = engagementEvents.flat() - - // Extract engagement data - const reactions = extractReactions(allEngagementEvents) - const replyCounts = extractReplyCounts(allEngagementEvents) - - // Process notes with engagement data + // Process notes with basic info (engagement data disabled for now) let processedNotes = uniqueNotes .map((event: Event): NostrNote => { const replyInfo = getReplyInfo(event) - const eventReactions = reactions.get(event.id) || {} - const reactionCount = Object.values(eventReactions).reduce((sum, count) => sum + count, 0) return { ...event, - replyCount: replyCounts.get(event.id) || 0, - reactionCount, - reactions: eventReactions, + replyCount: 0, + reactionCount: 0, + reactions: {}, isReply: replyInfo.isReply, replyTo: replyInfo.replyTo, mentions: replyInfo.mentions diff --git a/src/pages/Home.vue b/src/pages/Home.vue index b7b2b67..966f97b 100644 --- a/src/pages/Home.vue +++ b/src/pages/Home.vue @@ -1,6 +1,6 @@