manual post and nip19 rework, content page looks good for now.
@ -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,7 +151,7 @@ func main() {
|
||||
logger,
|
||||
)
|
||||
|
||||
// Create post content function
|
||||
// Create standard post content function
|
||||
postContentFunc := poster.CreatePostContentFunc(
|
||||
eventManager,
|
||||
relayManager,
|
||||
@ -154,7 +159,112 @@ func main() {
|
||||
logger,
|
||||
)
|
||||
|
||||
// Initialize scheduler
|
||||
// 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 with both posting functions
|
||||
posterScheduler := scheduler.NewScheduler(
|
||||
database,
|
||||
logger,
|
||||
@ -163,6 +273,7 @@ func main() {
|
||||
nip94Uploader,
|
||||
blossomUploader,
|
||||
postContentFunc,
|
||||
postContentEncodedFunc, // New encoded function
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -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,
|
||||
"event": encodedEvent,
|
||||
})
|
||||
}
|
||||
|
||||
|
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"
|
||||
"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()
|
||||
|
@ -40,6 +40,17 @@ 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
|
||||
@ -50,6 +61,7 @@ type Scheduler struct {
|
||||
nip94Uploader MediaUploader
|
||||
blossomUploader MediaUploader
|
||||
postContent ContentPoster
|
||||
postContentEncoded ContentPosterWithEncoding // New field for NIP-19 support
|
||||
botJobs map[int64]cron.EntryID
|
||||
mu sync.RWMutex
|
||||
}
|
||||
@ -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
|
||||
@ -85,6 +98,7 @@ func NewScheduler(
|
||||
nip94Uploader: nip94Uploader,
|
||||
blossomUploader: blossomUploader,
|
||||
postContent: postContent,
|
||||
postContentEncoded: postContentEncoded, // Initialize the new field
|
||||
botJobs: make(map[int64]cron.EntryID),
|
||||
}
|
||||
}
|
||||
@ -262,8 +276,12 @@ 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(
|
||||
// Post the content - use encoded version if available, otherwise use the original
|
||||
var postErr error
|
||||
|
||||
if s.postContentEncoded != nil {
|
||||
// Use the NIP-19 encoded version
|
||||
encodedEvent, err := s.postContentEncoded(
|
||||
bot.Pubkey,
|
||||
contentPath,
|
||||
contentType,
|
||||
@ -274,10 +292,49 @@ func (s *Scheduler) ScheduleBot(bot *models.Bot) error {
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to post content",
|
||||
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
|
||||
|
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
|
||||
============================================= */
|
||||
: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;
|
||||
@ -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
|
||||
============================================= */
|
||||
|
@ -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
|
||||
@ -253,6 +281,52 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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
|
||||
// ===================================================
|
||||
@ -296,17 +370,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Render the list of content files
|
||||
function renderContentFiles(botId, files) {
|
||||
// Updated renderContentFiles function
|
||||
function renderContentFiles(botId, files) {
|
||||
const contentArea = document.getElementById('contentArea');
|
||||
if (!contentArea) return;
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
contentArea.innerHTML = '<p>No files found. Upload some content!</p>';
|
||||
return;
|
||||
}
|
||||
// Generate ONLY the file list, no upload form
|
||||
let html = '';
|
||||
|
||||
let html = '<ul class="list-group">';
|
||||
if (!files || files.length === 0) {
|
||||
html = '<p>No files found. Upload some content!</p>';
|
||||
} else {
|
||||
html = '<ul class="list-group">';
|
||||
for (const file of files) {
|
||||
html += `
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
@ -316,10 +391,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
`;
|
||||
}
|
||||
html += '</ul>';
|
||||
|
||||
contentArea.innerHTML = html;
|
||||
}
|
||||
|
||||
// Set the content area HTML to just the files list
|
||||
contentArea.innerHTML = html;
|
||||
}
|
||||
|
||||
// Upload media for the manual post
|
||||
function quickUploadMedia(botId, file) {
|
||||
const formData = new FormData();
|
||||
@ -511,3 +588,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
|
@ -131,13 +131,15 @@
|
||||
<h4>Content Files</h4>
|
||||
</div>
|
||||
<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 -->
|
||||
<p>Select a bot and click "Load Content" to see files.</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Upload section - using existing structure without an ID -->
|
||||
<h5>Upload New Content</h5>
|
||||
<div class="mb-3">
|
||||
<input type="file" id="uploadFileInput" class="form-control" accept="image/*,video/*">
|
||||
@ -150,8 +152,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Post Section (Right) -->
|
||||
<div class="card mb-4">
|
||||
<!-- Manual Post Section (Right) - FIXED: added col-md-6 wrapper -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4>Create Manual Post</h4>
|
||||
</div>
|
||||
@ -195,12 +198,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Media Section - Enhanced for Kind 20 -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Media Attachment</h5>
|
||||
<div class="mb-3">
|
||||
<h5 class="mb-2">Media Attachment</h5>
|
||||
<span class="badge bg-info" id="kind20MediaRequired" style="display: none;">Required for kind: 20</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<!-- Upload Section -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Upload Image</label>
|
||||
@ -216,8 +217,7 @@
|
||||
<label for="mediaUrlInput" class="form-label">Media URL</label>
|
||||
<input type="text" class="form-control" id="mediaUrlInput"
|
||||
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
|
||||
the content)</small>
|
||||
<small class="form-text text-muted">For kind: 20 posts, an image URL is required</small>
|
||||
</div>
|
||||
|
||||
<!-- Alt Text Field (for accessibility) -->
|
||||
@ -225,22 +225,17 @@
|
||||
<label for="mediaAltText" class="form-label">Alt Text</label>
|
||||
<input type="text" class="form-control" id="mediaAltText"
|
||||
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>
|
||||
|
||||
<!-- Media Preview -->
|
||||
<div id="mediaPreviewContainer" class="d-none mt-2">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<img id="mediaPreview" class="upload-preview img-fluid mb-2">
|
||||
<div id="mediaLinkContainer" class="media-link">
|
||||
<small class="text-muted">Media will appear here after upload</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-success mt-2" id="submitPostBtn">Post Now</button>
|
||||
</div>
|
||||
@ -248,6 +243,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="eventInfoContainer" class="mt-3"></div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|