added hybrid posting mode and fixed kind20 formating

This commit is contained in:
Enki 2025-05-11 17:45:26 -07:00
parent 9670d04d68
commit 77643c2154
9 changed files with 659 additions and 80 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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,
})
}

View File

@ -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(`

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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,

View File

@ -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
}
};

View File

@ -508,6 +508,15 @@
<label for="botSettingsHashtags" class="form-label">Hashtags (comma-separated)</label>
<input type="text" class="form-control" id="botSettingsHashtags">
</div>
<div class="mb-3">
<label for="botSettingsPostMode" class="form-label">Post Mode</label>
<select class="form-select" id="botSettingsPostMode">
<option value="kind20">Kind 20 - Picture Posts (NIP-68)</option>
<option value="kind1">Kind 1 - Text Notes with Media (NIP-92)</option>
<option value="hybrid">Hybrid - Both Kind 1 and Kind 20</option>
</select>
<div class="form-text">Choose how this bot will post images to Nostr</div>
</div>
</form>
</div>
<div class="modal-footer">