72ca99b58d
- Added @Summary/@Router/@Param/@Success/@Security annotations to all API handlers - Regenerated docs/swagger.json, docs/swagger.yaml, docs/docs.go with full endpoint definitions - Fixed generated docs.go to remove unsupported LeftDelim/RightDelim fields
325 lines
9.4 KiB
Go
325 lines
9.4 KiB
Go
package bot
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// CommandHandler handles slash command CRUD routes.
|
|
type CommandHandler struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewCommandHandler creates a new CommandHandler.
|
|
func NewCommandHandler(db *sql.DB) *CommandHandler {
|
|
return &CommandHandler{db: db}
|
|
}
|
|
|
|
// RegisterCommandRoutes registers slash-command routes under the given router.
|
|
// Authenticated routes (bot-scoped):
|
|
//
|
|
// POST /{botID}/commands - create slash command
|
|
// GET /{botID}/commands - list bot's commands
|
|
// DELETE /{botID}/commands/{cmdID} - delete command
|
|
//
|
|
// Public route (for autocomplete):
|
|
//
|
|
// GET /servers/{serverID}/commands - list all commands for a server
|
|
func (h *CommandHandler) RegisterCommandRoutes(r chi.Router) {
|
|
r.Post("/{botID}/commands", h.Create)
|
|
r.Get("/{botID}/commands", h.ListByBot)
|
|
r.Delete("/{botID}/commands/{cmdID}", h.Delete)
|
|
r.Get("/servers/{serverID}/commands", h.ListByServer)
|
|
}
|
|
|
|
// ---- types ----
|
|
|
|
type slashCommandResponse struct {
|
|
ID string `json:"id"`
|
|
BotID string `json:"bot_id"`
|
|
ServerID string `json:"server_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Options interface{} `json:"options"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
type createCommandRequest struct {
|
|
ServerID string `json:"server_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Options interface{} `json:"options"`
|
|
}
|
|
|
|
// ---- handlers ----
|
|
|
|
// @Summary Create a slash command
|
|
// @Description Create a new slash command for a bot
|
|
// @Tags bot commands
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security SessionAuth
|
|
// @Param botID path string true "Bot ID"
|
|
// @Param body body createCommandRequest true "Command data"
|
|
// @Success 201 {object} slashCommandResponse
|
|
// @Failure 400 {object} map[string]string
|
|
// @Failure 401 {object} map[string]string
|
|
// @Failure 403 {object} map[string]string
|
|
// @Failure 409 {object} map[string]string
|
|
// @Router /bots/{botID}/commands [post]
|
|
func (h *CommandHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := middleware.UserIDFromContext(r.Context())
|
|
if !ok {
|
|
writeErr(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
|
|
botID := chi.URLParam(r, "botID")
|
|
|
|
// Verify bot ownership
|
|
if err := h.verifyBotOwnership(r, userID, botID); err != nil {
|
|
if err == errNotFound {
|
|
writeErr(w, http.StatusNotFound, "bot not found")
|
|
} else if err == errForbidden {
|
|
writeErr(w, http.StatusForbidden, "forbidden")
|
|
} else {
|
|
writeErr(w, http.StatusInternalServerError, "server error")
|
|
}
|
|
return
|
|
}
|
|
|
|
var req createCommandRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeErr(w, http.StatusBadRequest, "invalid request")
|
|
return
|
|
}
|
|
if req.ServerID == "" || req.Name == "" {
|
|
writeErr(w, http.StatusBadRequest, "server_id and name are required")
|
|
return
|
|
}
|
|
|
|
// Verify bot is added to the target server
|
|
added, err := h.isBotOnServer(r, botID, req.ServerID)
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "server error")
|
|
return
|
|
}
|
|
if !added {
|
|
writeErr(w, http.StatusForbidden, "bot is not added to this server")
|
|
return
|
|
}
|
|
|
|
// Marshal options to JSON for storage
|
|
optionsJSON := "[]"
|
|
if req.Options != nil {
|
|
b, err := json.Marshal(req.Options)
|
|
if err != nil {
|
|
writeErr(w, http.StatusBadRequest, "invalid options")
|
|
return
|
|
}
|
|
optionsJSON = string(b)
|
|
}
|
|
|
|
var cmd slashCommandResponse
|
|
var createdAt sql.NullString
|
|
err = h.db.QueryRowContext(r.Context(), `
|
|
INSERT INTO slash_commands (bot_id, server_id, name, description, options)
|
|
VALUES ($1, $2, $3, $4, $5::jsonb)
|
|
RETURNING id, bot_id, server_id, name, description, options, created_at::text
|
|
`, botID, req.ServerID, req.Name, req.Description, optionsJSON).Scan(
|
|
&cmd.ID, &cmd.BotID, &cmd.ServerID, &cmd.Name, &cmd.Description,
|
|
&cmd.Options, &createdAt,
|
|
)
|
|
if err != nil {
|
|
// Likely a UNIQUE(server_id, name) violation
|
|
writeErr(w, http.StatusConflict, "command name already exists in this server")
|
|
return
|
|
}
|
|
cmd.CreatedAt = createdAt.String
|
|
|
|
writeJSON(w, http.StatusCreated, cmd)
|
|
}
|
|
|
|
// @Summary List bot commands
|
|
// @Description List all slash commands for a bot
|
|
// @Tags bot commands
|
|
// @Produce json
|
|
// @Security SessionAuth
|
|
// @Param botID path string true "Bot ID"
|
|
// @Success 200 {array} slashCommandResponse
|
|
// @Failure 401 {object} map[string]string
|
|
// @Failure 403 {object} map[string]string
|
|
// @Failure 404 {object} map[string]string
|
|
// @Router /bots/{botID}/commands [get]
|
|
func (h *CommandHandler) ListByBot(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := middleware.UserIDFromContext(r.Context())
|
|
if !ok {
|
|
writeErr(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
|
|
botID := chi.URLParam(r, "botID")
|
|
|
|
if err := h.verifyBotOwnership(r, userID, botID); err != nil {
|
|
if err == errNotFound {
|
|
writeErr(w, http.StatusNotFound, "bot not found")
|
|
} else if err == errForbidden {
|
|
writeErr(w, http.StatusForbidden, "forbidden")
|
|
} else {
|
|
writeErr(w, http.StatusInternalServerError, "server error")
|
|
}
|
|
return
|
|
}
|
|
|
|
rows, err := h.db.QueryContext(r.Context(), `
|
|
SELECT id, bot_id, server_id, name, description, options, created_at::text
|
|
FROM slash_commands WHERE bot_id = $1 ORDER BY name
|
|
`, botID)
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "server error")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
cmds := make([]slashCommandResponse, 0)
|
|
for rows.Next() {
|
|
var cmd slashCommandResponse
|
|
var createdAt sql.NullString
|
|
if err := rows.Scan(&cmd.ID, &cmd.BotID, &cmd.ServerID, &cmd.Name, &cmd.Description, &cmd.Options, &createdAt); err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "server error")
|
|
return
|
|
}
|
|
cmd.CreatedAt = createdAt.String
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "server error")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, cmds)
|
|
}
|
|
|
|
// @Summary Delete a slash command
|
|
// @Description Delete a slash command
|
|
// @Tags bot commands
|
|
// @Security SessionAuth
|
|
// @Param botID path string true "Bot ID"
|
|
// @Param cmdID path string true "Command ID"
|
|
// @Success 204
|
|
// @Failure 401 {object} map[string]string
|
|
// @Failure 403 {object} map[string]string
|
|
// @Failure 404 {object} map[string]string
|
|
// @Router /bots/{botID}/commands/{cmdID} [delete]
|
|
func (h *CommandHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := middleware.UserIDFromContext(r.Context())
|
|
if !ok {
|
|
writeErr(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
|
|
botID := chi.URLParam(r, "botID")
|
|
cmdID := chi.URLParam(r, "cmdID")
|
|
|
|
if err := h.verifyBotOwnership(r, userID, botID); err != nil {
|
|
if err == errNotFound {
|
|
writeErr(w, http.StatusNotFound, "bot not found")
|
|
} else if err == errForbidden {
|
|
writeErr(w, http.StatusForbidden, "forbidden")
|
|
} else {
|
|
writeErr(w, http.StatusInternalServerError, "server error")
|
|
}
|
|
return
|
|
}
|
|
|
|
result, err := h.db.ExecContext(r.Context(), `
|
|
DELETE FROM slash_commands WHERE id = $1 AND bot_id = $2
|
|
`, cmdID, botID)
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "failed to delete command")
|
|
return
|
|
}
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
writeErr(w, http.StatusNotFound, "command not found")
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// @Summary List server commands
|
|
// @Description List all slash commands registered in a server (public)
|
|
// @Tags bot commands
|
|
// @Produce json
|
|
// @Param serverID path string true "Server ID"
|
|
// @Success 200 {array} slashCommandResponse
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /bots/servers/{serverID}/commands [get]
|
|
func (h *CommandHandler) ListByServer(w http.ResponseWriter, r *http.Request) {
|
|
serverID := chi.URLParam(r, "serverID")
|
|
|
|
rows, err := h.db.QueryContext(r.Context(), `
|
|
SELECT id, bot_id, server_id, name, description, options, created_at::text
|
|
FROM slash_commands WHERE server_id = $1 ORDER BY name
|
|
`, serverID)
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "server error")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
cmds := make([]slashCommandResponse, 0)
|
|
for rows.Next() {
|
|
var cmd slashCommandResponse
|
|
var createdAt sql.NullString
|
|
if err := rows.Scan(&cmd.ID, &cmd.BotID, &cmd.ServerID, &cmd.Name, &cmd.Description, &cmd.Options, &createdAt); err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "server error")
|
|
return
|
|
}
|
|
cmd.CreatedAt = createdAt.String
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "server error")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, cmds)
|
|
}
|
|
|
|
// ---- internal helpers ----
|
|
|
|
var (
|
|
errNotFound = errors.New("not found")
|
|
errForbidden = errors.New("forbidden")
|
|
)
|
|
|
|
func (h *CommandHandler) verifyBotOwnership(r *http.Request, userID, botID string) error {
|
|
var ownerID string
|
|
err := h.db.QueryRowContext(r.Context(), `SELECT owner_id FROM bots WHERE id = $1`, botID).Scan(&ownerID)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return errNotFound
|
|
}
|
|
return err
|
|
}
|
|
if ownerID != userID {
|
|
return errForbidden
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *CommandHandler) isBotOnServer(r *http.Request, botID, serverID string) (bool, error) {
|
|
var exists bool
|
|
err := h.db.QueryRowContext(r.Context(), `
|
|
SELECT EXISTS(SELECT 1 FROM bot_servers WHERE bot_id = $1 AND server_id = $2)
|
|
`, botID, serverID).Scan(&exists)
|
|
return exists, err
|
|
}
|