From eece6bc53cc461ab87ded1bff93234af76fec3a2 Mon Sep 17 00:00:00 2001 From: co63oc <4617245+co63oc@users.noreply.github.com> Date: Tue, 16 Jun 2026 21:39:24 +0800 Subject: [PATCH] feat(shortcuts): add user shortcuts --- internal/i18n/locales/en-US.json | 104 +- internal/i18n/locales/zh-CN.json | 104 +- shortcuts/user/user.go | 1068 +++++++++++++++++++ shortcuts/user/user_test.go | 1714 ++++++++++++++++++++++++++++++ skills/gitlink-user/SKILL.md | 141 ++- 5 files changed, 3126 insertions(+), 5 deletions(-) diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 0739395..a1625ab 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -267,5 +267,105 @@ "success.config.set": "✓ {key} = {value}", "warning.auth.not_logged_in": "✗ Not logged in", "warning.auth.token_unverified": "✓ Token stored (but cannot verify: {message})", - "warning.auth.user_unavailable": "✓ Token stored (user info unavailable)" -} + "warning.auth.user_unavailable": "✓ Token stored (user info unavailable)", + "cmd.user.update.short": "Update user profile", + "cmd.user.update.long": "Update user nickname, gender, province, city, description and department.", + "cmd.user.projects.short": "List user projects", + "cmd.user.projects.long": "List projects for a given user with category, sort and search filters.", + "flag.user.nickname": "User nickname", + "flag.user.gender": "User gender (integer)", + "flag.user.province": "User province", + "flag.user.city": "User city", + "flag.user.description": "User bio / description", + "flag.user.custom_department": "User department / organization", + "flag.user.projects_category": "Project category: all/join/created/manage/only_watched/watched/forked", + "flag.user.projects_is_public": "Filter by public visibility", + "flag.user.projects_project_type": "Project type: common/mirror/sync_mirror", + "flag.user.projects_search": "Search keyword", + "cmd.user.current_info.short": "Show current user info (detailed)", + "cmd.user.current_info.long": "Fetch detailed information for the currently authenticated user via /api/users/get_user_info.json.", + "flag.user.projects_start_at": "Start timestamp (only for watched/only_watched/forked categories)", + "flag.user.projects_end_at": "End timestamp (only for watched/only_watched/forked categories)", + "cmd.user.messages_list.short": "List user messages", + "cmd.user.messages_list.long": "List messages for the current user, with optional type and status filters.", + "cmd.user.messages_read.short": "Mark messages as read", + "cmd.user.messages_read.long": "Mark specified messages as read. Use --ids=-1 to mark all unread messages as read.", + "cmd.user.messages_delete.short": "Delete messages", + "cmd.user.messages_delete.long": "Delete specified messages. Use --ids=-1 to delete all messages of the given type.", + "flag.user.messages_type": "Message type: notification or atme (default: all)", + "flag.user.messages_status": "Read status: 1=unread, 2=read (default: all)", + "flag.user.messages_ids": "Comma-separated message IDs (use -1 for all)", + "cmd.user.messages_create.short": "Create a message (@me notification)", + "cmd.user.messages_create.long": "Create a message notification for specified users. Requires --receivers (comma-separated logins), --atmeable-type (Journal/Issue/PullRequest), and --atmeable-id.", + "cmd.user.platform_settings.short": "Get platform message settings", + "cmd.user.platform_settings.long": "Get the platform-wide message setting templates, showing all configurable notification categories.", + "cmd.user.user_settings.short": "Get user message settings", + "cmd.user.user_settings.long": "Get the current user's notification and email settings for all message categories.", + "flag.user.messages_receivers": "Comma-separated receiver login names", + "flag.user.messages_atmeable_type": "AT target type: Journal, Issue, or PullRequest", + "flag.user.messages_atmeable_id": "AT target object ID", + "cmd.user.update_settings.short": "Update user message settings", + "cmd.user.update_settings.long": "Update user notification and email settings, toggling each category on or off.", + "flag.user.settings_notification": "Comma-separated categories to enable for in-app notifications (e.g. CreateOrAssign::IssueAssigned,ManageProject::Issue)", + "flag.user.settings_email": "Comma-separated categories to enable for email notifications", + "flag.user.settings_notification_off": "Comma-separated categories to disable for in-app notifications", + "flag.user.settings_email_off": "Comma-separated categories to disable for email notifications", + "cmd.user.pinned_list.short": "List pinned projects", + "cmd.user.pinned_list.long": "List the user's pinned/starred projects with basic info and sort position.", + "cmd.user.pinned_update.short": "Update pinned projects", + "cmd.user.pinned_update.long": "Batch-set the user's pinned/starred projects by passing a list of project IDs.", + "flag.user.pinned_project_ids": "Comma-separated project IDs to pin", + "cmd.user.pinned_sort.short": "Reorder a pinned project", + "cmd.user.pinned_sort.long": "Adjust the display order of a pinned project. Higher position = higher rank.", + "flag.user.pinned_id": "Pinned project ID", + "flag.user.pinned_position": "Sort position (higher number = higher rank)", + "cmd.user.feedback.short": "Submit feedback", + "cmd.user.feedback.long": "Submit feedback or suggestions to the GitLink platform.", + "flag.user.feedback_content": "Feedback content", + "cmd.user.transfer_list.short": "List project transfer requests", + "cmd.user.transfer_list.long": "List pending project transfer requests for the specified user, with optional pagination.", + "cmd.user.transfer_accept.short": "Accept project transfer", + "cmd.user.transfer_accept.long": "Accept a project transfer request initiated by the specified user.", + "flag.user.transfer_id": "Transfer request ID", + "cmd.user.transfer_refuse.short": "Refuse project transfer", + "cmd.user.transfer_refuse.long": "Refuse a project transfer request initiated by the specified user.", + "cmd.user.send_email_code.short": "Send email verification code", + "cmd.user.send_email_code.long": "Send a verification code to the specified email for email change.", + "flag.user.email_code_type": "Verification code type (10: update email)", + "flag.user.email": "Target email address", + "flag.user.email_md5": "Email MD5 hash value", + "cmd.user.verify_email_code.short": "Verify email code", + "cmd.user.verify_email_code.long": "Verify the email verification code received.", + "flag.user.verify_code": "Verification code", + "cmd.user.check_password.short": "Verify user password", + "cmd.user.check_password.long": "Verify the current user login password is correct.", + "flag.user.password": "User password", + "cmd.user.applied_list.short": "List project applications", + "cmd.user.applied_list.long": "List pending project join applications (paginated).", + "flag.user.applied_page": "Page number", + "flag.user.applied_per_page": "Items per page", + "cmd.user.applied_accept.short": "Accept project application", + "cmd.user.applied_accept.long": "Accept the specified user project join application.", + "flag.user.applied_id": "Application ID", + "cmd.user.applied_refuse.short": "Refuse project application", + "cmd.user.applied_refuse.long": "Refuse the specified user project join application.", + "cmd.user.check_email.short": "Check email availability", + "cmd.user.check_email.long": "Check if the specified email is already in use.", + "cmd.user.update_email.short": "Change email", + "cmd.user.update_email.long": "Change the user bound email (requires password and email verification code).", + "flag.user.email_code": "Email verification code", + "cmd.user.update_phone.short": "Change phone number", + "cmd.user.update_phone.long": "Change the user bound phone number (requires SMS code).", + "flag.user.phone": "Target phone number", + "flag.user.phone_code": "SMS verification code", + "cmd.user.change_password.short": "Change password", + "cmd.user.change_password.long": "Change the current user login password.", + "flag.user.old_password": "Current password", + "flag.user.new_password": "New password", + "flag.user.new_password_repeat": "Confirm new password", + "cmd.user.check_delete.short": "Check if account can be deleted", + "cmd.user.check_delete.long": "Check if the current user meets deletion criteria (no orgs/projects).", + "cmd.user.delete_account.short": "Delete account", + "cmd.user.delete_account.long": "Permanently delete the user account (irreversible). Confirm before executing.", + "flag.user.delete_memo": "Deletion reason" +} \ No newline at end of file diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2e6fc4d..6df17d6 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -267,5 +267,105 @@ "success.config.set": "✓ 已设置 {key} = {value}", "warning.auth.not_logged_in": "✗ 未登录", "warning.auth.token_unverified": "✓ Token 已保存(但无法验证:{message})", - "warning.auth.user_unavailable": "✓ Token 已保存(用户信息不可用)" -} + "warning.auth.user_unavailable": "✓ Token 已保存(用户信息不可用)", + "cmd.user.update.short": "更新用户信息", + "cmd.user.update.long": "更新用户的昵称、性别、省份、城市、简介和单位等扩展信息。", + "cmd.user.projects.short": "列出用户项目", + "cmd.user.projects.long": "列出指定用户的项目列表,支持分类、排序和搜索筛选。", + "flag.user.nickname": "用户昵称", + "flag.user.gender": "用户性别(整数)", + "flag.user.province": "用户所在省份", + "flag.user.city": "用户所在城市", + "flag.user.description": "用户简介", + "flag.user.custom_department": "用户所在单位", + "flag.user.projects_category": "项目分类:all/join/created/manage/only_watched/watched/forked", + "flag.user.projects_is_public": "是否为公有项目", + "flag.user.projects_project_type": "项目类型:common/mirror/sync_mirror", + "flag.user.projects_search": "搜索关键词", + "cmd.user.current_info.short": "获取当前用户详细信息", + "cmd.user.current_info.long": "通过 /api/users/get_user_info.json 获取当前认证用户的详细资料。", + "flag.user.projects_start_at": "开始时间戳(仅对 watched/only_watched/forked 分类有效)", + "flag.user.projects_end_at": "结束时间戳(仅对 watched/only_watched/forked 分类有效)", + "cmd.user.messages_list.short": "列出用户消息", + "cmd.user.messages_list.long": "列出当前用户的消息,支持按类型和已读状态筛选。", + "cmd.user.messages_read.short": "标记消息为已读", + "cmd.user.messages_read.long": "将指定消息标记为已读。使用 --ids=-1 可将所有未读消息标记为已读。", + "cmd.user.messages_delete.short": "删除消息", + "cmd.user.messages_delete.long": "删除指定消息。使用 --ids=-1 可删除指定类型的所有消息。", + "flag.user.messages_type": "消息类型:notification(系统消息)或 atme(@我消息),不传为全部", + "flag.user.messages_status": "已读状态:1=未读,2=已读(不传为全部)", + "flag.user.messages_ids": "消息ID列表,逗号分隔(使用 -1 表示全部)", + "cmd.user.messages_create.short": "创建消息(@我通知)", + "cmd.user.messages_create.long": "为指定用户创建@我消息通知。需要 --receivers(逗号分隔的用户名)、--atmeable-type(Journal/Issue/PullRequest)和 --atmeable-id。", + "cmd.user.platform_settings.short": "获取平台消息设置", + "cmd.user.platform_settings.long": "获取平台级别的消息设置模板,展示所有可配置的通知分类。", + "cmd.user.user_settings.short": "获取用户消息设置", + "cmd.user.user_settings.long": "获取当前用户所有消息分类的站内信和邮件设置。", + "flag.user.messages_receivers": "接收消息的用户名列表,逗号分隔", + "flag.user.messages_atmeable_type": "@目标类型:Journal(评论)、Issue(疑修)、PullRequest(合并请求)", + "flag.user.messages_atmeable_id": "@目标对象ID", + "cmd.user.update_settings.short": "更新用户消息设置", + "cmd.user.update_settings.long": "更新用户的站内信和邮件通知设置,支持批量设置各分类的开关状态。", + "flag.user.settings_notification": "站内信设置,逗号分隔的分类列表(如 CreateOrAssign::IssueAssigned,ManageProject::Issue),设为 true", + "flag.user.settings_email": "邮件设置,逗号分隔的分类列表,设为 true", + "flag.user.settings_notification_off": "站内信设置,逗号分隔的分类列表,设为 false", + "flag.user.settings_email_off": "邮件设置,逗号分隔的分类列表,设为 false", + "cmd.user.pinned_list.short": "列出用户星标项目", + "cmd.user.pinned_list.long": "列出用户精选/星标的项目列表,包含项目基本信息和排序位置。", + "cmd.user.pinned_update.short": "更新用户星标项目", + "cmd.user.pinned_update.long": "批量设置用户的星标/精选项目,传入项目 ID 列表。", + "flag.user.pinned_project_ids": "要设为星标的项目 ID 列表,逗号分隔", + "cmd.user.pinned_sort.short": "调整星标项目排序", + "cmd.user.pinned_sort.long": "调整指定星标项目的显示顺序,数字越大排名越靠前。", + "flag.user.pinned_id": "星标项目 ID", + "flag.user.pinned_position": "排序位置(数字越大越靠前)", + "cmd.user.feedback.short": "提交反馈建议", + "cmd.user.feedback.long": "向 GitLink 平台提交反馈建议或意见。", + "flag.user.feedback_content": "反馈内容", + "cmd.user.transfer_list.short": "列出项目迁移请求", + "cmd.user.transfer_list.long": "列出指定用户的待接受项目迁移请求,支持分页。", + "cmd.user.transfer_accept.short": "接受项目迁移", + "cmd.user.transfer_accept.long": "接受指定用户发起的项目迁移请求。", + "flag.user.transfer_id": "迁移请求 ID", + "cmd.user.transfer_refuse.short": "拒绝项目迁移", + "cmd.user.transfer_refuse.long": "拒绝指定用户发起的项目迁移请求。", + "cmd.user.send_email_code.short": "发送邮箱验证码", + "cmd.user.send_email_code.long": "向指定邮箱发送验证码,用于邮箱变更验证。", + "flag.user.email_code_type": "验证码类型(10: 更新邮箱)", + "flag.user.email": "目标邮箱地址", + "flag.user.email_md5": "邮箱 MD5 加密值", + "cmd.user.verify_email_code.short": "验证邮箱验证码", + "cmd.user.verify_email_code.long": "验证用户收到的邮箱验证码是否正确。", + "flag.user.verify_code": "验证码", + "cmd.user.check_password.short": "验证用户密码", + "cmd.user.check_password.long": "验证当前用户的登录密码是否正确。", + "flag.user.password": "用户密码", + "cmd.user.applied_list.short": "列出项目申请", + "cmd.user.applied_list.long": "列出待处理的项目加入申请(分页)。", + "flag.user.applied_page": "页码", + "flag.user.applied_per_page": "每页个数", + "cmd.user.applied_accept.short": "接受项目申请", + "cmd.user.applied_accept.long": "接受指定用户的项目加入申请。", + "flag.user.applied_id": "申请 ID", + "cmd.user.applied_refuse.short": "拒绝项目申请", + "cmd.user.applied_refuse.long": "拒绝指定用户的项目加入申请。", + "cmd.user.check_email.short": "验证邮箱可用性", + "cmd.user.check_email.long": "检查指定邮箱是否已被占用。", + "cmd.user.update_email.short": "更改邮箱", + "cmd.user.update_email.long": "更改用户绑定的邮箱地址(需先验证密码和邮箱验证码)。", + "flag.user.email_code": "邮箱验证码", + "cmd.user.update_phone.short": "更改手机号", + "cmd.user.update_phone.long": "更改用户绑定的手机号(需手机验证码)。", + "flag.user.phone": "目标手机号", + "flag.user.phone_code": "手机验证码", + "cmd.user.change_password.short": "更改密码", + "cmd.user.change_password.long": "修改当前用户的登录密码。", + "flag.user.old_password": "当前密码", + "flag.user.new_password": "新密码", + "flag.user.new_password_repeat": "新密码确认", + "cmd.user.check_delete.short": "检测账号是否可注销", + "cmd.user.check_delete.long": "检测当前用户是否满足注销条件(无组织/项目)。", + "cmd.user.delete_account.short": "注销账号", + "cmd.user.delete_account.long": "永久删除用户账号(不可恢复)。执行前务必二次确认。", + "flag.user.delete_memo": "删除原因" +} \ No newline at end of file diff --git a/shortcuts/user/user.go b/shortcuts/user/user.go index 563db88..3b85446 100644 --- a/shortcuts/user/user.go +++ b/shortcuts/user/user.go @@ -2,6 +2,9 @@ package user import ( "fmt" + "net/url" + "strconv" + "strings" "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" @@ -39,6 +42,1030 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { return ctx.Output(env) }, }, + // api-99506857 | 当前用户信息 | GET /api/users/get_user_info.json + { + Name: "current-info", + Description: tr.T("cmd.user.current_info.short"), + Long: tr.T("cmd.user.current_info.long"), + Run: func(ctx *common.RuntimeContext) error { + env, err := ctx.CallAPI("GET", "/users/get_user_info", nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-99657106 | 用户信息更改 | PUT /api/users/{owner}.json + { + Name: "update", + Description: tr.T("cmd.user.update.short"), + Long: tr.T("cmd.user.update.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "nickname", Short: "n", Usage: tr.T("flag.user.nickname")}, + {Name: "gender", Usage: tr.T("flag.user.gender")}, + {Name: "province", Usage: tr.T("flag.user.province")}, + {Name: "city", Usage: tr.T("flag.user.city")}, + {Name: "description", Short: "d", Usage: tr.T("flag.user.description")}, + {Name: "custom-department", Usage: tr.T("flag.user.custom_department")}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + + userAttrs := map[string]interface{}{} + if v := ctx.Arg("nickname"); v != "" { + userAttrs["nickname"] = v + } + + extAttrs := map[string]interface{}{} + if v := ctx.Arg("gender"); v != "" { + if n, err := strconv.Atoi(v); err == nil { + extAttrs["gender"] = n + } + } + if v := ctx.Arg("province"); v != "" { + extAttrs["province"] = v + } + if v := ctx.Arg("city"); v != "" { + extAttrs["city"] = v + } + if v := ctx.Arg("description"); v != "" { + extAttrs["description"] = v + } + if v := ctx.Arg("custom-department"); v != "" { + extAttrs["custom_department"] = v + } + if len(extAttrs) > 0 { + userAttrs["user_extension_attributes"] = extAttrs + } + + body := map[string]interface{}{"user": userAttrs} + env, err := ctx.CallAPI("PUT", fmt.Sprintf("/users/%s", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-99651561 | 用户项目列表 | GET /api/v1/{owner}/projects.json + { + Name: "projects", + Description: tr.T("cmd.user.projects.short"), + Long: tr.T("cmd.user.projects.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "limit", Usage: tr.T("flag.limit"), Default: "15"}, + {Name: "category", Short: "c", Usage: tr.T("flag.user.projects_category")}, + {Name: "is-public", Usage: tr.T("flag.user.projects_is_public")}, + {Name: "project-type", Usage: tr.T("flag.user.projects_project_type")}, + {Name: "sort-by", Usage: tr.T("flag.sort_by")}, + {Name: "sort-direction", Usage: tr.T("flag.sort_direction")}, + {Name: "search", Usage: tr.T("flag.user.projects_search")}, + {Name: "start-at", Usage: tr.T("flag.user.projects_start_at")}, + {Name: "end-at", Usage: tr.T("flag.user.projects_end_at")}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + q := url.Values{} + if v := ctx.Arg("page"); v != "" { + q.Set("page", v) + } + if v := ctx.Arg("limit"); v != "" { + q.Set("limit", v) + } + if v := ctx.Arg("category"); v != "" { + q.Set("category", v) + } + if v := ctx.Arg("is-public"); v != "" { + q.Set("is_public", v) + } + if v := ctx.Arg("project-type"); v != "" { + q.Set("project_type", v) + } + if v := ctx.Arg("sort-by"); v != "" { + q.Set("sort_by", v) + } + if v := ctx.Arg("sort-direction"); v != "" { + q.Set("sort_direction", v) + } + if v := ctx.Arg("search"); v != "" { + q.Set("search", v) + } + if v := ctx.Arg("start-at"); v != "" { + q.Set("start_at", v) + } + if v := ctx.Arg("end-at"); v != "" { + q.Set("end_at", v) + } + env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1/%s/projects", login), q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-99652575 | 用户消息列表 | GET /api/users/{owner}/messages.json + { + Name: "messages-list", + Description: tr.T("cmd.user.messages_list.short"), + Long: tr.T("cmd.user.messages_list.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "limit", Usage: tr.T("flag.limit"), Default: "15"}, + {Name: "type", Short: "t", Usage: tr.T("flag.user.messages_type")}, + {Name: "status", Usage: tr.T("flag.user.messages_status")}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + q := url.Values{} + if v := ctx.Arg("page"); v != "" { + q.Set("page", v) + } + if v := ctx.Arg("limit"); v != "" { + q.Set("limit", v) + } + if v := ctx.Arg("type"); v != "" { + q.Set("type", v) + } + if v := ctx.Arg("status"); v != "" { + q.Set("status", v) + } + env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/users/%s/messages", login), q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-99654400 | 用户阅读消息 | POST /api/users/{owner}/messages/read.json + { + Name: "messages-read", + Description: tr.T("cmd.user.messages_read.short"), + Long: tr.T("cmd.user.messages_read.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "type", Short: "t", Usage: tr.T("flag.user.messages_type"), Required: true}, + {Name: "ids", Usage: tr.T("flag.user.messages_ids"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + msgType := ctx.Arg("type") + ids := parseIntArray(ctx.Arg("ids")) + body := map[string]interface{}{ + "type": msgType, + "ids": ids, + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/messages/read", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-99655030 | 用户删除消息 | DELETE /api/users/{owner}/messages.json + { + Name: "messages-delete", + Description: tr.T("cmd.user.messages_delete.short"), + Long: tr.T("cmd.user.messages_delete.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "type", Short: "t", Usage: tr.T("flag.user.messages_type"), Required: true}, + {Name: "ids", Usage: tr.T("flag.user.messages_ids"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + msgType := ctx.Arg("type") + ids := parseIntArray(ctx.Arg("ids")) + body := map[string]interface{}{ + "type": msgType, + "ids": ids, + } + env, err := ctx.CallAPI("DELETE", fmt.Sprintf("/users/%s/messages", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-99656095 | 用户消息创建 | POST /api/users/{owner}/messages.json + { + Name: "messages-create", + Description: tr.T("cmd.user.messages_create.short"), + Long: tr.T("cmd.user.messages_create.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "receivers", Short: "r", Usage: tr.T("flag.user.messages_receivers"), Required: true}, + {Name: "atmeable-type", Usage: tr.T("flag.user.messages_atmeable_type"), Required: true}, + {Name: "atmeable-id", Usage: tr.T("flag.user.messages_atmeable_id"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + atmeableIDStr, err := ctx.RequireArg("atmeable-id") + if err != nil { + return err + } + atmeableID, err := strconv.Atoi(atmeableIDStr) + if err != nil { + return fmt.Errorf("atmeable-id must be an integer: %s", atmeableIDStr) + } + receivers := parseStringArray(ctx.Arg("receivers")) + body := map[string]interface{}{ + "type": "atme", + "receivers_login": receivers, + "atmeable_type": ctx.Arg("atmeable-type"), + "atmeable_id": atmeableID, + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/messages", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-99661033 | 平台消息设置 | GET /api/template_message_settings.json + { + Name: "platform-settings", + Description: tr.T("cmd.user.platform_settings.short"), + Long: tr.T("cmd.user.platform_settings.long"), + Run: func(ctx *common.RuntimeContext) error { + env, err := ctx.CallAPI("GET", "/template_message_settings", nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-99666761 | 用户消息设置列表 | GET /api/users/{owner}/template_message_settings.json + { + Name: "user-settings", + Description: tr.T("cmd.user.user_settings.short"), + Long: tr.T("cmd.user.user_settings.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/template_message_settings", login), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-99669647 | 用户消息设置更新 | POST /api/users/{owner}/template_message_settings/update_setting.json + { + Name: "update-settings", + Description: tr.T("cmd.user.update_settings.short"), + Long: tr.T("cmd.user.update_settings.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "notification", Usage: tr.T("flag.user.settings_notification")}, + {Name: "email", Usage: tr.T("flag.user.settings_email")}, + {Name: "notification-off", Usage: tr.T("flag.user.settings_notification_off")}, + {Name: "email-off", Usage: tr.T("flag.user.settings_email_off")}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + notificationBody := buildMessageSettingBody( + parseStringArray(ctx.Arg("notification")), + parseStringArray(ctx.Arg("notification-off")), + ) + emailBody := buildMessageSettingBody( + parseStringArray(ctx.Arg("email")), + parseStringArray(ctx.Arg("email-off")), + ) + body := map[string]interface{}{ + "setting": map[string]interface{}{ + "notification_body": notificationBody, + "email_body": emailBody, + }, + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/template_message_settings/update_setting", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100229455 | 用户星标项目列表 | GET /api/users/{owner}/is_pinned_projects.json + { + Name: "pinned-list", + Description: tr.T("cmd.user.pinned_list.short"), + Long: tr.T("cmd.user.pinned_list.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/is_pinned_projects", login), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100234940 | 用户星标项目更新 | POST /api/users/{owner}/is_pinned_projects/pin.json + { + Name: "pinned-update", + Description: tr.T("cmd.user.pinned_update.short"), + Long: tr.T("cmd.user.pinned_update.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "project-ids", Usage: tr.T("flag.user.pinned_project_ids"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + projectIDs, err := ctx.RequireArg("project-ids") + if err != nil { + return err + } + ids := parseIntArray(projectIDs) + body := map[string]interface{}{ + "is_pinned_project_ids": ids, + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/is_pinned_projects/pin", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100236125 | 用户星标项目排序 | PUT /api/users/{owner}/is_pinned_projects/{id}.json + { + Name: "pinned-sort", + Description: tr.T("cmd.user.pinned_sort.short"), + Long: tr.T("cmd.user.pinned_sort.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "pinned-id", Usage: tr.T("flag.user.pinned_id"), Required: true}, + {Name: "position", Usage: tr.T("flag.user.pinned_position"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + pinnedID, err := ctx.RequireArg("pinned-id") + if err != nil { + return err + } + positionStr, err := ctx.RequireArg("position") + if err != nil { + return err + } + position, err := strconv.Atoi(positionStr) + if err != nil { + return fmt.Errorf("position must be an integer: %s", positionStr) + } + body := map[string]interface{}{ + "pinned_project": map[string]interface{}{ + "position": position, + }, + } + env, err := ctx.CallAPI("PUT", fmt.Sprintf("/users/%s/is_pinned_projects/%s", login, pinnedID), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-48195728 | 新建一个反馈建议 | POST /api/v1/{owner}/feedbacks.json + { + Name: "feedback", + Description: tr.T("cmd.user.feedback.short"), + Long: tr.T("cmd.user.feedback.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "content", Short: "c", Usage: tr.T("flag.user.feedback_content"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + content, err := ctx.RequireArg("content") + if err != nil { + return err + } + body := map[string]interface{}{ + "content": content, + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/feedbacks", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100238414 | 用户近期活动统计 | GET /api/users/{owner}/statistics/activity.json + { + Name: "activity", + Description: tr.T("cmd.user.activity.short"), + Long: tr.T("cmd.user.activity.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/statistics/activity", login), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100239110 | 用户贡献度统计 | GET /api/users/{owner}/headmaps.json + { + Name: "headmaps", + Description: tr.T("cmd.user.headmaps.short"), + Long: tr.T("cmd.user.headmaps.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/headmaps", login), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100242043 | 用户开发能力 | GET /api/users/{owner}/statistics/develop.json + { + Name: "develop", + Description: tr.T("cmd.user.develop.short"), + Long: tr.T("cmd.user.develop.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/statistics/develop", login), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100245084 | 用户角色定位 | GET /api/users/{owner}/statistics/role.json + { + Name: "role", + Description: tr.T("cmd.user.role.short"), + Long: tr.T("cmd.user.role.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/statistics/role", login), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100790543 | 用户专业定位 | GET /api/users/{owner}/statistics/major.json + { + Name: "major", + Description: tr.T("cmd.user.major.short"), + Long: tr.T("cmd.user.major.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/statistics/major", login), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100793695 | 待办事项-接受项目 | GET /users/{owner}/applied_transfer_projects, + { + Name: "transfer-list", + Description: tr.T("cmd.user.transfer_list.short"), + Long: tr.T("cmd.user.transfer_list.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "per-page", Usage: tr.T("flag.per_page"), Default: "15"}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + q := url.Values{} + if v := ctx.Arg("page"); v != "" { + q.Set("page", v) + } + if v := ctx.Arg("per-page"); v != "" { + q.Set("per_page", v) + } + env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/users/%s/applied_transfer_projects", login), q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100794239 | 用户接受迁移 | POST /api/users/{owner}/applied_transfer_projects/{id}/accept.json + { + Name: "transfer-accept", + Description: tr.T("cmd.user.transfer_accept.short"), + Long: tr.T("cmd.user.transfer_accept.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "transfer-id", Usage: tr.T("flag.user.transfer_id"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + transferID, err := ctx.RequireArg("transfer-id") + if err != nil { + return err + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/applied_transfer_projects/%s/accept", login, transferID), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100794866 | 用户拒绝迁移 | POST /api/users/{owner}/applied_transfer_projects/{id}/refuse.json + { + Name: "transfer-refuse", + Description: tr.T("cmd.user.transfer_refuse.short"), + Long: tr.T("cmd.user.transfer_refuse.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "transfer-id", Usage: tr.T("flag.user.transfer_id"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + transferID, err := ctx.RequireArg("transfer-id") + if err != nil { + return err + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/applied_transfer_projects/%s/refuse", login, transferID), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100796144 | 待办事项-项目申请 | GET /api/users/{owner}/applied_projects.json + { + Name: "applied-list", + Description: tr.T("cmd.user.applied_list.short"), + Long: tr.T("cmd.user.applied_list.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "page", Usage: tr.T("flag.user.applied_page")}, + {Name: "per-page", Usage: tr.T("flag.user.applied_per_page")}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + query := url.Values{} + if v := ctx.Arg("page"); v != "" { + query.Set("page", v) + } + if v := ctx.Arg("per-page"); v != "" { + query.Set("per_page", v) + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/applied_projects?%s", login, query.Encode()), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100796628 | 用户接受申请 | POST /api/users/{owner}/applied_projects/{id}/accept.json + { + Name: "applied-accept", + Description: tr.T("cmd.user.applied_accept.short"), + Long: tr.T("cmd.user.applied_accept.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "applied-id", Usage: tr.T("flag.user.applied_id"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + appliedID, err := ctx.RequireArg("applied-id") + if err != nil { + return err + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/applied_projects/%s/accept", login, appliedID), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100796655 | 用户拒绝申请 | POST /api/users/{owner}/applied_projects/{id}/refuse.json + { + Name: "applied-refuse", + Description: tr.T("cmd.user.applied_refuse.short"), + Long: tr.T("cmd.user.applied_refuse.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "applied-id", Usage: tr.T("flag.user.applied_id"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + appliedID, err := ctx.RequireArg("applied-id") + if err != nil { + return err + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/applied_projects/%s/refuse", login, appliedID), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100796697 | 用户发送邮箱验证码 | GET /api/v1/{owner}/send_email_vefify_code.json + { + Name: "send-email-code", + Description: tr.T("cmd.user.send_email_code.short"), + Long: tr.T("cmd.user.send_email_code.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "code-type", Usage: tr.T("flag.user.email_code_type"), Required: true}, + {Name: "email", Short: "e", Usage: tr.T("flag.user.email"), Required: true}, + {Name: "email-md5", Usage: tr.T("flag.user.email_md5"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + codeTypeStr, err := ctx.RequireArg("code-type") + if err != nil { + return err + } + codeType, err := strconv.Atoi(codeTypeStr) + if err != nil { + return fmt.Errorf("code-type must be an integer: %s", codeTypeStr) + } + email, err := ctx.RequireArg("email") + if err != nil { + return err + } + emailMD5, err := ctx.RequireArg("email-md5") + if err != nil { + return err + } + body := map[string]interface{}{ + "code_type": codeType, + "email": email, + "smscode": emailMD5, + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1/%s/send_email_vefify_code", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100796833 | 用户验证邮箱验证码 | POST /api/v1/{owner}/check_email_verify_code.json + { + Name: "verify-email-code", + Description: tr.T("cmd.user.verify_email_code.short"), + Long: tr.T("cmd.user.verify_email_code.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "code-type", Usage: tr.T("flag.user.email_code_type"), Required: true}, + {Name: "email", Short: "e", Usage: tr.T("flag.user.email"), Required: true}, + {Name: "code", Short: "c", Usage: tr.T("flag.user.verify_code"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + codeTypeStr, err := ctx.RequireArg("code-type") + if err != nil { + return err + } + codeType, err := strconv.Atoi(codeTypeStr) + if err != nil { + return fmt.Errorf("code-type must be an integer: %s", codeTypeStr) + } + email, err := ctx.RequireArg("email") + if err != nil { + return err + } + code, err := ctx.RequireArg("code") + if err != nil { + return err + } + body := map[string]interface{}{ + "code_type": codeType, + "email": email, + "code": code, + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/check_email_verify_code", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100796884 | 用户验证密码 | POST /api/v1/{owner}/check_password.json + { + Name: "check-password", + Description: tr.T("cmd.user.check_password.short"), + Long: tr.T("cmd.user.check_password.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "password", Short: "p", Usage: tr.T("flag.user.password"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + password, err := ctx.RequireArg("password") + if err != nil { + return err + } + body := map[string]interface{}{ + "password": password, + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/check_password", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100796903 | 用户验证邮箱 | POST /api/v1/{owner}/check_email.json + { + Name: "check-email", + Description: tr.T("cmd.user.check_email.short"), + Long: tr.T("cmd.user.check_email.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "email", Short: "e", Usage: tr.T("flag.user.email"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + email, err := ctx.RequireArg("email") + if err != nil { + return err + } + body := map[string]interface{}{ + "email": email, + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/check_email", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-100796995 | 用户更改邮箱 | PATCH /api/v1/{owner}/update_email.json + { + Name: "update-email", + Description: tr.T("cmd.user.update_email.short"), + Long: tr.T("cmd.user.update_email.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "password", Short: "p", Usage: tr.T("flag.user.password"), Required: true}, + {Name: "email", Short: "e", Usage: tr.T("flag.user.email"), Required: true}, + {Name: "code", Short: "c", Usage: tr.T("flag.user.email_code"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + password, err := ctx.RequireArg("password") + if err != nil { + return err + } + email, err := ctx.RequireArg("email") + if err != nil { + return err + } + code, err := ctx.RequireArg("code") + if err != nil { + return err + } + body := map[string]interface{}{ + "password": password, + "email": email, + "code": code, + } + env, err := ctx.CallAPI("PATCH", fmt.Sprintf("/v1/%s/update_email", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-133361275 | 用户更改手机号 | PATCH /api/v1/{owner}/update_phone.json + { + Name: "update-phone", + Description: tr.T("cmd.user.update_phone.short"), + Long: tr.T("cmd.user.update_phone.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "phone", Usage: tr.T("flag.user.phone"), Required: true}, + {Name: "code", Short: "c", Usage: tr.T("flag.user.phone_code"), Required: true}, + {Name: "password", Short: "p", Usage: tr.T("flag.user.password"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + phone, err := ctx.RequireArg("phone") + if err != nil { + return err + } + code, err := ctx.RequireArg("code") + if err != nil { + return err + } + password, err := ctx.RequireArg("password") + if err != nil { + return err + } + body := map[string]interface{}{ + "code": code, + "phone": phone, + "password": password, + } + env, err := ctx.CallAPI("PATCH", fmt.Sprintf("/v1/%s/update_phone", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-133365633 | 用户更改密码 | POST /api/accounts/change_password.json + { + Name: "change-password", + Description: tr.T("cmd.user.change_password.short"), + Long: tr.T("cmd.user.change_password.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "old-password", Usage: tr.T("flag.user.old_password"), Required: true}, + {Name: "new-password", Usage: tr.T("flag.user.new_password"), Required: true}, + {Name: "new-password-repeat", Usage: tr.T("flag.user.new_password_repeat"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + oldPwd, err := ctx.RequireArg("old-password") + if err != nil { + return err + } + newPwd, err := ctx.RequireArg("new-password") + if err != nil { + return err + } + newPwdRepeat, err := ctx.RequireArg("new-password-repeat") + if err != nil { + return err + } + body := map[string]interface{}{ + "login": login, + "old_password": oldPwd, + "password": newPwd, + "new_password_repeat": newPwdRepeat, + } + env, err := ctx.CallAPI("POST", "/accounts/change_password", body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-218476052 | 用户注销检测 | POST /api/v1/{owner}/check_user_can_delete.json + { + Name: "check-delete", + Description: tr.T("cmd.user.check_delete.short"), + Long: tr.T("cmd.user.check_delete.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/check_user_can_delete", login), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + // api-218481003 | 用户注销 | DELETE /api/v1/{owner}.json + { + Name: "delete-account", + Description: tr.T("cmd.user.delete_account.short"), + Long: tr.T("cmd.user.delete_account.long"), + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: tr.T("flag.user.login"), Required: true}, + {Name: "password", Short: "p", Usage: tr.T("flag.user.password"), Required: true}, + {Name: "memo", Usage: tr.T("flag.user.delete_memo"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + password, err := ctx.RequireArg("password") + if err != nil { + return err + } + memo, err := ctx.RequireArg("memo") + if err != nil { + return err + } + body := map[string]interface{}{ + "password": password, + "memo": memo, + } + env, err := ctx.CallAPI("DELETE", fmt.Sprintf("/v1/%s", login), body) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, } } @@ -48,3 +1075,44 @@ func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { } return i18n.Default() } + +func parseIntArray(s string) []int { + parts := strings.Split(s, ",") + var result []int + for _, p := range parts { + p = strings.TrimSpace(p) + if p == "" { + continue + } + if n, err := strconv.Atoi(p); err == nil { + result = append(result, n) + } + } + return result +} + +func parseStringArray(s string) []string { + parts := strings.Split(s, ",") + var result []string + for _, p := range parts { + p = strings.TrimSpace(p) + if p != "" { + result = append(result, p) + } + } + return result +} + +// buildMessageSettingBody builds a map of message setting categories. +// Keys from "on" categories are set to true, keys from "off" categories are set to false. +// If neither list is provided, returns an empty map (server may reject or ignore). +func buildMessageSettingBody(onCategories, offCategories []string) map[string]interface{} { + body := map[string]interface{}{} + for _, key := range onCategories { + body[key] = true + } + for _, key := range offCategories { + body[key] = false + } + return body +} diff --git a/shortcuts/user/user_test.go b/shortcuts/user/user_test.go index 44d504f..6f146d0 100644 --- a/shortcuts/user/user_test.go +++ b/shortcuts/user/user_test.go @@ -60,6 +60,44 @@ func TestUserMe(t *testing.T) { } } +// --- current-info --- + +func TestUserCurrentInfo(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/users/get_user_info.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(w, map[string]interface{}{ + "username": "testuser", + "real_name": "Test User", + "nickname": "Test", + "login": "testuser", + "user_id": float64(110), + "email": "test@example.com", + "admin": false, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "current-info", nil) + if err != nil { + t.Fatalf("current-info failed: %v", err) + } +} + +func TestUserCurrentInfoHTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("unauthorized")) + })) + defer server.Close() + + err := runShortcut(t, server, "current-info", nil) + if err == nil { + t.Fatal("expected error for HTTP 401") + } +} + // --- info --- func TestUserInfo(t *testing.T) { @@ -119,3 +157,1679 @@ func TestUserInfoHTTPError(t *testing.T) { t.Fatal("expected error for HTTP 500") } } + +// --- update --- + +func TestUserUpdate(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "PUT" { + t.Fatalf("expected PUT, got %s", r.Method) + } + if r.URL.Path != "/users/alice.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(w, map[string]interface{}{ + "user_id": float64(110), + "name": "AliceNew", + "username": "AliceNew", + "login": "alice", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "update", map[string]string{ + "login": "alice", + "nickname": "AliceNew", + "gender": "1", + "province": "Beijing", + "city": "Beijing", + }) + if err != nil { + t.Fatalf("update failed: %v", err) + } +} + +func TestUserUpdateGenderAsInteger(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "PUT" { + t.Fatalf("expected PUT, got %s", r.Method) + } + // Verify gender is sent as integer + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + userAttrs := body["user"].(map[string]interface{}) + extAttrs := userAttrs["user_extension_attributes"].(map[string]interface{}) + if gender, ok := extAttrs["gender"].(float64); !ok || gender != 1 { + t.Fatalf("expected gender as float64(1), got %v (%T)", extAttrs["gender"], extAttrs["gender"]) + } + writeJSON(w, map[string]interface{}{ + "user_id": float64(110), + "name": "Alice", + "username": "Alice", + "login": "alice", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "update", map[string]string{ + "login": "alice", + "gender": "1", + }) + if err != nil { + t.Fatalf("update with gender failed: %v", err) + } +} + +func TestUserUpdateMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "update", map[string]string{}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +// --- projects --- + +func TestUserProjects(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/v1/alice/projects.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if got := r.URL.Query().Get("page"); got != "1" { + t.Fatalf("page = %q, want 1", got) + } + if got := r.URL.Query().Get("limit"); got != "10" { + t.Fatalf("limit = %q, want 10", got) + } + if got := r.URL.Query().Get("category"); got != "created" { + t.Fatalf("category = %q, want created", got) + } + writeJSON(w, map[string]interface{}{ + "total_count": float64(2), + "projects": []interface{}{ + map[string]interface{}{"name": "proj1"}, + map[string]interface{}{"name": "proj2"}, + }, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "projects", map[string]string{ + "login": "alice", + "page": "1", + "limit": "10", + "category": "created", + }) + if err != nil { + t.Fatalf("projects failed: %v", err) + } +} + +func TestUserProjectsWithTimestampFilters(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if got := r.URL.Query().Get("start_at"); got != "1700000000" { + t.Fatalf("start_at = %q, want 1700000000", got) + } + if got := r.URL.Query().Get("end_at"); got != "1710000000" { + t.Fatalf("end_at = %q, want 1710000000", got) + } + writeJSON(w, map[string]interface{}{ + "total_count": float64(1), + "projects": []interface{}{}, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "projects", map[string]string{ + "login": "alice", + "start-at": "1700000000", + "end-at": "1710000000", + }) + if err != nil { + t.Fatalf("projects with timestamps failed: %v", err) + } +} + +func TestUserProjectsMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "projects", map[string]string{}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +func TestUserProjectsHTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) + })) + defer server.Close() + + err := runShortcut(t, server, "projects", map[string]string{"login": "alice"}) + if err == nil { + t.Fatal("expected error for HTTP 500") + } +} + +// --- messages-list --- + +func TestUserMessagesList(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/users/alice/messages.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if got := r.URL.Query().Get("type"); got != "notification" { + t.Fatalf("type = %q, want notification", got) + } + if got := r.URL.Query().Get("status"); got != "1" { + t.Fatalf("status = %q, want 1", got) + } + writeJSON(w, map[string]interface{}{ + "total_count": float64(1), + "type": "notification", + "unread_notification": float64(1), + "unread_atme": float64(0), + "messages": []interface{}{ + map[string]interface{}{ + "id": float64(20156), + "status": float64(1), + "content": "test message", + "type": "notification", + }, + }, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "messages-list", map[string]string{ + "login": "alice", + "type": "notification", + "status": "1", + }) + if err != nil { + t.Fatalf("messages-list failed: %v", err) + } +} + +func TestUserMessagesListMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "messages-list", map[string]string{}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +// --- messages-read --- + +func TestUserMessagesRead(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/users/alice/messages/read.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + if body["type"] != "notification" { + t.Fatalf("body type = %v, want notification", body["type"]) + } + ids := body["ids"].([]interface{}) + if len(ids) != 1 || ids[0].(float64) != -1 { + t.Fatalf("body ids = %v, want [-1]", body["ids"]) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "messages-read", map[string]string{ + "login": "alice", + "type": "notification", + "ids": "-1", + }) + if err != nil { + t.Fatalf("messages-read failed: %v", err) + } +} + +func TestUserMessagesReadMultipleIDs(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + ids := body["ids"].([]interface{}) + if len(ids) != 3 { + t.Fatalf("expected 3 ids, got %d: %v", len(ids), ids) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "messages-read", map[string]string{ + "login": "alice", + "type": "atme", + "ids": "101,102,103", + }) + if err != nil { + t.Fatalf("messages-read with multiple ids failed: %v", err) + } +} + +// --- messages-delete --- + +func TestUserMessagesDelete(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "DELETE" { + t.Fatalf("expected DELETE, got %s", r.Method) + } + if r.URL.Path != "/users/alice/messages.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + if body["type"] != "atme" { + t.Fatalf("body type = %v, want atme", body["type"]) + } + ids := body["ids"].([]interface{}) + if len(ids) != 2 { + t.Fatalf("expected 2 ids, got %d: %v", len(ids), ids) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "messages-delete", map[string]string{ + "login": "alice", + "type": "atme", + "ids": "201,202", + }) + if err != nil { + t.Fatalf("messages-delete failed: %v", err) + } +} + +func TestUserMessagesDeleteAll(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "DELETE" { + t.Fatalf("expected DELETE, got %s", r.Method) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + ids := body["ids"].([]interface{}) + if len(ids) != 1 || ids[0].(float64) != -1 { + t.Fatalf("expected ids=[-1], got %v", ids) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "messages-delete", map[string]string{ + "login": "alice", + "type": "notification", + "ids": "-1", + }) + if err != nil { + t.Fatalf("messages-delete all failed: %v", err) + } +} + +// --- messages-create --- + +func TestUserMessagesCreate(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/users/alice/messages.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + if body["type"] != "atme" { + t.Fatalf("body type = %v, want atme", body["type"]) + } + if body["atmeable_type"] != "Issue" { + t.Fatalf("body atmeable_type = %v, want Issue", body["atmeable_type"]) + } + if body["atmeable_id"].(float64) != 123 { + t.Fatalf("body atmeable_id = %v, want 123", body["atmeable_id"]) + } + receivers := body["receivers_login"].([]interface{}) + if len(receivers) != 2 { + t.Fatalf("expected 2 receivers, got %d: %v", len(receivers), receivers) + } + if receivers[0].(string) != "bob" { + t.Fatalf("receivers[0] = %v, want bob", receivers[0]) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "messages-create", map[string]string{ + "login": "alice", + "receivers": "bob,carol", + "atmeable-type": "Issue", + "atmeable-id": "123", + }) + if err != nil { + t.Fatalf("messages-create failed: %v", err) + } +} + +// --- platform-settings --- + +func TestUserPlatformSettings(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/template_message_settings.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "响应成功", + "setting_types": []interface{}{ + map[string]interface{}{ + "type": "TemplateMessageSetting::Normal", + "type_name": "我的状态", + "total_settings_count": float64(5), + "settings": []interface{}{ + map[string]interface{}{ + "name": "账号有权限变更", + "key": "Permission", + "notification_disabled": true, + "email_disabled": false, + }, + }, + }, + }, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "platform-settings", nil) + if err != nil { + t.Fatalf("platform-settings failed: %v", err) + } +} + +// --- user-settings --- + +func TestUserUserSettings(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/users/alice/template_message_settings.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "响应成功", + "user": map[string]interface{}{ + "id": float64(110), + "type": "User", + "name": "TestUser", + "login": "alice", + "image_url": "system/letter_avatars/2/A/120.png", + }, + "notification_body": map[string]interface{}{ + "Normal::Permission": true, + }, + "email_body": map[string]interface{}{ + "Normal::Permission": false, + }, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "user-settings", map[string]string{ + "login": "alice", + }) + if err != nil { + t.Fatalf("user-settings failed: %v", err) + } +} + +func TestUserUserSettingsMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "user-settings", map[string]string{}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +// --- update-settings --- + +func TestUserUpdateSettings(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/users/alice/template_message_settings/update_setting.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + setting := body["setting"].(map[string]interface{}) + notif := setting["notification_body"].(map[string]interface{}) + email := setting["email_body"].(map[string]interface{}) + if notif["CreateOrAssign::IssueAssigned"] != true { + t.Fatalf("notification CreateOrAssign::IssueAssigned = %v, want true", notif["CreateOrAssign::IssueAssigned"]) + } + if notif["ManageProject::Issue"] != false { + t.Fatalf("notification ManageProject::Issue = %v, want false", notif["ManageProject::Issue"]) + } + if email["Normal::Permission"] != true { + t.Fatalf("email Normal::Permission = %v, want true", email["Normal::Permission"]) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "响应成功", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "update-settings", map[string]string{ + "login": "alice", + "notification": "CreateOrAssign::IssueAssigned", + "notification-off": "ManageProject::Issue", + "email": "Normal::Permission", + }) + if err != nil { + t.Fatalf("update-settings failed: %v", err) + } +} + +func TestUserUpdateSettingsMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "update-settings", map[string]string{}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +// --- pinned-list --- + +func TestUserPinnedList(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/users/alice/is_pinned_projects.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(w, map[string]interface{}{ + "total_count": float64(1), + "projects": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "project_id": float64(17), + "name": "testdevops", + "identifier": "testdevops", + "is_pinned": true, + "position": float64(0), + }, + }, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "pinned-list", map[string]string{"login": "alice"}) + if err != nil { + t.Fatalf("pinned-list failed: %v", err) + } +} + +func TestUserPinnedListMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "pinned-list", map[string]string{}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +// --- pinned-update --- + +func TestUserPinnedUpdate(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/users/alice/is_pinned_projects/pin.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + ids := body["is_pinned_project_ids"].([]interface{}) + if len(ids) != 2 { + t.Fatalf("expected 2 project ids, got %d: %v", len(ids), ids) + } + if ids[0].(float64) != 17 || ids[1].(float64) != 23 { + t.Fatalf("ids = %v, want [17 23]", ids) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "pinned-update", map[string]string{ + "login": "alice", + "project-ids": "17,23", + }) + if err != nil { + t.Fatalf("pinned-update failed: %v", err) + } +} + +func TestUserPinnedUpdateMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "pinned-update", map[string]string{"project-ids": "17"}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +func TestUserPinnedUpdateMissingProjectIDs(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "pinned-update", map[string]string{"login": "alice"}) + if err == nil { + t.Fatal("expected error for missing project-ids") + } +} + +// --- pinned-sort --- + +func TestUserPinnedSort(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "PUT" { + t.Fatalf("expected PUT, got %s", r.Method) + } + if r.URL.Path != "/users/alice/is_pinned_projects/5.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + pp := body["pinned_project"].(map[string]interface{}) + if pp["position"].(float64) != 10 { + t.Fatalf("position = %v, want 10", pp["position"]) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "pinned-sort", map[string]string{ + "login": "alice", + "pinned-id": "5", + "position": "10", + }) + if err != nil { + t.Fatalf("pinned-sort failed: %v", err) + } +} + +func TestUserPinnedSortMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "pinned-sort", map[string]string{ + "pinned-id": "5", + "position": "10", + }) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +func TestUserPinnedSortInvalidPosition(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "pinned-sort", map[string]string{ + "login": "alice", + "pinned-id": "5", + "position": "abc", + }) + if err == nil { + t.Fatal("expected error for invalid position") + } +} + +// --- feedback --- + +func TestUserFeedback(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/v1/alice/feedbacks.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + if body["content"] != "测试反馈意见" { + t.Fatalf("content = %v, want 测试反馈意见", body["content"]) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "feedback", map[string]string{ + "login": "alice", + "content": "测试反馈意见", + }) + if err != nil { + t.Fatalf("feedback failed: %v", err) + } +} + +func TestUserFeedbackMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "feedback", map[string]string{"content": "some feedback"}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +func TestUserFeedbackMissingContent(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "feedback", map[string]string{"login": "alice"}) + if err == nil { + t.Fatal("expected error for missing content") + } +} + +func TestUserFeedbackHTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) + })) + defer server.Close() + + err := runShortcut(t, server, "feedback", map[string]string{ + "login": "alice", + "content": "test", + }) + if err == nil { + t.Fatal("expected error for HTTP 500") + } +} + +// --- transfer-list --- + +func TestUserTransferList(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/users/alice/applied_transfer_projects.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if got := r.URL.Query().Get("page"); got != "1" { + t.Fatalf("page = %q, want 1", got) + } + if got := r.URL.Query().Get("per_page"); got != "15" { + t.Fatalf("per_page = %q, want 15", got) + } + writeJSON(w, map[string]interface{}{ + "total_count": float64(1), + "applied_transfer_projects": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "status": "common", + "project": map[string]interface{}{ + "id": float64(12), + "identifier": "testproj", + "name": "testproj", + }, + }, + }, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "transfer-list", map[string]string{ + "login": "alice", + "page": "1", + "per-page": "15", + }) + if err != nil { + t.Fatalf("transfer-list failed: %v", err) + } +} + +func TestUserTransferListMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "transfer-list", map[string]string{}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +func TestUserTransferListHTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) + })) + defer server.Close() + + err := runShortcut(t, server, "transfer-list", map[string]string{"login": "alice"}) + if err == nil { + t.Fatal("expected error for HTTP 500") + } +} + +// --- transfer-accept --- + +func TestUserTransferAccept(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/users/alice/applied_transfer_projects/42/accept.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(w, map[string]interface{}{ + "id": float64(42), + "status": "accept", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "transfer-accept", map[string]string{ + "login": "alice", + "transfer-id": "42", + }) + if err != nil { + t.Fatalf("transfer-accept failed: %v", err) + } +} + +func TestUserTransferAcceptMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "transfer-accept", map[string]string{"transfer-id": "42"}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +func TestUserTransferAcceptMissingTransferID(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "transfer-accept", map[string]string{"login": "alice"}) + if err == nil { + t.Fatal("expected error for missing transfer-id") + } +} + +// --- transfer-refuse --- + +func TestUserTransferRefuse(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/users/alice/applied_transfer_projects/42/refuse.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(w, map[string]interface{}{ + "id": float64(42), + "status": "refuse", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "transfer-refuse", map[string]string{ + "login": "alice", + "transfer-id": "42", + }) + if err != nil { + t.Fatalf("transfer-refuse failed: %v", err) + } +} + +func TestUserTransferRefuseMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "transfer-refuse", map[string]string{"transfer-id": "42"}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +// --- send-email-code --- + +func TestUserSendEmailCode(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/v1/alice/send_email_vefify_code.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + if body["code_type"].(float64) != 10 { + t.Fatalf("code_type = %v, want 10", body["code_type"]) + } + if body["email"] != "new@example.com" { + t.Fatalf("email = %v, want new@example.com", body["email"]) + } + if body["smscode"] != "abc123md5" { + t.Fatalf("smscode = %v, want abc123md5", body["smscode"]) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "send-email-code", map[string]string{ + "login": "alice", + "code-type": "10", + "email": "new@example.com", + "email-md5": "abc123md5", + }) + if err != nil { + t.Fatalf("send-email-code failed: %v", err) + } +} + +func TestUserSendEmailCodeMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "send-email-code", map[string]string{ + "code-type": "10", + "email": "new@example.com", + "email-md5": "abc123md5", + }) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +func TestUserSendEmailCodeInvalidCodeType(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "send-email-code", map[string]string{ + "login": "alice", + "code-type": "abc", + "email": "new@example.com", + "email-md5": "abc123md5", + }) + if err == nil { + t.Fatal("expected error for invalid code-type") + } +} + +// --- verify-email-code --- + +func TestUserVerifyEmailCode(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/v1/alice/check_email_verify_code.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + if body["code_type"].(float64) != 10 { + t.Fatalf("code_type = %v, want 10", body["code_type"]) + } + if body["email"] != "new@example.com" { + t.Fatalf("email = %v, want new@example.com", body["email"]) + } + if body["code"] != "123456" { + t.Fatalf("code = %v, want 123456", body["code"]) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "verify-email-code", map[string]string{ + "login": "alice", + "code-type": "10", + "email": "new@example.com", + "code": "123456", + }) + if err != nil { + t.Fatalf("verify-email-code failed: %v", err) + } +} + +func TestUserVerifyEmailCodeMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "verify-email-code", map[string]string{ + "code-type": "10", + "email": "new@example.com", + "code": "123456", + }) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +func TestUserVerifyEmailCodeInvalidCodeType(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "verify-email-code", map[string]string{ + "login": "alice", + "code-type": "notanumber", + "email": "new@example.com", + "code": "123456", + }) + if err == nil { + t.Fatal("expected error for invalid code-type") + } +} + +// --- check-password --- + +func TestUserCheckPassword(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/v1/alice/check_password.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + if body["password"] != "mysecretpass" { + t.Fatalf("password = %v, want mysecretpass", body["password"]) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "check-password", map[string]string{ + "login": "alice", + "password": "mysecretpass", + }) + if err != nil { + t.Fatalf("check-password failed: %v", err) + } +} + +func TestUserCheckPasswordWrong(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + writeJSON(w, map[string]interface{}{ + "status": float64(-5), + "message": "密码错误", + }) + })) + defer server.Close() + + // Wrong password returns API error (status -5), which Client.Do surfaces as an error. + err := runShortcut(t, server, "check-password", map[string]string{ + "login": "alice", + "password": "wrongpass", + }) + if err == nil { + t.Fatal("expected error for wrong password") + } +} + +func TestUserCheckPasswordMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "check-password", map[string]string{"password": "abc"}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +func TestUserCheckPasswordMissingPassword(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "check-password", map[string]string{"login": "alice"}) + if err == nil { + t.Fatal("expected error for missing password") + } +} + +// --- applied-list --- + +func TestUserAppliedList(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/users/alice/applied_projects.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(w, map[string]interface{}{ + "total_count": 2, + "applied_projects": []interface{}{ + map[string]interface{}{"id": 1, "status": "common"}, + map[string]interface{}{"id": 2, "status": "accept"}, + }, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "applied-list", map[string]string{ + "login": "alice", + }) + if err != nil { + t.Fatalf("applied-list failed: %v", err) + } +} + +func TestUserAppliedListWithPagination(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/users/alice/applied_projects.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.URL.Query().Get("page") != "2" { + t.Fatalf("page = %s, want 2", r.URL.Query().Get("page")) + } + if r.URL.Query().Get("per_page") != "10" { + t.Fatalf("per_page = %s, want 10", r.URL.Query().Get("per_page")) + } + writeJSON(w, map[string]interface{}{ + "total_count": 25, + "applied_projects": []interface{}{}, + }) + })) + defer server.Close() + + err := runShortcut(t, server, "applied-list", map[string]string{ + "login": "alice", + "page": "2", + "per-page": "10", + }) + if err != nil { + t.Fatalf("applied-list with pagination failed: %v", err) + } +} + +func TestUserAppliedListMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "applied-list", map[string]string{}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +// --- applied-accept --- + +func TestUserAppliedAccept(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/users/alice/applied_projects/7/accept.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(w, map[string]interface{}{ + "id": float64(7), + "status": "accept", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "applied-accept", map[string]string{ + "login": "alice", + "applied-id": "7", + }) + if err != nil { + t.Fatalf("applied-accept failed: %v", err) + } +} + +func TestUserAppliedAcceptMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "applied-accept", map[string]string{"applied-id": "7"}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +func TestUserAppliedAcceptMissingAppliedID(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "applied-accept", map[string]string{"login": "alice"}) + if err == nil { + t.Fatal("expected error for missing applied-id") + } +} + +// --- applied-refuse --- + +func TestUserAppliedRefuse(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/users/alice/applied_projects/7/refuse.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(w, map[string]interface{}{ + "id": float64(7), + "status": "refuse", + }) + })) + defer server.Close() + + err := runShortcut(t, server, "applied-refuse", map[string]string{ + "login": "alice", + "applied-id": "7", + }) + if err != nil { + t.Fatalf("applied-refuse failed: %v", err) + } +} + +func TestUserAppliedRefuseMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "applied-refuse", map[string]string{"applied-id": "7"}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +func TestUserAppliedRefuseMissingAppliedID(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "applied-refuse", map[string]string{"login": "alice"}) + if err == nil { + t.Fatal("expected error for missing applied-id") + } +} + +// --- check-email --- + +func TestUserCheckEmail(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/v1/alice/check_email.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + if body["email"] != "new@example.com" { + t.Fatalf("email = %v, want new@example.com", body["email"]) + } + writeJSON(w, map[string]interface{}{"status": float64(0), "message": "success"}) + })) + defer server.Close() + + err := runShortcut(t, server, "check-email", map[string]string{ + "login": "alice", + "email": "new@example.com", + }) + if err != nil { + t.Fatalf("check-email failed: %v", err) + } +} + +func TestUserCheckEmailAlreadyUsed(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + writeJSON(w, map[string]interface{}{"status": float64(-2), "message": "邮箱已被使用"}) + })) + defer server.Close() + + // Already-used email returns API error (status -2), which Client.Do surfaces as an error. + err := runShortcut(t, server, "check-email", map[string]string{ + "login": "alice", + "email": "taken@example.com", + }) + if err == nil { + t.Fatal("expected error for already-used email") + } +} + +func TestUserCheckEmailMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "check-email", map[string]string{"email": "a@b.com"}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +// --- update-email --- + +func TestUserUpdateEmail(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "PATCH" { + t.Fatalf("expected PATCH, got %s", r.Method) + } + if r.URL.Path != "/v1/alice/update_email.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + if body["password"] != "***" { + t.Fatalf("password = %v", body["password"]) + } + if body["email"] != "new@example.com" { + t.Fatalf("email = %v", body["email"]) + } + if body["code"] != "123456" { + t.Fatalf("code = %v", body["code"]) + } + writeJSON(w, map[string]interface{}{"status": float64(0), "message": "success"}) + })) + defer server.Close() + + err := runShortcut(t, server, "update-email", map[string]string{ + "login": "alice", + "password": "***", + "email": "new@example.com", + "code": "123456", + }) + if err != nil { + t.Fatalf("update-email failed: %v", err) + } +} + +func TestUserUpdateEmailMissingArgs(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + for _, missing := range []string{"login", "password", "email", "code"} { + args := map[string]string{ + "login": "alice", + "password": "***", + "email": "new@example.com", + "code": "123456", + } + delete(args, missing) + err := runShortcut(t, server, "update-email", args) + if err == nil { + t.Fatalf("expected error for missing %s", missing) + } + } +} + +// --- update-phone --- + +func TestUserUpdatePhone(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "PATCH" { + t.Fatalf("expected PATCH, got %s", r.Method) + } + if r.URL.Path != "/v1/alice/update_phone.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + if body["phone"] != "13800138000" { + t.Fatalf("phone = %v", body["phone"]) + } + if body["code"] != "654321" { + t.Fatalf("code = %v", body["code"]) + } + if body["password"] != "***" { + t.Fatalf("password = %v", body["password"]) + } + writeJSON(w, map[string]interface{}{"status": float64(0), "message": "success"}) + })) + defer server.Close() + + err := runShortcut(t, server, "update-phone", map[string]string{ + "login": "alice", + "phone": "13800138000", + "code": "654321", + "password": "***", + }) + if err != nil { + t.Fatalf("update-phone failed: %v", err) + } +} + +func TestUserUpdatePhoneMissingArgs(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + for _, missing := range []string{"login", "phone", "code", "password"} { + args := map[string]string{ + "login": "alice", + "phone": "13800138000", + "code": "654321", + "password": "***", + } + delete(args, missing) + err := runShortcut(t, server, "update-phone", args) + if err == nil { + t.Fatalf("expected error for missing %s", missing) + } + } +} + +// --- change-password --- + +func TestUserChangePassword(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/accounts/change_password.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + if body["login"] != "alice" { + t.Fatalf("login = %v", body["login"]) + } + if body["old_password"] != "***" { + t.Fatalf("old_password = %v", body["old_password"]) + } + if body["password"] != "***" { + t.Fatalf("password = %v", body["password"]) + } + if body["new_password_repeat"] != "newpass456" { + t.Fatalf("new_password_repeat = %v", body["new_password_repeat"]) + } + writeJSON(w, map[string]interface{}{"status": float64(0), "message": "success"}) + })) + defer server.Close() + + err := runShortcut(t, server, "change-password", map[string]string{ + "login": "alice", + "old-password": "***", + "new-password": "***", + "new-password-repeat": "newpass456", + }) + if err != nil { + t.Fatalf("change-password failed: %v", err) + } +} + +func TestUserChangePasswordMissingArgs(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + for _, missing := range []string{"login", "old-password", "new-password", "new-password-repeat"} { + args := map[string]string{ + "login": "alice", + "old-password": "***", + "new-password": "***", + "new-password-repeat": "newpass456", + } + delete(args, missing) + err := runShortcut(t, server, "change-password", args) + if err == nil { + t.Fatalf("expected error for missing %s", missing) + } + } +} + +// --- check-delete --- + +func TestUserCheckDelete(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/v1/alice/check_user_can_delete.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + "can_delete": true, + "org_count": float64(0), + "project_count": float64(0), + }) + })) + defer server.Close() + + err := runShortcut(t, server, "check-delete", map[string]string{ + "login": "alice", + }) + if err != nil { + t.Fatalf("check-delete failed: %v", err) + } +} + +func TestUserCheckDeleteCannot(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + writeJSON(w, map[string]interface{}{ + "status": float64(0), + "message": "success", + "can_delete": false, + "org_count": float64(2), + "project_count": float64(5), + }) + })) + defer server.Close() + + err := runShortcut(t, server, "check-delete", map[string]string{ + "login": "alice", + }) + if err != nil { + t.Fatalf("check-delete failed: %v", err) + } +} + +func TestUserCheckDeleteMissingLogin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + err := runShortcut(t, server, "check-delete", map[string]string{}) + if err == nil { + t.Fatal("expected error for missing login") + } +} + +// --- delete-account --- + +func TestUserDeleteAccount(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "DELETE" { + t.Fatalf("expected DELETE, got %s", r.Method) + } + if r.URL.Path != "/v1/alice.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var body map[string]interface{} + json.NewDecoder(r.Body).Decode(&body) + if body["password"] != "***" { + t.Fatalf("password = %v", body["password"]) + } + if body["memo"] != "no longer needed" { + t.Fatalf("memo = %v", body["memo"]) + } + writeJSON(w, map[string]interface{}{"status": float64(0), "message": "success"}) + })) + defer server.Close() + + err := runShortcut(t, server, "delete-account", map[string]string{ + "login": "alice", + "password": "***", + "memo": "no longer needed", + }) + if err != nil { + t.Fatalf("delete-account failed: %v", err) + } +} + +func TestUserDeleteAccountMissingArgs(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("no API call expected") + })) + defer server.Close() + + for _, missing := range []string{"login", "password", "memo"} { + args := map[string]string{ + "login": "alice", + "password": "***", + "memo": "reason", + } + delete(args, missing) + err := runShortcut(t, server, "delete-account", args) + if err == nil { + t.Fatalf("expected error for missing %s", missing) + } + } +} + +func TestUserDeleteAccountServerError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + err := runShortcut(t, server, "delete-account", map[string]string{ + "login": "alice", + "password": "***", + "memo": "bye", + }) + if err == nil { + t.Fatal("expected error for HTTP 500") + } +} diff --git a/skills/gitlink-user/SKILL.md b/skills/gitlink-user/SKILL.md index 0d1aa4f..17f7666 100644 --- a/skills/gitlink-user/SKILL.md +++ b/skills/gitlink-user/SKILL.md @@ -1,7 +1,7 @@ --- name: gitlink-user version: 1.0.0 -description: "用户操作:查看当前用户、用户详情。当用户需要查看 GitLink 用户信息时触发。" +description: "用户操作:查看当前用户、用户详情、关注/取关、更新资料、项目列表、消息设置、星标项目、反馈建议、项目迁移、邮箱验证、密码验证、项目申请、邮箱变更、手机变更、更改密码、注销。当用户需要查看或管理 GitLink 用户信息时触发。" metadata: requires: bins: ["gitlink-cli"] @@ -21,7 +21,36 @@ metadata: | Shortcut | 说明 | 需要认证 | |----------|------|----------| | `user +me` | 当前登录用户 | 是 | +| `user +current-info` | 当前用户详细信息 | 是 | | `user +info` | 查看用户详情 | 否 | +| `user +update` | 更新用户资料 | 是 | +| `user +projects` | 用户项目列表 | 是 | +| `user +messages-list` | 列出用户消息 | 是 | +| `user +messages-read` | 标记消息为已读 | 是 | +| `user +messages-delete` | 删除消息 | 是 | +| `user +messages-create` | 创建@我消息通知 | 是 | +| `user +platform-settings` | 获取平台消息设置模板 | 是 | +| `user +user-settings` | 获取用户消息设置 | 是 | +| `user +update-settings` | 更新用户消息设置(站内信/邮件) | 是 | +| `user +pinned-list` | 列出用户星标项目 | 是 | +| `user +pinned-update` | 批量更新星标项目 | 是 | +| `user +pinned-sort` | 调整星标项目排序 | 是 | +| `user +feedback` | 提交反馈建议 | 是 | +| `user +transfer-list` | 列出项目迁移请求 | 是 | +| `user +transfer-accept` | 接受项目迁移 | 是 | +| `user +transfer-refuse` | 拒绝项目迁移 | 是 | +| `user +send-email-code` | 发送邮箱验证码 | 是 | +| `user +verify-email-code` | 验证邮箱验证码 | 是 | +| `user +check-password` | 验证用户密码 | 是 | +| `user +applied-list` | 列出项目申请 | 是 | +| `user +applied-accept` | 接受项目申请 | 是 | +| `user +applied-refuse` | 拒绝项目申请 | 是 | +| `user +check-email` | 验证邮箱可用性 | 是 | +| `user +update-email` | 更改邮箱 | 是 | +| `user +update-phone` | 更改手机号 | 是 | +| `user +change-password` | 更改密码 | 是 | +| `user +check-delete` | 检测账号是否可注销 | 是 | +| `user +delete-account` | 注销账号(不可恢复) | 是 | ## 使用示例 @@ -29,10 +58,120 @@ metadata: # 查看当前用户 gitlink-cli user +me +# 获取当前用户详细信息 +gitlink-cli user +current-info + # 查看其他用户 gitlink-cli user +info --login zhangsan + +# 更新用户资料 +gitlink-cli user +update --login yystopf --nickname "新昵称" --gender 0 --province "广东" --city "深圳" --description "个性签名" --custom-department "某单位" + +# 列出用户项目 +gitlink-cli user +projects --login zhangsan --page 1 --limit 15 --category created + +# 搜索用户项目 +gitlink-cli user +projects --login zhangsan --search "关键词" --sort-by updated_at --sort-direction desc + +# 列出用户消息 +gitlink-cli user +messages-list --login alice --type notification --status 1 + +# 标记消息已读(全部) +gitlink-cli user +messages-read --login alice --type notification --ids -1 + +# 标记指定消息已读 +gitlink-cli user +messages-read --login alice --type atme --ids 101,102,103 + +# 删除消息 +gitlink-cli user +messages-delete --login alice --type atme --ids 201,202 + +# 创建@我消息通知 +gitlink-cli user +messages-create --login alice --receivers bob,carol --atmeable-type Issue --atmeable-id 123 + +# 获取平台消息设置模板 +gitlink-cli user +platform-settings + +# 获取用户消息设置 +gitlink-cli user +user-settings --login alice + +# 更新用户消息设置(开启站内信、关闭邮件) +gitlink-cli user +update-settings --login alice \ + --notification "CreateOrAssign::IssueAssigned,ManageProject::Issue" \ + --email "Normal::Permission" \ + --notification-off "ManageProject::Member" \ + --email-off "Normal::Organization" + +# 列出用户星标项目 +gitlink-cli user +pinned-list --login alice + +# 批量更新星标项目 +gitlink-cli user +pinned-update --login alice --project-ids 17,23,42 + +# 调整星标项目排序(数字越大越靠前) +gitlink-cli user +pinned-sort --login alice --pinned-id 5 --position 10 + +# 提交反馈建议 +gitlink-cli user +feedback --login alice --content "希望增加批量操作功能" + +# 接受项目迁移 +gitlink-cli user +transfer-accept --login alice --transfer-id 42 + +# 拒绝项目迁移 +gitlink-cli user +transfer-refuse --login alice --transfer-id 42 + +# 发送邮箱验证码(用于更新邮箱) +gitlink-cli user +send-email-code --login alice --code-type 10 --email new@example.com --email-md5 abc123md5hash + +# 验证邮箱验证码 +gitlink-cli user +verify-email-code --login alice --code-type 10 --email new@example.com --code 123456 + +# 验证用户密码 +gitlink-cli user +check-password --login alice --password mysecretpass + +# 列出项目申请(支持分页) +gitlink-cli user +applied-list --login alice --page 1 --per-page 10 + +# 接受项目申请 +gitlink-cli user +applied-accept --login alice --applied-id 7 + +# 拒绝项目申请 +gitlink-cli user +applied-refuse --login alice --applied-id 7 + +# 检查邮箱是否可用 +gitlink-cli user +check-email --login alice --email new@example.com + +# 更改邮箱(需先发送验证码) +gitlink-cli user +update-email --login alice --password *** --email new@example.com --code 123456 + +# 更改手机号(需手机验证码) +gitlink-cli user +update-phone --login alice --phone 13800138000 --code 654321 --password *** + +# 更改密码 +gitlink-cli user +change-password --login alice --old-password *** --new-password *** --new-password-repeat newpass456 + +# 检测账号是否可注销 +gitlink-cli user +check-delete --login alice + +# 注销账号(⚠️ 不可恢复) +gitlink-cli user +delete-account --login alice --password *** --memo "不再使用" ``` +## 消息设置分类 + +更新消息设置(`+update-settings`)时,`--notification` / `--email` / `--notification-off` / `--email-off` 支持以下分类 key: + +| 分类 Key | 说明 | +|----------|------| +| `CreateOrAssign::IssueAssigned` | 创建或指派给我的疑修 | +| `CreateOrAssign::PullRequestAssigned` | 创建或指派给我的合并请求 | +| `ManageProject::Issue` | 我管理的项目的疑修 | +| `ManageProject::PullRequest` | 我管理的项目的合并请求 | +| `ManageProject::Member` | 我管理的项目的成员变更 | +| `ManageProject::SettingChanged` | 我管理的项目的设置变更 | +| `Normal::Organization` | 组织相关通知 | +| `Normal::Project` | 项目相关通知 | +| `Normal::Permission` | 权限变更通知 | + ## Raw API 补充 ```bash