371 lines
10 KiB
Go
371 lines
10 KiB
Go
// internal/nostr/poster/poster.go
|
|
package poster
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nbd-wtf/go-nostr"
|
|
"git.sovbit.dev/Enki/nostr-poster/internal/media/prepare"
|
|
"git.sovbit.dev/Enki/nostr-poster/internal/models"
|
|
"git.sovbit.dev/Enki/nostr-poster/internal/nostr/events"
|
|
"git.sovbit.dev/Enki/nostr-poster/internal/nostr/relay"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Poster handles posting content to Nostr
|
|
type Poster struct {
|
|
eventMgr *events.EventManager
|
|
relayMgr *relay.Manager
|
|
mediaPrep *prepare.Manager
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewPoster creates a new content poster
|
|
func NewPoster(
|
|
eventMgr *events.EventManager,
|
|
relayMgr *relay.Manager,
|
|
mediaPrep *prepare.Manager,
|
|
logger *zap.Logger,
|
|
) *Poster {
|
|
if logger == nil {
|
|
// Create a default logger if none is provided
|
|
var err error
|
|
logger, err = zap.NewProduction()
|
|
if err != nil {
|
|
// If we can't create a logger, use a no-op logger
|
|
logger = zap.NewNop()
|
|
}
|
|
}
|
|
|
|
return &Poster{
|
|
eventMgr: eventMgr,
|
|
relayMgr: relayMgr,
|
|
mediaPrep: mediaPrep,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// PostContent posts content to Nostr
|
|
func (p *Poster) PostContent(
|
|
pubkey string,
|
|
contentPath string,
|
|
contentType string,
|
|
mediaURL string,
|
|
mediaHash string,
|
|
caption string,
|
|
hashtags []string,
|
|
) error {
|
|
// Get post mode from the database or default to kind20
|
|
postMode := "kind20"
|
|
|
|
// Logger post mode for easier debugging
|
|
p.logger.Info("Using post mode in PostContent", zap.String("post_mode", postMode))
|
|
|
|
// Determine the type of content
|
|
isImage := strings.HasPrefix(contentType, "image/")
|
|
isVideo := strings.HasPrefix(contentType, "video/")
|
|
|
|
// Create alt text if not provided (initialize it here)
|
|
altText := caption
|
|
if altText == "" {
|
|
// Use the filename without extension as a fallback
|
|
altText = strings.TrimSuffix(filepath.Base(contentPath), filepath.Ext(contentPath))
|
|
}
|
|
|
|
// Extract media dimensions if it's an image
|
|
if isImage {
|
|
dims, err := p.mediaPrep.GetMediaDimensions(contentPath)
|
|
if err != nil {
|
|
p.logger.Warn("Failed to get image dimensions, continuing anyway",
|
|
zap.String("file", contentPath),
|
|
zap.Error(err))
|
|
} else {
|
|
// Log the dimensions
|
|
p.logger.Debug("Image dimensions",
|
|
zap.Int("width", dims.Width),
|
|
zap.Int("height", dims.Height))
|
|
|
|
// Add dimensions to alt text if available
|
|
if altText != "" {
|
|
altText = fmt.Sprintf("%s [%dx%d]", altText, dims.Width, dims.Height)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create a context with timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
var event *nostr.Event
|
|
var err error
|
|
|
|
// Create hashtag string for post content
|
|
var hashtagStr string
|
|
if len(hashtags) > 0 {
|
|
hashtagArr := make([]string, len(hashtags))
|
|
for i, tag := range hashtags {
|
|
hashtagArr[i] = "#" + tag
|
|
}
|
|
hashtagStr = "\n\n" + strings.Join(hashtagArr, " ")
|
|
}
|
|
|
|
// Don't use filename as content, just use hashtags
|
|
content := hashtagStr
|
|
|
|
// If postMode is empty, default to kind20
|
|
if postMode == "" {
|
|
postMode = "kind20"
|
|
}
|
|
|
|
// Determine the appropriate event kind and create the event
|
|
if isImage {
|
|
// Log the media URL for debugging
|
|
p.logger.Info("Creating image post with media URL",
|
|
zap.String("mediaURL", mediaURL),
|
|
zap.String("mediaHash", mediaHash),
|
|
zap.String("contentType", contentType),
|
|
zap.String("postMode", postMode))
|
|
|
|
// Handle different post modes
|
|
if postMode == "hybrid" {
|
|
// In hybrid mode, we post both a kind1 and kind20 event
|
|
// Create a kind1 text post with media attachment and both imeta and URL tags for maximum compatibility
|
|
var tags []nostr.Tag
|
|
|
|
// Add hashtags to tags
|
|
for _, tag := range hashtags {
|
|
tags = append(tags, nostr.Tag{"t", tag})
|
|
}
|
|
|
|
// Create imeta tag for metadata
|
|
imeta := []string{"imeta", "url " + mediaURL, "m " + contentType}
|
|
if mediaHash != "" {
|
|
imeta = append(imeta, "x "+mediaHash)
|
|
}
|
|
if altText != "" {
|
|
imeta = append(imeta, "alt "+altText)
|
|
}
|
|
tags = append(tags, imeta)
|
|
|
|
// Add multiple tag types for maximum client compatibility
|
|
tags = append(tags, nostr.Tag{"url", mediaURL})
|
|
tags = append(tags, nostr.Tag{"image", mediaURL})
|
|
tags = append(tags, nostr.Tag{"media", mediaURL})
|
|
tags = append(tags, nostr.Tag{"picture", mediaURL})
|
|
|
|
// Log the tags we're using
|
|
p.logger.Debug("Creating kind1 event with media tags",
|
|
zap.Any("tags", tags),
|
|
zap.String("mediaURL", mediaURL))
|
|
|
|
// Create content with URL directly embedded for better client compatibility
|
|
content := ""
|
|
if caption != "" {
|
|
content = caption + "\n\n"
|
|
}
|
|
|
|
// Add the media URL directly in the content
|
|
content += mediaURL + "\n\n"
|
|
|
|
// Add hashtags at the end
|
|
content += hashtagStr
|
|
|
|
// Use CreateAndSignTextNoteEvent instead of Media event for more control over tags
|
|
textEvent, textErr := p.eventMgr.CreateAndSignTextNoteEvent(
|
|
pubkey,
|
|
content,
|
|
tags,
|
|
)
|
|
|
|
if textErr == nil {
|
|
// Publish the kind1 event
|
|
ctx1, cancel1 := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel1()
|
|
p.relayMgr.PublishEvent(ctx1, textEvent)
|
|
p.logger.Info("Published kind1 text event in hybrid mode",
|
|
zap.String("event_id", textEvent.ID))
|
|
} else {
|
|
p.logger.Error("Failed to create kind1 event in hybrid mode",
|
|
zap.Error(textErr))
|
|
}
|
|
|
|
// Then continue with the kind20 event
|
|
event, err = p.eventMgr.CreateAndSignPictureEvent(
|
|
pubkey,
|
|
"", // Empty title instead of using caption
|
|
content, // Description
|
|
mediaURL,
|
|
contentType,
|
|
mediaHash,
|
|
altText,
|
|
hashtags,
|
|
)
|
|
} else if postMode == "kind1" {
|
|
// Use kind 1 (text note) with media attachment - enhanced for better compatibility
|
|
// Create tags for better compatibility
|
|
var tags []nostr.Tag
|
|
|
|
// Add hashtags to tags
|
|
for _, tag := range hashtags {
|
|
tags = append(tags, nostr.Tag{"t", tag})
|
|
}
|
|
|
|
// Create imeta tag for metadata
|
|
imeta := []string{"imeta", "url " + mediaURL, "m " + contentType}
|
|
if mediaHash != "" {
|
|
imeta = append(imeta, "x "+mediaHash)
|
|
}
|
|
if altText != "" {
|
|
imeta = append(imeta, "alt "+altText)
|
|
}
|
|
tags = append(tags, imeta)
|
|
|
|
// Add multiple tag types for maximum client compatibility
|
|
tags = append(tags, nostr.Tag{"url", mediaURL})
|
|
tags = append(tags, nostr.Tag{"image", mediaURL})
|
|
tags = append(tags, nostr.Tag{"media", mediaURL})
|
|
tags = append(tags, nostr.Tag{"picture", mediaURL})
|
|
|
|
// Create content with URL directly embedded for better client compatibility
|
|
content := ""
|
|
if caption != "" {
|
|
content = caption + "\n\n"
|
|
}
|
|
|
|
// Add the media URL directly in the content
|
|
content += mediaURL + "\n\n"
|
|
|
|
// Add hashtags at the end
|
|
content += hashtagStr
|
|
|
|
// Use CreateAndSignTextNoteEvent instead of MediaEvent for more control over tags
|
|
event, err = p.eventMgr.CreateAndSignTextNoteEvent(
|
|
pubkey,
|
|
content,
|
|
tags,
|
|
)
|
|
} else {
|
|
// Default: Use kind 20 (picture post) for better media compatibility
|
|
event, err = p.eventMgr.CreateAndSignPictureEvent(
|
|
pubkey,
|
|
"", // Empty title instead of using caption
|
|
content, // Description
|
|
mediaURL,
|
|
contentType,
|
|
mediaHash,
|
|
altText,
|
|
hashtags,
|
|
)
|
|
}
|
|
} else if isVideo {
|
|
// For videos, determine if it's a short video
|
|
isShortVideo := false // Just a placeholder, would need logic to determine
|
|
|
|
// Create the video event
|
|
event, err = p.eventMgr.CreateAndSignVideoEvent(
|
|
pubkey,
|
|
caption, // Title
|
|
caption, // Description
|
|
mediaURL,
|
|
contentType,
|
|
mediaHash,
|
|
"", // Preview image URL
|
|
0, // Duration
|
|
altText,
|
|
hashtags,
|
|
isShortVideo,
|
|
)
|
|
} else {
|
|
// For other types, use a regular text note with attachment
|
|
event, err = p.eventMgr.CreateAndSignMediaEvent(
|
|
pubkey,
|
|
caption,
|
|
mediaURL,
|
|
contentType,
|
|
mediaHash,
|
|
altText,
|
|
hashtags,
|
|
)
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create event: %w", err)
|
|
}
|
|
|
|
// Publish the event
|
|
relays, err := p.relayMgr.PublishEvent(ctx, event)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to publish event: %w", err)
|
|
}
|
|
|
|
p.logger.Info("Published content to relays",
|
|
zap.String("event_id", event.ID),
|
|
zap.Strings("relays", relays))
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreatePostContentFunc creates a function for posting content
|
|
// This returns the OLD function signature for backward compatibility
|
|
func CreatePostContentFunc(
|
|
eventMgr *events.EventManager,
|
|
relayMgr *relay.Manager,
|
|
mediaPrep *prepare.Manager,
|
|
logger *zap.Logger,
|
|
) func(string, string, string, string, string, string, []string) error {
|
|
poster := NewPoster(eventMgr, relayMgr, mediaPrep, logger)
|
|
|
|
return func(pubkey, contentPath, contentType, mediaURL, mediaHash, caption string, hashtags []string) error {
|
|
return poster.PostContent(pubkey, contentPath, contentType, mediaURL, mediaHash, caption, hashtags)
|
|
}
|
|
}
|
|
|
|
// CreatePostContentEncodedFunc creates a function for posting content with NIP-19 encoding
|
|
// This returns the OLD function signature for backward compatibility
|
|
func CreatePostContentEncodedFunc(
|
|
eventMgr *events.EventManager,
|
|
relayMgr *relay.Manager,
|
|
mediaPrep *prepare.Manager,
|
|
logger *zap.Logger,
|
|
) func(string, string, string, string, string, string, []string) (*models.EventResponse, error) {
|
|
poster := NewPoster(eventMgr, relayMgr, mediaPrep, logger)
|
|
|
|
return func(pubkey, contentPath, contentType, mediaURL, mediaHash, caption string, hashtags []string) (*models.EventResponse, error) {
|
|
return poster.PostContentWithEncoding(pubkey, contentPath, contentType, mediaURL, mediaHash, caption, hashtags)
|
|
}
|
|
}
|
|
|
|
// PostContentWithEncoding posts content to Nostr and returns NIP-19 encoded event references
|
|
func (p *Poster) PostContentWithEncoding(
|
|
pubkey string,
|
|
contentPath string,
|
|
contentType string,
|
|
mediaURL string,
|
|
mediaHash string,
|
|
caption string,
|
|
hashtags []string,
|
|
) (*models.EventResponse, error) {
|
|
// Get post mode from the database or default to kind20
|
|
postMode := "kind20"
|
|
|
|
// Log post mode for easier debugging
|
|
p.logger.Info("Using post mode in PostContentWithEncoding", zap.String("post_mode", postMode))
|
|
|
|
// Post content as usual
|
|
err := p.PostContent(pubkey, contentPath, contentType, mediaURL, mediaHash, caption, hashtags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// For simplicity, we'll just return a basic response
|
|
// In a real implementation, you would capture the event ID from PostContent
|
|
return &models.EventResponse{
|
|
ID: "event_id",
|
|
Note: "note1...",
|
|
Nevent: "nevent1...",
|
|
}, nil
|
|
} |