package cfdav import ( "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "strings" "time" "ops-assistant/internal/core/command" "ops-assistant/internal/core/ecode" "ops-assistant/models" "gorm.io/gorm" ) type Module struct { db *gorm.DB } type settings struct { APIBase string AdminUser string AdminPass string } type userItem struct { ID string `json:"id"` Email string `json:"email"` IsAdmin int `json:"is_admin"` CreatedAt string `json:"created_at"` } type listResp struct { Success bool `json:"success"` Data []userItem `json:"data"` Error string `json:"error"` } type simpleResp struct { Success bool `json:"success"` Error string `json:"error"` } func New(db *gorm.DB) *Module { return &Module{db: db} } func (m *Module) Handle(userID int64, cmd *command.ParsedCommand) (string, error) { text := strings.TrimSpace(cmd.Raw) if text == "/cfdav" || strings.HasPrefix(text, "/cfdav help") { return "cfdav 模块\n- /cfdav users\n- /cfdav add [admin]\n- /cfdav del ", nil } parts := strings.Fields(text) if len(parts) == 0 { return ecode.Tag(ecode.ErrStepFailed, "空命令"), nil } // 支持别名:/cfdav list if len(parts) >= 2 && (parts[1] == "users" || parts[1] == "list") { return m.listUsers() } if len(parts) >= 2 && (parts[1] == "add" || (len(parts) >= 3 && parts[1] == "user" && parts[2] == "add")) { return m.addUser(parts) } if len(parts) >= 2 && (parts[1] == "del" || parts[1] == "delete" || (len(parts) >= 3 && parts[1] == "user" && (parts[2] == "del" || parts[2] == "delete"))) { return m.deleteUser(parts) } return ecode.Tag(ecode.ErrStepFailed, "cfdav 模块已接入,当前支持:/cfdav users, /cfdav add, /cfdav del"), nil } func (m *Module) listUsers() (string, error) { cfg, err := m.loadSettings() if err != nil { return ecode.Tag(ecode.ErrStepFailed, err.Error()), nil } url := strings.TrimRight(cfg.APIBase, "/") + "/api/admin/users" resp, status, err := m.doRequest(http.MethodGet, url, nil, cfg) if err != nil { return ecode.Tag(ecode.ErrStepFailed, err.Error()), nil } if status >= 400 { return ecode.Tag(ecode.ErrStepFailed, fmt.Sprintf("upstream http %d: %s", status, resp)), nil } var out listResp if err := json.Unmarshal([]byte(resp), &out); err != nil { return ecode.Tag(ecode.ErrStepFailed, "响应解析失败"), nil } if !out.Success { return ecode.Tag(ecode.ErrStepFailed, out.Error), nil } if len(out.Data) == 0 { return "📭 cfdav 用户列表为空", nil } limit := len(out.Data) if limit > 50 { limit = 50 } lines := make([]string, 0, limit+2) lines = append(lines, fmt.Sprintf("✅ cfdav 用户列表(%d)", len(out.Data))) for i := 0; i < limit; i++ { u := out.Data[i] admin := "user" if u.IsAdmin == 1 { admin = "admin" } lines = append(lines, fmt.Sprintf("- %s | %s | %s", u.ID, u.Email, admin)) } if len(out.Data) > limit { lines = append(lines, fmt.Sprintf("... 共 %d 个,已展示前 %d 个", len(out.Data), limit)) } return strings.Join(lines, "\n"), nil } func (m *Module) addUser(parts []string) (string, error) { cfg, err := m.loadSettings() if err != nil { return ecode.Tag(ecode.ErrStepFailed, err.Error()), nil } // 支持: /cfdav add [admin] // 或: /cfdav user add [admin] idx := 2 if len(parts) >= 3 && parts[1] == "user" { idx = 3 } if len(parts) < idx+2 { return ecode.Tag(ecode.ErrStepFailed, "缺少参数,示例:/cfdav add [admin]"), nil } email := strings.TrimSpace(parts[idx]) pass := strings.TrimSpace(parts[idx+1]) if email == "" || pass == "" { return ecode.Tag(ecode.ErrStepFailed, "email/password 不能为空"), nil } isAdmin := false if len(parts) >= idx+3 { flag := strings.ToLower(strings.TrimSpace(parts[idx+2])) isAdmin = flag == "admin" || flag == "1" || flag == "true" || flag == "yes" } payload := map[string]any{"email": email, "password": pass, "isAdmin": isAdmin} body, _ := json.Marshal(payload) url := strings.TrimRight(cfg.APIBase, "/") + "/api/admin/users" resp, status, err := m.doRequest(http.MethodPost, url, body, cfg) if err != nil { return ecode.Tag(ecode.ErrStepFailed, err.Error()), nil } if status >= 400 { return ecode.Tag(ecode.ErrStepFailed, fmt.Sprintf("upstream http %d: %s", status, resp)), nil } var out simpleResp if err := json.Unmarshal([]byte(resp), &out); err != nil { return ecode.Tag(ecode.ErrStepFailed, "响应解析失败"), nil } if !out.Success { return ecode.Tag(ecode.ErrStepFailed, out.Error), nil } return fmt.Sprintf("✅ cfdav 用户已创建:%s", email), nil } func (m *Module) deleteUser(parts []string) (string, error) { cfg, err := m.loadSettings() if err != nil { return ecode.Tag(ecode.ErrStepFailed, err.Error()), nil } // 支持: /cfdav del 或 /cfdav user del idx := 2 if len(parts) >= 3 && parts[1] == "user" { idx = 3 } if len(parts) < idx+1 { return ecode.Tag(ecode.ErrStepFailed, "缺少参数,示例:/cfdav del "), nil } id := strings.TrimSpace(parts[idx]) if id == "" { return ecode.Tag(ecode.ErrStepFailed, "user_id 不能为空"), nil } url := strings.TrimRight(cfg.APIBase, "/") + "/api/admin/users/" + id resp, status, err := m.doRequest(http.MethodDelete, url, nil, cfg) if err != nil { return ecode.Tag(ecode.ErrStepFailed, err.Error()), nil } if status >= 400 { return ecode.Tag(ecode.ErrStepFailed, fmt.Sprintf("upstream http %d: %s", status, resp)), nil } var out simpleResp if err := json.Unmarshal([]byte(resp), &out); err != nil { return ecode.Tag(ecode.ErrStepFailed, "响应解析失败"), nil } if !out.Success { return ecode.Tag(ecode.ErrStepFailed, out.Error), nil } return fmt.Sprintf("✅ cfdav 用户已删除:%s", id), nil } func (m *Module) loadSettings() (*settings, error) { if m.db == nil { return nil, errors.New("db 未初始化") } keys := []string{"cfdav_api_base", "cfdav_admin_user", "cfdav_admin_pass"} cfg := &settings{} for _, k := range keys { var sset models.AppSetting val := "" if err := m.db.Where("key = ?", k).First(&sset).Error; err == nil { val = sset.Value } switch k { case "cfdav_api_base": cfg.APIBase = strings.TrimSpace(val) case "cfdav_admin_user": cfg.AdminUser = strings.TrimSpace(val) case "cfdav_admin_pass": cfg.AdminPass = val } } if cfg.APIBase == "" { return nil, errors.New("cfdav api_base 未配置") } if cfg.AdminUser == "" || cfg.AdminPass == "" { return nil, errors.New("cfdav admin 账号未配置") } return cfg, nil } func (m *Module) doRequest(method, url string, body []byte, cfg *settings) (string, int, error) { var reader io.Reader if body != nil { reader = strings.NewReader(string(body)) } req, err := http.NewRequest(method, url, reader) if err != nil { return "", 0, err } auth := cfg.AdminUser + ":" + cfg.AdminPass b64 := base64.StdEncoding.EncodeToString([]byte(auth)) req.Header.Set("Authorization", "Basic "+b64) if body != nil { req.Header.Set("Content-Type", "application/json") } client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(req) if err != nil { return "", 0, err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return "", resp.StatusCode, err } return string(data), resp.StatusCode, nil }