package admin import ( "encoding/json" "fmt" "net/http" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" "thehouseoficarus/internal/behavior" ) func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: tree, err := listYAMLTree(s.dataDir, "triggers") if err != nil { if os.IsNotExist(err) { writeJSON(w, &YAMLTree{Root: []string{}, Dirs: map[string][]string{}}) return } writeJSON(w, map[string]any{"error": err.Error()}) return } if tree == nil { tree = &YAMLTree{Root: []string{}, Dirs: map[string][]string{}} } writeJSON(w, tree) 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" } // 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, // 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") } // 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() {}