Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / Build Docker Images (push) Blocked by required conditions
CI Pipeline / E2E Tests (push) Blocked by required conditions
282 lines
7.2 KiB
Go
282 lines
7.2 KiB
Go
package nostr
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/nbd-wtf/go-nostr"
|
|
"github.com/nbd-wtf/go-nostr/nip19"
|
|
)
|
|
|
|
const (
|
|
// NIP-35: Torrent announcements
|
|
KindTorrent = 2003
|
|
)
|
|
|
|
type Publisher struct {
|
|
privateKey string
|
|
publicKey string
|
|
relays []string
|
|
}
|
|
|
|
type TorrentEventData struct {
|
|
Title string
|
|
InfoHash string
|
|
FileName string
|
|
FileSize int64
|
|
MagnetLink string
|
|
WebSeedURL string
|
|
BlossomHash string
|
|
Description string
|
|
}
|
|
|
|
// NewPublisher creates a new Nostr publisher
|
|
func NewPublisher(privateKeyHex string, relays []string) (*Publisher, error) {
|
|
if privateKeyHex == "" {
|
|
// Generate a new key if none provided
|
|
sk := nostr.GeneratePrivateKey()
|
|
privateKeyHex = sk
|
|
}
|
|
|
|
// Validate private key
|
|
privateKeyBytes, err := hex.DecodeString(privateKeyHex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid private key hex: %w", err)
|
|
}
|
|
|
|
if len(privateKeyBytes) != 32 {
|
|
return nil, fmt.Errorf("private key must be 32 bytes")
|
|
}
|
|
|
|
publicKey, err := nostr.GetPublicKey(privateKeyHex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error deriving public key: %w", err)
|
|
}
|
|
|
|
if len(relays) == 0 {
|
|
relays = []string{
|
|
"wss://relay.damus.io",
|
|
"wss://nos.lol",
|
|
"wss://relay.nostr.band",
|
|
}
|
|
}
|
|
|
|
return &Publisher{
|
|
privateKey: privateKeyHex,
|
|
publicKey: publicKey,
|
|
relays: relays,
|
|
}, nil
|
|
}
|
|
|
|
// CreateTorrentEvent creates a NIP-35 compliant torrent announcement event
|
|
func (p *Publisher) CreateTorrentEvent(data TorrentEventData) (*nostr.Event, error) {
|
|
event := &nostr.Event{
|
|
Kind: KindTorrent,
|
|
CreatedAt: nostr.Now(),
|
|
Content: data.Description,
|
|
Tags: nostr.Tags{},
|
|
}
|
|
|
|
// Add required tags according to NIP-35
|
|
if data.Title != "" {
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"title", data.Title})
|
|
}
|
|
|
|
if data.InfoHash != "" {
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"x", data.InfoHash})
|
|
}
|
|
|
|
if data.FileName != "" && data.FileSize > 0 {
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"file", data.FileName, strconv.FormatInt(data.FileSize, 10)})
|
|
}
|
|
|
|
if data.WebSeedURL != "" {
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"webseed", data.WebSeedURL})
|
|
}
|
|
|
|
if data.BlossomHash != "" {
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"blossom", data.BlossomHash})
|
|
}
|
|
|
|
if data.MagnetLink != "" {
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"magnet", data.MagnetLink})
|
|
}
|
|
|
|
// Add some additional useful tags
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"t", "torrent"})
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"t", "blossom"})
|
|
|
|
// Sign the event
|
|
err := event.Sign(p.privateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error signing event: %w", err)
|
|
}
|
|
|
|
return event, nil
|
|
}
|
|
|
|
// PublishEvent publishes an event to configured relays
|
|
func (p *Publisher) PublishEvent(ctx context.Context, event *nostr.Event) error {
|
|
if len(p.relays) == 0 {
|
|
return fmt.Errorf("no relays configured")
|
|
}
|
|
|
|
successCount := 0
|
|
errorCount := 0
|
|
|
|
for _, relayURL := range p.relays {
|
|
err := p.publishToRelay(ctx, relayURL, event)
|
|
if err != nil {
|
|
log.Printf("Failed to publish to relay %s: %v", relayURL, err)
|
|
errorCount++
|
|
} else {
|
|
log.Printf("Successfully published to relay %s", relayURL)
|
|
successCount++
|
|
}
|
|
}
|
|
|
|
if successCount == 0 {
|
|
return fmt.Errorf("failed to publish to any relay (%d errors)", errorCount)
|
|
}
|
|
|
|
log.Printf("Published to %d/%d relays successfully", successCount, len(p.relays))
|
|
return nil
|
|
}
|
|
|
|
// publishToRelay publishes an event to a single relay
|
|
func (p *Publisher) publishToRelay(ctx context.Context, relayURL string, event *nostr.Event) error {
|
|
relay, err := nostr.RelayConnect(ctx, relayURL)
|
|
if err != nil {
|
|
return fmt.Errorf("error connecting to relay: %w", err)
|
|
}
|
|
defer relay.Close()
|
|
|
|
// Set a reasonable timeout
|
|
publishCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
|
|
err = relay.Publish(publishCtx, *event)
|
|
if err != nil {
|
|
return fmt.Errorf("error publishing event: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PublishTorrentAnnouncement creates and publishes a NIP-35 torrent announcement
|
|
func (p *Publisher) PublishTorrentAnnouncement(ctx context.Context, data TorrentEventData) (*nostr.Event, error) {
|
|
event, err := p.CreateTorrentEvent(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating torrent event: %w", err)
|
|
}
|
|
|
|
err = p.PublishEvent(ctx, event)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error publishing torrent event: %w", err)
|
|
}
|
|
|
|
return event, nil
|
|
}
|
|
|
|
// GetPublicKeyBech32 returns the public key in bech32 format (npub)
|
|
func (p *Publisher) GetPublicKeyBech32() (string, error) {
|
|
return nip19.EncodePublicKey(p.publicKey)
|
|
}
|
|
|
|
// GetPrivateKeyBech32 returns the private key in bech32 format (nsec)
|
|
func (p *Publisher) GetPrivateKeyBech32() (string, error) {
|
|
return nip19.EncodePrivateKey(p.privateKey)
|
|
}
|
|
|
|
// GetEventID returns the event ID in hex format
|
|
func GetEventID(event *nostr.Event) string {
|
|
return event.ID
|
|
}
|
|
|
|
// GetEventIDBech32 returns the event ID in bech32 format (note)
|
|
func GetEventIDBech32(event *nostr.Event) (string, error) {
|
|
return nip19.EncodeNote(event.ID)
|
|
}
|
|
|
|
// CreateMockPublisher creates a publisher that logs instead of publishing (for testing)
|
|
func CreateMockPublisher() *MockPublisher {
|
|
sk := nostr.GeneratePrivateKey()
|
|
pk, _ := nostr.GetPublicKey(sk)
|
|
|
|
return &MockPublisher{
|
|
privateKey: sk,
|
|
publicKey: pk,
|
|
events: make([]*nostr.Event, 0),
|
|
}
|
|
}
|
|
|
|
// MockPublisher is a test implementation that doesn't actually publish
|
|
type MockPublisher struct {
|
|
privateKey string
|
|
publicKey string
|
|
events []*nostr.Event
|
|
}
|
|
|
|
func (m *MockPublisher) CreateTorrentEvent(data TorrentEventData) (*nostr.Event, error) {
|
|
event := &nostr.Event{
|
|
Kind: KindTorrent,
|
|
CreatedAt: nostr.Now(),
|
|
Content: data.Description,
|
|
Tags: nostr.Tags{},
|
|
}
|
|
|
|
// Add required tags
|
|
if data.Title != "" {
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"title", data.Title})
|
|
}
|
|
if data.InfoHash != "" {
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"x", data.InfoHash})
|
|
}
|
|
if data.FileName != "" && data.FileSize > 0 {
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"file", data.FileName, strconv.FormatInt(data.FileSize, 10)})
|
|
}
|
|
if data.WebSeedURL != "" {
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"webseed", data.WebSeedURL})
|
|
}
|
|
if data.BlossomHash != "" {
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"blossom", data.BlossomHash})
|
|
}
|
|
if data.MagnetLink != "" {
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"magnet", data.MagnetLink})
|
|
}
|
|
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"t", "torrent"})
|
|
event.Tags = event.Tags.AppendUnique(nostr.Tag{"t", "blossom"})
|
|
|
|
// Sign the event
|
|
err := event.Sign(m.privateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error signing event: %w", err)
|
|
}
|
|
|
|
return event, nil
|
|
}
|
|
|
|
func (m *MockPublisher) PublishTorrentAnnouncement(ctx context.Context, data TorrentEventData) (*nostr.Event, error) {
|
|
event, err := m.CreateTorrentEvent(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Store event instead of publishing
|
|
m.events = append(m.events, event)
|
|
|
|
log.Printf("Mock: Would publish NIP-35 event (ID: %s) to relays", event.ID)
|
|
log.Printf("Mock: Event content: %s", event.Content)
|
|
log.Printf("Mock: Event tags: %v", event.Tags)
|
|
|
|
return event, nil
|
|
}
|
|
|
|
func (m *MockPublisher) GetEvents() []*nostr.Event {
|
|
return m.events
|
|
} |