Documentation
¶
Overview ¶
Package errcode registers octo-server private business error codes.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrAppBotRequestInvalid is the catch-all for missing/malformed request // input (BindJSON failure, invalid robot_uid, empty update). The offending // field is surfaced via Details when identifiable. ErrAppBotRequestInvalid = register(codes.Code{ ID: "err.server.app_bot.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) // ErrAppBotIDInvalid covers a bot id that fails the format rule // (^[a-z0-9][a-z0-9_-]{0,29}$) or collides with a reserved id. ErrAppBotIDInvalid = register(codes.Code{ ID: "err.server.app_bot.id_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid bot id.", }) // ErrAppBotNotFound covers a missing/scope-mismatched bot, and (on the // apply/discover flow) a bot that does not exist or is not published. ErrAppBotNotFound = register(codes.Code{ ID: "err.server.app_bot.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Bot not found.", }) // ErrAppBotIDConflict covers a create colliding with an in-use bot id. ErrAppBotIDConflict = register(codes.Code{ ID: "err.server.app_bot.id_conflict", HTTPStatus: http.StatusConflict, DefaultMessage: "The bot id is already in use.", }) // ErrAppBotTokenRotationConflict covers a token rotation that lost the // optimistic-lock race (the token changed under us); retryable. ErrAppBotTokenRotationConflict = register(codes.Code{ ID: "err.server.app_bot.token_rotation_conflict", HTTPStatus: http.StatusConflict, DefaultMessage: "The token was rotated by another request; please retry.", }) // ErrAppBotQueryFailed covers read-path failures (bot list/detail SELECTs, // space-member and friend-relation lookups). Log the underlying err first. ErrAppBotQueryFailed = register(codes.Code{ ID: "err.server.app_bot.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query data.", Internal: true, }) // ErrAppBotStoreFailed covers mutation-path failures (bot create/update/ // delete/publish, token update, user-record and friend-relation creation). // Log the underlying err first. ErrAppBotStoreFailed = register(codes.Code{ ID: "err.server.app_bot.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update data.", Internal: true, }) // ErrAppBotIMTokenFailed covers IM-token issuance failures on bot create and // token rotation. Log the underlying err first. ErrAppBotIMTokenFailed = register(codes.Code{ ID: "err.server.app_bot.im_token_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to obtain an IM token.", Internal: true, }) // ErrAppBotInternal covers token generation failures and broken invariants // (e.g. a space bot missing its space_id). Log the underlying err first. ErrAppBotInternal = register(codes.Code{ ID: "err.server.app_bot.internal", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Internal error.", Internal: true, }) )
err.server.app_bot.* — modules/app_bot business error codes. These endpoints serve the management console (/v1/admin/app_bot, gated admin∪superAdmin) and space owners/admins (/v1/space/:space_id/app_bot), so — like bot_api — many sites returned real HTTP status codes the console branches on (404/409). Those are rendered via httperr.ResponseErrorLWithStatus to PRESERVE the wire status rather than the D14 fixed-400 path.
DefaultMessage holds the en-US source (D4); the zh-CN runtime translation lives in pkg/i18n/locales/active.zh-CN.toml. Internal=true codes never surface their message on the wire — callers MUST log the underlying err with full context (zap.Error) before responding.
var ( // ErrBotAPIRequestInvalid is the catch-all for missing/malformed request // input (BindJSON failure, "X 不能为空", invalid id/format, empty payload). // The offending field is surfaced via Details when identifiable. ErrBotAPIRequestInvalid = register(codes.Code{ ID: "err.server.bot_api.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) // ErrBotAPILimitExceeded covers a list parameter exceeding its cap // (members > 200, message_ids > 100). The cap is surfaced so the client can // render a localized hint without hard-coding it. ErrBotAPILimitExceeded = register(codes.Code{ ID: "err.server.bot_api.limit_exceeded", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The request exceeds the maximum allowed items.", SafeDetailKeys: []string{"field", "max"}, }) // ErrBotAPIContentTooLarge covers a content body exceeding its byte/char cap // (GROUP.md content, voice vocabulary context, OBO persona_prompt). The cap // is surfaced via Details; the raw content never is. ErrBotAPIContentTooLarge = register(codes.Code{ ID: "err.server.bot_api.content_too_large", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The content exceeds the maximum allowed size.", SafeDetailKeys: []string{"field", "max_size", "max_bytes"}, }) // ErrBotAPIFileTooLarge surfaces the upload size cap (in MB) for the legacy // (wire-400) upload path. ErrBotAPIFileTooLarge = register(codes.Code{ ID: "err.server.bot_api.file_too_large", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The file exceeds the maximum allowed size.", SafeDetailKeys: []string{"max_mb"}, }) // ErrBotAPIPayloadTooLarge is the status-preserving (413) variant used by the // voice transcribe proxy, whose external client branches on HTTP 413. ErrBotAPIPayloadTooLarge = register(codes.Code{ ID: "err.server.bot_api.payload_too_large", HTTPStatus: http.StatusRequestEntityTooLarge, DefaultMessage: "The uploaded file exceeds the maximum allowed size.", SafeDetailKeys: []string{"max_bytes"}, }) // ErrBotAPIFileTypeUnsupported covers an unsupported / extension-less upload. ErrBotAPIFileTypeUnsupported = register(codes.Code{ ID: "err.server.bot_api.file_type_unsupported", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Unsupported file type.", }) // ErrBotAPIMemberNotHuman covers the bot-API rule that only human users may // be added to a group through this API (no bot members, creator must not be a // bot). ErrBotAPIMemberNotHuman = register(codes.Code{ ID: "err.server.bot_api.member_not_human", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Only human members can be added through the bot API.", SafeDetailKeys: []string{"field"}, }) // ErrBotAPIThreadChannelNotAccepted rejects a thread channel id supplied to a // group-level GROUP.md endpoint (the caller must use the thread md route). ErrBotAPIThreadChannelNotAccepted = register(codes.Code{ ID: "err.server.bot_api.thread_channel_not_accepted", HTTPStatus: http.StatusBadRequest, DefaultMessage: "A thread channel id is not accepted here; use the thread GROUP.md endpoint instead.", }) // ErrBotAPIBotNotProvisioned covers a bot that cannot serve the request // because it is not fully provisioned (no owner, or not in any active space) // — a configuration gap, surfaced as a 400 to the voice client. ErrBotAPIBotNotProvisioned = register(codes.Code{ ID: "err.server.bot_api.bot_not_provisioned", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The bot is not fully provisioned for this operation.", }) // ErrBotAPIOBOModeUnsupported covers an OBO grant created/updated with a mode // other than the only supported value (auto, v0). ErrBotAPIOBOModeUnsupported = register(codes.Code{ ID: "err.server.bot_api.obo_mode_unsupported", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Unsupported OBO mode.", }) // ErrBotAPIOBOReservedField rejects an inbound message payload that carries a // server-only reserved OBO key (__obo_* / obo_* / actual_sender_uid). ErrBotAPIOBOReservedField = register(codes.Code{ ID: "err.server.bot_api.obo_reserved_field", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The payload uses reserved OBO fields.", }) // ErrBotAPINotGroupMember covers the bot-not-a-group-member guard. ErrBotAPINotGroupMember = register(codes.Code{ ID: "err.server.bot_api.not_group_member", HTTPStatus: http.StatusForbidden, DefaultMessage: "The bot is not a member of this group.", }) // ErrBotAPIGroupDisbanded covers the disbanded-group send guard: after a // group is disbanded (WeChat-Work style — history preserved, everyone // read-only), no one (including bots) may send. The deployed WuKongIM // /message/send returns HTTP 200 with no failure signal on a disband // rejection, so octo-server self-checks group.status here and returns this // explicit error instead of letting the bot believe the send succeeded. ErrBotAPIGroupDisbanded = register(codes.Code{ ID: "err.server.bot_api.group_disbanded", HTTPStatus: http.StatusForbidden, DefaultMessage: "The group has been disbanded; messages can no longer be sent.", }) // ErrBotAPINotGroupAdmin covers the bot-not-a-group-admin guard. ErrBotAPINotGroupAdmin = register(codes.Code{ ID: "err.server.bot_api.not_group_admin", HTTPStatus: http.StatusForbidden, DefaultMessage: "The bot is not an admin of this group.", }) // ErrBotAPICannotRemovePrivileged covers the bot-API member-remove role // guard: a bot admin is manager-level at most, so it may not remove the // group owner or a manager (mirrors the Web API memberRemove rule where a // manager cannot kick managers/creator). The first offending uid is // surfaced via Details so the adapter can pinpoint the rejected target. ErrBotAPICannotRemovePrivileged = register(codes.Code{ ID: "err.server.bot_api.cannot_remove_privileged", HTTPStatus: http.StatusForbidden, DefaultMessage: "The group owner and managers cannot be removed through the bot API.", SafeDetailKeys: []string{"uid"}, }) // ErrBotAPINotSpaceMember covers the bot/user-not-a-space-member guard. ErrBotAPINotSpaceMember = register(codes.Code{ ID: "err.server.bot_api.not_space_member", HTTPStatus: http.StatusForbidden, DefaultMessage: "Not a member of this space.", }) // ErrBotAPIAppBotUnsupported covers operations an App Bot may not perform // (group operations, voice operations) — App Bots are DM-scoped. ErrBotAPIAppBotUnsupported = register(codes.Code{ ID: "err.server.bot_api.app_bot_unsupported", HTTPStatus: http.StatusForbidden, DefaultMessage: "This operation is not supported for app bots.", }) // ErrBotAPIAppBotDMOnly covers the App Bot direct-message-only restriction // (App Bots may only access / edit direct message channels). ErrBotAPIAppBotDMOnly = register(codes.Code{ ID: "err.server.bot_api.app_bot_dm_only", HTTPStatus: http.StatusForbidden, DefaultMessage: "App bots can only operate on direct message channels.", }) // ErrBotAPINotFriend covers the user-bot friend gate (the target user has not // added / opted into the bot). ErrBotAPINotFriend = register(codes.Code{ ID: "err.server.bot_api.not_friend", HTTPStatus: http.StatusForbidden, DefaultMessage: "The bot is not a friend of this user.", }) // ErrBotAPIConversationNotStarted covers the App Bot gate where the user has // not started a conversation with the bot yet. ErrBotAPIConversationNotStarted = register(codes.Code{ ID: "err.server.bot_api.conversation_not_started", HTTPStatus: http.StatusForbidden, DefaultMessage: "The user has not started a conversation with this bot.", }) // ErrBotAPIMessageEditForbidden covers the bot-message edit guard (a bot may // only edit messages it sent). ErrBotAPIMessageEditForbidden = register(codes.Code{ ID: "err.server.bot_api.message_edit_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "You can only edit messages you sent.", }) // ErrBotAPIOBONotAuthorized covers a send/typing OBO request lacking an // active grant or per-channel scope (ErrOBONotAuthorized). ErrBotAPIOBONotAuthorized = register(codes.Code{ ID: "err.server.bot_api.obo_not_authorized", HTTPStatus: http.StatusForbidden, DefaultMessage: "Not authorized to act on behalf of this user.", }) // and therefore may not serve API requests. Status-preserving (403). ErrBotAPIBotUnavailable = register(codes.Code{ ID: "err.server.bot_api.bot_unavailable", HTTPStatus: http.StatusForbidden, DefaultMessage: "The bot is not available.", }) // ErrBotAPIGroupNotFound covers a missing target group. ErrBotAPIGroupNotFound = register(codes.Code{ ID: "err.server.bot_api.group_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Group not found.", }) // ErrBotAPIMessageNotFound covers a missing target message on edit. ErrBotAPIMessageNotFound = register(codes.Code{ ID: "err.server.bot_api.message_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Message not found.", }) // ErrBotAPIUserNotFound covers a missing target user (group member lookup, // command target). ErrBotAPIUserNotFound = register(codes.Code{ ID: "err.server.bot_api.user_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "User not found.", }) // ErrBotAPIBotNotRegistered covers an OBO grantee uid that is not a // registered bot (existence-leak-safe 404; ownership mismatch also maps here). ErrBotAPIBotNotRegistered = register(codes.Code{ ID: "err.server.bot_api.bot_not_registered", HTTPStatus: http.StatusNotFound, DefaultMessage: "The grantee is not a registered bot.", }) // ErrBotAPIOBOGrantNotFound covers a missing / inactive OBO grant. ErrBotAPIOBOGrantNotFound = register(codes.Code{ ID: "err.server.bot_api.obo_grant_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "OBO grant not found.", }) // ErrBotAPIOBOScopeNotFound covers a missing OBO per-channel scope. ErrBotAPIOBOScopeNotFound = register(codes.Code{ ID: "err.server.bot_api.obo_scope_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "OBO scope not found.", }) // ErrBotAPIOBOChannelNotFound covers an OBO scope referencing a channel that // does not exist / the grantor cannot reach. ErrBotAPIOBOChannelNotFound = register(codes.Code{ ID: "err.server.bot_api.obo_channel_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Channel not found.", }) // ErrBotAPIOBOGrantExists covers an OBO grant create that collides with an // already-active grant. ErrBotAPIOBOGrantExists = register(codes.Code{ ID: "err.server.bot_api.obo_grant_exists", HTTPStatus: http.StatusConflict, DefaultMessage: "An OBO grant already exists.", }) // ErrBotAPIOBOScopeExists covers an OBO scope create that collides with an // existing scope. ErrBotAPIOBOScopeExists = register(codes.Code{ ID: "err.server.bot_api.obo_scope_exists", HTTPStatus: http.StatusConflict, DefaultMessage: "An OBO scope already exists.", }) // ErrBotAPIMessageNotDelivered covers an edit targeting a message that has not // finished delivering yet (retryable). ErrBotAPIMessageNotDelivered = register(codes.Code{ ID: "err.server.bot_api.message_not_delivered", HTTPStatus: http.StatusConflict, DefaultMessage: "The message has not finished delivering; please retry.", }) // ErrBotAPIAuthFailed is the SINGLE anti-enumeration code for the bot-token // auth middleware and the legacy register endpoints: missing Authorization // header, invalid/unknown bot token, and unauthenticated OBO calls ALL // collapse to one 401 so an external caller cannot probe which factor was // wrong. The specific reason is logged, never returned. Rendered via // ResponseErrorLWithStatus to PRESERVE the real 401 (adapters branch on it). ErrBotAPIAuthFailed = register(codes.Code{ ID: "err.server.bot_api.auth_failed", HTTPStatus: http.StatusUnauthorized, DefaultMessage: "Bot authentication failed.", }) // ErrBotAPIUpstreamFailed covers a downstream speech-service failure on the // voice transcribe proxy. Status-preserving (502) so the client retries. ErrBotAPIUpstreamFailed = register(codes.Code{ ID: "err.server.bot_api.upstream_failed", HTTPStatus: http.StatusBadGateway, DefaultMessage: "The upstream service is unavailable.", Internal: true, }) // ErrBotAPIQueryFailed covers read-path failures (group/space/member/message // SELECTs, friend/membership verification, GROUP.md / voice-context reads, // OBO grant/scope lists). Log the underlying err before responding. ErrBotAPIQueryFailed = register(codes.Code{ ID: "err.server.bot_api.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query data.", Internal: true, }) // ErrBotAPIStoreFailed covers mutation-path failures (DB write/update/delete, // GROUP.md / voice-context writes, read-receipt inserts, message-extra // writes, OBO grant/scope create/revoke). Log the underlying err first. ErrBotAPIStoreFailed = register(codes.Code{ ID: "err.server.bot_api.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update data.", Internal: true, }) // ErrBotAPISendFailed covers WuKongIM dispatch failures (send message / // typing / heartbeat / messages-sync / CMD). Log the underlying err first. ErrBotAPISendFailed = register(codes.Code{ ID: "err.server.bot_api.send_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to send the message.", Internal: true, }) // ErrBotAPIUploadFailed covers the file proxy / upload / STS-credential / // presigned-URL path (read, storage write, COS misconfiguration). Log first. ErrBotAPIUploadFailed = register(codes.Code{ ID: "err.server.bot_api.upload_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to process the file.", Internal: true, }) // ErrBotAPIIMTokenFailed covers IM-token issuance failures on the bot // register endpoints. Log the underlying err before responding. ErrBotAPIIMTokenFailed = register(codes.Code{ ID: "err.server.bot_api.im_token_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to obtain an IM token.", Internal: true, }) // ErrBotAPIAuthCheckFailed covers the bot-auth middleware's own // infrastructure failures (DB lookup for robot / app bot errored) — distinct // from ErrBotAPIAuthFailed (a real credential failure). Preserves the real // 500 via ResponseErrorLWithStatus so adapters retry instead of treating it // as a permanent 401. Log the underlying err before responding. ErrBotAPIAuthCheckFailed = register(codes.Code{ ID: "err.server.bot_api.auth_check_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Bot authentication check failed.", Internal: true, }) // ErrBotAPIOBOInternal covers OBO grant/scope store failures and the OBO // permission-check infrastructure failure ("OBO 检查失败"). Log first. ErrBotAPIOBOInternal = register(codes.Code{ ID: "err.server.bot_api.obo_internal", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "OBO operation failed.", Internal: true, }) )
err.server.bot_api.* — modules/bot_api business error codes. Unlike the dmwork-facing modules, every bot_api endpoint serves EXTERNAL bot adapters / integrations authenticated by bf_/app_ tokens (plus the OBO grant-management endpoints, which serve the logged-in user). Many sites already returned real HTTP status codes that adapters branch on, so those are rendered via httperr.ResponseErrorLWithStatus to PRESERVE the wire status rather than the D14 fixed-400 path; the legacy c.ResponseError sites stay at wire 400.
DefaultMessage holds the en-US source (D4); the zh-CN runtime translation lives in pkg/i18n/locales/active.zh-CN.toml. Internal=true codes never surface their message on the wire — callers MUST log the underlying err with full context (zap.Error) before responding.
var ( // ErrBotProvisionRequestInvalid is the catch-all for malformed/missing mint // request input: BindJSON failure (field omitted) and the required-field // guards (field = "display_name" / "space_id"). The offending field is // surfaced via Details when identifiable; the raw parse error is logged. ErrBotProvisionRequestInvalid = register(codes.Code{ ID: "err.server.bot_provision.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) // ErrBotProvisionAuthFailed is the SINGLE anti-enumeration code for the // daemon token endpoint's credential failures: missing Authorization header, // empty Bearer token, and unresolvable api_key ALL collapse here so an // external caller cannot probe which factor was wrong. The specific reason is // logged, never returned. Rendered via ResponseErrorLWithStatus to PRESERVE // the real 401 (the daemon branches on it). ErrBotProvisionAuthFailed = register(codes.Code{ ID: "err.server.bot_provision.auth_failed", HTTPStatus: http.StatusUnauthorized, DefaultMessage: "Authentication failed.", }) // ErrBotProvisionSpaceForbidden covers a mint caller who is not a member of // the target space (PR-D.1 #2 guard — a user may not mint a bot into a space // they do not belong to). DB errors from the membership check are logged and // also map here (the original handler returned a single 403 for any failure). ErrBotProvisionSpaceForbidden = register(codes.Code{ ID: "err.server.bot_provision.space_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "You are not a member of this space.", }) // ErrBotProvisionBotForbidden covers the daemon token endpoint when the // caller's api_key uid is not the bot's creator — a clean 403 with no leak of // whether the bot exists. ErrBotProvisionBotForbidden = register(codes.Code{ ID: "err.server.bot_provision.bot_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "Not authorized for this bot.", }) // ErrBotProvisionBotNotFound covers the daemon token endpoint when no bot row // (status=1, in the caller's space) yields a token. ErrBotProvisionBotNotFound = register(codes.Code{ ID: "err.server.bot_provision.bot_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Bot not found.", }) )
err.server.bot_provision.* — modules/bot_provision business error codes. bot_provision is the cross-service "open an account for a daemon" surface:
- POST /v1/bot/mint — web/session caller mints a bot OBO.
- GET /v1/bot/:uid/token — daemon (api_key Bearer) fetches a bot_token.
Wire status: the mint validation sites historically returned the legacy compatibility 400 via c.ResponseError, so they are rendered with httperr.ResponseErrorL (wire 400, body error.http_status = the code's status). The sites that returned a real semantic status (401/403/404 via c.ResponseErrorWithStatus) are rendered with httperr.ResponseErrorLWithStatus to PRESERVE that wire status, which the daemon branches on.
Anti-enumeration (CLAUDE.md): the daemon token endpoint's credential failures (missing/empty Bearer, unresolvable api_key) ALL collapse to a single ErrBotProvisionAuthFailed 401 — the specific reason is logged, never returned, so a caller cannot probe which factor was wrong. The mint endpoint's defensive "no session uid" guard reuses the shared auth-required code.
DefaultMessage holds the en-US source (D4); the zh-CN runtime translation lives in pkg/i18n/locales/active.zh-CN.toml.
var ( // ErrBotfatherRequestInvalid is the catch-all for missing/malformed request // input (BindJSON failure, "X 不能为空", invalid id/short_id/group_no format, // name/username length, fileSize invalid, event_id format, unsupported file // type, empty GROUP.md content, invalid content_edit). The offending field is // surfaced via Details when identifiable. ErrBotfatherRequestInvalid = register(codes.Code{ ID: "err.server.botfather.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) // ErrBotfatherCannotApplyOwnBot covers a user applying to use a bot they own. ErrBotfatherCannotApplyOwnBot = register(codes.Code{ ID: "err.server.botfather.cannot_apply_own_bot", HTTPStatus: http.StatusBadRequest, DefaultMessage: "No need to apply for a bot you own.", }) // ErrBotfatherRuntimeOnboardingSpaceRequired covers missing space context // on GET /v1/runtime-onboarding. The route intentionally has no implicit // "first space" fallback because the api_key it returns is space-bound. ErrBotfatherRuntimeOnboardingSpaceRequired = register(codes.Code{ ID: "err.server.botfather.runtime_onboarding_space_required", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Space is required.", SafeDetailKeys: []string{"field"}, }) // ErrBotfatherNotOwner covers the apply approve/reject guard where the caller // is not the bot's owner. Legacy wire 400 (D14). ErrBotfatherNotOwner = register(codes.Code{ ID: "err.server.botfather.not_owner", HTTPStatus: http.StatusForbidden, DefaultMessage: "You are not the owner of this bot.", }) // ErrBotfatherBotNotInSpace covers the User-API Space-isolation guard where // the bot does not belong to the API key's Space (AbortWithStatusJSON 403 → // preserve 403). ErrBotfatherBotNotInSpace = register(codes.Code{ ID: "err.server.botfather.bot_not_in_space", HTTPStatus: http.StatusForbidden, DefaultMessage: "The bot does not belong to the current space.", }) // ErrBotfatherRuntimeOnboardingForbidden covers a session user who cannot // create onboarding material for the requested space (not an active member // or the account is inactive). ErrBotfatherRuntimeOnboardingForbidden = register(codes.Code{ ID: "err.server.botfather.runtime_onboarding_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "You cannot create runtime onboarding for this space.", }) // ErrBotfatherRobotNotFound covers a target robot that does not exist or has // been deleted on the apply path. Legacy wire 400 (D14). ErrBotfatherRobotNotFound = register(codes.Code{ ID: "err.server.botfather.robot_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The bot does not exist or has been deleted.", }) // ErrBotfatherApplyNotFound covers a missing apply record. Legacy wire 400. ErrBotfatherApplyNotFound = register(codes.Code{ ID: "err.server.botfather.apply_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The application record does not exist.", }) // ErrBotfatherBotNotFound covers a missing / not-owned User-API bot // (AbortWithStatusJSON 404 → preserve 404). Existence-leak-safe: ownership // mismatch also maps here so a caller cannot probe other users' bot ids. ErrBotfatherBotNotFound = register(codes.Code{ ID: "err.server.botfather.bot_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The bot does not exist or you do not have permission.", }) // ErrBotfatherApplyProcessed covers an approve/reject on an already-processed // apply. Legacy wire 400 (D14). ErrBotfatherApplyProcessed = register(codes.Code{ ID: "err.server.botfather.apply_processed", HTTPStatus: http.StatusConflict, DefaultMessage: "The application has already been processed.", }) // ErrBotfatherApplyExists covers a duplicate pending apply. Legacy wire 400. ErrBotfatherApplyExists = register(codes.Code{ ID: "err.server.botfather.apply_exists", HTTPStatus: http.StatusConflict, DefaultMessage: "You have already submitted an application; please wait for the owner to review it.", }) // ErrBotfatherAlreadyFriends covers an apply where the user and bot are // already friends. Legacy wire 400 (D14). ErrBotfatherAlreadyFriends = register(codes.Code{ ID: "err.server.botfather.already_friends", HTTPStatus: http.StatusConflict, DefaultMessage: "You are already friends.", }) // ErrBotfatherUsernameTaken covers a username collision on bot create // (ResponseErrorWithStatus 409 → preserve 409). The offending username is // surfaced via Details so the client can render a localized hint. ErrBotfatherUsernameTaken = register(codes.Code{ ID: "err.server.botfather.username_taken", HTTPStatus: http.StatusConflict, DefaultMessage: "The username is already taken.", SafeDetailKeys: []string{"username"}, }) // ErrBotfatherAuthFailed is the SINGLE anti-enumeration code for the bot/User // API Key auth middleware and the legacy register endpoint: missing // Authorization header, invalid/unknown bot token, invalid API Key, and // authentication-lookup denials ALL collapse to one 401 so an external caller // cannot probe which factor was wrong. The specific reason is logged, never // returned. The middleware sites preserve the real 401 via // ResponseErrorLWithStatus (adapters branch on it) + c.Abort; the legacy // register endpoint stays wire 400 (D14). ErrBotfatherAuthFailed = register(codes.Code{ ID: "err.server.botfather.auth_failed", HTTPStatus: http.StatusUnauthorized, DefaultMessage: "Bot authentication failed.", }) // ErrBotfatherQueryFailed covers read-path failures (robot/group/member/ // message/apply SELECTs, friend/membership verification, GROUP.md reads, apply // list/count). Log the underlying err before responding. ErrBotfatherQueryFailed = register(codes.Code{ ID: "err.server.botfather.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query data.", Internal: true, }) // ErrBotfatherStoreFailed covers mutation-path failures (bot/apply/friend/ // command create-update-delete, GROUP.md writes, message_extra writes, group/ // thread service mutations, event ack). Log the underlying err first. ErrBotfatherStoreFailed = register(codes.Code{ ID: "err.server.botfather.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update data.", Internal: true, }) // ErrBotfatherRuntimeOnboardingFailed covers internal failures preparing // onboarding material, such as membership/user lookups or api_key allocation. ErrBotfatherRuntimeOnboardingFailed = register(codes.Code{ ID: "err.server.botfather.runtime_onboarding_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to prepare runtime onboarding.", Internal: true, }) // ErrBotfatherRuntimeOnboardingConfigInvalid covers a deployment missing // enough external URL config to render runnable daemon commands. ErrBotfatherRuntimeOnboardingConfigInvalid = register(codes.Code{ ID: "err.server.botfather.runtime_onboarding_config_invalid", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Runtime onboarding is not configured.", Internal: true, }) )
err.server.botfather.* — modules/botfather business error codes. botfather mixes two audiences: dmwork-facing management/apply endpoints (historically c.ResponseError at wire 400) and external bot-adapter endpoints (bf_/uk_ tokens) — several of which already returned a real HTTP status (401/403/404) that adapters branch on. Sites that already returned a non-400 real status are rendered via httperr.ResponseErrorLWithStatus to PRESERVE the wire status; the legacy c.ResponseError sites stay at wire 400 (D14).
DefaultMessage holds the en-US source (D4); the zh-CN runtime translation lives in pkg/i18n/locales/active.zh-CN.toml. Internal=true codes never surface their message on the wire — callers MUST log the underlying err with full context (zap.Error) before responding.
var ( // ErrCategoryRequestInvalid is the catch-all for missing/malformed request // input (BindJSON failure, empty category name, empty sort list). The // offending field is surfaced via Details when the caller can identify it. ErrCategoryRequestInvalid = register(codes.Code{ ID: "err.server.category.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) ErrCategoryNameTooLong = register(codes.Code{ ID: "err.server.category.name_too_long", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The category name cannot exceed 100 characters.", SafeDetailKeys: []string{"field", "max_length"}, }) ErrCategorySortListMismatch = register(codes.Code{ ID: "err.server.category.sort_list_mismatch", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The category list does not match the existing categories.", }) ErrCategorySortListDuplicate = register(codes.Code{ ID: "err.server.category.sort_list_duplicate", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The category list contains duplicates.", }) ErrCategoryGroupSpaceMissing = register(codes.Code{ ID: "err.server.category.group_space_missing", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The group does not belong to any space.", }) ErrCategorySpaceMismatch = register(codes.Code{ ID: "err.server.category.space_mismatch", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The group and the category are not in the same space.", }) ErrCategorySpaceMemberRequired = register(codes.Code{ ID: "err.server.category.space_member_required", HTTPStatus: http.StatusForbidden, DefaultMessage: "You are not a member of this space.", }) ErrCategoryGroupMemberRequired = register(codes.Code{ ID: "err.server.category.group_member_required", HTTPStatus: http.StatusForbidden, DefaultMessage: "You are not a member of this group.", }) // ErrCategoryPermissionDenied covers the ownership guards on update / delete / // move (cat.UID != loginUID) — all the same "not your category" condition. ErrCategoryPermissionDenied = register(codes.Code{ ID: "err.server.category.permission_denied", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have permission to operate on this category.", }) ErrCategoryDefaultImmutable = register(codes.Code{ ID: "err.server.category.default_immutable", HTTPStatus: http.StatusForbidden, DefaultMessage: "The default category cannot be modified.", }) ErrCategoryDefaultUndeletable = register(codes.Code{ ID: "err.server.category.default_undeletable", HTTPStatus: http.StatusForbidden, DefaultMessage: "The default category cannot be deleted.", }) // ErrCategoryNotFound covers both a missing category and the deliberate // "not found or no permission" merge in the sort handler (no enumeration). ErrCategoryNotFound = register(codes.Code{ ID: "err.server.category.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Category not found.", }) ErrCategoryLimitExceeded = register(codes.Code{ ID: "err.server.category.limit_exceeded", HTTPStatus: http.StatusConflict, DefaultMessage: "You can create at most 20 categories per space.", SafeDetailKeys: []string{"max"}, }) // ErrCategoryQueryFailed covers read-path failures (membership check, DB // SELECT/count). Log the underlying err before responding. ErrCategoryQueryFailed = register(codes.Code{ ID: "err.server.category.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query category data.", Internal: true, }) // ErrCategoryStoreFailed covers mutation-path failures (DB write, transaction // begin/commit/rollback, sequence generation, follow-version bump). Log the // underlying err before responding. ErrCategoryStoreFailed = register(codes.Code{ ID: "err.server.category.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update category data.", Internal: true, }) )
err.server.category.* — modules/category business error codes (api.go). DefaultMessage holds the en-US source (D4); the zh-CN runtime translation lives in pkg/i18n/locales/active.zh-CN.toml. Internal=true codes never surface their message on the wire — callers MUST log the underlying err with full context via the module logger before responding.
var ( // ErrGroupRequestInvalid is the catch-all for missing/malformed request // input (empty group_no, BindJSON failure, common.ErrData, bad action type, // nothing-to-update, etc.). The offending field is surfaced via Details when // the caller can identify it. ErrGroupRequestInvalid = register(codes.Code{ ID: "err.server.group.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) ErrGroupMemberNotFriend = register(codes.Code{ ID: "err.server.group.member_not_friend", HTTPStatus: http.StatusBadRequest, DefaultMessage: "You can only add a friend to the group. Please add this user as a friend first.", }) ErrGroupFileHelperNotAllowed = register(codes.Code{ ID: "err.server.group.file_helper_not_allowed", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The File Helper cannot be added to a group.", }) ErrGroupCategorySpaceMismatch = register(codes.Code{ ID: "err.server.group.category_space_mismatch", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The group category does not match the space.", }) ErrGroupTargetNotBot = register(codes.Code{ ID: "err.server.group.target_not_bot", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The target member is not a bot.", }) ErrGroupMdContentTooLarge = register(codes.Code{ ID: "err.server.group.group_md_content_too_large", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The GROUP.md content exceeds the maximum size.", SafeDetailKeys: []string{"field", "max_size"}, }) ErrGroupAuthCodeInvalid = register(codes.Code{ ID: "err.server.group.auth_code_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The authorization code is invalid or has expired.", }) ErrGroupInviteExpired = register(codes.Code{ ID: "err.server.group.invite_expired", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The invite link has expired.", }) ErrGroupCreatorOnly = register(codes.Code{ ID: "err.server.group.creator_only", HTTPStatus: http.StatusForbidden, DefaultMessage: "Only the group owner can perform this action.", }) ErrGroupManagerOnly = register(codes.Code{ ID: "err.server.group.manager_only", HTTPStatus: http.StatusForbidden, DefaultMessage: "Only a group administrator can perform this action.", }) ErrGroupCreatorOrManagerOnly = register(codes.Code{ ID: "err.server.group.creator_or_manager_only", HTTPStatus: http.StatusForbidden, DefaultMessage: "Only the group owner or an administrator can perform this action.", }) ErrGroupNotMember = register(codes.Code{ ID: "err.server.group.not_group_member", HTTPStatus: http.StatusForbidden, DefaultMessage: "You are not a member of this group.", }) ErrGroupViewForbidden = register(codes.Code{ ID: "err.server.group.view_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have permission to view this.", }) ErrGroupMemberCannotRemove = register(codes.Code{ ID: "err.server.group.member_cannot_remove", HTTPStatus: http.StatusForbidden, DefaultMessage: "Regular members cannot remove group members.", }) ErrGroupCannotRemoveAdmin = register(codes.Code{ ID: "err.server.group.cannot_remove_admin", HTTPStatus: http.StatusForbidden, DefaultMessage: "An administrator cannot remove another administrator.", }) ErrGroupCannotRemoveOwner = register(codes.Code{ ID: "err.server.group.cannot_remove_owner", HTTPStatus: http.StatusForbidden, DefaultMessage: "An administrator cannot remove the group owner.", }) ErrGroupExternalCannotBeAdmin = register(codes.Code{ ID: "err.server.group.external_cannot_be_admin", HTTPStatus: http.StatusForbidden, DefaultMessage: "External members cannot be promoted to administrator.", }) ErrGroupExternalCannotBeOwner = register(codes.Code{ ID: "err.server.group.external_cannot_be_owner", HTTPStatus: http.StatusForbidden, DefaultMessage: "Group ownership cannot be transferred to an external member.", }) ErrGroupExternalJoinForbidden = register(codes.Code{ ID: "err.server.group.external_join_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "This group does not allow external members to join. Please contact a group administrator.", }) ErrGroupInviteModeCannotAdd = register(codes.Code{ ID: "err.server.group.invite_mode_cannot_add", HTTPStatus: http.StatusForbidden, DefaultMessage: "Invite mode is enabled; members cannot be added directly.", }) ErrGroupInviteModeCannotJoin = register(codes.Code{ ID: "err.server.group.invite_mode_cannot_join", HTTPStatus: http.StatusForbidden, DefaultMessage: "Invite mode is enabled; you cannot join the group directly.", }) ErrGroupCategoryForbidden = register(codes.Code{ ID: "err.server.group.category_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have permission to use this group category.", }) ErrGroupBotOwnershipDenied = register(codes.Code{ ID: "err.server.group.bot_ownership_denied", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have permission to invite this bot.", }) ErrGroupBotNotInSpace = register(codes.Code{ ID: "err.server.group.bot_not_in_space", HTTPStatus: http.StatusForbidden, DefaultMessage: "This bot does not belong to your space.", }) ErrGroupAuthCodeUserMismatch = register(codes.Code{ ID: "err.server.group.auth_code_user_mismatch", HTTPStatus: http.StatusForbidden, DefaultMessage: "The authorization code does not match the current user.", }) ErrGroupQRCodeMemberOnly = register(codes.Code{ ID: "err.server.group.qrcode_member_only", HTTPStatus: http.StatusForbidden, DefaultMessage: "Only group members can generate a QR code.", }) ErrGroupNotFound = register(codes.Code{ ID: "err.server.group.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Group not found.", }) ErrGroupMemberNotInGroup = register(codes.Code{ ID: "err.server.group.member_not_in_group", HTTPStatus: http.StatusNotFound, DefaultMessage: "The member is not in this group.", }) ErrGroupCategoryNotFound = register(codes.Code{ ID: "err.server.group.category_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Group category not found.", }) ErrGroupTransferTargetNotFound = register(codes.Code{ ID: "err.server.group.transfer_target_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The target user does not exist or has been deactivated.", }) ErrGroupInviteNotFound = register(codes.Code{ ID: "err.server.group.invite_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Invite information not found.", }) ErrGroupCannotTargetSelf = register(codes.Code{ ID: "err.server.group.cannot_target_self", HTTPStatus: http.StatusConflict, DefaultMessage: "You cannot perform this action on yourself.", }) ErrGroupAlreadyMember = register(codes.Code{ ID: "err.server.group.already_member", HTTPStatus: http.StatusConflict, DefaultMessage: "You are already a member of this group.", }) ErrGroupInviteStatusInvalid = register(codes.Code{ ID: "err.server.group.invite_status_invalid", HTTPStatus: http.StatusConflict, DefaultMessage: "The invite is not in a pending state.", }) ErrGroupTooLargeToSync = register(codes.Code{ ID: "err.server.group.too_large_to_sync", HTTPStatus: http.StatusBadRequest, DefaultMessage: "This group is too large to sync members.", }) ErrGroupDailyCreateLimit = register(codes.Code{ ID: "err.server.group.daily_create_limit", HTTPStatus: http.StatusTooManyRequests, DefaultMessage: "You have reached the daily group creation limit.", }) // ErrGroupQueryFailed covers read-path failures (DB SELECT/exist/count, // cache GET). Log the underlying err before responding. ErrGroupQueryFailed = register(codes.Code{ ID: "err.server.group.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query group data.", Internal: true, }) // ErrGroupStoreFailed covers mutation-path failures (DB write, transaction // begin/commit/rollback, event begin/commit, cache SET, serialization, file // upload, whitelist setup). Log the underlying err before responding. ErrGroupStoreFailed = register(codes.Code{ ID: "err.server.group.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update group data.", Internal: true, }) // ErrGroupNotifyFailed covers outbound IM-side failures (send command // message, channel update, subscribe/unsubscribe). Log the underlying err // before responding. ErrGroupNotifyFailed = register(codes.Code{ ID: "err.server.group.notify_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to send group notification.", Internal: true, }) )
err.server.group.* — modules/group business error codes (api.go / api_manager.go / invite.go). DefaultMessage holds the en-US source (D4); the zh-CN runtime translation lives in pkg/i18n/locales/active.zh-CN.toml. Internal=true codes never surface their message on the wire — callers MUST log the underlying err with full context via the module logger before responding.
var ( // every auth failure on the push path; never differentiate the reason. ErrIncomingWebhookPushUnauthorized = register(codes.Code{ ID: "err.server.incomingwebhook.push_unauthorized", HTTPStatus: http.StatusUnauthorized, DefaultMessage: "Unauthorized.", }) // ErrIncomingWebhookPushRateLimited (429) — the per-webhook token bucket // rejected this request. ErrIncomingWebhookPushRateLimited = register(codes.Code{ ID: "err.server.incomingwebhook.push_rate_limited", HTTPStatus: http.StatusTooManyRequests, DefaultMessage: "Too many requests. Please retry later.", }) // ErrIncomingWebhookPushPayloadInvalid (400) — unreadable body, malformed // JSON, empty content, malformed rich-text blocks, an unknown msg_type, or a // platform-adapter request that cannot be translated (missing X-GitHub-Event // header, unsupported WeCom msgtype). The offending stage is surfaced via // Details["reason"] (body / json / content / blocks / msg_type / no_event). // Note: no_event is the missing-X-GitHub-Event 400; a GitHub event that is // merely outside the rendered subset is NOT an error — it answers 200 with // skipped=event (see modules/incomingwebhook/adapter_github.go). ErrIncomingWebhookPushPayloadInvalid = register(codes.Code{ ID: "err.server.incomingwebhook.push_payload_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request payload.", SafeDetailKeys: []string{"reason"}, }) // ErrIncomingWebhookPushPayloadTooLarge (413) — body exceeds the configured // per-shape byte cap: native/wecom use DM_INCOMINGWEBHOOK_MAX_BYTES (8KiB // default, caller-authored bodies), GitHub events use the wider // DM_INCOMINGWEBHOOK_GITHUB_MAX_BYTES (1MiB default, clamped to 25MiB; the // platform-generated event JSON the sender cannot shorten). Rich-text payloads // also 413 here when finalized output exceeds the richtext size limit. ErrIncomingWebhookPushPayloadTooLarge = register(codes.Code{ ID: "err.server.incomingwebhook.push_payload_too_large", HTTPStatus: http.StatusRequestEntityTooLarge, DefaultMessage: "Request payload is too large.", }) // ErrIncomingWebhookPushDeliveryFailed (502, Internal) — the downstream // SendMessage failed; the underlying error is logged, not surfaced. ErrIncomingWebhookPushDeliveryFailed = register(codes.Code{ ID: "err.server.incomingwebhook.push_delivery_failed", HTTPStatus: http.StatusBadGateway, DefaultMessage: "Failed to deliver the webhook message.", Internal: true, }) // ErrIncomingWebhookPushDisabled (404) — the incoming-webhook feature is // globally disabled via system_setting incomingwebhook.enabled=0. Returned // for every push while disabled; it is a global state (not per-webhook), so // a uniform 404 does not leak whether any specific webhook exists. ErrIncomingWebhookPushDisabled = register(codes.Code{ ID: "err.server.incomingwebhook.push_disabled", HTTPStatus: http.StatusNotFound, DefaultMessage: "Not found.", }) )
err.server.incomingwebhook.* — modules/incomingwebhook push-endpoint error codes (api.go push path). DefaultMessage holds the en-US source; the zh-CN runtime translation lives in pkg/i18n/locales/active.zh-CN.toml.
The push endpoint is an unauthenticated, token-in-URL surface. Every authentication failure (missing/disabled webhook, bad token, disbanded group) intentionally collapses to the SINGLE push_unauthorized code with a uniform RESPONSE (same code/message/status), so a probe cannot distinguish "no such webhook" from "wrong token" by the response body. Timing is only best-effort, not constant: not-found returns before any hash, wrong-token pays the SHA-256 compare, valid-token+dead-group pays an extra DB round-trip. The 128-bit unguessable webhook_id plus per-IP rate limit make timing-based enumeration impractical; the response-uniformity is the primary defense. See modules/incomingwebhook/api.go pushUnauthorized. Migrated off the raw c.AbortWithStatusJSON pattern to satisfy the D23 i18n lint gate (PR Mininglamp-OSS/octo-server#31, yujiawei review #3).
var ( // ErrIncomingWebhookForbidden (403) — caller lacks permission: not an // (internal, active) member of the group, or a plain member/bot touching a // webhook created by someone else. Owners/admins manage any webhook; // members and bots manage only their own. One generic code for every // permission failure — the precise reason is not surfaced. ErrIncomingWebhookForbidden = register(codes.Code{ ID: "err.server.incomingwebhook.mgmt_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have permission to manage this webhook.", }) // ErrIncomingWebhookRequestInvalid (400) — malformed body or invalid field // (blank/over-long name, status not in {0,1}). The offending field is // surfaced via Details["reason"] (body / name / status). ErrIncomingWebhookRequestInvalid = register(codes.Code{ ID: "err.server.incomingwebhook.mgmt_request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"reason"}, }) // ErrIncomingWebhookGroupNotFound (404) — group does not exist or is no // longer Normal (disbanded/disabled). Blocks create / enable / regenerate // from reviving webhooks on a dead group. ErrIncomingWebhookGroupNotFound = register(codes.Code{ ID: "err.server.incomingwebhook.mgmt_group_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The group does not exist or has been disbanded.", }) // ErrIncomingWebhookThreadNotFound (404) — the target thread (子区) does not // exist, is not under the path group, or is not active (archived/deleted). // Returned by create on the thread-scoped mount // (/v1/groups/:group_no/threads/:short_id/incoming-webhooks). One generic 404 // for every "thread unavailable" reason — kept simple and consistent with // mgmt_group_not_found / mgmt_not_found; the precise reason is not surfaced. ErrIncomingWebhookThreadNotFound = register(codes.Code{ ID: "err.server.incomingwebhook.mgmt_thread_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The thread does not exist or is not available.", }) // ErrIncomingWebhookNotFound (404) — webhook does not exist or does not // belong to the group in the path (cross-group guard). ErrIncomingWebhookNotFound = register(codes.Code{ ID: "err.server.incomingwebhook.mgmt_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The webhook does not exist.", }) // ErrIncomingWebhookQuotaExceeded (409) — the group already holds the // maximum number of webhooks. Params["max"] carries the configured cap. ErrIncomingWebhookQuotaExceeded = register(codes.Code{ ID: "err.server.incomingwebhook.mgmt_quota_exceeded", HTTPStatus: http.StatusConflict, DefaultMessage: "Each group allows at most {{.max}} webhooks.", }) // ErrIncomingWebhookCreatorQuotaExceeded (409) — the calling member/bot has // reached its per-creator cap in this group (owners/admins are exempt and // only bounded by the group cap). Params["max"] carries the configured cap. ErrIncomingWebhookCreatorQuotaExceeded = register(codes.Code{ ID: "err.server.incomingwebhook.mgmt_creator_quota_exceeded", HTTPStatus: http.StatusConflict, DefaultMessage: "You can create at most {{.max}} webhooks in this group.", }) // ErrIncomingWebhookCreatorLeft (409) — the webhook's creator is no longer // an (internal, active) member of the group, so operations that would make // or keep it pushable (enable / regenerate / test push) are refused. The // webhook can still be deleted; pushes are lazily disabled by the push // path's creator-membership gate. Recovery: the creator rejoins the group, // after which the creator or an admin can re-enable it — the message must // state that path and offer deletion only as the cleanup option (PR #340 // review, Jerry-Xin: do not tell users delete-and-recreate is the only way). ErrIncomingWebhookCreatorLeft = register(codes.Code{ ID: "err.server.incomingwebhook.mgmt_creator_left", HTTPStatus: http.StatusConflict, DefaultMessage: "The webhook's creator has left the group, so it cannot be enabled. It can be re-enabled after the creator rejoins, or deleted and recreated by someone in the group.", }) // ErrIncomingWebhookQueryFailed (500, Internal) — a read (group / webhook / // list / permission) failed; the underlying error is logged, not surfaced. ErrIncomingWebhookQueryFailed = register(codes.Code{ ID: "err.server.incomingwebhook.mgmt_query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query webhook information.", Internal: true, }) // ErrIncomingWebhookOperationFailed (500, Internal) — a write // (create / update / delete / regenerate / token generation) failed; the // underlying error is logged, not surfaced. ErrIncomingWebhookOperationFailed = register(codes.Code{ ID: "err.server.incomingwebhook.mgmt_operation_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "The operation failed. Please try again later.", Internal: true, }) // ErrIncomingWebhookDisabled (403) — the incoming-webhook feature is globally // disabled via system_setting incomingwebhook.enabled=0. Returned for every // management write (create / update / delete / regenerate) while disabled; // list (read) stays available so operators can still inspect existing config. ErrIncomingWebhookDisabled = register(codes.Code{ ID: "err.server.incomingwebhook.mgmt_disabled", HTTPStatus: http.StatusForbidden, DefaultMessage: "The incoming webhook feature is currently disabled.", }) )
err.server.incomingwebhook.mgmt_* — management-endpoint error codes (create / list / update / delete / regenerate in modules/incomingwebhook/ api.go). These are authenticated, admin-only endpoints with no legacy clients, so — like the push path — they are rendered via httperr.ResponseErrorLWithStatus and keep their real semantic HTTP status (403/404/409/400/500). This replaces the legacy raw-string c.ResponseError(errors.New(...)) pattern (#246 follow-up).
var ( // ErrIntegrationDisabled —— exchange 能力被全局开关禁用(选项 A)。 ErrIntegrationDisabled = register(codes.Code{ ID: "err.server.integration.disabled", HTTPStatus: http.StatusForbidden, DefaultMessage: "OIDC integration exchange is disabled.", DefaultMessages: map[string]string{"zh-CN": "OIDC 集成换取能力已禁用。"}, }) // ErrIntegrationUserNotLinked —— (issuer, sub) 未绑定 Octo 用户。 ErrIntegrationUserNotLinked = register(codes.Code{ ID: "err.server.integration.user_not_linked", HTTPStatus: http.StatusForbidden, DefaultMessage: "User is not linked to an Octo account.", DefaultMessages: map[string]string{"zh-CN": "用户尚未关联 Octo 账号。"}, }) // ErrBotOccupied —— Bot 已被其他 Agent 占用;occupied_by 透传当前占用方。 ErrBotOccupied = register(codes.Code{ ID: "err.server.bot.occupied", HTTPStatus: http.StatusConflict, DefaultMessage: "Bot is already occupied by another agent.", DefaultMessages: map[string]string{"zh-CN": "该 Bot 已被其他 Agent 占用。"}, SafeDetailKeys: []string{"occupied_by"}, }) // ErrIntegrationIdempotencyInFlight —— 同一 Idempotency-Key 的建群请求仍在处理中(可重试, // 响应带 Retry-After)。与冲突区分开:这是「稍后重试」,不是「键被误用」。 ErrIntegrationIdempotencyInFlight = register(codes.Code{ ID: "err.server.integration.idempotency_in_flight", HTTPStatus: http.StatusConflict, DefaultMessage: "A request with this idempotency key is still in progress; please retry.", DefaultMessages: map[string]string{"zh-CN": "相同 Idempotency-Key 的请求仍在处理中,请稍后重试。"}, }) // ErrIntegrationIdempotencyConflict —— 同一 Idempotency-Key 复用于不同的请求体(终态冲突,不可重试)。 ErrIntegrationIdempotencyConflict = register(codes.Code{ ID: "err.server.integration.idempotency_conflict", HTTPStatus: http.StatusConflict, DefaultMessage: "This idempotency key was already used with a different request.", DefaultMessages: map[string]string{"zh-CN": "该 Idempotency-Key 已用于不同的请求。"}, }) )
Integration / Octo-link 错误码。
integration 接口对外固定英文输出(路由层强制 en-US,外部 API 不做多语言), 这里登记的 zh-CN 译文仅为 i18n 体系完整性兜底,正常不会被 integration 路由使用。
var ( )
复用 pkg/i18n/codes 已注册的 err.shared.* 码:integration / Octo-link 与 Bot 占用端点的非业务错误(参数、未找到、无权限、内部错误)走这些通用码。在此以 强类型 var 暴露,调用点与上面的 err.server.* 对称,避免裸字符串引用 ID。 这些码由 codes 包 init 注册(shared.go),Go 保证被导入包的 init 先于本包 var 初始化执行,故 shared() 查找一定命中。
var ( // ErrMessageRequestInvalid is the catch-all for missing / malformed request // input (empty ids, bad channel-id / message-id / seq format, BindJSON // failure, unsupported channel type, etc.). The offending field is surfaced // via Details when the caller can identify it. ErrMessageRequestInvalid = register(codes.Code{ ID: "err.server.message.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) ErrMessageIDSeqMismatch = register(codes.Code{ ID: "err.server.message.id_seq_mismatch", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Message ID does not match the message sequence.", }) ErrMessageNotFriend = register(codes.Code{ ID: "err.server.message.not_friend", HTTPStatus: http.StatusForbidden, DefaultMessage: "You are not friends with this user.", }) ErrMessagePeerNotInSpace = register(codes.Code{ ID: "err.server.message.peer_not_in_space", HTTPStatus: http.StatusForbidden, DefaultMessage: "The other party is not in this space.", }) ErrMessageConversationForbidden = register(codes.Code{ ID: "err.server.message.conversation_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have permission to operate on this conversation.", }) ErrMessageCannotDeleteSelfConversation = register(codes.Code{ ID: "err.server.message.cannot_delete_self_conversation", HTTPStatus: http.StatusBadRequest, DefaultMessage: "You cannot delete the conversation with yourself.", }) ErrMessageChannelAccessDenied = register(codes.Code{ ID: "err.server.message.channel_access_denied", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have permission to operate on this channel.", }) ErrMessageNotGroupMember = register(codes.Code{ ID: "err.server.message.not_group_member", HTTPStatus: http.StatusForbidden, DefaultMessage: "You are not a member of this group.", }) // ErrMessageGroupDisbanded covers the disbanded-group send guard on the // regular user path (/v1/message/send). After a group is disbanded // (WeChat-Work style — history preserved, everyone read-only), no one may // send. The deployed WuKongIM /message/send returns HTTP 200 with no // failure signal on a disband rejection, so octo-server self-checks // group.status here (parity with the bot path's ErrBotAPIGroupDisbanded). ErrMessageGroupDisbanded = register(codes.Code{ ID: "err.server.message.group_disbanded", HTTPStatus: http.StatusForbidden, DefaultMessage: "The group has been disbanded; messages can no longer be sent.", }) ErrMessageDeleteForbidden = register(codes.Code{ ID: "err.server.message.delete_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have permission to delete this message.", }) ErrMessageRecallForbidden = register(codes.Code{ ID: "err.server.message.recall_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have permission to recall this message.", }) ErrMessagePinnedForbidden = register(codes.Code{ ID: "err.server.message.pinned_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have permission to pin or unpin messages.", }) ErrMessageEditOwnOnly = register(codes.Code{ ID: "err.server.message.edit_own_only", HTTPStatus: http.StatusForbidden, DefaultMessage: "You can only edit your own messages.", }) ErrMessageProxySendUnsupported = register(codes.Code{ ID: "err.server.message.proxy_send_unsupported", HTTPStatus: http.StatusForbidden, DefaultMessage: "Sending messages on behalf of another user is not supported.", }) ErrMessageNotFound = register(codes.Code{ ID: "err.server.message.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Message not found or already deleted.", }) ErrMessageGroupNotFound = register(codes.Code{ ID: "err.server.message.group_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The target group does not exist or has been deleted.", }) ErrMessageReceiverNotFound = register(codes.Code{ ID: "err.server.message.receiver_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The message recipient does not exist.", }) ErrMessageBanwordNotFound = register(codes.Code{ ID: "err.server.message.banword_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The banned word does not exist.", }) ErrMessagePinnedLimitExceeded = register(codes.Code{ ID: "err.server.message.pinned_limit_exceeded", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The pinned-message limit has been reached.", SafeDetailKeys: []string{"max"}, }) ErrMessageRecallTimeExceeded = register(codes.Code{ ID: "err.server.message.recall_time_exceeded", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The message is past the recall time window.", }) // ErrMessageQueryFailed covers read-path failures (DB SELECT/count, cache // GET, membership / permission checks, search-result decoding, sync reads). ErrMessageQueryFailed = register(codes.Code{ ID: "err.server.message.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query message data.", Internal: true, }) // ErrMessageStoreFailed covers mutation-path failures (DB write, transaction // begin/commit, sequence generation, cache SET, offset/read/pin/recall/edit // persistence). ErrMessageStoreFailed = register(codes.Code{ ID: "err.server.message.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to persist message data.", Internal: true, }) // ErrMessageNotifyFailed covers outbound IM command / sync-command / recall // dispatch failures. ErrMessageNotifyFailed = register(codes.Code{ ID: "err.server.message.notify_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to dispatch the message command.", Internal: true, }) // ErrMessageSearchFailed covers failures calling / parsing the external // search service. ErrMessageSearchFailed = register(codes.Code{ ID: "err.server.message.search_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Message search failed.", Internal: true, }) )
err.server.message.* — modules/message business error codes (api.go, api_manager.go, api_pinned.go, api_conversation.go, api_message_get.go, api_reminders.go, api_channel_files.go, api_sidebar.go). DefaultMessage holds the en-US source (D4); zh-CN runtime translations live in pkg/i18n/locales/active.zh-CN.toml. Internal=true codes never surface their message on the wire — callers MUST log the underlying err with full context before responding.
var ( // VALIDATION_ERROR — bad request body / cursor / filter. ErrMessagesSearchValidationFailed = register(codes.Code{ ID: "err.server.messages_search.validation_failed", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid search request.", SafeDetailKeys: []string{"field", "reason", "max_length"}, }) ErrMessagesSearchUpstreamUnavailable = register(codes.Code{ ID: "err.server.messages_search.upstream_unavailable", HTTPStatus: http.StatusServiceUnavailable, DefaultMessage: "Search service is temporarily unavailable.", Internal: true, }) // INTERNAL_ERROR — OS 4xx (DSL bug) / unexpected. ErrMessagesSearchInternal = register(codes.Code{ ID: "err.server.messages_search.internal", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Internal search error.", Internal: true, }) // RATE_LIMITED — per-loginUID 5 QPS / 20 burst exceeded. ErrMessagesSearchRateLimited = register(codes.Code{ ID: "err.server.messages_search.rate_limited", HTTPStatus: http.StatusTooManyRequests, DefaultMessage: "Search rate limit exceeded.", SafeDetailKeys: []string{"retry_after"}, }) // NOT_FOUND — channel not visible / Space rejection. ErrMessagesSearchNotFound = register(codes.Code{ ID: "err.server.messages_search.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Channel or resource not found for search.", SafeDetailKeys: []string{"resource"}, }) // SEARCH_DISABLED — the deployment has OCTO_SEARCH_BACKEND=disabled, so // every search surface (the new _search* endpoints, the legacy // /v1/message/search path, and modules/search global search) refuses // uniformly instead of 500/panic/leaking results. Core IM stays fully // functional; the client uses appconfig.search_enabled to hide the search // box rather than probe this code per request. ErrMessagesSearchDisabled = register(codes.Code{ ID: "err.server.messages_search.disabled", HTTPStatus: http.StatusServiceUnavailable, DefaultMessage: "Search is not enabled on this deployment.", }) // DEPTH_EXCEEDED — the caller paged past the maximum cumulative result // depth (aligned with OpenSearch max_result_window=10000). The cap is on // the cumulative number of results already returned (encoded in the // cursor), NOT on the per-request page_size, so shrinking/growing // page_size cannot be used to walk past it. Clients that hit this should // narrow the query (keyword / time window / sender) instead of paging // deeper. ErrMessagesSearchDepthExceeded = register(codes.Code{ ID: "err.server.messages_search.depth_exceeded", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Search pagination depth limit reached; narrow your query.", SafeDetailKeys: []string{"max_depth"}, }) )
err.server.messages_search.* — modules/messages_search business error codes.
These codes back the R2 12-item enum shipped to API clients (per the docs/messages-search/octo-server-search-dev.md §8 mapping table). The R2 wire codes are surfaced through the i18n localized error envelope and map to the HTTPStatus values declared here (renderer keeps wire status pinned to 400 for legacy compatibility while exposing the real status in error.http_status).
var ( // to initialise (Discovery failed → o.bind is nil) but the routes are still // mounted. Transient: ops can recover by fixing Discovery / restarting. // // NOTE: Internal=true (5xx hygiene) means the renderer emits the shared // internal-error copy, not this DefaultMessage — the specific text and its // zh-CN translation are intentionally unreachable on the wire. They are kept // to satisfy Register and to document intent; clients distinguish the // transient 503 via error.http_status, not the message. ErrOIDCBindServiceUnavailable = register(codes.Code{ ID: "err.server.oidc.bind_service_unavailable", HTTPStatus: http.StatusServiceUnavailable, DefaultMessage: "The account binding service is temporarily unavailable. Please try again later.", Internal: true, }) // ErrOIDCBindRequestInvalid is the catch-all for malformed bind input: // BindJSON failure, missing/blank token, bad authcode format, or missing // identifier/password/code fields. ErrOIDCBindRequestInvalid = register(codes.Code{ ID: "err.server.oidc.bind_request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", }) // verified phone, so SMS verification cannot proceed — the client should // switch to the password method rather than retry. ErrOIDCBindSMSUnavailable = register(codes.Code{ ID: "err.server.oidc.bind_sms_unavailable", HTTPStatus: http.StatusBadRequest, DefaultMessage: "SMS verification is not available for this account.", }) // ErrOIDCBindMethodDisabled maps ErrBindMethodDisabled: the verification // method was turned off via OCTO_OIDC_BIND_METHODS — the client should not // retry it. ErrOIDCBindMethodDisabled = register(codes.Code{ ID: "err.server.oidc.bind_method_disabled", HTTPStatus: http.StatusBadRequest, DefaultMessage: "This verification method is currently disabled.", }) // ErrOIDCBindInvalidCredentials maps ErrBindAuthRejected (wrong password / // bad OTP / phone not matched) and the confirm-stage TOCTOU rejection. A // single generic 401 across all causes prevents account enumeration. ErrOIDCBindInvalidCredentials = register(codes.Code{ ID: "err.server.oidc.bind_invalid_credentials", HTTPStatus: http.StatusUnauthorized, DefaultMessage: "Verification failed. Please check your details and try again.", }) // ErrOIDCBindVerifyRequired maps ErrBindStatusConflict on the confirm path: // the session is not yet verified (user skipped the second factor or a // concurrent confirm raced). ErrOIDCBindVerifyRequired = register(codes.Code{ ID: "err.server.oidc.bind_verify_required", HTTPStatus: http.StatusUnauthorized, DefaultMessage: "Please complete verification before confirming.", }) // ErrOIDCBindAlreadyVerified maps ErrBindStatusConflict on the verify path: // the session is already verified; the client should skip ahead to confirm. ErrOIDCBindAlreadyVerified = register(codes.Code{ ID: "err.server.oidc.bind_already_verified", HTTPStatus: http.StatusConflict, DefaultMessage: "This binding session has already been verified.", }) // ErrOIDCBindStatusConflict maps ErrBindStatusConflict on the create path: // the session state does not allow account creation. ErrOIDCBindStatusConflict = register(codes.Code{ ID: "err.server.oidc.bind_status_conflict", HTTPStatus: http.StatusConflict, DefaultMessage: "The binding session is in a conflicting state.", }) // ErrOIDCBindAlreadyBound maps ErrBindAlreadyBound: the identity row already // exists (a retry after a successful bind, or a create race). The client is // guided back to the OIDC sign-in entry point. ErrOIDCBindAlreadyBound = register(codes.Code{ ID: "err.server.oidc.bind_already_bound", HTTPStatus: http.StatusConflict, DefaultMessage: "This identity is already bound. Please sign in via OIDC to continue.", }) // ErrOIDCBindConflictNeedManual maps ErrBindConflictNeedManual / // ErrBindCreateConflictNeedManual: the claims match multiple accounts, which // the self-service flow cannot resolve — route to admin/manual handling. ErrOIDCBindConflictNeedManual = register(codes.Code{ ID: "err.server.oidc.bind_conflict_need_manual", HTTPStatus: http.StatusConflict, DefaultMessage: "Multiple accounts match; manual resolution is required. Please contact support.", }) // ErrOIDCBindTokenInvalid maps ErrBindNotFound: the single-use 5-minute // bind_token has expired or was already consumed. The client must restart // the OIDC flow. ErrOIDCBindTokenInvalid = register(codes.Code{ ID: "err.server.oidc.bind_token_invalid", HTTPStatus: http.StatusGone, DefaultMessage: "The binding session has expired or is invalid. Please start over.", }) // ErrOIDCBindClaimsIncomplete maps ErrBindCreateClaimsIncomplete: the SSO // claims carry neither a verified email nor a verified phone, so /bind/create // cannot provision an account. ErrOIDCBindClaimsIncomplete = register(codes.Code{ ID: "err.server.oidc.bind_claims_incomplete", HTTPStatus: http.StatusUnprocessableEntity, DefaultMessage: "The identity provider did not supply the account details required to create an account.", }) )
err.server.oidc.* — modules/oidc self-service bind error codes (api_bind.go). DefaultMessage holds the en-US source (D4); the zh-CN runtime translation lives in pkg/i18n/locales/active.zh-CN.toml.
These codes are rendered via httperr.ResponseErrorLWithStatus (NOT the fixed-400 ResponseErrorL): the bind endpoints are a recent feature with no legacy clients, and have always returned semantic status codes (400/401/409/410/422/429/503). The wire status therefore equals HTTPStatus.
Anti-enumeration: ErrBindAuthRejected (wrong password / bad OTP / phone not matched) and the verify→confirm TOCTOU rejection all map to the single ErrOIDCBindInvalidCredentials (401) with a generic message — the specific reason is logged via zap only, never surfaced, so the response cannot be used to probe "account exists vs password wrong".
Only the 503 code is 5xx and therefore Internal=true (its message is a generic placeholder; clients identify the transient condition from error.http_status). Genuine internal failures (the default branch of each handle*Err) reuse err.shared.internal rather than a bespoke oidc code.
var ( ErrOpanalyticsForbidden = register(codes.Code{ ID: "err.server.opanalytics.forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "Only a super admin can access the analytics dashboard.", }) ErrOpanalyticsRequestInvalid = register(codes.Code{ ID: "err.server.opanalytics.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid analytics request.", SafeDetailKeys: []string{"reason"}, }) ErrOpanalyticsNotFound = register(codes.Code{ ID: "err.server.opanalytics.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The space does not exist.", }) ErrOpanalyticsQueryFailed = register(codes.Code{ ID: "err.server.opanalytics.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query analytics data.", Internal: true, }) ErrOpanalyticsETLAlreadyRunning = register(codes.Code{ ID: "err.server.opanalytics.etl_already_running", HTTPStatus: http.StatusConflict, DefaultMessage: "Analytics ETL is already running.", }) ErrOpanalyticsETLTriggerFailed = register(codes.Code{ ID: "err.server.opanalytics.etl_trigger_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to trigger analytics ETL.", Internal: true, }) )
err.server.opanalytics.* — modules/opanalytics 运营分析看板(管理端 superAdmin 跨 space 只读)的业务错误码。DefaultMessage 是 en-US 源;zh-CN 运行时翻译在 pkg/i18n/locales/active.zh-CN.toml。5xx ⟺ Internal=true(渲染层隐藏 message/details)。
var ( ErrQRCodeRequestInvalid = register(codes.Code{ ID: "err.server.qrcode.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid QR code request.", SafeDetailKeys: []string{"field"}, }) ErrQRCodeTokenRequired = register(codes.Code{ ID: "err.server.qrcode.token_required", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Token is required.", SafeDetailKeys: []string{"field"}, }) ErrQRCodeTokenInvalid = register(codes.Code{ ID: "err.server.qrcode.token_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Token information is invalid.", }) ErrQRCodeNotFound = register(codes.Code{ ID: "err.server.qrcode.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The QR code is invalid or has expired.", }) ErrQRCodeUserNotFound = register(codes.Code{ ID: "err.server.qrcode.user_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "User not found.", }) ErrQRCodeGroupNotFound = register(codes.Code{ ID: "err.server.qrcode.group_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Group not found.", }) ErrQRCodeGroupSpaceForbidden = register(codes.Code{ ID: "err.server.qrcode.group_space_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "Only members of this space can join the group.", }) ErrQRCodeQueryFailed = register(codes.Code{ ID: "err.server.qrcode.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query QR code data.", Internal: true, }) ErrQRCodeStoreFailed = register(codes.Code{ ID: "err.server.qrcode.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update QR code data.", Internal: true, }) )
err.server.qrcode.* — modules/qrcode business error codes (api.go).
var ( ErrReportRequestInvalid = register(codes.Code{ ID: "err.server.report.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid report request.", SafeDetailKeys: []string{"field"}, }) ErrReportSessionInvalid = register(codes.Code{ ID: "err.server.report.session_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The report session is invalid or has expired.", }) ErrReportQueryFailed = register(codes.Code{ ID: "err.server.report.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query report data.", Internal: true, }) ErrReportStoreFailed = register(codes.Code{ ID: "err.server.report.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update report data.", Internal: true, }) )
err.server.report.* — modules/report business error codes (api.go / api_manager.go).
var ( // ErrRobotRequestInvalid is the catch-all for missing/malformed request // input (BindJSON failure, "X 不能为空", invalid params). The offending // field is surfaced via Details when the caller can identify it. ErrRobotRequestInvalid = register(codes.Code{ ID: "err.server.robot.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) // ErrRobotContentInvalid covers an invalid message payload / content_edit // (empty payload, missing payload.type, payload that fails the content-type // contract, or content_edit that fails RichText normalization). The raw // payload is intentionally NOT surfaced (it can be large / sensitive); only // the offending field name is exposed. ErrRobotContentInvalid = register(codes.Code{ ID: "err.server.robot.content_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid message content.", SafeDetailKeys: []string{"field"}, }) // ErrRobotContentTypeUnsupported covers an unsupported message content type. ErrRobotContentTypeUnsupported = register(codes.Code{ ID: "err.server.robot.content_type_unsupported", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Unsupported message type.", SafeDetailKeys: []string{"type"}, }) // ErrRobotFileTypeUnsupported covers an unsupported / extension-less upload. ErrRobotFileTypeUnsupported = register(codes.Code{ ID: "err.server.robot.file_type_unsupported", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Unsupported file type.", }) // ErrRobotFileTooLarge surfaces the upload size cap so the client can render // a localized hint without hard-coding the limit. ErrRobotFileTooLarge = register(codes.Code{ ID: "err.server.robot.file_too_large", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The file exceeds the maximum allowed size.", SafeDetailKeys: []string{"max_mb"}, }) // ErrRobotNoFieldsToUpdate covers an update request with no mutable fields. ErrRobotNoFieldsToUpdate = register(codes.Code{ ID: "err.server.robot.no_fields_to_update", HTTPStatus: http.StatusBadRequest, DefaultMessage: "No fields to update.", }) // ErrRobotCreatorOnly covers the creator-ownership guards on the owner-facing // endpoints (set description / auto-approve / mention preference) — all the // same "only the bot creator may operate" condition. ErrRobotCreatorOnly = register(codes.Code{ ID: "err.server.robot.creator_only", HTTPStatus: http.StatusForbidden, DefaultMessage: "Only the bot creator can perform this operation.", }) // ErrRobotMessageEditForbidden covers the bot-message edit guard (a bot may // only edit messages it sent). ErrRobotMessageEditForbidden = register(codes.Code{ ID: "err.server.robot.message_edit_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "You can only edit messages you sent.", }) // ErrRobotChannelSendForbidden covers the channel-membership send guard. ErrRobotChannelSendForbidden = register(codes.Code{ ID: "err.server.robot.channel_send_forbidden", HTTPStatus: http.StatusForbidden, DefaultMessage: "Sending messages to this channel is not allowed.", }) // ErrRobotNotFound covers a missing / disabled / not-fully-provisioned robot // (no creator_uid, no app_id). No enumeration: the specific reason goes to // the log, the client sees one generic "not found". ErrRobotNotFound = register(codes.Code{ ID: "err.server.robot.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Robot not found.", }) // ErrRobotMessageNotFound covers a missing target message on edit. ErrRobotMessageNotFound = register(codes.Code{ ID: "err.server.robot.message_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Message not found.", }) // ErrRobotAuthFailed is the SINGLE anti-enumeration code for the robot // webhook auth middleware (authRobot): robot not found, app not found, and // app_key mismatch ALL collapse to one 401 so an external caller cannot probe // which factor was wrong. The specific reason is logged, never returned. // // This middleware serves external bot adapters (app_key auth), not the dmwork // front-end, so it is rendered via ResponseErrorLWithStatus to PRESERVE the // real 401 wire status rather than the D14 compatibility 400 — adapters branch // on HTTP 401. (Divergence from D14 flagged for maintainer sign-off.) ErrRobotAuthFailed = register(codes.Code{ ID: "err.server.robot.auth_failed", HTTPStatus: http.StatusUnauthorized, DefaultMessage: "Robot authentication failed.", }) // ErrRobotInlineQueryTimeout covers the inline-query long-poll giving up after // the adapter fails to answer in time. Rendered via ResponseErrorLWithStatus // to preserve the 408 so the front-end's retry logic still sees a timeout. ErrRobotInlineQueryTimeout = register(codes.Code{ ID: "err.server.robot.inline_query_timeout", HTTPStatus: http.StatusRequestTimeout, DefaultMessage: "The inline query timed out.", }) // ErrRobotQueryFailed covers read-path failures (robot / menu / command / // message-extra SELECTs, corrupt stored command JSON). Log the underlying err // before responding. ErrRobotQueryFailed = register(codes.Code{ ID: "err.server.robot.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query robot data.", Internal: true, }) // ErrRobotStoreFailed covers mutation-path failures (DB write/update/delete, // transaction begin/commit/rollback, sequence generation, IM connection // cleanup). Log the underlying err before responding. ErrRobotStoreFailed = register(codes.Code{ ID: "err.server.robot.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update robot data.", Internal: true, }) // ErrRobotSendFailed covers WuKongIM dispatch failures (send message / typing // / stream start-end / CMD sync). Log the underlying err before responding. ErrRobotSendFailed = register(codes.Code{ ID: "err.server.robot.send_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to send the message.", Internal: true, }) // ErrRobotUploadFailed covers the file proxy / upload / STS-credential / // presigned-URL path (read, storage write, COS misconfiguration). Log the // underlying err before responding. ErrRobotUploadFailed = register(codes.Code{ ID: "err.server.robot.upload_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to process the file.", Internal: true, }) // ErrRobotTokenGenFailed covers bot-token (re)generation failures. Log the // underlying err before responding. ErrRobotTokenGenFailed = register(codes.Code{ ID: "err.server.robot.token_gen_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to generate the robot token.", Internal: true, }) // ErrRobotAuthCheckFailed covers the robot webhook auth middleware's own // infrastructure failures (DB query for robot / app errored) — distinct from // ErrRobotAuthFailed (a real credential failure). Preserves the real 500 via // ResponseErrorLWithStatus so adapters retry instead of treating it as a // permanent 401. Log the underlying err before responding. ErrRobotAuthCheckFailed = register(codes.Code{ ID: "err.server.robot.auth_check_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Robot authentication check failed.", Internal: true, }) // ErrRobotGroupDisbanded covers the disbanded-group guard for legacy robot // endpoints: after a group is disbanded (group.status=2), robot write // operations (sendMessage, streamStart, streamEnd, botMessageEdit) are // rejected with 403 Forbidden (parity with ErrBotAPIGroupDisbanded). ErrRobotGroupDisbanded = register(codes.Code{ ID: "err.server.robot.group_disbanded", HTTPStatus: http.StatusForbidden, DefaultMessage: "This group has been disbanded and is read-only.", }) )
err.server.robot.* — modules/robot business error codes (api.go / api_manager.go / mention_pref.go). DefaultMessage holds the en-US source (D4); the zh-CN runtime translation lives in pkg/i18n/locales/active.zh-CN.toml. Internal=true codes never surface their message on the wire — callers MUST log the underlying err with full context (zap.Error) before responding.
var ( ErrSearchRequestInvalid = register(codes.Code{ ID: "err.server.search.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid search request.", SafeDetailKeys: []string{"field"}, }) ErrSearchMessageQueryFailed = register(codes.Code{ ID: "err.server.search.message_query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query message search data.", Internal: true, }) ErrSearchGroupQueryFailed = register(codes.Code{ ID: "err.server.search.group_query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query group search data.", Internal: true, }) ErrSearchUserQueryFailed = register(codes.Code{ ID: "err.server.search.user_query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query user search data.", Internal: true, }) )
err.server.search.* — modules/search business error codes (api.go).
var ( ErrThreadGroupNoInvalid = register(codes.Code{ ID: "err.server.thread.group_no_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid group number.", SafeDetailKeys: []string{"field"}, }) ErrThreadShortIDInvalid = register(codes.Code{ ID: "err.server.thread.short_id_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid thread short ID.", SafeDetailKeys: []string{"field"}, }) ErrThreadRequestInvalid = register(codes.Code{ ID: "err.server.thread.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field", "max_size"}, }) ErrThreadNameInvalid = register(codes.Code{ ID: "err.server.thread.name_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Thread name is required and must not exceed 100 characters.", SafeDetailKeys: []string{"field", "max_length"}, }) ErrThreadSourceMessageInvalid = register(codes.Code{ ID: "err.server.thread.source_message_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid source message payload.", SafeDetailKeys: []string{"field", "max_size"}, }) ErrThreadStatusInvalid = register(codes.Code{ ID: "err.server.thread.status_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid thread status.", SafeDetailKeys: []string{"field"}, }) ErrThreadNotGroupMember = register(codes.Code{ ID: "err.server.thread.not_group_member", HTTPStatus: http.StatusForbidden, DefaultMessage: "You are not a group member.", }) ErrThreadGroupDisbanded = register(codes.Code{ ID: "err.server.thread.group_disbanded", HTTPStatus: http.StatusForbidden, DefaultMessage: "The group has been disbanded.", }) ErrThreadPermissionDenied = register(codes.Code{ ID: "err.server.thread.permission_denied", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have permission to perform this action.", }) ErrThreadNotFound = register(codes.Code{ ID: "err.server.thread.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Thread not found.", }) ErrThreadDeleted = register(codes.Code{ ID: "err.server.thread.deleted", HTTPStatus: http.StatusGone, DefaultMessage: "Thread has been deleted.", }) ErrThreadNotActive = register(codes.Code{ ID: "err.server.thread.not_active", HTTPStatus: http.StatusConflict, DefaultMessage: "Thread is not active.", }) ErrThreadStatusChanged = register(codes.Code{ ID: "err.server.thread.status_changed", HTTPStatus: http.StatusConflict, DefaultMessage: "Thread status changed concurrently.", }) ErrThreadCreatorCannotLeave = register(codes.Code{ ID: "err.server.thread.creator_cannot_leave", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Thread creator cannot leave the thread.", }) ErrThreadGroupMDNotFound = register(codes.Code{ ID: "err.server.thread.group_md_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Thread GROUP.md not found.", }) ErrThreadGroupMDContentEmpty = register(codes.Code{ ID: "err.server.thread.group_md_content_empty", HTTPStatus: http.StatusBadRequest, DefaultMessage: "GROUP.md content must not be empty.", SafeDetailKeys: []string{"field"}, }) ErrThreadGroupMDContentTooLarge = register(codes.Code{ ID: "err.server.thread.group_md_content_too_large", HTTPStatus: http.StatusBadRequest, DefaultMessage: "GROUP.md content exceeds the maximum size.", SafeDetailKeys: []string{"field", "max_size"}, }) ErrThreadSettingInvalid = register(codes.Code{ ID: "err.server.thread.setting_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid thread setting.", SafeDetailKeys: []string{"field"}, }) ErrThreadStoreFailed = register(codes.Code{ ID: "err.server.thread.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Thread storage operation failed.", Internal: true, }) )
var ( // ErrSpaceRequestInvalid is the catch-all for missing/malformed request // input (BindJSON failure, "X 不能为空", invalid enum / mode / status / role // values, "at least one of ... required", negative numbers). The offending // field is surfaced via Details when the caller can identify it. ErrSpaceRequestInvalid = register(codes.Code{ ID: "err.server.space.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) // ErrSpaceFieldTooLong covers the name / description / logo length caps. The // field name and its max length are surfaced so the client can render a // localized hint without hard-coding the limit. ErrSpaceFieldTooLong = register(codes.Code{ ID: "err.server.space.field_too_long", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The field exceeds the maximum allowed length.", SafeDetailKeys: []string{"field", "max_chars"}, }) // ErrSpaceBatchTooLarge covers the per-request member batch cap. ErrSpaceBatchTooLarge = register(codes.Code{ ID: "err.server.space.batch_too_large", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Too many members in a single request.", SafeDetailKeys: []string{"max"}, }) // ErrSpacePermissionDenied covers the space-level authorization guards // ("no permission to ...", "only the owner may ..."). Distinct from the // route-level err.shared.auth.forbidden role guard. ErrSpacePermissionDenied = register(codes.Code{ ID: "err.server.space.permission_denied", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have permission to perform this operation.", }) // ErrSpaceNotMember covers the "you are not a member of this space" guard. ErrSpaceNotMember = register(codes.Code{ ID: "err.server.space.not_member", HTTPStatus: http.StatusForbidden, DefaultMessage: "You are not a member of this space.", }) // ErrSpaceCreationDisabled covers the admin-disabled space-creation switch. ErrSpaceCreationDisabled = register(codes.Code{ ID: "err.server.space.creation_disabled", HTTPStatus: http.StatusForbidden, DefaultMessage: "Space creation has been disabled by the administrator.", }) // ErrSpaceNotFound covers a missing / disbanded space. ErrSpaceNotFound = register(codes.Code{ ID: "err.server.space.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Space not found or has been disbanded.", }) // ErrSpaceApplyNotFound covers a missing join-application record, or one that // does not belong to the current space (merged so callers cannot probe // cross-space application IDs). ErrSpaceApplyNotFound = register(codes.Code{ ID: "err.server.space.apply_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Join application not found.", }) // ErrSpaceInviteCodeNotFound covers a missing invite code. ErrSpaceInviteCodeNotFound = register(codes.Code{ ID: "err.server.space.invite_code_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Invite code not found.", }) // ErrSpaceMemberNotFound covers a missing / already-removed target member or // target user. ErrSpaceMemberNotFound = register(codes.Code{ ID: "err.server.space.member_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The target member does not exist or has been removed.", }) // ErrSpaceInviteCodeInvalid is the single anti-enumeration code for an // invalid / expired / malformed invite or auth code (public preview and // join-approve flows). The specific reason is logged, never returned, so an // unauthenticated caller cannot probe which state a code is in. ErrSpaceInviteCodeInvalid = register(codes.Code{ ID: "err.server.space.invite_code_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The invite code is invalid or has expired.", }) // ErrSpaceInviteCodeExhausted covers an invite code that has reached its use // cap or is otherwise spent. ErrSpaceInviteCodeExhausted = register(codes.Code{ ID: "err.server.space.invite_code_exhausted", HTTPStatus: http.StatusConflict, DefaultMessage: "The invite code has reached its usage limit.", }) // ErrSpaceAlreadyMember covers re-joining a space the caller already belongs // to. ErrSpaceAlreadyMember = register(codes.Code{ ID: "err.server.space.already_member", HTTPStatus: http.StatusConflict, DefaultMessage: "You are already a member of this space.", }) // ErrSpaceApplyProcessed covers approving / rejecting an application that has // already been handled. ErrSpaceApplyProcessed = register(codes.Code{ ID: "err.server.space.apply_processed", HTTPStatus: http.StatusConflict, DefaultMessage: "This application has already been processed.", }) // ErrSpaceFull covers hitting the space member cap. ErrSpaceFull = register(codes.Code{ ID: "err.server.space.full", HTTPStatus: http.StatusConflict, DefaultMessage: "The space is full.", }) // ErrSpaceImmutable covers mutations rejected because the space is disbanded // or banned (cannot modify / add members / update status). ErrSpaceImmutable = register(codes.Code{ ID: "err.server.space.immutable", HTTPStatus: http.StatusConflict, DefaultMessage: "The space has been disbanded or banned and cannot be modified.", }) // ErrSpaceOwnerConstraint covers owner-specific transfer/leave/demote // constraints (owner must transfer ownership before leaving; cannot remove or // directly demote the owner). ErrSpaceOwnerConstraint = register(codes.Code{ ID: "err.server.space.owner_constraint", HTTPStatus: http.StatusConflict, DefaultMessage: "Transfer ownership before performing this operation.", }) // ErrSpaceEmailInviteNotFound covers a missing email invite on the admin // revoke path. ErrSpaceEmailInviteNotFound = register(codes.Code{ ID: "err.server.space.email_invite_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Email invite not found.", }) // ErrSpaceEmailInviteInvalid covers an invite that cannot be accepted because // it is invalid / expired / missing required data / of an unknown type. The // accept endpoint is authenticated (the caller already proved identity and // must match the invited email), so this stays a plain 400. ErrSpaceEmailInviteInvalid = register(codes.Code{ ID: "err.server.space.email_invite_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The invite is invalid or has expired.", }) // ErrSpaceEmailInviteProcessed covers accepting/revoking an invite that has // already been consumed, revoked, or is no longer pending. ErrSpaceEmailInviteProcessed = register(codes.Code{ ID: "err.server.space.email_invite_processed", HTTPStatus: http.StatusConflict, DefaultMessage: "This invite has already been processed.", }) // ErrSpaceEmailInviteEmailMismatch covers the typed-email / login-account // email not matching the invite target (defense-in-depth identity check). ErrSpaceEmailInviteEmailMismatch = register(codes.Code{ ID: "err.server.space.email_invite_email_mismatch", HTTPStatus: http.StatusForbidden, DefaultMessage: "The email does not match the invite.", }) // ErrSpaceQueryFailed covers read-path failures (space / member / invite / // application SELECTs and counts, use-count checks, user validation). Log the // underlying err before responding. ErrSpaceQueryFailed = register(codes.Code{ ID: "err.server.space.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query space data.", Internal: true, }) // ErrSpaceStoreFailed covers mutation-path failures (create / update / join / // add / remove / leave / disband / role change / ownership transfer / invite // write, transaction begin/commit). Log the underlying err before responding. ErrSpaceStoreFailed = register(codes.Code{ ID: "err.server.space.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update space data.", Internal: true, }) )
err.server.space.* — modules/space business error codes (api.go / api_manager.go / api_email_invite*.go). DefaultMessage holds the en-US source (D4); the zh-CN runtime translation lives in pkg/i18n/locales/active.zh-CN.toml. Internal=true codes never surface their message on the wire — callers MUST log the underlying err with full context (zap.Error) before responding.
var ( ErrStatisticsRequestInvalid = register(codes.Code{ ID: "err.server.statistics.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid statistics request.", SafeDetailKeys: []string{"field"}, }) ErrStatisticsQueryFailed = register(codes.Code{ ID: "err.server.statistics.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query statistics data.", Internal: true, }) )
err.server.statistics.* — modules/statistics business error codes (api.go).
var ( // ErrStickerRequestInvalid is the catch-all for missing/malformed request // input (BindJSON failure, empty path). The offending field is surfaced via // Details when the caller can identify it. ErrStickerRequestInvalid = register(codes.Code{ ID: "err.server.sticker.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) // ErrStickerFormatUnsupported covers an empty or out-of-whitelist sticker // format. The attempted format is surfaced so the client can render a // localized hint without hard-coding the accepted set. ErrStickerFormatUnsupported = register(codes.Code{ ID: "err.server.sticker.format_unsupported", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Unsupported sticker format. Allowed: gif, png, jpg, jpeg, webp.", SafeDetailKeys: []string{"field", "format"}, }) ErrStickerShortcodeInvalid = register(codes.Code{ ID: "err.server.sticker.shortcode_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid sticker shortcode.", SafeDetailKeys: []string{"field"}, }) ErrStickerKeywordsInvalid = register(codes.Code{ ID: "err.server.sticker.keywords_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid sticker keywords.", SafeDetailKeys: []string{"field"}, }) // ErrStickerNotFound covers both a missing sticker and the deliberate // "not found or not yours" merge on delete (no cross-user enumeration). ErrStickerNotFound = register(codes.Code{ ID: "err.server.sticker.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Sticker not found.", }) // ErrStickerQuotaExceeded is returned when a user is already at their // per-user custom-sticker cap (admin-configurable system_setting // sticker.user_max_count, default 100). The effective cap is surfaced via // the `max` detail. ErrStickerQuotaExceeded = register(codes.Code{ ID: "err.server.sticker.quota_exceeded", HTTPStatus: http.StatusConflict, DefaultMessage: "You have reached the maximum number of custom stickers.", SafeDetailKeys: []string{"max"}, }) ErrStickerShortcodeConflict = register(codes.Code{ ID: "err.server.sticker.shortcode_conflict", HTTPStatus: http.StatusConflict, DefaultMessage: "Sticker shortcode already exists.", SafeDetailKeys: []string{"field"}, }) // ErrStickerQueryFailed covers read-path failures (DB SELECT/count). Log the // underlying err before responding. ErrStickerQueryFailed = register(codes.Code{ ID: "err.server.sticker.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query sticker data.", Internal: true, }) // ErrStickerStoreFailed covers mutation-path failures (DB insert/update). Log // the underlying err before responding. ErrStickerStoreFailed = register(codes.Code{ ID: "err.server.sticker.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update sticker data.", Internal: true, }) )
err.server.sticker.* — modules/sticker business error codes (api.go). DefaultMessage holds the en-US source (D4); the zh-CN runtime translation lives in pkg/i18n/locales/active.zh-CN.toml. Internal=true codes never surface their message on the wire — callers MUST log the underlying err with full context via the module logger before responding.
var ( ErrUserRequestInvalid = register(codes.Code{ ID: "err.server.user.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) ErrUserLockMinuteOutOfRange = register(codes.Code{ ID: "err.server.user.lock_minute_out_of_range", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Lock screen delay must be between 0 and 60 minutes.", SafeDetailKeys: []string{"field", "min", "max"}, }) ErrUserShortNoFormatInvalid = register(codes.Code{ ID: "err.server.user.short_no_format_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Short ID must start with a letter and contain 6-20 letters, digits, underscores or hyphens.", }) ErrUserLanguageUnsupported = register(codes.Code{ ID: "err.server.user.language_unsupported", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Unsupported language.", }) ErrUserTokenRequired = register(codes.Code{ ID: "err.server.user.token_required", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Token is required.", SafeDetailKeys: []string{"field"}, }) ErrUserInvalidCredentials = register(codes.Code{ ID: "err.server.user.invalid_credentials", HTTPStatus: http.StatusUnauthorized, DefaultMessage: "Invalid username or password.", }) // ErrUserAPIKeyInvalid is the anti-enumeration 401 for the daemon // verify-api-key endpoint: both an unresolvable/unknown key and a key whose // owner is not a member of the bound space collapse here, so a caller cannot // probe which factor was wrong. The specific reason is logged, never returned. ErrUserAPIKeyInvalid = register(codes.Code{ ID: "err.server.user.api_key_invalid", HTTPStatus: http.StatusUnauthorized, DefaultMessage: "Invalid api_key.", }) ErrUserCodeInvalid = register(codes.Code{ ID: "err.server.user.code_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid verification code.", }) ErrUserAccountBanned = register(codes.Code{ ID: "err.server.user.account_banned", HTTPStatus: http.StatusForbidden, DefaultMessage: "This account has been banned.", }) ErrUserLoginDeviceExpired = register(codes.Code{ ID: "err.server.user.login_device_expired", HTTPStatus: http.StatusUnauthorized, DefaultMessage: "Login device session expired, please sign in again.", }) // ErrUserLoginLocked maps the anti-brute-force lockout returned by // LoginGuard.Check (ErrLoginLocked). Not Internal=true: the wire message // is the user-actionable explanation. 429 is the standard HTTP status // for rate-limited / lockout states even though the count window is // per-account rather than per-IP. ErrUserLoginLocked = register(codes.Code{ ID: "err.server.user.login_locked", HTTPStatus: http.StatusTooManyRequests, DefaultMessage: "Too many failed login attempts, account temporarily locked. Please try again later.", }) ErrUserNotFound = register(codes.Code{ ID: "err.server.user.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "User not found.", }) ErrUserCurrentNotFound = register(codes.Code{ ID: "err.server.user.current_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Current user not found.", }) ErrUserDeviceNotFound = register(codes.Code{ ID: "err.server.user.device_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Device not found.", }) ErrUserAlreadyExists = register(codes.Code{ ID: "err.server.user.already_exists", HTTPStatus: http.StatusConflict, DefaultMessage: "User already exists.", }) ErrUserRegistrationClosed = register(codes.Code{ ID: "err.server.user.registration_closed", HTTPStatus: http.StatusForbidden, DefaultMessage: "Registration is currently closed.", }) ErrUserLocalLoginDisabled = register(codes.Code{ ID: "err.server.user.local_login_disabled", HTTPStatus: http.StatusForbidden, DefaultMessage: "Local login is disabled.", }) ErrUserPhoneRegionUnsupported = register(codes.Code{ ID: "err.server.user.phone_region_unsupported", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Only mainland China phone numbers are supported.", }) ErrUserInviteCodeNotFound = register(codes.Code{ ID: "err.server.user.invite_code_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Invite code does not exist.", }) ErrUserAccountDestroyed = register(codes.Code{ ID: "err.server.user.account_destroyed", HTTPStatus: http.StatusForbidden, DefaultMessage: "This account has been deactivated.", }) ErrUserAccountDestroying = register(codes.Code{ ID: "err.server.user.account_destroying", HTTPStatus: http.StatusForbidden, DefaultMessage: "Account is in the deactivation cooldown period, please use a newer client to revoke or check status.", }) ErrUserUpdateNotAllowed = register(codes.Code{ ID: "err.server.user.update_not_allowed", HTTPStatus: http.StatusForbidden, DefaultMessage: "This field cannot be updated.", SafeDetailKeys: []string{"field"}, }) ErrUserShortNoAlreadyChanged = register(codes.Code{ ID: "err.server.user.short_no_already_changed", HTTPStatus: http.StatusForbidden, DefaultMessage: "Short ID can only be changed once.", }) ErrUserDemoLockUnsupported = register(codes.Code{ ID: "err.server.user.demo_lock_unsupported", HTTPStatus: http.StatusForbidden, DefaultMessage: "Demo accounts cannot enable device lock.", }) ErrUserAuthCodeNotFound = register(codes.Code{ ID: "err.server.user.auth_code_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Authorization code is invalid or has expired.", }) ErrUserAuthCodeWrongType = register(codes.Code{ ID: "err.server.user.auth_code_wrong_type", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Authorization code is not a login code.", }) ErrUserAuthInfoInvalid = register(codes.Code{ ID: "err.server.user.auth_info_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Authorization payload is invalid.", SafeDetailKeys: []string{"missing_field"}, }) ErrUserAuthScannerMismatch = register(codes.Code{ ID: "err.server.user.auth_scanner_mismatch", HTTPStatus: http.StatusForbidden, DefaultMessage: "Scanner and authorizer are not the same user.", }) ErrUserQRVerCodeMissing = register(codes.Code{ ID: "err.server.user.qr_ver_code_missing", HTTPStatus: http.StatusForbidden, DefaultMessage: "User has no QR verification code.", }) ErrUserWeChatExchangeFailed = register(codes.Code{ ID: "err.server.user.wechat_exchange_failed", HTTPStatus: http.StatusBadGateway, DefaultMessage: "Failed to exchange WeChat access token.", Internal: true, }) ErrUserWeChatProfileFailed = register(codes.Code{ ID: "err.server.user.wechat_profile_failed", HTTPStatus: http.StatusBadGateway, DefaultMessage: "Failed to fetch WeChat user profile.", Internal: true, }) ErrUserWeChatResponseInvalid = register(codes.Code{ ID: "err.server.user.wechat_response_invalid", HTTPStatus: http.StatusBadGateway, DefaultMessage: "WeChat response is malformed.", Internal: true, }) ErrUserChatPwdUpdateFailed = register(codes.Code{ ID: "err.server.user.chat_pwd_update_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update chat password.", Internal: true, }) ErrUserLoginPwdUpdateFailed = register(codes.Code{ ID: "err.server.user.login_pwd_update_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update login password.", Internal: true, }) ErrUserLockScreenPwdUpdateFailed = register(codes.Code{ ID: "err.server.user.lock_screen_pwd_update_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update lock-screen password.", Internal: true, }) ErrUserPasswordProcessFailed = register(codes.Code{ ID: "err.server.user.password_process_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to process password.", Internal: true, }) ErrUserQueryFailed = register(codes.Code{ ID: "err.server.user.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query user data.", Internal: true, }) ErrUserStoreFailed = register(codes.Code{ ID: "err.server.user.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to persist user data.", Internal: true, }) ErrUserIMCallFailed = register(codes.Code{ ID: "err.server.user.im_call_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to call IM service.", Internal: true, }) ErrUserDecodeFailed = register(codes.Code{ ID: "err.server.user.decode_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to decode internal payload.", Internal: true, }) ErrUserFileOperationFailed = register(codes.Code{ ID: "err.server.user.file_operation_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to process file.", Internal: true, }) ErrUserSMSSendFailed = register(codes.Code{ ID: "err.server.user.sms_send_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to send SMS.", Internal: true, }) ErrUserDestroyFailed = register(codes.Code{ ID: "err.server.user.destroy_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to deactivate account.", Internal: true, }) ErrUserRegisterFailed = register(codes.Code{ ID: "err.server.user.register_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to register user.", Internal: true, }) ErrUserLanguageSetFailed = register(codes.Code{ ID: "err.server.user.language_set_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to set language preference.", Internal: true, }) // ErrUserManagerPermissionRequired fires at /v1/manager/login after the // password check succeeds but the account carries no admin/superAdmin role. // Distinct from err.shared.auth.forbidden (a route-level role guard) so the // login page can show a login-specific hint. ErrUserManagerPermissionRequired = register(codes.Code{ ID: "err.server.user.manager_permission_required", HTTPStatus: http.StatusForbidden, DefaultMessage: "This account does not have management permission.", }) ErrUserPasswordTooShort = register(codes.Code{ ID: "err.server.user.password_too_short", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Password must be at least 6 characters.", }) ErrUserPasswordMismatch = register(codes.Code{ ID: "err.server.user.password_mismatch", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The two passwords do not match.", }) ErrUserOldPasswordIncorrect = register(codes.Code{ ID: "err.server.user.old_password_incorrect", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The current password is incorrect.", }) ErrUserNewPasswordSameAsOld = register(codes.Code{ ID: "err.server.user.new_password_same_as_old", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The new password must be different from the current one.", }) // ErrUserNotAdminAccount guards the delete-admin endpoint: the target user // exists but is not an administrator account, so it cannot be deleted here. ErrUserNotAdminAccount = register(codes.Code{ ID: "err.server.user.not_admin_account", HTTPStatus: http.StatusBadRequest, DefaultMessage: "This user is not an administrator account and cannot be deleted.", }) ErrUserCannotDeleteSuperAdmin = register(codes.Code{ ID: "err.server.user.cannot_delete_super_admin", HTTPStatus: http.StatusForbidden, DefaultMessage: "The super administrator account cannot be deleted.", }) // ErrUserListFilterConflict reports mutually-exclusive list filters // (bot_only + exclude_bot, system_only + exclude_system). The conflicting // filter names are surfaced so a frontend dev can spot the bad query. ErrUserListFilterConflict = register(codes.Code{ ID: "err.server.user.list_filter_conflict", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Conflicting list filters.", SafeDetailKeys: []string{"filter", "conflicts_with"}, }) // ErrUserTokenCacheFailed covers the session-token cache read/write/delete // failures in the manager login / password-change / delete-admin flows. ErrUserTokenCacheFailed = register(codes.Code{ ID: "err.server.user.token_cache_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update the session token cache.", Internal: true, }) ErrUserShortNoGenFailed = register(codes.Code{ ID: "err.server.user.short_no_gen_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to generate a short ID.", Internal: true, }) ErrUserCannotAddSelf = register(codes.Code{ ID: "err.server.user.cannot_add_self", HTTPStatus: http.StatusBadRequest, DefaultMessage: "You cannot add yourself as a friend.", }) ErrUserAlreadyFriend = register(codes.Code{ ID: "err.server.user.already_friend", HTTPStatus: http.StatusConflict, DefaultMessage: "You are already friends.", }) ErrUserBotNotInSpace = register(codes.Code{ ID: "err.server.user.bot_not_in_space", HTTPStatus: http.StatusForbidden, DefaultMessage: "This bot is not in the current space and cannot be added.", }) ErrUserFriendApplyNotFound = register(codes.Code{ ID: "err.server.user.friend_apply_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Friend request not found.", }) ErrUserFriendNotFound = register(codes.Code{ ID: "err.server.user.friend_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Friend not found.", }) // ErrUserFriendApplyInvalid covers an expired / malformed friend-request // token or payload (the apply token decoded but its referenced records are // gone or inconsistent). ErrUserFriendApplyInvalid = register(codes.Code{ ID: "err.server.user.friend_apply_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Friend request is invalid or has expired.", }) ErrUserPasswordNotSet = register(codes.Code{ ID: "err.server.user.password_not_set", HTTPStatus: http.StatusBadRequest, DefaultMessage: "This account has no password set; identity cannot be verified.", }) ErrUserPasswordIncorrect = register(codes.Code{ ID: "err.server.user.password_incorrect", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Incorrect password.", }) // ErrUserAccountStateChanged maps the optimistic-lock conflict // (ErrDestroyStateConflict) when a concurrent request changed the // deactivation state out from under this one. ErrUserAccountStateChanged = register(codes.Code{ ID: "err.server.user.account_state_changed", HTTPStatus: http.StatusConflict, DefaultMessage: "Account status has changed, please refresh and try again.", }) ErrUserNotDestroying = register(codes.Code{ ID: "err.server.user.not_destroying", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Account is not pending deactivation.", }) ErrUserPinnedAlreadyExists = register(codes.Code{ ID: "err.server.user.pinned_already_exists", HTTPStatus: http.StatusConflict, DefaultMessage: "This channel is already pinned.", }) ErrUserPinnedLimitExceeded = register(codes.Code{ ID: "err.server.user.pinned_limit_exceeded", HTTPStatus: http.StatusBadRequest, DefaultMessage: "The pinned channel limit has been reached.", SafeDetailKeys: []string{"max"}, }) // ErrUserChannelAccessDenied collapses the validateChannelAccess guard // rejections (not a friend / not a group member / bot not added). Internal // check failures inside that helper are logged server-side and also surface // here; splitting them into a 5xx is a follow-up. ErrUserChannelAccessDenied = register(codes.Code{ ID: "err.server.user.channel_access_denied", HTTPStatus: http.StatusForbidden, DefaultMessage: "You do not have access to this channel.", }) ErrUserPinnedSortInvalid = register(codes.Code{ ID: "err.server.user.pinned_sort_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid pinned sort request.", }) ErrUserOAuthStateExpired = register(codes.Code{ ID: "err.server.user.oauth_state_expired", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Login session has expired, please try again.", }) ErrUserOAuthExchangeFailed = register(codes.Code{ ID: "err.server.user.oauth_exchange_failed", HTTPStatus: http.StatusBadGateway, DefaultMessage: "Failed to exchange the OAuth access token.", Internal: true, }) ErrUserOAuthProfileFailed = register(codes.Code{ ID: "err.server.user.oauth_profile_failed", HTTPStatus: http.StatusBadGateway, DefaultMessage: "Failed to fetch the OAuth user profile.", Internal: true, }) ErrUserEmailInvalid = register(codes.Code{ ID: "err.server.user.email_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid email address.", }) ErrUserEmailRegisterDisabled = register(codes.Code{ ID: "err.server.user.email_register_disabled", HTTPStatus: http.StatusForbidden, DefaultMessage: "Email registration is not available.", }) ErrUserEmailLoginDisabled = register(codes.Code{ ID: "err.server.user.email_login_disabled", HTTPStatus: http.StatusForbidden, DefaultMessage: "Email login is not available.", }) // deactivated-or-banned account (the password branch unifies onto // invalid_credentials to avoid enumeration). ErrUserAccountUnavailable = register(codes.Code{ ID: "err.server.user.account_unavailable", HTTPStatus: http.StatusForbidden, DefaultMessage: "This account has been deactivated or disabled.", }) ErrUserEmailSendFailed = register(codes.Code{ ID: "err.server.user.email_send_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to send the email verification code.", Internal: true, }) // ErrUserEmailRateLimited is the client-actionable resend cooldown (the // 1-minute throttle in EmailService.SendVerifyCode). 429, not Internal, so // the actionable "try again shortly" message reaches the client. ErrUserEmailRateLimited = register(codes.Code{ ID: "err.server.user.email_rate_limited", HTTPStatus: http.StatusTooManyRequests, DefaultMessage: "Verification codes are being sent too frequently, please try again in a minute.", }) ErrUserUsernameRegisterDisabled = register(codes.Code{ ID: "err.server.user.username_register_disabled", HTTPStatus: http.StatusForbidden, DefaultMessage: "Username registration is not available.", }) ErrUserUsernameFormatInvalid = register(codes.Code{ ID: "err.server.user.username_format_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Username must be 8-22 characters.", }) ErrUserPublicKeyNotFound = register(codes.Code{ ID: "err.server.user.public_key_not_found", HTTPStatus: http.StatusBadRequest, DefaultMessage: "This user has not uploaded a public key.", }) ErrUserPublicKeyAlreadyExists = register(codes.Code{ ID: "err.server.user.public_key_already_exists", HTTPStatus: http.StatusConflict, DefaultMessage: "A public key has already been uploaded for this user.", }) // ErrUserSignatureNotFound covers a missing / expired Web3 verify-text // challenge (the cached nonce the client must sign is gone or mismatched). ErrUserSignatureNotFound = register(codes.Code{ ID: "err.server.user.signature_not_found", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Signature challenge does not exist or has expired.", }) ErrUserSignatureInvalid = register(codes.Code{ ID: "err.server.user.signature_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Signature verification failed.", }) ErrUserVerifyTypeInvalid = register(codes.Code{ ID: "err.server.user.verify_type_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid verification type.", }) )
User module error codes. Migrated from modules/user/api.go legacy c.ResponseError sites in Phase 2.1 (~244 sites collapsed to ~42 codes).
SafeDetailKeys are intentionally minimal: `field` for parameter / state guards so the client can highlight the offending input, plus a handful of per-code keys where the message inherently needs more context (lock-screen minute bounds, WeChat response missing field).
Internal=true is set on 5xx failures so the renderer suppresses the DefaultMessage / params / details on the wire — operators still get the underlying error in zap logs.
var ( // ErrUserSecretRequestInvalid 入参缺失/格式非法:BindJSON 失败、display_name // 或 key 为空、超长、kind 非法等。 ErrUserSecretRequestInvalid = register(codes.Code{ ID: "err.server.usersecret.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) // 本用户的 channel 插件在取。anti-enumeration:不区分「凭证错」与「无此 owner」。 ErrUserSecretUnauthorized = register(codes.Code{ ID: "err.server.usersecret.unauthorized", HTTPStatus: http.StatusUnauthorized, DefaultMessage: "Authentication failed.", }) // ErrUserSecretNotFound 按 secret_id/display_name 未解到任何 key(CRUD 的目标 // 不存在,或 resolve 零命中)。 ErrUserSecretNotFound = register(codes.Code{ ID: "err.server.usersecret.not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "Secret not found.", }) // ErrUserSecretDuplicateName create/rename 时归一化别名撞已有别名,提示换名。 ErrUserSecretDuplicateName = register(codes.Code{ ID: "err.server.usersecret.duplicate_name", HTTPStatus: http.StatusConflict, DefaultMessage: "A secret with this name already exists. Please choose another name.", }) // ErrUserSecretAmbiguous resolve 匹配到多个候选,需上层消歧。响应体携带候选 // 列表(脱敏,不含明文),由 handler 以 details 形式走统一 i18n 错误信封返回 // (error.details.candidates),保留 error.http_status 与本地化 message。 ErrUserSecretAmbiguous = register(codes.Code{ ID: "err.server.usersecret.ambiguous", HTTPStatus: http.StatusUnprocessableEntity, DefaultMessage: "The name matches multiple secrets. Please disambiguate.", SafeDetailKeys: []string{"candidates"}, }) // ErrUserSecretResolveFailed 解引用失败:密文解密/认证失败等内部异常。 // Internal=true,wire 上只暴露通用内部错误文案,真实原因仅 zap。 ErrUserSecretResolveFailed = register(codes.Code{ ID: "err.server.usersecret.resolve_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to resolve the secret.", Internal: true, }) )
err.server.usersecret.* — modules/usersecret 用户外部密钥别名表 CRUD + resolve 的业务错误码(YUJ-3538)。
这些端点是全新功能,无 legacy client 依赖固定 400,因此全部通过 httperr.ResponseErrorLWithStatus 渲染,保留真实 HTTP 状态码 (400/401/404/409/422/500),让 octo-web / channel 插件可直接按 wire status 分支。
安全约束:任何错误响应都不回显明文/密文。decrypt 失败统一归 5xx 内部错误 (Internal=true),具体原因仅 zap 记录,不上 wire。
var ( // ErrWorkplaceRequestInvalid is the catch-all for missing/malformed request // input (empty app_id / category_no / banner_no path params, empty required // body fields, BindJSON failure, common.ErrData). The offending field is // surfaced via Details when the caller can identify it. ErrWorkplaceRequestInvalid = register(codes.Code{ ID: "err.server.workplace.request_invalid", HTTPStatus: http.StatusBadRequest, DefaultMessage: "Invalid request.", SafeDetailKeys: []string{"field"}, }) // ErrWorkplaceAppNotFound covers an app that does not exist, has been // deleted, or is disabled (add/update/record paths). ErrWorkplaceAppNotFound = register(codes.Code{ ID: "err.server.workplace.app_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The app does not exist or is unavailable.", }) ErrWorkplaceCategoryNotFound = register(codes.Code{ ID: "err.server.workplace.category_not_found", HTTPStatus: http.StatusNotFound, DefaultMessage: "The category does not exist.", }) ErrWorkplaceAppNameExists = register(codes.Code{ ID: "err.server.workplace.app_name_exists", HTTPStatus: http.StatusConflict, DefaultMessage: "An app with this name already exists.", }) ErrWorkplaceCategoryNameExists = register(codes.Code{ ID: "err.server.workplace.category_name_exists", HTTPStatus: http.StatusConflict, DefaultMessage: "A category with this name already exists.", }) // ErrWorkplaceQueryFailed covers read-path failures (DB SELECT/search/count). // Log the underlying err before responding. ErrWorkplaceQueryFailed = register(codes.Code{ ID: "err.server.workplace.query_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to query workplace data.", Internal: true, }) // ErrWorkplaceStoreFailed covers mutation-path failures (DB write, transaction // begin/commit/rollback). Log the underlying err before responding. ErrWorkplaceStoreFailed = register(codes.Code{ ID: "err.server.workplace.store_failed", HTTPStatus: http.StatusInternalServerError, DefaultMessage: "Failed to update workplace data.", Internal: true, }) )
err.server.workplace.* — modules/workplace business error codes (api.go / api_manager.go). DefaultMessage holds the en-US source (D4); the zh-CN runtime translation lives in pkg/i18n/locales/active.zh-CN.toml. Internal=true codes never surface their message on the wire — callers MUST log the underlying err with full context via the module logger before responding.
var (
)
Shared codes reused by bot_api (resolved at init from the codes registry). The bot-auth-required defensive check on the OBO endpoints maps to the shared 401; generic internal asserts map to the shared 500.
Functions ¶
This section is empty.
Types ¶
This section is empty.