585 lines
15 KiB
Go

// internal/api/routes.go
package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"git.sovbit.dev/Enki/nostr-poster/internal/auth"
"git.sovbit.dev/Enki/nostr-poster/internal/models"
"git.sovbit.dev/Enki/nostr-poster/internal/scheduler"
"go.uber.org/zap"
)
// API represents the HTTP API for managing bots
type API struct {
router *gin.Engine
logger *zap.Logger
botService *BotService
authService *auth.Service
scheduler *scheduler.Scheduler
}
// NewAPI creates a new API instance
func NewAPI(
logger *zap.Logger,
botService *BotService,
authService *auth.Service,
scheduler *scheduler.Scheduler,
) *API {
router := gin.Default()
api := &API{
router: router,
logger: logger,
botService: botService,
authService: authService,
scheduler: scheduler,
}
// Set up routes
api.setupRoutes()
return api
}
// SetupRoutes configures the API routes
func (a *API) setupRoutes() {
// Public routes
a.router.GET("/health", a.healthCheck)
// API routes
apiGroup := a.router.Group("/api")
// Authentication
apiGroup.POST("/auth/login", a.login)
apiGroup.GET("/auth/verify", a.requireAuth, a.verifyAuth)
// Bot management
botGroup := apiGroup.Group("/bots")
botGroup.Use(a.requireAuth)
{
botGroup.GET("", a.listBots)
botGroup.POST("", a.createBot)
botGroup.GET("/:id", a.getBot)
botGroup.PUT("/:id", a.updateBot)
botGroup.DELETE("/:id", a.deleteBot)
// Bot configuration
botGroup.GET("/:id/config", a.getBotConfig)
botGroup.PUT("/:id/config", a.updateBotConfig)
// Relay management
botGroup.GET("/:id/relays", a.getBotRelays)
botGroup.PUT("/:id/relays", a.updateBotRelays)
// Actions
botGroup.POST("/:id/profile/publish", a.publishBotProfile)
botGroup.POST("/:id/run", a.runBotNow)
botGroup.POST("/:id/enable", a.enableBot)
botGroup.POST("/:id/disable", a.disableBot)
}
// Content management
contentGroup := apiGroup.Group("/content")
contentGroup.Use(a.requireAuth)
{
contentGroup.GET("/:botId", a.listBotContent)
contentGroup.POST("/:botId/upload", a.uploadContent)
contentGroup.DELETE("/:botId/:filename", a.deleteContent)
}
// Stats
statsGroup := apiGroup.Group("/stats")
statsGroup.Use(a.requireAuth)
{
statsGroup.GET("", a.getStats)
statsGroup.GET("/:botId", a.getBotStats)
}
// Serve the web UI
a.router.StaticFile("/", "./web/index.html")
a.router.Static("/assets", "./web/assets")
// Handle 404s for SPA
a.router.NoRoute(func(c *gin.Context) {
c.File("./web/index.html")
})
}
// Run starts the API server
func (a *API) Run(addr string) error {
return a.router.Run(addr)
}
// Middleware for requiring authentication
func (a *API) requireAuth(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Authentication required",
})
return
}
// Verify the auth token
pubkey, err := a.authService.VerifyToken(token)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Invalid authentication token",
})
return
}
// Store the pubkey in the context
c.Set("pubkey", pubkey)
c.Next()
}
// API Handlers
// healthCheck responds with a simple health check
func (a *API) healthCheck(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
}
// login handles user login with NIP-07 signature
func (a *API) login(c *gin.Context) {
var req struct {
Pubkey string `json:"pubkey" binding:"required"`
Signature string `json:"signature" binding:"required"`
Event string `json:"event" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// Verify the signature
token, err := a.authService.Login(req.Pubkey, req.Signature, req.Event)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
})
}
// verifyAuth verifies the current auth token
func (a *API) verifyAuth(c *gin.Context) {
pubkey, exists := c.Get("pubkey")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
c.JSON(http.StatusOK, gin.H{
"pubkey": pubkey,
})
}
// listBots lists all bots owned by the user
func (a *API) listBots(c *gin.Context) {
pubkey := c.GetString("pubkey")
bots, err := a.botService.ListUserBots(pubkey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list bots"})
a.logger.Error("Failed to list bots", zap.Error(err))
return
}
c.JSON(http.StatusOK, bots)
}
// createBot creates a new bot
func (a *API) createBot(c *gin.Context) {
pubkey := c.GetString("pubkey")
var bot models.Bot
if err := c.ShouldBindJSON(&bot); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot data"})
return
}
// Set the owner
bot.OwnerPubkey = pubkey
// Create the bot
createdBot, err := a.botService.CreateBot(&bot)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bot"})
a.logger.Error("Failed to create bot", zap.Error(err))
return
}
c.JSON(http.StatusCreated, createdBot)
}
// getBot gets a specific bot by ID
func (a *API) getBot(c *gin.Context) {
pubkey := c.GetString("pubkey")
botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
return
}
// Get the bot
bot, err := a.botService.GetBotByID(botID, pubkey)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
return
}
c.JSON(http.StatusOK, bot)
}
// updateBot updates a bot
func (a *API) updateBot(c *gin.Context) {
pubkey := c.GetString("pubkey")
botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
return
}
var bot models.Bot
if err := c.ShouldBindJSON(&bot); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot data"})
return
}
// Set the ID and owner
bot.ID = botID
bot.OwnerPubkey = pubkey
// Update the bot
updatedBot, err := a.botService.UpdateBot(&bot)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update bot"})
a.logger.Error("Failed to update bot", zap.Error(err))
return
}
c.JSON(http.StatusOK, updatedBot)
}
// deleteBot deletes a bot
func (a *API) deleteBot(c *gin.Context) {
pubkey := c.GetString("pubkey")
botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
return
}
// Delete the bot
err = a.botService.DeleteBot(botID, pubkey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bot"})
a.logger.Error("Failed to delete bot", zap.Error(err))
return
}
c.Status(http.StatusNoContent)
}
// getBotConfig gets a bot's configuration
func (a *API) getBotConfig(c *gin.Context) {
pubkey := c.GetString("pubkey")
botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
return
}
// Get the bot with config
bot, err := a.botService.GetBotByID(botID, pubkey)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"post_config": bot.PostConfig,
"media_config": bot.MediaConfig,
})
}
// updateBotConfig updates a bot's configuration
func (a *API) updateBotConfig(c *gin.Context) {
pubkey := c.GetString("pubkey")
botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
return
}
var config struct {
PostConfig *models.PostConfig `json:"post_config"`
MediaConfig *models.MediaConfig `json:"media_config"`
}
if err := c.ShouldBindJSON(&config); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid config data"})
return
}
// Set the bot ID
if config.PostConfig != nil {
config.PostConfig.BotID = botID
}
if config.MediaConfig != nil {
config.MediaConfig.BotID = botID
}
// Update the configs
err = a.botService.UpdateBotConfig(botID, pubkey, config.PostConfig, config.MediaConfig)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update config"})
a.logger.Error("Failed to update bot config", zap.Error(err))
return
}
// Get the updated bot
bot, err := a.botService.GetBotByID(botID, pubkey)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
return
}
// Update the scheduler if needed
if config.PostConfig != nil && config.PostConfig.Enabled {
err = a.scheduler.UpdateBotSchedule(bot)
if err != nil {
a.logger.Error("Failed to update bot schedule",
zap.Int64("botID", botID),
zap.Error(err))
}
} else if config.PostConfig != nil && !config.PostConfig.Enabled {
a.scheduler.UnscheduleBot(botID)
}
c.JSON(http.StatusOK, gin.H{
"post_config": bot.PostConfig,
"media_config": bot.MediaConfig,
})
}
// getBotRelays gets a bot's relay configuration
func (a *API) getBotRelays(c *gin.Context) {
pubkey := c.GetString("pubkey")
botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
return
}
// Get the relays
relays, err := a.botService.GetBotRelays(botID, pubkey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get relays"})
a.logger.Error("Failed to get bot relays", zap.Error(err))
return
}
c.JSON(http.StatusOK, relays)
}
// updateBotRelays updates a bot's relay configuration
func (a *API) updateBotRelays(c *gin.Context) {
pubkey := c.GetString("pubkey")
botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
return
}
var relays []*models.Relay
if err := c.ShouldBindJSON(&relays); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid relay data"})
return
}
// Set the bot ID for each relay
for _, relay := range relays {
relay.BotID = botID
}
// Update the relays
err = a.botService.UpdateBotRelays(botID, pubkey, relays)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update relays"})
a.logger.Error("Failed to update bot relays", zap.Error(err))
return
}
// Get the updated relays
updatedRelays, err := a.botService.GetBotRelays(botID, pubkey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get updated relays"})
a.logger.Error("Failed to get updated bot relays", zap.Error(err))
return
}
c.JSON(http.StatusOK, updatedRelays)
}
// publishBotProfile publishes a bot's profile to Nostr
func (a *API) publishBotProfile(c *gin.Context) {
pubkey := c.GetString("pubkey")
botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
return
}
// Publish the profile
err = a.botService.PublishBotProfile(botID, pubkey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to publish profile"})
a.logger.Error("Failed to publish bot profile", zap.Error(err))
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Profile published successfully",
})
}
// runBotNow runs a bot immediately
func (a *API) runBotNow(c *gin.Context) {
pubkey := c.GetString("pubkey")
botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
return
}
// Check if the bot belongs to the user
_, err = a.botService.GetBotByID(botID, pubkey)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
return
}
// Run the bot
err = a.scheduler.RunNow(botID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to run bot"})
a.logger.Error("Failed to run bot now", zap.Error(err))
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Bot is running",
})
}
// enableBot enables a bot
func (a *API) enableBot(c *gin.Context) {
pubkey := c.GetString("pubkey")
botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
return
}
// Get the bot
bot, err := a.botService.GetBotByID(botID, pubkey)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
return
}
// Enable the bot
bot.PostConfig.Enabled = true
err = a.botService.UpdateBotConfig(botID, pubkey, bot.PostConfig, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to enable bot"})
a.logger.Error("Failed to enable bot", zap.Error(err))
return
}
// Schedule the bot
err = a.scheduler.ScheduleBot(bot)
if err != nil {
a.logger.Error("Failed to schedule bot",
zap.Int64("botID", botID),
zap.Error(err))
}
c.JSON(http.StatusOK, gin.H{
"message": "Bot enabled",
})
}
// disableBot disables a bot
func (a *API) disableBot(c *gin.Context) {
pubkey := c.GetString("pubkey")
botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
return
}
// Get the bot
bot, err := a.botService.GetBotByID(botID, pubkey)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
return
}
// Disable the bot
bot.PostConfig.Enabled = false
err = a.botService.UpdateBotConfig(botID, pubkey, bot.PostConfig, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to disable bot"})
a.logger.Error("Failed to disable bot", zap.Error(err))
return
}
// Unschedule the bot
a.scheduler.UnscheduleBot(botID)
c.JSON(http.StatusOK, gin.H{
"message": "Bot disabled",
})
}
// listBotContent lists the content files for a bot
func (a *API) listBotContent(c *gin.Context) {
// This will be implemented when we add content management
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
// uploadContent uploads content for a bot
func (a *API) uploadContent(c *gin.Context) {
// This will be implemented when we add content management
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
// deleteContent deletes content for a bot
func (a *API) deleteContent(c *gin.Context) {
// This will be implemented when we add content management
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
// getStats gets overall statistics
func (a *API) getStats(c *gin.Context) {
// This will be implemented when we add statistics
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}
// getBotStats gets statistics for a specific bot
func (a *API) getBotStats(c *gin.Context) {
// This will be implemented when we add statistics
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}