package consenttool import ( "encoding/csv" "encoding/json" "fmt" "io" "os" "path/filepath" "regexp" "strings" ) func Run(configPath string, mode string) error { cfg, err := LoadConfig(configPath) if err != nil { return err } baseDir := filepath.Dir(configPath) switch mode { case "generate-json": return generateJSON(cfg, baseDir) case "extract-log": return extractLog(cfg, baseDir) default: return fmt.Errorf("unknown mode %q", mode) } } func generateJSON(cfg *Config, baseDir string) error { inputPath := resolvePath(baseDir, cfg.Consent.InputCSV) outputPath := resolvePath(baseDir, cfg.Consent.OutputJSON) rows, err := readCSVRows(inputPath) if err != nil { return err } names := collectConsentedNames(rows, cfg.Consent) body, err := json.MarshalIndent(names, "", " ") if err != nil { return err } if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil { return err } return os.WriteFile(outputPath, append(body, '\n'), 0o644) } func extractLog(cfg *Config, baseDir string) error { inputPath := resolvePath(baseDir, cfg.Log.InputPath) outputPath := resolvePath(baseDir, cfg.Log.OutputPath) data, err := os.ReadFile(inputPath) if err != nil { return err } re, err := regexp.Compile(cfg.Log.IncludeRegex) if err != nil { return err } var kept []string for _, line := range strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") { if re.MatchString(line) { kept = append(kept, line) continue } if cfg.Log.KeepNonMatch { kept = append(kept, line) } } body := strings.Join(kept, "\n") if body != "" { body += "\n" } if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil { return err } return os.WriteFile(outputPath, []byte(body), 0o644) } func readCSVRows(path string) ([]map[string]string, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() r := csv.NewReader(file) headers, err := r.Read() if err != nil { return nil, err } for i, header := range headers { headers[i] = strings.TrimPrefix(strings.TrimSpace(header), "\ufeff") } var rows []map[string]string for { record, err := r.Read() if err != nil { if err == io.EOF { break } return nil, err } row := make(map[string]string, len(headers)) for i, header := range headers { if i < len(record) { row[strings.TrimSpace(header)] = strings.TrimPrefix(strings.TrimSpace(record[i]), "\ufeff") } } rows = append(rows, row) } return rows, nil } func collectConsentedNames(rows []map[string]string, cfg ConsentConfig) []string { displayFields := cfg.DisplayFields if len(displayFields) == 0 { displayFields = []string{"display_name", "vrchat_name", "name"} } consentFields := cfg.ConsentFields if len(consentFields) == 0 { consentFields = []string{"consent", "agree", "agreed", "checked"} } accepted := make(map[string]struct{}) var names []string for _, row := range rows { if !rowIsConsented(row, consentFields, cfg.ConsentValues) { continue } name := firstNonEmpty(row, displayFields) if name == "" { continue } key := name if cfg.NormalizeCase { key = strings.ToLower(key) } if !cfg.AllowDuplicates { if _, ok := accepted[key]; ok { continue } accepted[key] = struct{}{} } names = append(names, name) } return names } func rowIsConsented(row map[string]string, fields []string, values []string) bool { if len(values) == 0 { values = []string{"1", "true", "yes", "y", "on", "checked"} } allowed := make(map[string]struct{}, len(values)) for _, v := range values { allowed[strings.ToLower(strings.TrimSpace(v))] = struct{}{} } for _, field := range fields { if v, ok := row[field]; ok { if _, ok := allowed[strings.ToLower(strings.TrimSpace(v))]; ok { return true } } } return false } func firstNonEmpty(row map[string]string, fields []string) string { for _, field := range fields { if v := strings.TrimSpace(row[field]); v != "" { return v } } return "" }