feat(bots): built-in bot runner + steamfree from UI
- BotRunner: server-side goroutine manager for built-in bot types - steamfree bot embedded in server (polls Steam API, posts free games) - bot_type + config JSONB columns on bots table - Create/Update/Delete handlers manage runner lifecycle - GET /bots/types returns registered bot types - BotManager: type selector dropdown + config fields on create - No SSH needed: create a 'Steam Free Games' bot from /bots/manage
This commit is contained in:
+77
-23
@@ -12,17 +12,19 @@ import (
|
||||
|
||||
// Handler handles bot CRUD and server-assignment routes.
|
||||
type Handler struct {
|
||||
db *sql.DB
|
||||
db *sql.DB
|
||||
runner *Runner
|
||||
}
|
||||
|
||||
// NewHandler creates a new bot Handler.
|
||||
func NewHandler(db *sql.DB) *Handler {
|
||||
return &Handler{db: db}
|
||||
func NewHandler(db *sql.DB, runner *Runner) *Handler {
|
||||
return &Handler{db: db, runner: runner}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers authenticated bot routes under the given router.
|
||||
func (h *Handler) RegisterRoutes(r chi.Router) {
|
||||
r.Get("/store", h.Store)
|
||||
r.Get("/types", h.ListTypes)
|
||||
r.Post("/", h.Create)
|
||||
r.Get("/", h.List)
|
||||
r.Get("/{botID}", h.Get)
|
||||
@@ -36,12 +38,14 @@ func (h *Handler) RegisterRoutes(r chi.Router) {
|
||||
// ---- response / request types ----
|
||||
|
||||
type botResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Avatar *string `json:"avatar"`
|
||||
Description string `json:"description"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Avatar *string `json:"avatar"`
|
||||
Description string `json:"description"`
|
||||
BotType string `json:"bot_type"`
|
||||
Config json.RawMessage `json:"config"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// botWithToken is returned only on create / regenerate-token.
|
||||
@@ -51,14 +55,18 @@ type botWithToken struct {
|
||||
}
|
||||
|
||||
type createBotRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
BotType string `json:"bot_type"`
|
||||
Config json.RawMessage `json:"config"`
|
||||
}
|
||||
|
||||
type updateBotRequest struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Avatar *string `json:"avatar"`
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Avatar *string `json:"avatar"`
|
||||
BotType *string `json:"bot_type"`
|
||||
Config json.RawMessage `json:"config"`
|
||||
}
|
||||
|
||||
type addToServerRequest struct {
|
||||
@@ -110,15 +118,21 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
token := GenerateToken()
|
||||
tokenHash := HashToken(token)
|
||||
|
||||
configJSON := req.Config
|
||||
if configJSON == nil {
|
||||
configJSON = json.RawMessage(`{}`)
|
||||
}
|
||||
|
||||
var bot botWithToken
|
||||
var avatar sql.NullString
|
||||
var createdAt sql.NullString
|
||||
var configOut sql.NullString
|
||||
err := h.db.QueryRowContext(r.Context(), `
|
||||
INSERT INTO bots (name, description, owner_id, token)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, name, avatar, description, owner_id, created_at::text
|
||||
`, req.Name, req.Description, userID, tokenHash).Scan(
|
||||
&bot.ID, &bot.Name, &avatar, &bot.Description, &bot.OwnerID, &createdAt,
|
||||
INSERT INTO bots (name, description, owner_id, token, bot_type, config)
|
||||
VALUES ($1, $2, $3, $4, $5, $6::jsonb)
|
||||
RETURNING id, name, avatar, description, bot_type, config::text, owner_id, created_at::text
|
||||
`, req.Name, req.Description, userID, tokenHash, req.BotType, string(configJSON)).Scan(
|
||||
&bot.ID, &bot.Name, &avatar, &bot.Description, &bot.BotType, &configOut, &bot.OwnerID, &createdAt,
|
||||
)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, "failed to create bot")
|
||||
@@ -127,9 +141,17 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
if avatar.Valid {
|
||||
bot.Avatar = &avatar.String
|
||||
}
|
||||
if configOut.Valid {
|
||||
bot.Config = json.RawMessage(configOut.String)
|
||||
}
|
||||
bot.CreatedAt = createdAt.String
|
||||
bot.Token = token
|
||||
|
||||
// Start built-in bot if type is set
|
||||
if req.BotType != "" && h.runner != nil {
|
||||
h.runner.Start(bot.ID, req.BotType, bot.Config)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, bot)
|
||||
}
|
||||
|
||||
@@ -282,15 +304,18 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
var b botResponse
|
||||
var avatar sql.NullString
|
||||
var createdAt sql.NullString
|
||||
var configOut sql.NullString
|
||||
err = h.db.QueryRowContext(r.Context(), `
|
||||
UPDATE bots
|
||||
SET name = COALESCE($1, name),
|
||||
description = COALESCE($2, description),
|
||||
avatar = COALESCE($3, avatar)
|
||||
avatar = COALESCE($3, avatar),
|
||||
bot_type = COALESCE($5, bot_type),
|
||||
config = COALESCE($6::jsonb, config)
|
||||
WHERE id = $4
|
||||
RETURNING id, name, avatar, description, owner_id, created_at::text
|
||||
`, req.Name, req.Description, req.Avatar, botID).Scan(
|
||||
&b.ID, &b.Name, &avatar, &b.Description, &b.OwnerID, &createdAt,
|
||||
RETURNING id, name, avatar, description, bot_type, config::text, owner_id, created_at::text
|
||||
`, req.Name, req.Description, req.Avatar, botID, req.BotType, string(req.Config)).Scan(
|
||||
&b.ID, &b.Name, &avatar, &b.Description, &b.BotType, &configOut, &b.OwnerID, &createdAt,
|
||||
)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, "failed to update bot")
|
||||
@@ -299,8 +324,22 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
if avatar.Valid {
|
||||
b.Avatar = &avatar.String
|
||||
}
|
||||
if configOut.Valid {
|
||||
b.Config = json.RawMessage(configOut.String)
|
||||
}
|
||||
b.CreatedAt = createdAt.String
|
||||
|
||||
// Restart built-in bot if type/config changed
|
||||
if req.BotType != nil && h.runner != nil {
|
||||
h.runner.Stop(b.ID)
|
||||
if *req.BotType != "" {
|
||||
h.runner.Start(b.ID, *req.BotType, b.Config)
|
||||
}
|
||||
} else if req.Config != nil && h.runner != nil && b.BotType != "" {
|
||||
h.runner.Stop(b.ID)
|
||||
h.runner.Start(b.ID, b.BotType, b.Config)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, b)
|
||||
}
|
||||
|
||||
@@ -346,6 +385,11 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Stop built-in bot if running
|
||||
if h.runner != nil {
|
||||
h.runner.Stop(botID)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -599,3 +643,13 @@ func (h *Handler) Store(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
writeJSON(w, http.StatusOK, bots)
|
||||
}
|
||||
|
||||
// ListTypes returns available built-in bot types.
|
||||
func (h *Handler) ListTypes(w http.ResponseWriter, r *http.Request) {
|
||||
if h.runner == nil {
|
||||
writeJSON(w, http.StatusOK, []string{})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, h.runner.RegisteredTypes())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user