aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/api_triggers.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin/api_triggers.go')
-rw-r--r--internal/admin/api_triggers.go335
1 files changed, 335 insertions, 0 deletions
diff --git a/internal/admin/api_triggers.go b/internal/admin/api_triggers.go
new file mode 100644
index 0000000..b2fecbc
--- /dev/null
+++ b/internal/admin/api_triggers.go
@@ -0,0 +1,335 @@
+package admin
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "gopkg.in/yaml.v3"
+
+ "thehouseoficarus/internal/behavior"
+)
+
+type triggerEntry struct {
+ ID string `json:"id"`
+ Kind string `json:"kind"`
+}
+
+func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case http.MethodGet:
+ entries, err := listTriggerEntries(s.dataDir)
+ if err != nil {
+ writeJSON(w, map[string]any{"error": err.Error()})
+ return
+ }
+ if entries == nil {
+ entries = []triggerEntry{}
+ }
+ writeJSON(w, entries)
+ case http.MethodPost:
+ var m map[string]any
+ if err := readJSON(r, &m); err != nil {
+ writeJSON(w, map[string]any{"error": "invalid json"})
+ return
+ }
+ id, ok := m["id"].(string)
+ if !ok || strings.TrimSpace(id) == "" {
+ writeJSON(w, map[string]any{"error": "missing id"})
+ return
+ }
+ delete(m, "id")
+
+ kind := triggerKind(m)
+ subdir := kindToSubdir(kind)
+ path := filepath.Join(s.dataDir, "triggers", subdir, id+".yaml")
+ os.MkdirAll(filepath.Dir(path), 0755)
+ if _, err := os.Stat(path); err == nil {
+ writeJSON(w, map[string]any{"error": "trigger already exists"})
+ return
+ }
+
+ newContent, err := writeMapAsYAML(path, m)
+ if err != nil {
+ writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ return
+ }
+ s.undoStack.Push(ChangeDesc{
+ Description: "Created trigger " + id,
+ FilePath: path,
+ NewContent: newContent,
+ IsCreate: true,
+ })
+ writeJSON(w, map[string]any{"ok": true, "id": id, "kind": kind})
+ default:
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ }
+}
+
+func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request) {
+ id := strings.TrimPrefix(r.URL.Path, "/api/triggers/")
+ if id == "" {
+ http.Error(w, `{"error":"missing id"}`, http.StatusBadRequest)
+ return
+ }
+
+ switch r.Method {
+ case http.MethodGet:
+ path, _, err := findTriggerFile(s.dataDir, id)
+ if err != nil {
+ writeJSON(w, map[string]any{"error": "not found: " + id})
+ return
+ }
+ data, err := os.ReadFile(path)
+ if err != nil {
+ writeJSON(w, map[string]any{"error": "not found: " + id})
+ return
+ }
+ raw, err := yamlToMap(data)
+ if err != nil {
+ writeJSON(w, map[string]any{"error": "parse error: " + err.Error()})
+ return
+ }
+ raw["id"] = id
+ raw["_path"] = path
+ raw["_raw"] = string(data)
+ raw["_kind"] = triggerKind(raw)
+ writeJSON(w, raw)
+
+ case http.MethodPut:
+ var body map[string]any
+ if err := readJSON(r, &body); err != nil {
+ writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()})
+ return
+ }
+ delete(body, "id")
+ delete(body, "_path")
+ delete(body, "_raw")
+
+ oldPath, oldDir, findErr := findTriggerFile(s.dataDir, id)
+ newKind := triggerKind(body)
+ newSubdir := kindToSubdir(newKind)
+ newPath := filepath.Join(s.dataDir, "triggers", newSubdir, id+".yaml")
+
+ var oldContent []byte
+ if existing, err := snapshotFile(oldPath); err == nil {
+ oldContent = existing
+ }
+
+ // If the kind changed, ensure the target directory exists and delete
+ // the old path after writing the new one.
+ if findErr == nil && newSubdir != oldDir {
+ os.MkdirAll(filepath.Dir(newPath), 0755)
+ }
+
+ triggerData, err := triggerMapToYAML(body)
+ if err != nil {
+ writeJSON(w, map[string]any{"error": "serialize error: " + err.Error()})
+ return
+ }
+
+ if err := os.WriteFile(newPath, triggerData, 0644); err != nil {
+ writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ return
+ }
+
+ if findErr == nil && newSubdir != oldDir {
+ os.Remove(oldPath)
+ }
+
+ s.undoStack.Push(ChangeDesc{
+ Description: "Update trigger " + id,
+ FilePath: newPath,
+ OldContent: oldContent,
+ NewContent: triggerData,
+ })
+ s.rebuildTriggerIndex()
+ writeJSON(w, map[string]any{"ok": true, "id": id, "kind": newKind})
+
+ case http.MethodDelete:
+ path, _, err := findTriggerFile(s.dataDir, id)
+ if err != nil {
+ writeJSON(w, map[string]any{"error": "not found: " + id})
+ return
+ }
+ oldContent, err := snapshotFile(path)
+ if err != nil {
+ writeJSON(w, map[string]any{"error": "not found: " + id})
+ return
+ }
+ if err := os.Remove(path); err != nil {
+ writeJSON(w, map[string]any{"error": "delete error: " + err.Error()})
+ return
+ }
+ s.undoStack.Push(ChangeDesc{
+ Description: "Delete trigger " + id,
+ FilePath: path,
+ OldContent: oldContent,
+ })
+ s.rebuildTriggerIndex()
+ writeJSON(w, map[string]any{"ok": true})
+
+ default:
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ }
+}
+
+// triggerKind returns "player", "global", or "" based on the map content.
+func triggerKind(m map[string]any) string {
+ if _, ok := m["on_player_flag"]; ok {
+ if s, is := m["on_player_flag"].(string); is && s != "" {
+ return "player"
+ }
+ }
+ if _, ok := m["on_global_flag"]; ok {
+ if s, is := m["on_global_flag"].(string); is && s != "" {
+ return "global"
+ }
+ }
+ // Fallback: if neither is present, return empty.
+ return ""
+}
+
+func kindToSubdir(kind string) string {
+ if kind == "global" {
+ return "global_flag"
+ }
+ return "player_flag"
+}
+
+func listTriggerEntries(dataDir string) ([]triggerEntry, error) {
+ root := filepath.Join(dataDir, "triggers")
+ if _, err := os.Stat(root); os.IsNotExist(err) {
+ return []triggerEntry{}, nil
+ }
+ var entries []triggerEntry
+ var walk func(p string) error
+ walk = func(p string) error {
+ dirs, err := os.ReadDir(p)
+ if err != nil {
+ return err
+ }
+ for _, entry := range dirs {
+ full := filepath.Join(p, entry.Name())
+ if entry.IsDir() {
+ if err := walk(full); err != nil {
+ return err
+ }
+ continue
+ }
+ ext := filepath.Ext(entry.Name())
+ if ext != ".yaml" && ext != ".yml" {
+ continue
+ }
+ id := strings.TrimSuffix(entry.Name(), ext)
+ // Determine kind from the parent directory name.
+ parent := filepath.Base(filepath.Dir(full))
+ kind := ""
+ if parent == "global_flag" {
+ kind = "global"
+ } else if parent == "player_flag" {
+ kind = "player"
+ }
+ entries = append(entries, triggerEntry{ID: id, Kind: kind})
+ }
+ return nil
+ }
+ if err := walk(root); err != nil {
+ return nil, err
+ }
+ return entries, nil
+}
+
+// findTriggerFile searches for a trigger file in the triggers directory tree
+// and returns its full path and the parent directory name (relative to the
+// triggers root). Searches player_flag/ then global_flag/ then root.
+func findTriggerFile(dataDir, id string) (path string, dir string, err error) {
+ root := filepath.Join(dataDir, "triggers")
+ for _, sub := range []string{"player_flag", "global_flag", ""} {
+ var p string
+ if sub == "" {
+ p = filepath.Join(root, id+".yaml")
+ } else {
+ p = filepath.Join(root, sub, id+".yaml")
+ }
+ if _, statErr := os.Stat(p); statErr == nil {
+ return p, sub, nil
+ }
+ p2 := filepath.Join(root, sub, id+".yml")
+ if _, statErr := os.Stat(p2); statErr == nil {
+ return p2, sub, nil
+ }
+ }
+ return "", "", fmt.Errorf("trigger %q not found", id)
+}
+
+// triggerMapToYAML serializes a flat map into a Trigger YAML document.
+// The map contains keys like on_player_flag, on_global_flag, value,
+// lock, condition, steps, etc.
+func triggerMapToYAML(m map[string]any) ([]byte, error) {
+ var t behavior.Trigger
+
+ // Handle steps specially — need to convert from []any to []behavior.Step.
+ if rawSteps, ok := m["steps"]; ok {
+ delete(m, "steps")
+ stepsJSON, err := json.Marshal(rawSteps)
+ if err != nil {
+ return nil, fmt.Errorf("marshal steps: %w", err)
+ }
+ var steps []behavior.Step
+ if err := json.Unmarshal(stepsJSON, &steps); err != nil {
+ return nil, fmt.Errorf("unmarshal steps: %w", err)
+ }
+ t.Steps = steps
+ } else {
+ t.Steps = []behavior.Step{}
+ }
+
+ // Handle condition.
+ if rawCond, ok := m["condition"]; ok {
+ delete(m, "condition")
+ condJSON, err := json.Marshal(rawCond)
+ if err == nil {
+ var cond behavior.Condition
+ if err := json.Unmarshal(condJSON, &cond); err == nil {
+ t.Condition = &cond
+ }
+ }
+ }
+
+ // Handle simple fields.
+ if v, ok := m["on_player_flag"]; ok {
+ if s, ok := v.(string); ok {
+ t.OnPlayerFlag = s
+ }
+ delete(m, "on_player_flag")
+ }
+ if v, ok := m["on_global_flag"]; ok {
+ if s, ok := v.(string); ok {
+ t.OnGlobalFlag = s
+ }
+ delete(m, "on_global_flag")
+ }
+ if v, ok := m["lock"]; ok {
+ if b, ok := v.(bool); ok {
+ t.Lock = b
+ }
+ delete(m, "lock")
+ }
+ if v, ok := m["value"]; ok {
+ t.Value = v
+ delete(m, "value")
+ }
+
+ // Serialize to YAML.
+ yamlBytes, err := yaml.Marshal(t)
+ if err != nil {
+ return nil, fmt.Errorf("yaml marshal: %w", err)
+ }
+ return yamlBytes, nil
+}
+
+func (s *AdminServer) rebuildTriggerIndex() {}