Compare commits
No commits in common. "8d78eca070d15d82f379b4e8ef988484085beb6f" and "41ad467618b76f7f5504d392173053acd47ace12" have entirely different histories.
8d78eca070
...
41ad467618
22
.gitignore
vendored
@ -1,22 +0,0 @@
|
|||||||
# Ignore build output
|
|
||||||
/build/
|
|
||||||
|
|
||||||
# Ignore content and archive folders
|
|
||||||
/content/
|
|
||||||
/archive/
|
|
||||||
|
|
||||||
# Ignore config files except the sample
|
|
||||||
/config/*
|
|
||||||
!/config/config.sample.json
|
|
||||||
|
|
||||||
# Ignore keys.json
|
|
||||||
/keys.json
|
|
||||||
|
|
||||||
|
|
||||||
# Ignore go build files
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Ignore Go module cache
|
|
||||||
/pkg/
|
|
81
README.md
@ -3,62 +3,39 @@
|
|||||||
An automated content posting bot for Nostr networks. This tool allows you to schedule regular posting of images and videos to Nostr relays, supporting both NIP-94 and Blossom upload services.
|
An automated content posting bot for Nostr networks. This tool allows you to schedule regular posting of images and videos to Nostr relays, supporting both NIP-94 and Blossom upload services.
|
||||||
|
|
||||||
|
|
||||||
# Nostr Poster
|
### Core Implementation
|
||||||
|
- [x] replace hex format
|
||||||
|
|
||||||
## Table of Contents
|
### Essential Settings
|
||||||
- [Plan for Nostr Poster Improvements](#plan-for-nostr-poster-improvements)
|
- [x] Make enable button functional
|
||||||
- [To-Do List](#to-do-list)
|
- [ ] Proper nostr Profile:
|
||||||
|
- [x] Bio
|
||||||
|
- [x] NIP-05
|
||||||
|
- [ ] Username/display name
|
||||||
|
- [ ] Zap address
|
||||||
|
- [ ] PFP/banner upload
|
||||||
|
- [ ] Posting interval controls
|
||||||
|
- [ ] Content album selection
|
||||||
|
|
||||||
### Priority Order
|
### Content System
|
||||||
|
- [x] Create upload/organization page
|
||||||
|
- [x] Implement manual post interface:
|
||||||
|
- [x] Text
|
||||||
|
- [x] Media upload
|
||||||
|
- [x] Kind:20 posts
|
||||||
|
|
||||||
1. [ ] Bot Profile Management: Complete proper profile creation, editing, and publishing
|
### Bot Interaction
|
||||||
2. [ ] Relay Management: Improve relay selection and management interface
|
- [ ] Develop basic bot feed:
|
||||||
3. [ ] Test Auto-Posting: Ensure the scheduling system works correctly
|
- [ ] Display Comments
|
||||||
|
- [ ] Reply
|
||||||
|
|
||||||
## To-Do List
|
### Validation
|
||||||
|
- [x] Test NSEC key import
|
||||||
### Bot Profile Management Enhancements
|
- [ ] Test manual posts:
|
||||||
|
-[x] Text only
|
||||||
- [ ] Add upload fields to both the "Create Bot" modal and "Bot Settings" modal
|
- [x] blossom Media upload
|
||||||
- [ ] Create a consistent interface for profile images across both modals
|
- [ ] NIP-94 uploads
|
||||||
- [ ] Implement preview functionality for selected images
|
- [ ] Verify bot reply works
|
||||||
- [ ] Connect these uploads to the NIP-96/Blossom media servers
|
|
||||||
- [ ] Ensure proper kind 0 event creation with all required metadata fields
|
|
||||||
- [ ] Display confirmation after successful publishing with NIP-19 encoded identifiers
|
|
||||||
- [ ] Add a visual indicator to show when a bot's profile needs publishing (outdated)
|
|
||||||
- [ ] Create new file input fields in both modals with preview capability
|
|
||||||
- [ ] Create a shared function for uploading profile images to media servers
|
|
||||||
- [ ] Update the `CreateAndSignMetadataEvent` function to include all necessary fields for a proper NIP-01 metadata event
|
|
||||||
- [ ] Add an explicit "Publish Profile" button to push the kind 0 event to relays
|
|
||||||
|
|
||||||
### Relay Management Improvements
|
|
||||||
|
|
||||||
- [ ] Create a more intuitive relay management interface
|
|
||||||
- [ ] Add ability to categorize relays (read, write, both)
|
|
||||||
- [ ] Include relay testing functionality
|
|
||||||
- [ ] Add a "recommended relays" quick-add option
|
|
||||||
- [ ] Create a dedicated relay management component in the bot settings modal
|
|
||||||
- [ ] Add quick-add buttons for popular relays
|
|
||||||
- [ ] Include relay status indicators
|
|
||||||
- [ ] Implement relay tests (ping/connection checks)
|
|
||||||
|
|
||||||
### Post Scheduling and Auto-Posting Testing
|
|
||||||
|
|
||||||
- [ ] Add detailed logging for scheduled posts
|
|
||||||
- [ ] Create a manual trigger for testing scheduled posts
|
|
||||||
- [ ] Implement a post history view
|
|
||||||
- [ ] Add a "test now" button that triggers an immediate post attempt
|
|
||||||
- [ ] Create a dedicated tab or view for post history
|
|
||||||
- [ ] Implement proper error logging and status tracking
|
|
||||||
|
|
||||||
### Code Changes Required
|
|
||||||
|
|
||||||
- [ ] Modify `index.html` - Add profile image/banner upload fields
|
|
||||||
- [ ] Modify `main.js` - Add functions for handling profile image uploads
|
|
||||||
- [ ] Create a new component for relay management
|
|
||||||
- [ ] Modify `events.go` - Ensure metadata events are properly formatted
|
|
||||||
- [ ] Modify `bot_service.go` - Enhance profile publishing logic
|
|
||||||
- [ ] Modify `routes.go` - Add endpoints for profile image management
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
@ -2,15 +2,11 @@
|
|||||||
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"
|
||||||
@ -19,7 +15,6 @@ 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"
|
||||||
@ -151,7 +146,7 @@ func main() {
|
|||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create standard post content function
|
// Create post content function
|
||||||
postContentFunc := poster.CreatePostContentFunc(
|
postContentFunc := poster.CreatePostContentFunc(
|
||||||
eventManager,
|
eventManager,
|
||||||
relayManager,
|
relayManager,
|
||||||
@ -159,112 +154,7 @@ func main() {
|
|||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create the NIP-19 encoded post content function
|
// Initialize scheduler
|
||||||
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(
|
posterScheduler := scheduler.NewScheduler(
|
||||||
database,
|
database,
|
||||||
logger,
|
logger,
|
||||||
@ -273,7 +163,6 @@ func main() {
|
|||||||
nip94Uploader,
|
nip94Uploader,
|
||||||
blossomUploader,
|
blossomUploader,
|
||||||
postContentFunc,
|
postContentFunc,
|
||||||
postContentEncodedFunc, // New encoded function
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize API
|
// Initialize API
|
||||||
|
Before Width: | Height: | Size: 971 KiB |
Before Width: | Height: | Size: 827 KiB |
Before Width: | Height: | Size: 2.0 MiB |
Before Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 1.0 MiB |
Before Width: | Height: | Size: 467 KiB |
@ -539,38 +539,6 @@ 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
|
// Updated createManualPost function in routes.go to properly process kind 20 posts
|
||||||
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,8 +883,14 @@ 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
|
// 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
|
||||||
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
|
||||||
@ -935,22 +941,22 @@ func (a *API) createManualPost(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish to relays with NIP-19 encoding
|
// Publish to relays
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Use the new method that includes NIP-19 encoding
|
publishedRelays, err := a.botService.relayMgr.PublishEvent(ctx, event)
|
||||||
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 the encoded event response
|
// Return success
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "Post published successfully",
|
"message": "Post published successfully",
|
||||||
"event": encodedEvent,
|
"event_id": event.ID,
|
||||||
|
"relays": publishedRelays,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
// 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,8 +6,7 @@ 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"
|
||||||
)
|
)
|
||||||
@ -21,19 +20,6 @@ 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 {
|
||||||
@ -200,36 +186,6 @@ 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,17 +40,6 @@ 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
|
||||||
@ -61,7 +50,6 @@ type Scheduler struct {
|
|||||||
nip94Uploader MediaUploader
|
nip94Uploader MediaUploader
|
||||||
blossomUploader MediaUploader
|
blossomUploader MediaUploader
|
||||||
postContent ContentPoster
|
postContent ContentPoster
|
||||||
postContentEncoded ContentPosterWithEncoding // New field for NIP-19 support
|
|
||||||
botJobs map[int64]cron.EntryID
|
botJobs map[int64]cron.EntryID
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
@ -75,7 +63,6 @@ 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
|
||||||
@ -98,7 +85,6 @@ func NewScheduler(
|
|||||||
nip94Uploader: nip94Uploader,
|
nip94Uploader: nip94Uploader,
|
||||||
blossomUploader: blossomUploader,
|
blossomUploader: blossomUploader,
|
||||||
postContent: postContent,
|
postContent: postContent,
|
||||||
postContentEncoded: postContentEncoded, // Initialize the new field
|
|
||||||
botJobs: make(map[int64]cron.EntryID),
|
botJobs: make(map[int64]cron.EntryID),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -276,12 +262,8 @@ 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 - use encoded version if available, otherwise use the original
|
// Post the content
|
||||||
var postErr error
|
err = s.postContent(
|
||||||
|
|
||||||
if s.postContentEncoded != nil {
|
|
||||||
// Use the NIP-19 encoded version
|
|
||||||
encodedEvent, err := s.postContentEncoded(
|
|
||||||
bot.Pubkey,
|
bot.Pubkey,
|
||||||
contentPath,
|
contentPath,
|
||||||
contentType,
|
contentType,
|
||||||
@ -292,49 +274,10 @@ func (s *Scheduler) ScheduleBot(bot *models.Bot) error {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
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",
|
s.logger.Error("Failed to post content",
|
||||||
zap.Int64("bot_id", bot.ID),
|
zap.Int64("bot_id", bot.ID),
|
||||||
zap.String("file", contentPath),
|
zap.String("file", contentPath),
|
||||||
zap.Error(postErr))
|
zap.Error(err))
|
||||||
} 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,6 +291,11 @@ 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
|
||||||
@ -401,7 +349,6 @@ 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
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
// 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,19 +1,16 @@
|
|||||||
/* =============================================
|
/* =============================================
|
||||||
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;
|
--primary-purple: #9370DB; /* Medium Purple */
|
||||||
/* Medium Purple */
|
--secondary-purple: #7B68EE; /* Medium Slate Blue */
|
||||||
--secondary-purple: #7B68EE;
|
--dark-purple: #6A5ACD; /* Slate Blue */
|
||||||
/* 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;
|
||||||
@ -278,38 +275,6 @@ 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
|
||||||
============================================= */
|
============================================= */
|
||||||
|
@ -177,35 +177,7 @@ 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
|
||||||
@ -281,52 +253,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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
|
||||||
// ===================================================
|
// ===================================================
|
||||||
@ -370,18 +296,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated renderContentFiles function
|
// Render the list of content files
|
||||||
function renderContentFiles(botId, files) {
|
function renderContentFiles(botId, files) {
|
||||||
const contentArea = document.getElementById('contentArea');
|
const contentArea = document.getElementById('contentArea');
|
||||||
if (!contentArea) return;
|
if (!contentArea) return;
|
||||||
|
|
||||||
// Generate ONLY the file list, no upload form
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
if (!files || files.length === 0) {
|
if (!files || files.length === 0) {
|
||||||
html = '<p>No files found. Upload some content!</p>';
|
contentArea.innerHTML = '<p>No files found. Upload some content!</p>';
|
||||||
} else {
|
return;
|
||||||
html = '<ul class="list-group">';
|
}
|
||||||
|
|
||||||
|
let 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">
|
||||||
@ -391,11 +316,9 @@ function renderContentFiles(botId, files) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
html += '</ul>';
|
html += '</ul>';
|
||||||
}
|
|
||||||
|
|
||||||
// Set the content area HTML to just the files list
|
|
||||||
contentArea.innerHTML = html;
|
contentArea.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload media for the manual post
|
// Upload media for the manual post
|
||||||
function quickUploadMedia(botId, file) {
|
function quickUploadMedia(botId, file) {
|
||||||
@ -588,22 +511,3 @@ function renderContentFiles(botId, files) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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,93 +476,6 @@ 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
|
||||||
|
@ -131,15 +131,13 @@
|
|||||||
<h4>Content Files</h4>
|
<h4>Content Files</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- File list area - with scrollable container -->
|
<div id="contentArea">
|
||||||
<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/*">
|
||||||
@ -152,9 +150,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Manual Post Section (Right) - FIXED: added col-md-6 wrapper -->
|
<!-- Manual Post Section (Right) -->
|
||||||
<div class="col-md-6 mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4>Create Manual Post</h4>
|
<h4>Create Manual Post</h4>
|
||||||
</div>
|
</div>
|
||||||
@ -198,10 +195,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Media Section - Enhanced for Kind 20 -->
|
<!-- Media Section - Enhanced for Kind 20 -->
|
||||||
<div class="mb-3">
|
<div class="card mb-3">
|
||||||
<h5 class="mb-2">Media Attachment</h5>
|
<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>
|
||||||
@ -217,7 +216,8 @@
|
|||||||
<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</small>
|
<small class="form-text text-muted">For kind: 20 posts, an image URL is required (either here or in
|
||||||
|
the content)</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alt Text Field (for accessibility) -->
|
<!-- Alt Text Field (for accessibility) -->
|
||||||
@ -225,17 +225,22 @@
|
|||||||
<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</small>
|
<small class="form-text text-muted">Recommended for image description (improves accessibility)</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">
|
||||||
|
<div class="card-body text-center">
|
||||||
<img id="mediaPreview" class="upload-preview img-fluid mb-2">
|
<img id="mediaPreview" class="upload-preview img-fluid mb-2">
|
||||||
<div id="mediaLinkContainer" class="media-link">
|
<div id="mediaLinkContainer" class="media-link">
|
||||||
<small class="text-muted">Media will appear here after upload</small>
|
<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>
|
||||||
@ -243,8 +248,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="eventInfoContainer" class="mt-3"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|