You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

260 lines
7.0 KiB

package nostr
import (
"context"
"encoding/hex"
"fmt"
"time"
"github.com/nbd-wtf/go-nostr"
)
// IssueService handles publishing issue events
type IssueService struct {
client *Client
issueKind int // Issue event kind (from config)
repoAnnouncementKind int // Repo announcement kind (from config)
}
// NewIssueService creates a new issue service
func NewIssueService(client *Client, issueKind int, repoAnnouncementKind int) *IssueService {
return &IssueService{
client: client,
issueKind: issueKind,
repoAnnouncementKind: repoAnnouncementKind,
}
}
// RepoAnnouncement represents a parsed kind 30617 repository announcement
type RepoAnnouncement struct {
Event *nostr.Event
Pubkey string
DTag string
Relays []string
Maintainers []string
}
// ParseRepoAnnouncement parses a repository announcement event
func ParseRepoAnnouncement(event *nostr.Event, expectedKind int) (*RepoAnnouncement, error) {
if event.Kind != expectedKind {
return nil, fmt.Errorf("expected kind %d, got %d", expectedKind, event.Kind)
}
// Validate that PubKey is set
if event.PubKey == "" {
return nil, fmt.Errorf("repository announcement event missing pubkey")
}
repo := &RepoAnnouncement{
Event: event,
Pubkey: event.PubKey,
}
// Extract d tag
repo.DTag = getDTag(event.Tags)
// Validate that DTag is set
if repo.DTag == "" {
return nil, fmt.Errorf("repository announcement event missing d tag")
}
// Extract relays and maintainers tags
repo.Relays = getAllTagValues(event.Tags, "relays")
repo.Maintainers = getAllTagValues(event.Tags, "maintainers")
return repo, nil
}
// FetchRepoAnnouncement fetches a repository announcement by naddr
func (s *IssueService) FetchRepoAnnouncement(ctx context.Context, repoNaddr string) (*RepoAnnouncement, error) {
// Parse the naddr
naddr, err := ParseNaddr(repoNaddr)
if err != nil {
return nil, fmt.Errorf("failed to parse repo naddr: %w", err)
}
if naddr.Kind != s.repoAnnouncementKind {
return nil, fmt.Errorf("expected kind %d, got %d", s.repoAnnouncementKind, naddr.Kind)
}
// Fetch the event
filter := naddr.ToFilter()
logFilter(filter, fmt.Sprintf("repo announcement (kind %d)", s.repoAnnouncementKind))
event, err := s.client.FetchEvent(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to fetch repo announcement: %w", err)
}
return ParseRepoAnnouncement(event, s.repoAnnouncementKind)
}
// IssueRequest represents a request to create an issue
type IssueRequest struct {
Subject string
Content string
Labels []string
}
// PublishIssue publishes a kind 1621 issue event to the repository
// If privateKey is empty, a random key will be generated (anonymous submission)
func (s *IssueService) PublishIssue(ctx context.Context, repoAnnouncement *RepoAnnouncement, req *IssueRequest, privateKey string) (string, error) {
// Generate or use provided private key
var privKeyHex string
if privateKey == "" {
// Generate a random key for anonymous submission
privKeyHex = nostr.GeneratePrivateKey()
} else {
// Validate the provided key
keyBytes, err := hex.DecodeString(privateKey)
if err != nil {
return "", fmt.Errorf("invalid private key: %w", err)
}
if len(keyBytes) != 32 {
return "", fmt.Errorf("private key must be 32 bytes (64 hex characters)")
}
privKeyHex = privateKey
}
// Get public key from private key
pubkey, err := nostr.GetPublicKey(privKeyHex)
if err != nil {
return "", fmt.Errorf("failed to get public key: %w", err)
}
// Create the issue event
event := &nostr.Event{
PubKey: pubkey,
Kind: s.issueKind,
Content: req.Content,
CreatedAt: nostr.Timestamp(time.Now().Unix()),
Tags: nostr.Tags{},
}
// Add 'a' tag pointing to the repository announcement
// Format: ["a", "<kind>:<pubkey>:<d-tag>"]
event.Tags = append(event.Tags, nostr.Tag{"a", fmt.Sprintf("%d:%s:%s", s.repoAnnouncementKind, repoAnnouncement.Pubkey, repoAnnouncement.DTag)})
// Add 'p' tag for repository owner
event.Tags = append(event.Tags, nostr.Tag{"p", repoAnnouncement.Pubkey})
// Add maintainers as 'p' tags
for _, maintainer := range repoAnnouncement.Maintainers {
event.Tags = append(event.Tags, nostr.Tag{"p", maintainer})
}
// Add subject tag if provided
if req.Subject != "" {
event.Tags = append(event.Tags, nostr.Tag{"subject", req.Subject})
}
// Add label tags
for _, label := range req.Labels {
if label != "" {
event.Tags = append(event.Tags, nostr.Tag{"t", label})
}
}
// Sign the event
if err := event.Sign(privKeyHex); err != nil {
return "", fmt.Errorf("failed to sign event: %w", err)
}
// Determine which relays to publish to
relays := repoAnnouncement.Relays
if len(relays) == 0 {
// Fallback to default relays if none specified
clientRelays := s.client.GetRelays()
if len(clientRelays) > 0 {
relays = clientRelays
}
}
// Publish to relays
var lastErr error
successCount := 0
for _, relayURL := range relays {
relay, err := s.client.ConnectToRelay(ctx, relayURL)
if err != nil {
lastErr = err
continue
}
err = relay.Publish(ctx, *event)
// Note: SimplePool manages connections, but we close here for explicit cleanup
// Closing after publish attempt ensures cleanup regardless of success/failure
relay.Close()
if err != nil {
lastErr = err
continue
}
// Publish succeeded
successCount++
}
if successCount == 0 && lastErr != nil {
return "", fmt.Errorf("failed to publish to any relay: %w", lastErr)
}
return event.ID, nil
}
// PublishSignedIssue publishes a pre-signed issue event (signed by browser)
func (s *IssueService) PublishSignedIssue(ctx context.Context, signedEvent *nostr.Event) (string, error) {
// Validate the event
if signedEvent.Kind != s.issueKind {
return "", fmt.Errorf("expected kind %d, got %d", s.issueKind, signedEvent.Kind)
}
// Verify the event signature
valid, err := signedEvent.CheckSignature()
if err != nil {
return "", fmt.Errorf("failed to check signature: %w", err)
}
if !valid {
return "", fmt.Errorf("invalid event signature")
}
// Determine which relays to publish to
// Try to extract relays from the event tags or use defaults
relays := s.client.GetRelays()
// Look for relay hints in tags
for _, tag := range signedEvent.Tags {
if len(tag) > 0 && tag[0] == "relays" {
if len(tag) > 1 {
relays = tag[1:]
break
}
}
}
// Publish to relays
var lastErr error
successCount := 0
for _, relayURL := range relays {
relay, err := s.client.ConnectToRelay(ctx, relayURL)
if err != nil {
lastErr = err
continue
}
err = relay.Publish(ctx, *signedEvent)
// Note: SimplePool manages connections, but we close here for explicit cleanup
// Closing after publish attempt ensures cleanup regardless of success/failure
relay.Close()
if err != nil {
lastErr = err
continue
}
// Publish succeeded
successCount++
}
if successCount == 0 && lastErr != nil {
return "", fmt.Errorf("failed to publish to any relay: %w", lastErr)
}
return signedEvent.ID, nil
}