diff options
Diffstat (limited to 'internal/admin/api_triggers.go')
| -rw-r--r-- | internal/admin/api_triggers.go | 335 |
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() {} |
