manual post and nip19 rework, content page looks good for now.
@ -2,11 +2,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"git.sovbit.dev/Enki/nostr-poster/internal/api"
|
"git.sovbit.dev/Enki/nostr-poster/internal/api"
|
||||||
"git.sovbit.dev/Enki/nostr-poster/internal/auth"
|
"git.sovbit.dev/Enki/nostr-poster/internal/auth"
|
||||||
"git.sovbit.dev/Enki/nostr-poster/internal/config"
|
"git.sovbit.dev/Enki/nostr-poster/internal/config"
|
||||||
@ -15,6 +19,7 @@ import (
|
|||||||
"git.sovbit.dev/Enki/nostr-poster/internal/media/prepare"
|
"git.sovbit.dev/Enki/nostr-poster/internal/media/prepare"
|
||||||
"git.sovbit.dev/Enki/nostr-poster/internal/media/upload/blossom"
|
"git.sovbit.dev/Enki/nostr-poster/internal/media/upload/blossom"
|
||||||
"git.sovbit.dev/Enki/nostr-poster/internal/media/upload/nip94"
|
"git.sovbit.dev/Enki/nostr-poster/internal/media/upload/nip94"
|
||||||
|
"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/events"
|
||||||
"git.sovbit.dev/Enki/nostr-poster/internal/nostr/poster"
|
"git.sovbit.dev/Enki/nostr-poster/internal/nostr/poster"
|
||||||
"git.sovbit.dev/Enki/nostr-poster/internal/nostr/relay"
|
"git.sovbit.dev/Enki/nostr-poster/internal/nostr/relay"
|
||||||
@ -146,15 +151,120 @@ func main() {
|
|||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create post content function
|
// Create standard post content function
|
||||||
postContentFunc := poster.CreatePostContentFunc(
|
postContentFunc := poster.CreatePostContentFunc(
|
||||||
eventManager,
|
eventManager,
|
||||||
relayManager,
|
relayManager,
|
||||||
mediaPrep,
|
mediaPrep,
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Create the NIP-19 encoded post content function
|
||||||
|
postContentEncodedFunc := func(
|
||||||
|
pubkey string,
|
||||||
|
contentPath string,
|
||||||
|
contentType string,
|
||||||
|
mediaURL string,
|
||||||
|
mediaHash string,
|
||||||
|
caption string,
|
||||||
|
hashtags []string,
|
||||||
|
) (*models.EventResponse, error) {
|
||||||
|
// Create a context
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to get image dimensions, continuing anyway",
|
||||||
|
zap.String("file", contentPath),
|
||||||
|
zap.Error(err))
|
||||||
|
} else {
|
||||||
|
// Add dimensions to alt text if available
|
||||||
|
if altText != "" {
|
||||||
|
altText = fmt.Sprintf("%s [%dx%d]", altText, dims.Width, dims.Height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an appropriate event
|
||||||
|
var event *nostr.Event
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if isImage {
|
||||||
|
// 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, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
content := caption + hashtagStr
|
||||||
|
|
||||||
|
event, err = eventManager.CreateAndSignMediaEvent(
|
||||||
|
pubkey,
|
||||||
|
content,
|
||||||
|
mediaURL,
|
||||||
|
contentType,
|
||||||
|
mediaHash,
|
||||||
|
altText,
|
||||||
|
hashtags,
|
||||||
|
)
|
||||||
|
} else if isVideo {
|
||||||
|
// For videos, determine if it's a short video
|
||||||
|
isShortVideo := false // Just a placeholder, would need logic to determine
|
||||||
|
|
||||||
|
// Create the video event
|
||||||
|
event, err = eventManager.CreateAndSignVideoEvent(
|
||||||
|
pubkey,
|
||||||
|
caption, // Title
|
||||||
|
caption, // Description
|
||||||
|
mediaURL,
|
||||||
|
contentType,
|
||||||
|
mediaHash,
|
||||||
|
"", // Preview image URL
|
||||||
|
0, // Duration
|
||||||
|
altText,
|
||||||
|
hashtags,
|
||||||
|
isShortVideo,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// For other types, use a regular text note with attachment
|
||||||
|
event, err = eventManager.CreateAndSignMediaEvent(
|
||||||
|
pubkey,
|
||||||
|
caption,
|
||||||
|
mediaURL,
|
||||||
|
contentType,
|
||||||
|
mediaHash,
|
||||||
|
altText,
|
||||||
|
hashtags,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish the event with NIP-19 encoding
|
||||||
|
return relayManager.PublishEventWithEncoding(ctx, event)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize scheduler
|
// Initialize scheduler with both posting functions
|
||||||
posterScheduler := scheduler.NewScheduler(
|
posterScheduler := scheduler.NewScheduler(
|
||||||
database,
|
database,
|
||||||
logger,
|
logger,
|
||||||
@ -163,6 +273,7 @@ func main() {
|
|||||||
nip94Uploader,
|
nip94Uploader,
|
||||||
blossomUploader,
|
blossomUploader,
|
||||||
postContentFunc,
|
postContentFunc,
|
||||||
|
postContentEncodedFunc, // New encoded function
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize API
|
// Initialize API
|
||||||
|
BIN
content/bot_18/VeniceAI_9o8uX4y.png
Normal file
After Width: | Height: | Size: 971 KiB |
BIN
content/bot_18/VeniceAI_Ef25af1.png
Normal file
After Width: | Height: | Size: 827 KiB |
BIN
content/bot_18/VeniceAI_FbjA9Sf.png
Normal file
After Width: | Height: | Size: 2.0 MiB |
BIN
content/bot_18/VeniceAI_ORee53E.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
content/bot_18/VeniceAI_PNQMHHy.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
content/bot_18/VeniceAI_dCaNsqe.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
content/bot_18/wallhaven-lmq39l.jpg
Normal file
After Width: | Height: | Size: 467 KiB |
@ -539,6 +539,38 @@ func (s *BotService) PublishBotProfile(botID int64, ownerPubkey string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishBotProfileWithEncoding publishes a bot's profile and returns the encoded event
|
||||||
|
func (s *BotService) PublishBotProfileWithEncoding(botID int64, ownerPubkey string) (*models.EventResponse, error) {
|
||||||
|
// Get the bot
|
||||||
|
bot, err := s.GetBotByID(botID, ownerPubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("bot not found or not owned by user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and sign the metadata event
|
||||||
|
event, err := s.eventMgr.CreateAndSignMetadataEvent(bot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create metadata event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up relay connections
|
||||||
|
for _, relay := range bot.Relays {
|
||||||
|
if relay.Write {
|
||||||
|
if err := s.relayMgr.AddRelay(relay.URL, relay.Read, relay.Write); err != nil {
|
||||||
|
s.logger.Warn("Failed to add relay",
|
||||||
|
zap.String("url", relay.URL),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish the event with encoding
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return s.relayMgr.PublishEventWithEncoding(ctx, event)
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to load related data for a bot
|
// Helper function to load related data for a bot
|
||||||
func (s *BotService) loadBotRelatedData(bot *models.Bot) error {
|
func (s *BotService) loadBotRelatedData(bot *models.Bot) error {
|
||||||
// Load post config
|
// Load post config
|
||||||
|
@ -834,7 +834,7 @@ func (a *API) uploadToMediaServer(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated createManualPost function in routes.go to properly process kind 20 posts
|
// Updated createManualPost function in routes.go
|
||||||
func (a *API) createManualPost(c *gin.Context) {
|
func (a *API) createManualPost(c *gin.Context) {
|
||||||
pubkey := c.GetString("pubkey")
|
pubkey := c.GetString("pubkey")
|
||||||
botIDStr := c.Param("id")
|
botIDStr := c.Param("id")
|
||||||
@ -883,14 +883,8 @@ func (a *API) createManualPost(c *gin.Context) {
|
|||||||
if len(matches) > 0 {
|
if len(matches) > 0 {
|
||||||
mediaURL = matches[0] // Use the first URL found
|
mediaURL = matches[0] // Use the first URL found
|
||||||
|
|
||||||
// Try to determine media type - this is a placeholder
|
// Try to determine media type
|
||||||
// In a real implementation, you might want to make an HTTP head request
|
|
||||||
// or infer from URL extension
|
|
||||||
mediaType = inferMediaTypeFromURL(mediaURL)
|
mediaType = inferMediaTypeFromURL(mediaURL)
|
||||||
|
|
||||||
// We don't have a hash yet, but we could calculate it if needed
|
|
||||||
// This would require downloading the file, which might be too heavy
|
|
||||||
mediaHash = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the appropriate event
|
// Create the appropriate event
|
||||||
@ -941,22 +935,22 @@ func (a *API) createManualPost(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish to relays
|
// Publish to relays with NIP-19 encoding
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
publishedRelays, err := a.botService.relayMgr.PublishEvent(ctx, event)
|
// Use the new method that includes NIP-19 encoding
|
||||||
|
encodedEvent, err := a.botService.relayMgr.PublishEventWithEncoding(ctx, event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("Failed to publish event", zap.Error(err))
|
a.logger.Error("Failed to publish event", zap.Error(err))
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to publish post: " + err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to publish post: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return success
|
// Return the encoded event response
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "Post published successfully",
|
"message": "Post published successfully",
|
||||||
"event_id": event.ID,
|
"event": encodedEvent,
|
||||||
"relays": publishedRelays,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
16
internal/models/response.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// internal/models/response.go
|
||||||
|
package models
|
||||||
|
|
||||||
|
// EventResponse represents an event with NIP-19 encoded identifiers
|
||||||
|
type EventResponse struct {
|
||||||
|
ID string `json:"id"` // Raw hex event ID
|
||||||
|
Nevent string `json:"nevent"` // NIP-19 encoded event ID with relay info
|
||||||
|
Note string `json:"note"` // NIP-19 encoded event ID without relay info
|
||||||
|
Pubkey string `json:"pubkey"` // Raw hex pubkey
|
||||||
|
Npub string `json:"npub"` // NIP-19 encoded pubkey
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
Kind int `json:"kind"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Tags [][]string `json:"tags"`
|
||||||
|
Relays []string `json:"relays"` // Relays where the event was published
|
||||||
|
}
|
56
internal/nostr/events/encoder.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// internal/nostr/events/encoder.go
|
||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"git.sovbit.dev/Enki/nostr-poster/internal/models"
|
||||||
|
"git.sovbit.dev/Enki/nostr-poster/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncodeEvent converts a nostr.Event to models.EventResponse with NIP-19 encoded IDs
|
||||||
|
func EncodeEvent(event *nostr.Event, publishedRelays []string) (*models.EventResponse, error) {
|
||||||
|
// Create the basic response
|
||||||
|
response := &models.EventResponse{
|
||||||
|
ID: event.ID,
|
||||||
|
Pubkey: event.PubKey,
|
||||||
|
CreatedAt: int64(event.CreatedAt),
|
||||||
|
Kind: event.Kind,
|
||||||
|
Content: event.Content,
|
||||||
|
Tags: convertTags(event.Tags), // Use a helper function to convert tags
|
||||||
|
Relays: publishedRelays,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the event ID as nevent (with relay info)
|
||||||
|
nevent, err := utils.EncodeEventAsNevent(event.ID, publishedRelays, event.PubKey)
|
||||||
|
if err == nil {
|
||||||
|
response.Nevent = nevent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the event ID as note (without relay info)
|
||||||
|
note, err := utils.EncodeEventAsNote(event.ID)
|
||||||
|
if err == nil {
|
||||||
|
response.Note = note
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the pubkey as npub
|
||||||
|
npub, err := utils.EncodePubkey(event.PubKey)
|
||||||
|
if err == nil {
|
||||||
|
response.Npub = npub
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert nostr.Tags to [][]string
|
||||||
|
func convertTags(tags nostr.Tags) [][]string {
|
||||||
|
result := make([][]string, len(tags))
|
||||||
|
for i, tag := range tags {
|
||||||
|
// Create a new string slice for each tag
|
||||||
|
tagStrings := make([]string, len(tag))
|
||||||
|
for j, v := range tag {
|
||||||
|
tagStrings[j] = v
|
||||||
|
}
|
||||||
|
result[i] = tagStrings
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
@ -6,7 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"git.sovbit.dev/Enki/nostr-poster/internal/models"
|
||||||
|
"git.sovbit.dev/Enki/nostr-poster/internal/nostr/events"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@ -20,6 +21,19 @@ type Manager struct {
|
|||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertTags(tags nostr.Tags) [][]string {
|
||||||
|
result := make([][]string, len(tags))
|
||||||
|
for i, tag := range tags {
|
||||||
|
// Create a new string slice for each tag
|
||||||
|
tagStrings := make([]string, len(tag))
|
||||||
|
for j, v := range tag {
|
||||||
|
tagStrings[j] = v
|
||||||
|
}
|
||||||
|
result[i] = tagStrings
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// NewManager creates a new relay manager
|
// NewManager creates a new relay manager
|
||||||
func NewManager(logger *zap.Logger) *Manager {
|
func NewManager(logger *zap.Logger) *Manager {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
@ -186,6 +200,36 @@ func (m *Manager) PublishEvent(ctx context.Context, event *nostr.Event) ([]strin
|
|||||||
return successful, nil
|
return successful, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishEventWithEncoding publishes an event and returns encoded identifiers
|
||||||
|
func (m *Manager) PublishEventWithEncoding(ctx context.Context, event *nostr.Event) (*models.EventResponse, error) {
|
||||||
|
// First publish the event using the existing method
|
||||||
|
publishedRelays, err := m.PublishEvent(ctx, event)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now encode the event with NIP-19 identifiers
|
||||||
|
encodedEvent, err := events.EncodeEvent(event, publishedRelays)
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Warn("Failed to encode event with NIP-19",
|
||||||
|
zap.String("event_id", event.ID),
|
||||||
|
zap.Error(err))
|
||||||
|
|
||||||
|
// Return a basic response if encoding fails
|
||||||
|
return &models.EventResponse{
|
||||||
|
ID: event.ID,
|
||||||
|
Pubkey: event.PubKey,
|
||||||
|
CreatedAt: int64(event.CreatedAt),
|
||||||
|
Kind: event.Kind,
|
||||||
|
Content: event.Content,
|
||||||
|
Tags: convertTags(event.Tags), // Use the helper function here
|
||||||
|
Relays: publishedRelays,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodedEvent, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SubscribeToEvents subscribes to events matching the given filters
|
// SubscribeToEvents subscribes to events matching the given filters
|
||||||
func (m *Manager) SubscribeToEvents(ctx context.Context, filters []nostr.Filter) (<-chan *nostr.Event, error) {
|
func (m *Manager) SubscribeToEvents(ctx context.Context, filters []nostr.Filter) (<-chan *nostr.Event, error) {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
|
@ -40,18 +40,30 @@ type ContentPoster func(
|
|||||||
hashtags []string,
|
hashtags []string,
|
||||||
) error
|
) error
|
||||||
|
|
||||||
|
// ContentPosterWithEncoding defines a function to post content and return encoded event
|
||||||
|
type ContentPosterWithEncoding func(
|
||||||
|
pubkey string,
|
||||||
|
contentPath string,
|
||||||
|
contentType string,
|
||||||
|
mediaURL string,
|
||||||
|
mediaHash string,
|
||||||
|
caption string,
|
||||||
|
hashtags []string,
|
||||||
|
) (*models.EventResponse, error)
|
||||||
|
|
||||||
// Scheduler manages scheduled content posting
|
// Scheduler manages scheduled content posting
|
||||||
type Scheduler struct {
|
type Scheduler struct {
|
||||||
db *db.DB
|
db *db.DB
|
||||||
cron *cron.Cron
|
cron *cron.Cron
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
contentDir string
|
contentDir string
|
||||||
archiveDir string
|
archiveDir string
|
||||||
nip94Uploader MediaUploader
|
nip94Uploader MediaUploader
|
||||||
blossomUploader MediaUploader
|
blossomUploader MediaUploader
|
||||||
postContent ContentPoster
|
postContent ContentPoster
|
||||||
botJobs map[int64]cron.EntryID
|
postContentEncoded ContentPosterWithEncoding // New field for NIP-19 support
|
||||||
mu sync.RWMutex
|
botJobs map[int64]cron.EntryID
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScheduler creates a new content scheduler
|
// NewScheduler creates a new content scheduler
|
||||||
@ -63,6 +75,7 @@ func NewScheduler(
|
|||||||
nip94Uploader MediaUploader,
|
nip94Uploader MediaUploader,
|
||||||
blossomUploader MediaUploader,
|
blossomUploader MediaUploader,
|
||||||
postContent ContentPoster,
|
postContent ContentPoster,
|
||||||
|
postContentEncoded ContentPosterWithEncoding, // New parameter for NIP-19 support
|
||||||
) *Scheduler {
|
) *Scheduler {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
// Create a default logger
|
// Create a default logger
|
||||||
@ -77,15 +90,16 @@ func NewScheduler(
|
|||||||
cronScheduler := cron.New(cron.WithSeconds())
|
cronScheduler := cron.New(cron.WithSeconds())
|
||||||
|
|
||||||
return &Scheduler{
|
return &Scheduler{
|
||||||
db: db,
|
db: db,
|
||||||
cron: cronScheduler,
|
cron: cronScheduler,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
contentDir: contentDir,
|
contentDir: contentDir,
|
||||||
archiveDir: archiveDir,
|
archiveDir: archiveDir,
|
||||||
nip94Uploader: nip94Uploader,
|
nip94Uploader: nip94Uploader,
|
||||||
blossomUploader: blossomUploader,
|
blossomUploader: blossomUploader,
|
||||||
postContent: postContent,
|
postContent: postContent,
|
||||||
botJobs: make(map[int64]cron.EntryID),
|
postContentEncoded: postContentEncoded, // Initialize the new field
|
||||||
|
botJobs: make(map[int64]cron.EntryID),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,22 +276,65 @@ func (s *Scheduler) ScheduleBot(bot *models.Bot) error {
|
|||||||
filename := filepath.Base(contentPath)
|
filename := filepath.Base(contentPath)
|
||||||
caption := strings.TrimSuffix(filename, filepath.Ext(filename))
|
caption := strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||||
|
|
||||||
// Post the content
|
// Post the content - use encoded version if available, otherwise use the original
|
||||||
err = s.postContent(
|
var postErr error
|
||||||
bot.Pubkey,
|
|
||||||
contentPath,
|
|
||||||
contentType,
|
|
||||||
mediaURL,
|
|
||||||
mediaHash,
|
|
||||||
caption,
|
|
||||||
hashtags,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
if s.postContentEncoded != nil {
|
||||||
s.logger.Error("Failed to post content",
|
// Use the NIP-19 encoded version
|
||||||
zap.Int64("bot_id", bot.ID),
|
encodedEvent, err := s.postContentEncoded(
|
||||||
zap.String("file", contentPath),
|
bot.Pubkey,
|
||||||
zap.Error(err))
|
contentPath,
|
||||||
|
contentType,
|
||||||
|
mediaURL,
|
||||||
|
mediaHash,
|
||||||
|
caption,
|
||||||
|
hashtags,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to post content with encoding",
|
||||||
|
zap.Int64("bot_id", bot.ID),
|
||||||
|
zap.String("file", contentPath),
|
||||||
|
zap.Error(err))
|
||||||
|
postErr = err
|
||||||
|
} else {
|
||||||
|
// Success with encoded version
|
||||||
|
s.logger.Info("Successfully posted content with NIP-19 encoding",
|
||||||
|
zap.Int64("bot_id", bot.ID),
|
||||||
|
zap.String("file", contentPath),
|
||||||
|
zap.String("media_url", mediaURL),
|
||||||
|
zap.String("event_id", encodedEvent.ID),
|
||||||
|
zap.String("note", encodedEvent.Note),
|
||||||
|
zap.String("nevent", encodedEvent.Nevent))
|
||||||
|
postErr = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fall back to original function
|
||||||
|
postErr = s.postContent(
|
||||||
|
bot.Pubkey,
|
||||||
|
contentPath,
|
||||||
|
contentType,
|
||||||
|
mediaURL,
|
||||||
|
mediaHash,
|
||||||
|
caption,
|
||||||
|
hashtags,
|
||||||
|
)
|
||||||
|
|
||||||
|
if postErr != nil {
|
||||||
|
s.logger.Error("Failed to post content",
|
||||||
|
zap.Int64("bot_id", bot.ID),
|
||||||
|
zap.String("file", contentPath),
|
||||||
|
zap.Error(postErr))
|
||||||
|
} else {
|
||||||
|
s.logger.Info("Successfully posted content",
|
||||||
|
zap.Int64("bot_id", bot.ID),
|
||||||
|
zap.String("file", contentPath),
|
||||||
|
zap.String("media_url", mediaURL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If posting failed, return without archiving the file
|
||||||
|
if postErr != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,11 +348,6 @@ func (s *Scheduler) ScheduleBot(bot *models.Bot) error {
|
|||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Info("Successfully posted content",
|
|
||||||
zap.Int64("bot_id", bot.ID),
|
|
||||||
zap.String("file", contentPath),
|
|
||||||
zap.String("media_url", mediaURL))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule the job
|
// Schedule the job
|
||||||
@ -349,6 +401,7 @@ func (s *Scheduler) GetBlossomUploader() MediaUploader {
|
|||||||
func (s *Scheduler) GetContentDir() string {
|
func (s *Scheduler) GetContentDir() string {
|
||||||
return s.contentDir
|
return s.contentDir
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunNow triggers an immediate post for a bot
|
// RunNow triggers an immediate post for a bot
|
||||||
func (s *Scheduler) RunNow(botID int64) error {
|
func (s *Scheduler) RunNow(botID int64) error {
|
||||||
// Load the bot with its configurations
|
// Load the bot with its configurations
|
||||||
@ -395,4 +448,4 @@ func (s *Scheduler) RunNow(botID int64) error {
|
|||||||
entry.Job.Run()
|
entry.Job.Run()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
30
internal/utils/nip-19.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// internal/utils/nip19.go
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncodeEventAsNevent encodes an event ID and relays into a NIP-19 "nevent" identifier
|
||||||
|
func EncodeEventAsNevent(eventID string, relays []string, authorPubkey string) (string, error) {
|
||||||
|
// Use the direct function signature that your version of nip19 expects
|
||||||
|
return nip19.EncodeEvent(eventID, relays, authorPubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeEventAsNote encodes an event ID into a NIP-19 "note" identifier (simpler)
|
||||||
|
func EncodeEventAsNote(eventID string) (string, error) {
|
||||||
|
note, err := nip19.EncodeNote(eventID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return note, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodePubkey encodes a public key into a NIP-19 "npub" identifier
|
||||||
|
func EncodePubkey(pubkey string) (string, error) {
|
||||||
|
npub, err := nip19.EncodePublicKey(pubkey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return npub, nil
|
||||||
|
}
|
@ -1,16 +1,19 @@
|
|||||||
/* =============================================
|
/* =============================================
|
||||||
VARIABLES
|
VARIABLES
|
||||||
============================================= */
|
============================================= */
|
||||||
:root {
|
:root {
|
||||||
/* Color Palette */
|
/* Color Palette */
|
||||||
--primary-black: #121212;
|
--primary-black: #121212;
|
||||||
--secondary-black: #1e1e1e;
|
--secondary-black: #1e1e1e;
|
||||||
--primary-gray: #2d2d2d;
|
--primary-gray: #2d2d2d;
|
||||||
--secondary-gray: #3d3d3d;
|
--secondary-gray: #3d3d3d;
|
||||||
--light-gray: #aaaaaa;
|
--light-gray: #aaaaaa;
|
||||||
--primary-purple: #9370DB; /* Medium Purple */
|
--primary-purple: #9370DB;
|
||||||
--secondary-purple: #7B68EE; /* Medium Slate Blue */
|
/* Medium Purple */
|
||||||
--dark-purple: #6A5ACD; /* Slate Blue */
|
--secondary-purple: #7B68EE;
|
||||||
|
/* Medium Slate Blue */
|
||||||
|
--dark-purple: #6A5ACD;
|
||||||
|
/* Slate Blue */
|
||||||
--text-color: #e0e0e0;
|
--text-color: #e0e0e0;
|
||||||
--success-color: #4CAF50;
|
--success-color: #4CAF50;
|
||||||
--border-color: #444;
|
--border-color: #444;
|
||||||
@ -93,7 +96,7 @@ body {
|
|||||||
border-color: var(--primary-purple) !important;
|
border-color: var(--primary-purple) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover,
|
.btn-primary:hover,
|
||||||
.btn-primary:focus {
|
.btn-primary:focus {
|
||||||
background-color: var(--secondary-purple) !important;
|
background-color: var(--secondary-purple) !important;
|
||||||
border-color: var(--secondary-purple) !important;
|
border-color: var(--secondary-purple) !important;
|
||||||
@ -112,14 +115,14 @@ body {
|
|||||||
/* =============================================
|
/* =============================================
|
||||||
FORMS
|
FORMS
|
||||||
============================================= */
|
============================================= */
|
||||||
.form-control,
|
.form-control,
|
||||||
.form-select {
|
.form-select {
|
||||||
background-color: var(--primary-black);
|
background-color: var(--primary-black);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus,
|
.form-control:focus,
|
||||||
.form-select:focus {
|
.form-select:focus {
|
||||||
background-color: var(--primary-black);
|
background-color: var(--primary-black);
|
||||||
border-color: var(--primary-purple);
|
border-color: var(--primary-purple);
|
||||||
@ -224,7 +227,7 @@ body {
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#uploadPreviewContainer,
|
#uploadPreviewContainer,
|
||||||
#mediaPreviewContainer {
|
#mediaPreviewContainer {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 250px;
|
max-height: 250px;
|
||||||
@ -256,7 +259,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Server URL input improvements */
|
/* Server URL input improvements */
|
||||||
#primaryServerURL,
|
#primaryServerURL,
|
||||||
#fallbackServerURL {
|
#fallbackServerURL {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@ -275,6 +278,38 @@ body {
|
|||||||
background-color: var(--primary-gray);
|
background-color: var(--primary-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Event info display */
|
||||||
|
.event-info {
|
||||||
|
background-color: var(--primary-gray);
|
||||||
|
border-left: 3px solid var(--success-color);
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-info .card-header {
|
||||||
|
background-color: rgba(76, 175, 80, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-info .input-group {
|
||||||
|
background-color: var(--secondary-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-info input.form-control {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
background-color: var(--secondary-black);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
background-color: var(--primary-purple);
|
||||||
|
border-color: var(--primary-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:active {
|
||||||
|
background-color: var(--dark-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* =============================================
|
/* =============================================
|
||||||
RESPONSIVE STYLES
|
RESPONSIVE STYLES
|
||||||
============================================= */
|
============================================= */
|
||||||
@ -282,12 +317,12 @@ body {
|
|||||||
.card {
|
.card {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-selection-container {
|
.bot-selection-container {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-preview {
|
.upload-preview {
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
}
|
}
|
||||||
|
@ -105,18 +105,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
postKindRadios.forEach(radio => {
|
postKindRadios.forEach(radio => {
|
||||||
radio.addEventListener('change', () => {
|
radio.addEventListener('change', () => {
|
||||||
titleField.classList.toggle('d-none', radio.value !== '20');
|
titleField.classList.toggle('d-none', radio.value !== '20');
|
||||||
|
|
||||||
// Show/hide required media elements for kind 20
|
// Show/hide required media elements for kind 20
|
||||||
const isKind20 = radio.value === '20';
|
const isKind20 = radio.value === '20';
|
||||||
const kind20MediaRequired = document.getElementById('kind20MediaRequired');
|
const kind20MediaRequired = document.getElementById('kind20MediaRequired');
|
||||||
if (kind20MediaRequired) {
|
if (kind20MediaRequired) {
|
||||||
kind20MediaRequired.style.display = isKind20 ? 'inline-block' : 'none';
|
kind20MediaRequired.style.display = isKind20 ? 'inline-block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// For kind 20, validate that media is provided before posting
|
// For kind 20, validate that media is provided before posting
|
||||||
if (submitPostBtn) {
|
if (submitPostBtn) {
|
||||||
submitPostBtn.title = isKind20 ?
|
submitPostBtn.title = isKind20 ?
|
||||||
'Picture posts require a title and an image URL' :
|
'Picture posts require a title and an image URL' :
|
||||||
'Create a standard text post';
|
'Create a standard text post';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -177,7 +177,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createManualPost(currentBotId);
|
createManualPost(currentBotId)
|
||||||
|
.then(data => {
|
||||||
|
alert('Post created successfully!');
|
||||||
|
console.log('Post success response:', data);
|
||||||
|
|
||||||
|
// Display event information if present
|
||||||
|
if (data.event) {
|
||||||
|
displayEventInfo(data.event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
manualPostContent.value = '';
|
||||||
|
postHashtags.value = '';
|
||||||
|
postTitle.value = '';
|
||||||
|
postMediaInput.value = '';
|
||||||
|
if (mediaUrlInput) mediaUrlInput.value = '';
|
||||||
|
if (mediaAltText) mediaAltText.value = '';
|
||||||
|
mediaPreviewContainer.classList.add('d-none');
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
submitPostBtn.disabled = false;
|
||||||
|
submitPostBtn.textContent = 'Post Now';
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error creating post:', error);
|
||||||
|
alert('Error creating post: ' + error.message);
|
||||||
|
submitPostBtn.disabled = false;
|
||||||
|
submitPostBtn.textContent = 'Post Now';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle saving media server settings
|
// Handle saving media server settings
|
||||||
@ -224,13 +252,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
function validatePostData() {
|
function validatePostData() {
|
||||||
const kind = parseInt(document.querySelector('input[name="postKind"]:checked').value);
|
const kind = parseInt(document.querySelector('input[name="postKind"]:checked').value);
|
||||||
const content = manualPostContent.value.trim();
|
const content = manualPostContent.value.trim();
|
||||||
|
|
||||||
// Basic validation for all post types
|
// Basic validation for all post types
|
||||||
if (!content) {
|
if (!content) {
|
||||||
alert('Post content is required');
|
alert('Post content is required');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional validation for kind 20 posts
|
// Additional validation for kind 20 posts
|
||||||
if (kind === 20) {
|
if (kind === 20) {
|
||||||
const title = postTitle.value.trim();
|
const title = postTitle.value.trim();
|
||||||
@ -238,21 +266,67 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
alert('Title is required for Picture Posts (kind: 20)');
|
alert('Title is required for Picture Posts (kind: 20)');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have a media URL either in the content or in the mediaUrlInput
|
// Check if we have a media URL either in the content or in the mediaUrlInput
|
||||||
const mediaUrl = mediaUrlInput.value.trim();
|
const mediaUrl = mediaUrlInput.value.trim();
|
||||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||||
const contentContainsUrl = urlRegex.test(content);
|
const contentContainsUrl = urlRegex.test(content);
|
||||||
|
|
||||||
if (!mediaUrl && !contentContainsUrl) {
|
if (!mediaUrl && !contentContainsUrl) {
|
||||||
alert('Picture posts require an image URL. Please upload an image or enter a URL in the Media URL field or in the content.');
|
alert('Picture posts require an image URL. Please upload an image or enter a URL in the Media URL field or in the content.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add this function to content.js
|
||||||
|
function displayEventInfo(event) {
|
||||||
|
// Get the dedicated container
|
||||||
|
const container = document.getElementById('eventInfoContainer');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Create HTML for the event info
|
||||||
|
const html = `
|
||||||
|
<div class="event-info card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Post Published Successfully</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Note ID (NIP-19):</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" value="${event.note || ''}" readonly>
|
||||||
|
<button class="btn btn-outline-secondary copy-btn" data-value="${event.note || ''}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
|
||||||
|
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Event with Relays (NIP-19):</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" value="${event.nevent || ''}" readonly>
|
||||||
|
<button class="btn btn-outline-secondary copy-btn" data-value="${event.nevent || ''}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
|
||||||
|
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Update the container and make it visible
|
||||||
|
container.innerHTML = html;
|
||||||
|
container.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
// ===================================================
|
// ===================================================
|
||||||
// API Functions
|
// API Functions
|
||||||
// ===================================================
|
// ===================================================
|
||||||
@ -296,17 +370,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the list of content files
|
// Updated renderContentFiles function
|
||||||
function renderContentFiles(botId, files) {
|
function renderContentFiles(botId, files) {
|
||||||
const contentArea = document.getElementById('contentArea');
|
const contentArea = document.getElementById('contentArea');
|
||||||
if (!contentArea) return;
|
if (!contentArea) return;
|
||||||
|
|
||||||
if (!files || files.length === 0) {
|
// Generate ONLY the file list, no upload form
|
||||||
contentArea.innerHTML = '<p>No files found. Upload some content!</p>';
|
let html = '';
|
||||||
return;
|
|
||||||
}
|
if (!files || files.length === 0) {
|
||||||
|
html = '<p>No files found. Upload some content!</p>';
|
||||||
let html = '<ul class="list-group">';
|
} else {
|
||||||
|
html = '<ul class="list-group">';
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
html += `
|
html += `
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
@ -316,9 +391,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
html += '</ul>';
|
html += '</ul>';
|
||||||
|
|
||||||
contentArea.innerHTML = html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the content area HTML to just the files list
|
||||||
|
contentArea.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
// Upload media for the manual post
|
// Upload media for the manual post
|
||||||
function quickUploadMedia(botId, file) {
|
function quickUploadMedia(botId, file) {
|
||||||
@ -371,7 +448,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Insert the media URL into the post content
|
// Insert the media URL into the post content
|
||||||
const textArea = document.getElementById('manualPostContent');
|
const textArea = document.getElementById('manualPostContent');
|
||||||
let mediaUrl = data.url;
|
let mediaUrl = data.url;
|
||||||
|
|
||||||
// Also update the media URL input field
|
// Also update the media URL input field
|
||||||
const mediaUrlInput = document.getElementById('mediaUrlInput');
|
const mediaUrlInput = document.getElementById('mediaUrlInput');
|
||||||
if (mediaUrlInput) {
|
if (mediaUrlInput) {
|
||||||
@ -402,7 +479,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (!validatePostData()) {
|
if (!validatePostData()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = manualPostContent.value.trim();
|
const content = manualPostContent.value.trim();
|
||||||
const hashtagsValue = postHashtags.value.trim();
|
const hashtagsValue = postHashtags.value.trim();
|
||||||
const hashtags = hashtagsValue
|
const hashtags = hashtagsValue
|
||||||
@ -419,7 +496,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Extract media URLs and alt text
|
// Extract media URLs and alt text
|
||||||
const mediaUrl = mediaUrlInput ? mediaUrlInput.value.trim() : '';
|
const mediaUrl = mediaUrlInput ? mediaUrlInput.value.trim() : '';
|
||||||
const altText = mediaAltText ? mediaAltText.value.trim() : '';
|
const altText = mediaAltText ? mediaAltText.value.trim() : '';
|
||||||
|
|
||||||
// Build the post data based on kind
|
// Build the post data based on kind
|
||||||
const postData = {
|
const postData = {
|
||||||
kind: kind,
|
kind: kind,
|
||||||
@ -429,13 +506,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
if (kind === 20) {
|
if (kind === 20) {
|
||||||
postData.title = title;
|
postData.title = title;
|
||||||
|
|
||||||
// For kind 20, we need to ensure we have a valid URL
|
// For kind 20, we need to ensure we have a valid URL
|
||||||
// If we have a specific media URL field value, add it to the content if not already there
|
// If we have a specific media URL field value, add it to the content if not already there
|
||||||
if (mediaUrl && !content.includes(mediaUrl)) {
|
if (mediaUrl && !content.includes(mediaUrl)) {
|
||||||
postData.content = content + '\n\n' + mediaUrl;
|
postData.content = content + '\n\n' + mediaUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add alt text if provided
|
// Add alt text if provided
|
||||||
if (altText) {
|
if (altText) {
|
||||||
postData.alt = altText;
|
postData.alt = altText;
|
||||||
@ -510,4 +587,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
console.error('Error parsing saved media config:', e);
|
console.error('Error parsing saved media config:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (e.target.closest('.copy-btn')) {
|
||||||
|
const btn = e.target.closest('.copy-btn');
|
||||||
|
const valueToCopy = btn.getAttribute('data-value');
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(valueToCopy)
|
||||||
|
.then(() => {
|
||||||
|
const originalHTML = btn.innerHTML;
|
||||||
|
btn.innerHTML = 'Copied!';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = originalHTML;
|
||||||
|
}, 2000);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
@ -476,6 +476,93 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.publishBotProfile = async function(botId) {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('authToken');
|
||||||
|
const response = await fetch(`${API_ENDPOINT}/api/bots/${botId}/profile/publish`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errData.error || 'Failed to publish profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Check if event data with NIP-19 encoding is available
|
||||||
|
if (data.event) {
|
||||||
|
// Create a modal to show the encoded event info
|
||||||
|
const modalId = 'eventInfoModal';
|
||||||
|
|
||||||
|
// Remove any existing modal with the same ID
|
||||||
|
const existingModal = document.getElementById(modalId);
|
||||||
|
if (existingModal) {
|
||||||
|
existingModal.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventModal = document.createElement('div');
|
||||||
|
eventModal.className = 'modal fade';
|
||||||
|
eventModal.id = modalId;
|
||||||
|
eventModal.setAttribute('tabindex', '-1');
|
||||||
|
eventModal.innerHTML = `
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Profile Published Successfully</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Note ID (NIP-19):</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" value="${data.event.note || ''}" readonly>
|
||||||
|
<button class="btn btn-outline-secondary copy-btn" data-value="${data.event.note || ''}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
|
||||||
|
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Event with Relays (NIP-19):</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" value="${data.event.nevent || ''}" readonly>
|
||||||
|
<button class="btn btn-outline-secondary copy-btn" data-value="${data.event.nevent || ''}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
|
||||||
|
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add the modal to the document
|
||||||
|
document.body.appendChild(eventModal);
|
||||||
|
|
||||||
|
// Show the modal using Bootstrap
|
||||||
|
const bsModal = new bootstrap.Modal(document.getElementById(modalId));
|
||||||
|
bsModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
alert('Profile published successfully!');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error publishing profile:', err);
|
||||||
|
alert(`Error publishing profile: ${err.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
* Show Create Bot Modal
|
* Show Create Bot Modal
|
||||||
|
113
web/content.html
@ -131,13 +131,15 @@
|
|||||||
<h4>Content Files</h4>
|
<h4>Content Files</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="contentArea">
|
<!-- File list area - with scrollable container -->
|
||||||
|
<div id="contentArea" style="max-height: 300px; overflow-y: auto;">
|
||||||
<!-- Files will be listed here -->
|
<!-- Files will be listed here -->
|
||||||
<p>Select a bot and click "Load Content" to see files.</p>
|
<p>Select a bot and click "Load Content" to see files.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
<!-- Upload section - using existing structure without an ID -->
|
||||||
<h5>Upload New Content</h5>
|
<h5>Upload New Content</h5>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<input type="file" id="uploadFileInput" class="form-control" accept="image/*,video/*">
|
<input type="file" id="uploadFileInput" class="form-control" accept="image/*,video/*">
|
||||||
@ -150,57 +152,56 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Manual Post Section (Right) -->
|
<!-- Manual Post Section (Right) - FIXED: added col-md-6 wrapper -->
|
||||||
<div class="card mb-4">
|
<div class="col-md-6 mb-4">
|
||||||
<div class="card-header">
|
<div class="card">
|
||||||
<h4>Create Manual Post</h4>
|
<div class="card-header">
|
||||||
</div>
|
<h4>Create Manual Post</h4>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<!-- Post Type Selection -->
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<!-- Post Type Selection -->
|
||||||
<label class="form-label">Post Type</label>
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<label class="form-label">Post Type</label>
|
||||||
<input class="form-check-input" type="radio" name="postKind" id="postKind1" value="1" checked>
|
<div class="form-check">
|
||||||
<label class="form-check-label" for="postKind1">
|
<input class="form-check-input" type="radio" name="postKind" id="postKind1" value="1" checked>
|
||||||
Standard Post (kind: 1)
|
<label class="form-check-label" for="postKind1">
|
||||||
</label>
|
Standard Post (kind: 1)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="postKind" id="postKind20" value="20">
|
||||||
|
<label class="form-check-label" for="postKind20">
|
||||||
|
<strong>Picture Post (kind: 20)</strong> - Images displayed in a gallery-like feed
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="radio" name="postKind" id="postKind20" value="20">
|
<!-- Title Field (only for kind 20) -->
|
||||||
<label class="form-check-label" for="postKind20">
|
<div class="mb-3 d-none" id="titleField">
|
||||||
<strong>Picture Post (kind: 20)</strong> - Images displayed in a gallery-like feed
|
<label for="postTitle" class="form-label">Title <span class="text-danger">*</span></label>
|
||||||
</label>
|
<input type="text" class="form-control" id="postTitle" placeholder="Post title (required for kind: 20)">
|
||||||
|
<small class="form-text text-muted">Required for picture posts (kind: 20)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Title Field (only for kind 20) -->
|
<!-- Content Field -->
|
||||||
<div class="mb-3 d-none" id="titleField">
|
<div class="mb-3">
|
||||||
<label for="postTitle" class="form-label">Title <span class="text-danger">*</span></label>
|
<label for="manualPostContent" class="form-label">Content</label>
|
||||||
<input type="text" class="form-control" id="postTitle" placeholder="Post title (required for kind: 20)">
|
<textarea class="form-control" id="manualPostContent" rows="4"
|
||||||
<small class="form-text text-muted">Required for picture posts (kind: 20)</small>
|
placeholder="Write your post content here..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content Field -->
|
<!-- Hashtags Field -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="manualPostContent" class="form-label">Content</label>
|
<label for="postHashtags" class="form-label">Hashtags</label>
|
||||||
<textarea class="form-control" id="manualPostContent" rows="4"
|
<input type="text" class="form-control" id="postHashtags"
|
||||||
placeholder="Write your post content here..."></textarea>
|
placeholder="comma separated: art, photography, etc">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hashtags Field -->
|
<!-- Media Section - Enhanced for Kind 20 -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="postHashtags" class="form-label">Hashtags</label>
|
<h5 class="mb-2">Media Attachment</h5>
|
||||||
<input type="text" class="form-control" id="postHashtags"
|
|
||||||
placeholder="comma separated: art, photography, etc">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Media Section - Enhanced for Kind 20 -->
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Media Attachment</h5>
|
|
||||||
<span class="badge bg-info" id="kind20MediaRequired" style="display: none;">Required for kind: 20</span>
|
<span class="badge bg-info" id="kind20MediaRequired" style="display: none;">Required for kind: 20</span>
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- Upload Section -->
|
<!-- Upload Section -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Upload Image</label>
|
<label class="form-label">Upload Image</label>
|
||||||
@ -216,8 +217,7 @@
|
|||||||
<label for="mediaUrlInput" class="form-label">Media URL</label>
|
<label for="mediaUrlInput" class="form-label">Media URL</label>
|
||||||
<input type="text" class="form-control" id="mediaUrlInput"
|
<input type="text" class="form-control" id="mediaUrlInput"
|
||||||
placeholder="Enter media URL or upload an image">
|
placeholder="Enter media URL or upload an image">
|
||||||
<small class="form-text text-muted">For kind: 20 posts, an image URL is required (either here or in
|
<small class="form-text text-muted">For kind: 20 posts, an image URL is required</small>
|
||||||
the content)</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alt Text Field (for accessibility) -->
|
<!-- Alt Text Field (for accessibility) -->
|
||||||
@ -225,28 +225,25 @@
|
|||||||
<label for="mediaAltText" class="form-label">Alt Text</label>
|
<label for="mediaAltText" class="form-label">Alt Text</label>
|
||||||
<input type="text" class="form-control" id="mediaAltText"
|
<input type="text" class="form-control" id="mediaAltText"
|
||||||
placeholder="Describe the image for accessibility">
|
placeholder="Describe the image for accessibility">
|
||||||
<small class="form-text text-muted">Recommended for image description (improves accessibility)</small>
|
<small class="form-text text-muted">Recommended for image description</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Media Preview -->
|
<!-- Media Preview -->
|
||||||
<div id="mediaPreviewContainer" class="d-none mt-2">
|
<div id="mediaPreviewContainer" class="d-none mt-2">
|
||||||
<div class="card">
|
<img id="mediaPreview" class="upload-preview img-fluid mb-2">
|
||||||
<div class="card-body text-center">
|
<div id="mediaLinkContainer" class="media-link">
|
||||||
<img id="mediaPreview" class="upload-preview img-fluid mb-2">
|
<small class="text-muted">Media will appear here after upload</small>
|
||||||
<div id="mediaLinkContainer" class="media-link">
|
|
||||||
<small class="text-muted">Media will appear here after upload</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-success mt-2" id="submitPostBtn">Post Now</button>
|
<button type="button" class="btn btn-success mt-2" id="submitPostBtn">Post Now</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="eventInfoContainer" class="mt-3"></div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|