added hybrid posting mode and fixed kind20 formating
This commit is contained in:
parent
9670d04d68
commit
77643c2154
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
@ -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(`
|
||||
|
@ -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"`
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
};
|
||||
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user