3471d40576
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m27s
The finished-game export now works identically on every platform: the client mints a signed relative URL (game.export_url) carrying its date locale, IANA time zone and localized non-play labels, resolves it against its own origin and hands it to the platform's native download — Telegram downloadFile, VKWebAppDownloadFile, or a plain browser anchor (also the desktop VK iframe). Both artifacts ride the route: the .gcg text (no more clipboard mode, except the legacy pre-8.0 Telegram fallback) and the PNG of the final position. The PNG is rasterized by a new internal 'renderer' sidecar (node:22-slim + skia-canvas + baked Liberation/Noto Color Emoji fonts) executing the SAME ui/src/lib/gameimage.ts the web project unit-tests, bundled at image build time — one renderer, no drift; the browser no longer draws or delivers bytes itself. The backend rebuilds the render payload from the journal + engine.AlphabetTable, verifies the HMAC (10-minute TTL, BACKEND_EXPORT_SIGN_KEY, constant-time, uniform 404s) on its public group, and streams the artifact as a named attachment; the gateway forwards /dl/* behind the per-IP public rate limiter (caddy @gateway matcher extended — the landing catch-all trap). Deploy: renderer service (compose + prod overlay + roll before backend + prod push list), EXPORT_SIGN_KEY env (TEST_/PROD_ secrets), CI runs the sidecar smoke in the ui job. Docs: ARCHITECTURE export-delivery section, FUNCTIONAL(+_ru), UI_DESIGN, TESTING, deploy/README.
62 lines
2.0 KiB
Go
62 lines
2.0 KiB
Go
// Package render is the backend's client for the internal image-render sidecar: it POSTs
|
|
// a finished game's export payload and receives the rasterized PNG. The sidecar runs the
|
|
// same drawing code the web client unit-tests (ui/src/lib/gameimage.ts on skia-canvas);
|
|
// authentication and the signed public URL live in the server layer — this client only
|
|
// carries bytes.
|
|
package render
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// maxPNG caps a render response; a scale-2 export of the largest plausible game is well
|
|
// under 2 MiB, so anything bigger is a sidecar malfunction, not a valid image.
|
|
const maxPNG = 8 << 20
|
|
|
|
// Client calls the render sidecar over the internal network.
|
|
type Client struct {
|
|
base string
|
|
http *http.Client
|
|
}
|
|
|
|
// New returns a client for the sidecar at baseURL (e.g. http://renderer:8090). A render
|
|
// of a full game takes well under a second; the timeout leaves room for a cold start.
|
|
func New(baseURL string) *Client {
|
|
return &Client{base: baseURL, http: &http.Client{Timeout: 15 * time.Second}}
|
|
}
|
|
|
|
// Render posts payload (the JSON request shape the sidecar consumes) and returns the PNG.
|
|
func (c *Client) Render(ctx context.Context, payload any) ([]byte, error) {
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("render: marshal payload: %w", err)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.base+"/render", bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("render: build request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("render: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("render: sidecar status %d", resp.StatusCode)
|
|
}
|
|
png, err := io.ReadAll(io.LimitReader(resp.Body, maxPNG+1))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("render: read response: %w", err)
|
|
}
|
|
if len(png) > maxPNG {
|
|
return nil, fmt.Errorf("render: response exceeds %d bytes", maxPNG)
|
|
}
|
|
return png, nil
|
|
}
|