diff --git a/build/bin/nostr-poster b/build/bin/nostr-poster index c0f0d06..040aa3f 100755 Binary files a/build/bin/nostr-poster and b/build/bin/nostr-poster differ diff --git a/cmd/server/main.go b/cmd/server/main.go index 226d0c2..36e7ae1 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,11 +2,15 @@ package main import ( + "context" "flag" "fmt" "os" + "path/filepath" + "strings" "time" + "github.com/nbd-wtf/go-nostr" "git.sovbit.dev/Enki/nostr-poster/internal/api" "git.sovbit.dev/Enki/nostr-poster/internal/auth" "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/upload/blossom" "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/poster" "git.sovbit.dev/Enki/nostr-poster/internal/nostr/relay" @@ -146,15 +151,120 @@ func main() { logger, ) - // Create post content function + // Create standard post content function postContentFunc := poster.CreatePostContentFunc( eventManager, relayManager, mediaPrep, 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( database, logger, @@ -163,6 +273,7 @@ func main() { nip94Uploader, blossomUploader, postContentFunc, + postContentEncodedFunc, // New encoded function ) // Initialize API diff --git a/content/bot_18/VeniceAI_9o8uX4y.png b/content/bot_18/VeniceAI_9o8uX4y.png new file mode 100644 index 0000000..a410edc Binary files /dev/null and b/content/bot_18/VeniceAI_9o8uX4y.png differ diff --git a/content/bot_18/VeniceAI_Ef25af1.png b/content/bot_18/VeniceAI_Ef25af1.png new file mode 100644 index 0000000..7b0c511 Binary files /dev/null and b/content/bot_18/VeniceAI_Ef25af1.png differ diff --git a/content/bot_18/VeniceAI_FbjA9Sf.png b/content/bot_18/VeniceAI_FbjA9Sf.png new file mode 100644 index 0000000..a48877c Binary files /dev/null and b/content/bot_18/VeniceAI_FbjA9Sf.png differ diff --git a/content/bot_18/VeniceAI_ORee53E.png b/content/bot_18/VeniceAI_ORee53E.png new file mode 100644 index 0000000..321aea8 Binary files /dev/null and b/content/bot_18/VeniceAI_ORee53E.png differ diff --git a/content/bot_18/VeniceAI_PNQMHHy.png b/content/bot_18/VeniceAI_PNQMHHy.png new file mode 100644 index 0000000..5c3b0ab Binary files /dev/null and b/content/bot_18/VeniceAI_PNQMHHy.png differ diff --git a/content/bot_18/VeniceAI_dCaNsqe.png b/content/bot_18/VeniceAI_dCaNsqe.png new file mode 100644 index 0000000..2887a32 Binary files /dev/null and b/content/bot_18/VeniceAI_dCaNsqe.png differ diff --git a/content/bot_18/wallhaven-lmq39l.jpg b/content/bot_18/wallhaven-lmq39l.jpg new file mode 100644 index 0000000..5107831 Binary files /dev/null and b/content/bot_18/wallhaven-lmq39l.jpg differ diff --git a/internal/api/bot_service.go b/internal/api/bot_service.go index 279df13..f24cc18 100644 --- a/internal/api/bot_service.go +++ b/internal/api/bot_service.go @@ -539,6 +539,38 @@ func (s *BotService) PublishBotProfile(botID int64, ownerPubkey string) error { 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 func (s *BotService) loadBotRelatedData(bot *models.Bot) error { // Load post config diff --git a/internal/api/routes.go b/internal/api/routes.go index bd6cef3..d0267ac 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -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) { pubkey := c.GetString("pubkey") botIDStr := c.Param("id") @@ -883,14 +883,8 @@ func (a *API) createManualPost(c *gin.Context) { if len(matches) > 0 { mediaURL = matches[0] // Use the first URL found - // Try to determine media type - this is a placeholder - // In a real implementation, you might want to make an HTTP head request - // or infer from URL extension + // Try to determine media type 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 @@ -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) 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 { a.logger.Error("Failed to publish event", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to publish post: " + err.Error()}) return } - // Return success + // Return the encoded event response c.JSON(http.StatusOK, gin.H{ - "message": "Post published successfully", - "event_id": event.ID, - "relays": publishedRelays, + "message": "Post published successfully", + "event": encodedEvent, }) } diff --git a/internal/models/response.go b/internal/models/response.go new file mode 100644 index 0000000..e916980 --- /dev/null +++ b/internal/models/response.go @@ -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 +} \ No newline at end of file diff --git a/internal/nostr/events/encoder.go b/internal/nostr/events/encoder.go new file mode 100644 index 0000000..6ec5298 --- /dev/null +++ b/internal/nostr/events/encoder.go @@ -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 +} \ No newline at end of file diff --git a/internal/nostr/relay/manager.go b/internal/nostr/relay/manager.go index b844208..426fc08 100644 --- a/internal/nostr/relay/manager.go +++ b/internal/nostr/relay/manager.go @@ -6,7 +6,8 @@ import ( "fmt" "sync" "time" - + "git.sovbit.dev/Enki/nostr-poster/internal/models" + "git.sovbit.dev/Enki/nostr-poster/internal/nostr/events" "github.com/nbd-wtf/go-nostr" "go.uber.org/zap" ) @@ -20,6 +21,19 @@ type Manager struct { 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 func NewManager(logger *zap.Logger) *Manager { if logger == nil { @@ -186,6 +200,36 @@ func (m *Manager) PublishEvent(ctx context.Context, event *nostr.Event) ([]strin 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 func (m *Manager) SubscribeToEvents(ctx context.Context, filters []nostr.Filter) (<-chan *nostr.Event, error) { m.mu.RLock() diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index c4e6c8a..3bd2a8f 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -40,18 +40,30 @@ type ContentPoster func( hashtags []string, ) 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 type Scheduler struct { - db *db.DB - cron *cron.Cron - logger *zap.Logger - contentDir string - archiveDir string - nip94Uploader MediaUploader - blossomUploader MediaUploader - postContent ContentPoster - botJobs map[int64]cron.EntryID - mu sync.RWMutex + db *db.DB + cron *cron.Cron + logger *zap.Logger + contentDir string + archiveDir string + nip94Uploader MediaUploader + blossomUploader MediaUploader + postContent ContentPoster + postContentEncoded ContentPosterWithEncoding // New field for NIP-19 support + botJobs map[int64]cron.EntryID + mu sync.RWMutex } // NewScheduler creates a new content scheduler @@ -63,6 +75,7 @@ func NewScheduler( nip94Uploader MediaUploader, blossomUploader MediaUploader, postContent ContentPoster, + postContentEncoded ContentPosterWithEncoding, // New parameter for NIP-19 support ) *Scheduler { if logger == nil { // Create a default logger @@ -77,15 +90,16 @@ func NewScheduler( cronScheduler := cron.New(cron.WithSeconds()) return &Scheduler{ - db: db, - cron: cronScheduler, - logger: logger, - contentDir: contentDir, - archiveDir: archiveDir, - nip94Uploader: nip94Uploader, - blossomUploader: blossomUploader, - postContent: postContent, - botJobs: make(map[int64]cron.EntryID), + db: db, + cron: cronScheduler, + logger: logger, + contentDir: contentDir, + archiveDir: archiveDir, + nip94Uploader: nip94Uploader, + blossomUploader: blossomUploader, + postContent: postContent, + 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) caption := strings.TrimSuffix(filename, filepath.Ext(filename)) - // Post the content - err = s.postContent( - bot.Pubkey, - contentPath, - contentType, - mediaURL, - mediaHash, - caption, - hashtags, - ) + // Post the content - use encoded version if available, otherwise use the original + var postErr error - if err != nil { - s.logger.Error("Failed to post content", - zap.Int64("bot_id", bot.ID), - zap.String("file", contentPath), - zap.Error(err)) + if s.postContentEncoded != nil { + // Use the NIP-19 encoded version + encodedEvent, err := s.postContentEncoded( + bot.Pubkey, + 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 } @@ -291,11 +348,6 @@ func (s *Scheduler) ScheduleBot(bot *models.Bot) error { zap.Error(err)) 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 @@ -349,6 +401,7 @@ func (s *Scheduler) GetBlossomUploader() MediaUploader { func (s *Scheduler) GetContentDir() string { return s.contentDir } + // RunNow triggers an immediate post for a bot func (s *Scheduler) RunNow(botID int64) error { // Load the bot with its configurations @@ -395,4 +448,4 @@ func (s *Scheduler) RunNow(botID int64) error { entry.Job.Run() return nil -} +} \ No newline at end of file diff --git a/internal/utils/nip-19.go b/internal/utils/nip-19.go new file mode 100644 index 0000000..8ffae14 --- /dev/null +++ b/internal/utils/nip-19.go @@ -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 +} \ No newline at end of file diff --git a/web/assets/css/style.css b/web/assets/css/style.css index d3cd539..c7cffcd 100644 --- a/web/assets/css/style.css +++ b/web/assets/css/style.css @@ -1,16 +1,19 @@ /* ============================================= VARIABLES ============================================= */ - :root { +:root { /* Color Palette */ --primary-black: #121212; --secondary-black: #1e1e1e; --primary-gray: #2d2d2d; --secondary-gray: #3d3d3d; --light-gray: #aaaaaa; - --primary-purple: #9370DB; /* Medium Purple */ - --secondary-purple: #7B68EE; /* Medium Slate Blue */ - --dark-purple: #6A5ACD; /* Slate Blue */ + --primary-purple: #9370DB; + /* Medium Purple */ + --secondary-purple: #7B68EE; + /* Medium Slate Blue */ + --dark-purple: #6A5ACD; + /* Slate Blue */ --text-color: #e0e0e0; --success-color: #4CAF50; --border-color: #444; @@ -93,7 +96,7 @@ body { border-color: var(--primary-purple) !important; } -.btn-primary:hover, +.btn-primary:hover, .btn-primary:focus { background-color: var(--secondary-purple) !important; border-color: var(--secondary-purple) !important; @@ -112,14 +115,14 @@ body { /* ============================================= FORMS ============================================= */ -.form-control, +.form-control, .form-select { background-color: var(--primary-black); border: 1px solid var(--border-color); color: var(--text-color); } -.form-control:focus, +.form-control:focus, .form-select:focus { background-color: var(--primary-black); border-color: var(--primary-purple); @@ -224,7 +227,7 @@ body { margin-right: auto; } -#uploadPreviewContainer, +#uploadPreviewContainer, #mediaPreviewContainer { max-width: 100%; max-height: 250px; @@ -256,7 +259,7 @@ body { } /* Server URL input improvements */ -#primaryServerURL, +#primaryServerURL, #fallbackServerURL { margin-top: 5px; font-size: 0.9rem; @@ -275,6 +278,38 @@ body { 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 ============================================= */ @@ -282,12 +317,12 @@ body { .card { margin-bottom: 20px; } - + .bot-selection-container { padding: 15px; margin-bottom: 20px; } - + .upload-preview { max-height: 150px; } diff --git a/web/assets/js/content.js b/web/assets/js/content.js index 7a93215..18fc6bf 100644 --- a/web/assets/js/content.js +++ b/web/assets/js/content.js @@ -105,18 +105,18 @@ document.addEventListener('DOMContentLoaded', () => { postKindRadios.forEach(radio => { radio.addEventListener('change', () => { titleField.classList.toggle('d-none', radio.value !== '20'); - + // Show/hide required media elements for kind 20 const isKind20 = radio.value === '20'; const kind20MediaRequired = document.getElementById('kind20MediaRequired'); if (kind20MediaRequired) { kind20MediaRequired.style.display = isKind20 ? 'inline-block' : 'none'; } - + // For kind 20, validate that media is provided before posting if (submitPostBtn) { - submitPostBtn.title = isKind20 ? - 'Picture posts require a title and an image URL' : + submitPostBtn.title = isKind20 ? + 'Picture posts require a title and an image URL' : 'Create a standard text post'; } }); @@ -177,7 +177,35 @@ document.addEventListener('DOMContentLoaded', () => { 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 @@ -224,13 +252,13 @@ document.addEventListener('DOMContentLoaded', () => { function validatePostData() { const kind = parseInt(document.querySelector('input[name="postKind"]:checked').value); const content = manualPostContent.value.trim(); - + // Basic validation for all post types if (!content) { alert('Post content is required'); return false; } - + // Additional validation for kind 20 posts if (kind === 20) { const title = postTitle.value.trim(); @@ -238,21 +266,67 @@ document.addEventListener('DOMContentLoaded', () => { alert('Title is required for Picture Posts (kind: 20)'); return false; } - + // Check if we have a media URL either in the content or in the mediaUrlInput const mediaUrl = mediaUrlInput.value.trim(); const urlRegex = /(https?:\/\/[^\s]+)/g; const contentContainsUrl = urlRegex.test(content); - + 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.'); return false; } } - + 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 = ` +
No files found. Upload some content!
'; - return; - } - - let html = 'No files found. Upload some content!
'; + } else { + html = 'Select a bot and click "Load Content" to see files.