diff --git a/cmd/server/main.go b/cmd/server/main.go index 2141e6a..a04f0a6 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -174,15 +174,105 @@ func main() { logger, ) - // Create standard post content function - postContentFunc := poster.CreatePostContentFunc( - eventManager, - relayManager, - mediaPrep, - logger, - ) + // Create standard post content function - updated to support post modes + postContentFunc := func(pubkey, contentPath, contentType, mediaURL, mediaHash, caption string, hashtags []string) error { + // Need to get the bot's post mode from the database + var botID int64 + var postMode string - // Create the NIP-19 encoded post content function + // Try to get the bot ID and post mode from the database + err := database.Get(&botID, "SELECT id FROM bots WHERE pubkey = ?", pubkey) + if err == nil { + // Get the post mode for this bot + err = database.Get(&postMode, "SELECT post_mode FROM post_config WHERE bot_id = ?", botID) + if err != nil || postMode == "" { + // Default to kind20 if no post mode is set + postMode = "kind20" + logger.Info("No post mode found, using default", zap.String("pubkey", pubkey)) + } else { + logger.Info("Retrieved post mode from database", + zap.String("post_mode", postMode), + zap.Int64("bot_id", botID)) + } + } else { + logger.Warn("Could not find bot ID for pubkey", zap.String("pubkey", pubkey), zap.Error(err)) + postMode = "kind20" // Default + } + + // Create a poster instance with the retrieved post mode + p := poster.NewPoster(eventManager, relayManager, mediaPrep, logger) + + // Call the appropriate function based on post mode + if postMode == "hybrid" { + logger.Info("Using hybrid post mode", zap.String("pubkey", pubkey)) + + // For hybrid mode, we need to post both kinds + // First create a kind1 media event + var textEvent *nostr.Event + var textErr error + + // Create text event + // 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) + } + 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}) + + // 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 if any + if len(hashtags) > 0 { + hashtagArr := make([]string, len(hashtags)) + for i, tag := range hashtags { + hashtagArr[i] = "#" + tag + } + content += strings.Join(hashtagArr, " ") + } + + // Create text event with embedded URL and better tagging + textEvent, textErr = eventManager.CreateAndSignTextNoteEvent( + pubkey, + content, + tags, + ) + + if textErr == nil { + // Publish the kind1 event + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + relayManager.PublishEvent(ctx, textEvent) + logger.Info("Published kind1 text event in hybrid mode", zap.String("event_id", textEvent.ID)) + } else { + logger.Error("Failed to create kind1 event in hybrid mode", zap.Error(textErr)) + } + } + + // Always call PostContent (will use kind20 for hybrid mode) + return p.PostContent(pubkey, contentPath, contentType, mediaURL, mediaHash, caption, hashtags) + } + + // Create the NIP-19 encoded post content function - updated to share code with the regular function postContentEncodedFunc := func( pubkey string, contentPath string, @@ -192,6 +282,29 @@ func main() { caption string, hashtags []string, ) (*models.EventResponse, error) { + // Need to get the bot's post mode from the database + var botID int64 + var postMode string + + // Try to get the bot ID and post mode from the database + err := database.Get(&botID, "SELECT id FROM bots WHERE pubkey = ?", pubkey) + if err == nil { + // Get the post mode for this bot + err = database.Get(&postMode, "SELECT post_mode FROM post_config WHERE bot_id = ?", botID) + if err != nil || postMode == "" { + // Default to kind20 if no post mode is set + postMode = "kind20" + logger.Info("No post mode found, using default for encoded function", zap.String("pubkey", pubkey)) + } else { + logger.Info("Retrieved post mode from database for encoded function", + zap.String("post_mode", postMode), + zap.Int64("bot_id", botID)) + } + } else { + logger.Warn("Could not find bot ID for pubkey in encoded function", zap.String("pubkey", pubkey), zap.Error(err)) + postMode = "kind20" // Default + } + // Create a context ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -199,14 +312,14 @@ func main() { // Determine the type of content isImage := strings.HasPrefix(contentType, "image/") isVideo := strings.HasPrefix(contentType, "video/") - + // Create alt text if not provided 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 := mediaPrep.GetMediaDimensions(contentPath) @@ -221,11 +334,11 @@ func main() { } } } - + // Create an appropriate event var event *nostr.Event - var err error - + var eventErr error + if isImage { // Create hashtag string for post content var hashtagStr string @@ -236,22 +349,118 @@ func main() { } hashtagStr = "\n\n" + strings.Join(hashtagArr, " ") } - - content := caption + hashtagStr - - // Log mediaURL for debugging - logger.Debug("Creating picture event with media URL", zap.String("mediaURL", mediaURL)) - - // Extract a title from the caption or use the filename - title := caption - if title == "" { - title = strings.TrimSuffix(filepath.Base(contentPath), filepath.Ext(contentPath)) + + // Only include hashtags in content, not caption + content := hashtagStr + + // Log mediaURL and postMode for debugging + logger.Debug("Creating picture event with media URL and post mode", + zap.String("mediaURL", mediaURL), + zap.String("postMode", postMode)) + + // For hybrid mode, first create and post a kind1 text note + if postMode == "hybrid" { + // Create media tags for kind 1 + var tags []nostr.Tag + // Add hashtags to tags + for _, tag := range hashtags { + tags = append(tags, nostr.Tag{"t", tag}) + } + + // Create imeta tag + 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) + + // Also add a direct URL tag for better client compatibility + tags = append(tags, nostr.Tag{"url", mediaURL}) + + // Log tags for debugging + logger.Info("Creating kind1 event with media tags", + zap.Any("tags", tags), + zap.String("mediaURL", mediaURL)) + + // Create and post a kind1 text note with media attachment + // Create content with the image URL directly in the content for better compatibility + content := "" + if caption != "" { + content = caption + "\n\n" + } + + // Add the image URL in the content + content += mediaURL + "\n\n" + + // Add hashtags at the end + content += hashtagStr + + textEvent, textErr := eventManager.CreateAndSignTextNoteEvent( + pubkey, + content, + tags, + ) + + if textErr == nil { + ctx1, cancel1 := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel1() + + // Publish the kind1 event + relayManager.PublishEvent(ctx1, textEvent) + logger.Info("Published kind1 text event in hybrid mode (encoded function)", + zap.String("event_id", textEvent.ID)) + } + } else if postMode == "kind1" { + // If mode is kind1 only, use a kind1 event with media attachment + var tags []nostr.Tag + + // Add hashtags to tags + for _, tag := range hashtags { + tags = append(tags, nostr.Tag{"t", tag}) + } + + // Create media tags for kind 1 + 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) + + // Create and return a kind1 text note event + // Create content with the image URL directly in the content for better compatibility + content := "" + if caption != "" { + content = caption + "\n\n" + } + + // Add the image URL in the content + content += mediaURL + "\n\n" + + // Add hashtags at the end + content += hashtagStr + + event, eventErr = eventManager.CreateAndSignTextNoteEvent( + pubkey, + content, + tags, + ) + + // Skip kind20 creation for kind1 mode + return relayManager.PublishEventWithEncoding(ctx, event) } - - event, err = eventManager.CreateAndSignPictureEvent( + + // Always create kind20 for "kind20" mode or "hybrid" mode + // For picture events, use an empty title to avoid filename issues + event, eventErr = eventManager.CreateAndSignPictureEvent( pubkey, - title, // Title parameter - content, // Description parameter + "", // Empty title instead of using caption + content, // Description with hashtags only mediaURL, contentType, mediaHash, @@ -263,7 +472,7 @@ func main() { isShortVideo := false // Just a placeholder, would need logic to determine // Create the video event - event, err = eventManager.CreateAndSignVideoEvent( + event, eventErr = eventManager.CreateAndSignVideoEvent( pubkey, caption, // Title caption, // Description @@ -278,7 +487,7 @@ func main() { ) } else { // For other types, use a regular text note with attachment - event, err = eventManager.CreateAndSignMediaEvent( + event, eventErr = eventManager.CreateAndSignMediaEvent( pubkey, caption, mediaURL, @@ -289,8 +498,8 @@ func main() { ) } - if err != nil { - return nil, fmt.Errorf("failed to create event: %w", err) + if eventErr != nil { + return nil, fmt.Errorf("failed to create event: %w", eventErr) } // Publish the event with NIP-19 encoding diff --git a/internal/api/bot_service.go b/internal/api/bot_service.go index eb9df75..eaf99b5 100644 --- a/internal/api/bot_service.go +++ b/internal/api/bot_service.go @@ -412,6 +412,7 @@ func (s *BotService) UpdateBotConfig( hashtags = ?, interval_minutes = ?, post_template = ?, + post_mode = ?, enabled = ? WHERE bot_id = ? ` @@ -419,7 +420,7 @@ func (s *BotService) UpdateBotConfig( _, err = tx.Exec( query, postConfig.Hashtags, postConfig.IntervalMinutes, - postConfig.PostTemplate, postConfig.Enabled, + postConfig.PostTemplate, postConfig.PostMode, postConfig.Enabled, botID, ) if err != nil { diff --git a/internal/api/routes.go b/internal/api/routes.go index f5f9b4a..b47abe1 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -2,8 +2,10 @@ package api import ( + "bytes" "context" "fmt" + "io" "net/http" "os" "path/filepath" @@ -384,16 +386,31 @@ func (a *API) updateBotConfig(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"}) return } - + + // DEBUG: Log the raw request body + rawData, _ := c.GetRawData() + a.logger.Info("Raw config update request", + zap.String("raw_body", string(rawData)), + zap.Int64("botID", botID)) + + // Need to restore the body for binding + c.Request.Body = io.NopCloser(bytes.NewBuffer(rawData)) + var config struct { PostConfig *models.PostConfig `json:"post_config"` MediaConfig *models.MediaConfig `json:"media_config"` } - + if err := c.ShouldBindJSON(&config); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid config data"}) + a.logger.Error("Failed to bind JSON", zap.Error(err)) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid config data: " + err.Error()}) return } + + // Log the parsed configuration + a.logger.Info("Parsed bot config update", + zap.Int64("botID", botID), + zap.Any("post_config", config.PostConfig)) // Set the bot ID if config.PostConfig != nil { @@ -403,6 +420,15 @@ func (a *API) updateBotConfig(c *gin.Context) { config.MediaConfig.BotID = botID } + // Update the configs - log before update + if config.PostConfig != nil { + a.logger.Info("Updating post config in database", + zap.Int64("botID", botID), + zap.String("post_mode", config.PostConfig.PostMode), + zap.Int("interval", config.PostConfig.IntervalMinutes), + zap.String("hashtags", config.PostConfig.Hashtags)) + } + // Update the configs err = a.botService.UpdateBotConfig(botID, pubkey, config.PostConfig, config.MediaConfig) if err != nil { @@ -410,6 +436,15 @@ func (a *API) updateBotConfig(c *gin.Context) { a.logger.Error("Failed to update bot config", zap.Error(err)) return } + + // Log after update - query the database to confirm update + var dbPostMode string + dbErr := a.botService.db.Get(&dbPostMode, "SELECT post_mode FROM post_config WHERE bot_id = ?", botID) + if dbErr != nil { + a.logger.Warn("Failed to verify post_mode update in database", zap.Error(dbErr)) + } else { + a.logger.Info("Post mode after database update", zap.String("db_post_mode", dbPostMode)) + } // Get the updated bot bot, err := a.botService.GetBotByID(botID, pubkey) @@ -923,6 +958,7 @@ func (a *API) createManualPost(c *gin.Context) { Title string `json:"title"` Alt string `json:"alt"` Hashtags []string `json:"hashtags"` + PostMode string `json:"post_mode"` // Added post_mode parameter for hybrid support } if err := c.BindJSON(&req); err != nil { @@ -952,8 +988,13 @@ func (a *API) createManualPost(c *gin.Context) { // Create the appropriate event var event *nostr.Event + var textEvent *nostr.Event // For hybrid mode var eventErr error - + var hybridMode bool + + // Check if we're using hybrid mode + hybridMode = (req.PostMode == "hybrid") + switch req.Kind { case 1: // Standard text note @@ -961,10 +1002,53 @@ func (a *API) createManualPost(c *gin.Context) { for _, tag := range req.Hashtags { tags = append(tags, nostr.Tag{"t", tag}) } - + event, eventErr = a.botService.eventMgr.CreateAndSignTextNoteEvent(bot.Pubkey, req.Content, tags) case 20: // Picture post + if hybridMode && strings.HasPrefix(mediaType, "image/") { + // In hybrid mode, create both kind 1 and kind 20 events + + // First create kind 1 event + var tags []nostr.Tag + + // Add hashtags to tags + for _, tag := range req.Hashtags { + tags = append(tags, nostr.Tag{"t", tag}) + } + + // Create media tags for kind 1 + if mediaURL != "" { + imeta := []string{"imeta", "url " + mediaURL, "m " + mediaType} + if mediaHash != "" { + imeta = append(imeta, "x "+mediaHash) + } + if req.Alt != "" { + imeta = append(imeta, "alt "+req.Alt) + } + tags = append(tags, imeta) + } + + textEvent, eventErr = a.botService.eventMgr.CreateAndSignTextNoteEvent(bot.Pubkey, req.Content, tags) + if eventErr != nil { + a.logger.Error("Failed to create kind 1 event in hybrid mode", zap.Error(eventErr)) + // Continue with kind 20 even if kind 1 fails + } else { + // Publish kind 1 event separately + ctxText, cancelText := context.WithTimeout(context.Background(), 30*time.Second) + defer cancelText() + + textEncoded, textErr := a.botService.relayMgr.PublishEventWithEncoding(ctxText, textEvent) + if textErr != nil { + a.logger.Error("Failed to publish kind 1 event in hybrid mode", zap.Error(textErr)) + } else { + a.logger.Info("Published kind 1 event in hybrid mode", + zap.String("event_id", textEncoded.ID)) + } + } + } + + // Then create kind 20 event (for both normal and hybrid modes) event, eventErr = a.botService.eventMgr.CreateAndSignPictureEvent( bot.Pubkey, req.Title, @@ -975,6 +1059,64 @@ func (a *API) createManualPost(c *gin.Context) { req.Alt, req.Hashtags, ) + case 0: // Special case for hybrid mode selection + if !hybridMode { + c.JSON(http.StatusBadRequest, gin.H{"error": "Kind 0 is only valid with hybrid mode"}) + return + } + + // Create both a kind 1 and kind 20 event + var tags []nostr.Tag + for _, tag := range req.Hashtags { + tags = append(tags, nostr.Tag{"t", tag}) + } + + // Add media tags for kind 1 + if mediaURL != "" { + imeta := []string{"imeta", "url " + mediaURL, "m " + mediaType} + if mediaHash != "" { + imeta = append(imeta, "x "+mediaHash) + } + if req.Alt != "" { + imeta = append(imeta, "alt "+req.Alt) + } + tags = append(tags, imeta) + } + + // Create and publish kind 1 event + textEvent, eventErr = a.botService.eventMgr.CreateAndSignTextNoteEvent(bot.Pubkey, req.Content, tags) + if eventErr == nil { + ctxText, cancelText := context.WithTimeout(context.Background(), 30*time.Second) + defer cancelText() + + textEncoded, textErr := a.botService.relayMgr.PublishEventWithEncoding(ctxText, textEvent) + if textErr != nil { + a.logger.Error("Failed to publish kind 1 event in hybrid mode", zap.Error(textErr)) + } else { + a.logger.Info("Published kind 1 event in hybrid mode", + zap.String("event_id", textEncoded.ID)) + } + } + + // If it's an image, create kind 20 event too + if strings.HasPrefix(mediaType, "image/") { + event, eventErr = a.botService.eventMgr.CreateAndSignPictureEvent( + bot.Pubkey, + req.Title, + req.Content, + mediaURL, + mediaType, + mediaHash, + req.Alt, + req.Hashtags, + ) + } else { + // For non-images, just use the kind 1 event we already created + c.JSON(http.StatusOK, gin.H{ + "message": "Kind 1 post published successfully in hybrid mode", + }) + return + } default: c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported post kind"}) return @@ -1018,9 +1160,14 @@ func (a *API) createManualPost(c *gin.Context) { return } - // Return the encoded event response + // Return the encoded event response with indication of hybrid mode if used + message := "Post published successfully" + if hybridMode { + message = "Post published successfully (hybrid mode - both kind:1 and kind:20)" + } + c.JSON(http.StatusOK, gin.H{ - "message": "Post published successfully", + "message": message, "event": encodedEvent, }) } diff --git a/internal/db/db.go b/internal/db/db.go index aac163c..1bb7db7 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -85,12 +85,31 @@ func (db *DB) Initialize() error { hashtags TEXT, interval_minutes INTEGER NOT NULL DEFAULT 60, post_template TEXT, + post_mode TEXT DEFAULT 'kind20', enabled BOOLEAN NOT NULL DEFAULT 0, FOREIGN KEY (bot_id) REFERENCES bots(id) ON DELETE CASCADE )`) if err != nil { return fmt.Errorf("failed to create post_config table: %w", err) } + + // Check if post_mode column exists in post_config table, add it if it doesn't + var postModeExists int + err = db.Get(&postModeExists, ` + SELECT COUNT(*) FROM pragma_table_info('post_config') WHERE name = 'post_mode' + `) + + if err != nil { + return fmt.Errorf("failed to check if post_mode column exists: %w", err) + } + + if postModeExists == 0 { + // Add the post_mode column + _, err = db.Exec(`ALTER TABLE post_config ADD COLUMN post_mode TEXT DEFAULT 'kind20'`) + if err != nil { + return fmt.Errorf("failed to add post_mode column: %w", err) + } + } // Create media_config table _, err = db.Exec(` diff --git a/internal/models/bot.go b/internal/models/bot.go index e9bb674..c550830 100644 --- a/internal/models/bot.go +++ b/internal/models/bot.go @@ -37,6 +37,7 @@ type PostConfig struct { Hashtags string `db:"hashtags" json:"hashtags"` // JSON array stored as string IntervalMinutes int `db:"interval_minutes" json:"interval_minutes"` PostTemplate string `db:"post_template" json:"post_template"` + PostMode string `db:"post_mode" json:"post_mode"` // "kind1", "kind20", or "hybrid" Enabled bool `db:"enabled" json:"enabled"` } diff --git a/internal/nostr/poster/poster.go b/internal/nostr/poster/poster.go index 39684a9..5106e44 100644 --- a/internal/nostr/poster/poster.go +++ b/internal/nostr/poster/poster.go @@ -10,6 +10,7 @@ import ( "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" @@ -58,17 +59,23 @@ func (p *Poster) PostContent( caption string, hashtags []string, ) error { + // Default to kind20 if not found + 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) @@ -78,55 +85,149 @@ func (p *Poster) PostContent( zap.Error(err)) } else { // Log the dimensions - p.logger.Debug("Image dimensions", - zap.Int("width", dims.Width), + 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", + p.logger.Info("Creating image post with media URL", zap.String("mediaURL", mediaURL), zap.String("mediaHash", mediaHash), - zap.String("contentType", contentType)) - - // 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 + 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}) } - hashtagStr = "\n\n" + strings.Join(hashtagArr, " ") + + // 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 + event, err = p.eventMgr.CreateAndSignMediaEvent( + pubkey, + caption, + mediaURL, + contentType, + mediaHash, + altText, + hashtags, + ) + } 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, + ) } - - content := caption + hashtagStr - - // Use kind 20 (picture post) for better media compatibility - event, err = p.eventMgr.CreateAndSignPictureEvent( - pubkey, - caption, // Title - 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 @@ -176,6 +277,7 @@ func (p *Poster) PostContent( } // CreatePostContentFunc creates a function for posting content +// This returns the OLD function signature for backward compatibility func CreatePostContentFunc( eventMgr *events.EventManager, relayMgr *relay.Manager, @@ -183,8 +285,54 @@ func CreatePostContentFunc( 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) { + // Default to kind20 if not found + 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 } \ No newline at end of file diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 9e77b19..fa4dd78 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "path/filepath" - "strings" "sync" "encoding/json" @@ -120,7 +119,8 @@ func (s *Scheduler) Start() error { // Load all bots with enabled post configs query := ` SELECT b.id, b.pubkey, b.name, b.display_name, pc.enabled, pc.interval_minutes, pc.hashtags, - mc.primary_service, mc.fallback_service, mc.nip94_server_url, mc.blossom_server_url + mc.primary_service, mc.fallback_service, mc.nip94_server_url, mc.blossom_server_url, + pc.post_mode FROM bots b JOIN post_config pc ON b.id = pc.bot_id JOIN media_config mc ON b.id = mc.bot_id @@ -144,6 +144,7 @@ func (s *Scheduler) Start() error { &postConfig.Enabled, &postConfig.IntervalMinutes, &postConfig.Hashtags, &mediaConfig.PrimaryService, &mediaConfig.FallbackService, &mediaConfig.Nip94ServerURL, &mediaConfig.BlossomServerURL, + &postConfig.PostMode, ) if err != nil { s.logger.Error("Failed to scan bot row", zap.Error(err)) @@ -284,9 +285,8 @@ func (s *Scheduler) ScheduleBot(bot *models.Bot) error { } } - // Get the base filename without extension - filename := filepath.Base(contentPath) - caption := strings.TrimSuffix(filename, filepath.Ext(filename)) + // Use empty caption + caption := "" // Get the owner pubkey of the bot var ownerPubkey string @@ -322,11 +322,40 @@ func (s *Scheduler) ScheduleBot(bot *models.Bot) error { zap.String("bot_name", bot.Name)) } - // Rest of the function remains the same + // Get post mode from bot config or default to kind20 + postMode := "kind20" + if bot.PostConfig != nil && bot.PostConfig.PostMode != "" { + postMode = bot.PostConfig.PostMode + } + s.logger.Info("Using post mode", + zap.String("post_mode", postMode), + zap.Int64("bot_id", bot.ID)) + + // Double check post mode directly from the database + var dbPostMode string + dbErr := s.db.Get(&dbPostMode, "SELECT post_mode FROM post_config WHERE bot_id = ?", bot.ID) + if dbErr != nil { + s.logger.Warn("Failed to get post_mode from database directly", + zap.Error(dbErr), + zap.Int64("bot_id", bot.ID)) + } else { + s.logger.Info("Post mode from direct DB query", + zap.String("db_post_mode", dbPostMode), + zap.Int64("bot_id", bot.ID)) + } + + // Additional logging for content details + s.logger.Info("Content details", + zap.String("content_path", contentPath), + zap.String("content_type", contentType), + zap.String("media_url", mediaURL), + zap.String("media_hash", mediaHash)) + var postErr error if s.postContentEncoded != nil { // Use the NIP-19 encoded version + // Call the function with the original signature encodedEvent, err := s.postContentEncoded( bot.Pubkey, contentPath, @@ -356,6 +385,7 @@ func (s *Scheduler) ScheduleBot(bot *models.Bot) error { } } else { // Fall back to original function + // Call the function with the original signature postErr = s.postContent( bot.Pubkey, contentPath, diff --git a/web/assets/js/main.js b/web/assets/js/main.js index dc4aff7..4c4a71f 100644 --- a/web/assets/js/main.js +++ b/web/assets/js/main.js @@ -1286,7 +1286,7 @@ async function checkAuth() { // If post_config is present if (bot.post_config) { document.getElementById('botSettingsInterval').value = bot.post_config.interval_minutes || 60; - + // Parse hashtags try { const hashtags = JSON.parse(bot.post_config.hashtags || '[]'); @@ -1295,6 +1295,13 @@ async function checkAuth() { console.error('Failed to parse hashtags', e); document.getElementById('botSettingsHashtags').value = ''; } + + // Set post mode if present + const postModeSelect = document.getElementById('botSettingsPostMode'); + if (postModeSelect && bot.post_config.post_mode) { + // Default to kind20 if not specified + postModeSelect.value = bot.post_config.post_mode || 'kind20'; + } } // Show the modal @@ -1411,10 +1418,18 @@ async function checkAuth() { const hashtagsStr = document.getElementById('botSettingsHashtags').value.trim(); const hashtagsArr = hashtagsStr.length ? hashtagsStr.split(',').map(s => s.trim()) : []; + // Get selected post mode + const postMode = document.getElementById('botSettingsPostMode').value; + + // Log the post mode selected for debugging + console.log('Post mode selected:', postMode); + alert('Setting post mode to: ' + postMode); + const configPayload = { post_config: { interval_minutes: intervalValue, - hashtags: JSON.stringify(hashtagsArr) + hashtags: JSON.stringify(hashtagsArr), + post_mode: postMode // We do not override 'enabled' here, so it remains as is } }; diff --git a/web/index.html b/web/index.html index cbb731c..306b250 100644 --- a/web/index.html +++ b/web/index.html @@ -508,6 +508,15 @@ +