Skip to content

Latest commit

Β 

History

History
174 lines (129 loc) Β· 9.4 KB

File metadata and controls

174 lines (129 loc) Β· 9.4 KB

CLAUDE.md

This file provides guidance to Claude Code when working with code in this repository.

Project Overview

Octo-server is the Go backend for DMWork (enterprise IM platform). It handles business logic on top of WuKongIM for messaging transport.

  • Go Module: github.com/Mininglamp-OSS/octo-server
  • Go Version: 1.25
  • Shared Library: github.com/Mininglamp-OSS/octo-lib (config, wkhttp, testutil, register, model)
  • Default Branch: main

Common Commands

# Build
docker build -t octo-server .

# Run tests (single module)
go test ./modules/group/...
go test ./modules/message/ -run TestSendMsg

# Run all tests
go test ./...

# Lint
golangci-lint run ./...

Architecture

Request Flow

HTTP (Gin/wkhttp) β†’ Auth Middleware (pkg/auth/) β†’ Space Middleware β†’ API Handler β†’ Service β†’ DB (MySQL/DBR)
                                                                          ↓
                                                                    WuKongIM (gRPC)

Module System

27 modules in modules/, each auto-registered via init() + register.AddModule().

Standard module structure:

  • 1module.go β€” registration entry (init() + register.AddModule())
  • api*.go β€” HTTP handlers implementing register.APIRouter.Route(r *wkhttp.WKHttp)
  • service.go β€” business logic, typically defines IService interface
  • db*.go β€” database operations using gocraft/dbr
  • model.go β€” data models and response structs
  • sql/ β€” SQL migrations embedded via //go:embed sql

Key Packages

Package Purpose
pkg/auth/ Token parsing, CacheTokenParser, auth middleware
pkg/errcode/ Error code definitions per module (group.go, message.go, user.go, oidc.go)
pkg/httperr/ ResponseErrorL / ResponseErrorLWithStatus error facades
pkg/i18n/ Localization SDK: codes registry, localizer, renderer, language negotiation, locales/
internal/ Internal wiring, module imports
modules/base/event/ Async event system

Error Handling & i18n (Localization)

All user-facing error responses go through the i18n error envelope. Never use c.ResponseError(errors.New(...)), c.ResponseErrorf(...), c.AbortWithStatusJSON(...), or non-OK c.JSON(...) β€” these are legacy and bypass the localized envelope.

Two facades (pkg/httperr) β€” the envelope body is identical, only the wire status differs:

Facade Wire status Use for
ResponseErrorL(c, code, params, details) pinned 400 (D14 compat); real status in error.http_status default β€” every legacy-bearing endpoint
ResponseErrorLWithStatus(c, code, params, details) the code's real HTTPStatus new endpoints only with no clients depending on fixed-400 (currently just modules/oidc bind); diverging from D14 needs maintainer sign-off
httperr.ResponseErrorL(c, errcode.ErrGroupQueryFailed, nil, nil)

Error codes β€” register in pkg/errcode/<module>.go:

ErrXxx = register(codes.Code{
    ID:             "err.server.<module>.<reason>", // or reuse err.shared.* (auth/rate/param/internal/not_found)
    HTTPStatus:     http.StatusBadRequest,
    DefaultMessage: "English source (D4).",          // zh-CN runtime translation goes in active.zh-CN.toml
    SafeDetailKeys: []string{"field"},               // whitelist for details; all other keys are dropped
    Internal:       false,                            // see invariant below
})
  • 5xx ⟺ Internal=true (renderer hides the message + details; log the cause via zap.Error before responding). 4xx codes must NOT be Internal.
  • Anti-enumeration: auth / verify failures map to ONE generic code (e.g. a single 401), never a per-reason code β€” the specific reason goes to logs only.
  • Params vs Details (D15): params interpolate into the message template; details are structured fields surfaced to the client, filtered by SafeDetailKeys.

Per-module helpers live in modules/<module>/api_i18n.go (respond<Module>Xxx for detail-carrying shapes; mustLookupSharedCode resolves shared codes at init, panicking loudly if unregistered).

After adding/changing any code, these must pass (also enforced in CI):

make i18n-extract        # regenerate en-US markers from codes.Register call sites
make i18n-extract-check  # 100% recall: every registered code has a marker
make i18n-lint           # D23 guard (no new raw error responses) + unregistered-code check

Then add the zh-CN translation to pkg/i18n/locales/active.zh-CN.toml (one ["id"] + other = "..." block per code).

Guard test: each migrated module has a Test<Module>NoLegacyResponseError source guard forbidding legacy/raw responses β€” add any new handler files to its list. Protocol endpoints that intentionally keep raw responses (e.g. OAuth2/OIDC browser-redirect flow) are exempted and tracked in tools/lint-direct-error-response/baseline.txt.

Emails: localized templates live in modules/base/common/emailtmpl/templates/{lang}/ (per-language subject/html/text, go:embed). Send functions take a lang arg resolved via i18n.OutboundLanguage(ctx) β€” never hardcode subject/body strings.

Rate Limiting

Use the shared middleware in octo-lib pkg/wkhttp/ratelimit.go β€” do NOT hand-roll Redis INCR/TTL counters for request-frequency limiting. Three layers, each sets X-RateLimit-Limit/Remaining/Scope/Retry-After headers, returns i18n rate.limited, and is fail-open on Redis errors:

Middleware Scope header Dimension Use for
RateLimitMiddleware ip global per-IP DDoS floor β€” already mounted globally in main.go (route.Use), don't re-add
StrictIPRateLimitMiddleware(tag, rps, burst) strict:{tag} per-IP, per-endpoint unauthenticated sensitive endpoints (login/register/sms/search/group_invite/space_invite)
SharedUIDRateLimiter(r, ctx) (wraps UIDRateLimitMiddleware) uid per-login-user, shared bucket ratelimit:uid:{uid} default for authenticated endpoints

SharedUIDRateLimiter (pkg/wkhttp/ratelimit_helper.go) is a process-wide singleton β€” one quota per UID across all mounted routes (default 2 rps / burst 60, tunable via DM_API_UID_RATELIMIT_RPS/_BURST). Mount it AFTER AuthMiddleware on the route group, else it can't read the uid and silently fails open:

auth := r.Group("/v1/foo", ctx.AuthMiddleware(r), appwkhttp.SharedUIDRateLimiter(r, ctx))

Exception β€” per-resource cooldowns keyed by a business identity (phone/email/bind-session), which the IP/UID buckets cannot express, may use a hand-written Redis counter: e.g. sms_rate_limit:{zone}@{phone} (base/common/service_sms.go), email_rate_limit:{email} (base/common/service_email.go), OIDC bind attempt caps. These are intentional; generic HTTP request-frequency limiting is not.

Tests that hit UID-limited routes must reset the bucket in setup (ratelimit:uid:*) β€” see category test's resetUIDRateLimit; the bucket persists in Redis and is NOT cleared by CleanAllTables.

Database

  • ORM: gocraft/dbr v2
  • Migration files: modules/<name>/sql/<yyyyMMdd>-<seq>_<name>.sql, embedded via //go:embed sql
  • Field naming: underscore (util.AttrToUnderscore())

Testing

_, ctx := testutil.NewTestServer()
defer testutil.CleanAllTables(ctx)

Tests require MySQL + Redis + WuKongIM running (see CI or make env-test in dmworkim).

Coding Conventions

  • Commit messages: English, Conventional Commits (feat:, fix:, test:, refactor:)
  • API routes: prefix /v1/
  • New modules: add blank import in internal/modules.go
  • Auth: all routes go through AuthMiddleware unless explicitly excluded β€” document why if skipping
  • i18n: user-facing errors use httperr.ResponseErrorL + a registered pkg/errcode code; never raw c.ResponseError/c.JSON/AbortWithStatusJSON. Run make i18n-extract-check + make i18n-lint after touching codes (see Architecture β€Ί Error Handling & i18n)
  • Rate limiting: mount SharedUIDRateLimiter (auth routes) or StrictIPRateLimitMiddleware (unauth) β€” never hand-roll a Redis counter for request-frequency limiting (see Architecture β€Ί Rate Limiting)
  • Space isolation: handlers that access user data must go through Space middleware
  • Bot API (modules/bot_api/): validate bot ownership before operations
  • Thread (modules/thread/): verify parent channel access

octo-spec workflow

This repo participates in the octo-spec engineering standard. Shared rules live in .octospec/.

  • Rules: .octospec/rules/ β€” the source of truth for this repo's conventions (error-handling, rate-limit, space-isolation, testing, commit-style). The global "constitution" rules are pulled into git-ignored .octospec/_global/ via octospec-sync (pin in .octospec/manifest.yaml).
  • Before changing load-bearing behavior: read the rules whose inject_when matches the files you are touching (see .octospec/rules/_index.yaml).
  • Tasks: capture goal / load-bearing list / out-of-scope / acceptance in .octospec/tasks/<slug>/brief.md (template: tasks/_brief.template.md).
  • PRs: fill Linked Spec + the COMPREHENSION three questions for load-bearing, architectural, or P0 changes. Trivial changes (typo/docs/lint/config) are exempt.

This region is managed by octospec-sync; edit outside the markers.