package backendclient import ( "context" "net/http" "net/url" "strconv" ) // The structs below mirror the backend's JSON DTOs (backend/internal/server // /dto.go). The transcode layer maps them to and from the FlatBuffers edge // payloads. // SessionResp is the credential minted by an auth operation. type SessionResp struct { Token string `json:"token"` UserID string `json:"user_id"` IsGuest bool `json:"is_guest"` DisplayName string `json:"display_name"` } // ProfileResp is an account's own profile. type ProfileResp struct { UserID string `json:"user_id"` DisplayName string `json:"display_name"` PreferredLanguage string `json:"preferred_language"` TimeZone string `json:"time_zone"` AwayStart string `json:"away_start"` AwayEnd string `json:"away_end"` HintBalance int `json:"hint_balance"` BlockChat bool `json:"block_chat"` BlockFriendRequests bool `json:"block_friend_requests"` IsGuest bool `json:"is_guest"` NotificationsInAppOnly bool `json:"notifications_in_app_only"` } // LinkResultResp is the result of an account link/merge step (Stage 11). Status is // "linked", "merge_required" (the secondary_* fields summarise the other account) or // "merged". Token is a switched-session token (a guest initiator's durable // counterpart won); Profile is the surviving/active account's profile. type LinkResultResp struct { Status string `json:"status"` SecondaryUserID string `json:"secondary_user_id"` SecondaryName string `json:"secondary_display_name"` SecondaryGames int `json:"secondary_games"` SecondaryFriends int `json:"secondary_friends"` Token string `json:"token"` Profile *ProfileResp `json:"profile"` } // TileJSON is one tile in a decoded move response (history, move result, hint); its Letter // is a concrete character (Stage 13 keeps the move journal in letters). type TileJSON struct { Row int `json:"row"` Col int `json:"col"` Letter string `json:"letter"` Blank bool `json:"blank"` } // PlayTileJSON is one inbound tile to place, addressed by alphabet index (Stage 13). For a // blank, Letter is the designated letter's index and Blank is true. type PlayTileJSON struct { Row int `json:"row"` Col int `json:"col"` Letter int `json:"letter"` Blank bool `json:"blank"` } // MoveRecordResp is a decoded move. type MoveRecordResp struct { Player int `json:"player"` Action string `json:"action"` Dir string `json:"dir"` MainRow int `json:"main_row"` MainCol int `json:"main_col"` Tiles []TileJSON `json:"tiles"` Words []string `json:"words"` Count int `json:"count"` Score int `json:"score"` Total int `json:"total"` } // SeatResp is one seat's public standing. type SeatResp struct { Seat int `json:"seat"` AccountID string `json:"account_id"` DisplayName string `json:"display_name"` Score int `json:"score"` HintsUsed int `json:"hints_used"` IsWinner bool `json:"is_winner"` } // GameResp is the shared game summary. type GameResp struct { ID string `json:"id"` Variant string `json:"variant"` DictVersion string `json:"dict_version"` Status string `json:"status"` Players int `json:"players"` ToMove int `json:"to_move"` TurnTimeoutSecs int `json:"turn_timeout_secs"` MoveCount int `json:"move_count"` EndReason string `json:"end_reason"` Seats []SeatResp `json:"seats"` } // MoveResultResp is the outcome of a committed move. type MoveResultResp struct { Move MoveRecordResp `json:"move"` Game GameResp `json:"game"` } // AlphabetEntryJSON is one letter of a variant's alphabet (its index, concrete letter and // tile value), present in StateResp only when the client requested it (Stage 13). type AlphabetEntryJSON struct { Index int `json:"index"` Letter string `json:"letter"` Value int `json:"value"` } // StateResp is a player's view of a game. Rack carries wire alphabet indices (Stage 13); // Alphabet is present only when the request asked for it. type StateResp struct { Game GameResp `json:"game"` Seat int `json:"seat"` Rack []int `json:"rack"` BagLen int `json:"bag_len"` HintsRemaining int `json:"hints_remaining"` Alphabet []AlphabetEntryJSON `json:"alphabet,omitempty"` } // MatchResp reports an auto-match outcome. type MatchResp struct { Matched bool `json:"matched"` Game *GameResp `json:"game,omitempty"` } // ChatResp is a stored chat message. type ChatResp struct { ID string `json:"id"` GameID string `json:"game_id"` SenderID string `json:"sender_id"` Kind string `json:"kind"` Body string `json:"body"` CreatedAtUnix int64 `json:"created_at_unix"` } // TelegramAuth provisions/finds the Telegram account and mints a session, seeding a // brand-new account's display name and language from the validated launch fields and // recording the validating bot's serviceLanguage (which routes the account's later // out-of-app push). func (c *Client) TelegramAuth(ctx context.Context, externalID, languageCode, username, firstName, serviceLanguage string) (SessionResp, error) { var out SessionResp err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/telegram", "", "", map[string]string{ "external_id": externalID, "language_code": languageCode, "username": username, "first_name": firstName, "service_language": serviceLanguage, }, &out) return out, err } // PushTargetResp is a recipient's out-of-app push routing data: their Telegram // external_id (empty when they have no Telegram identity), preferred language, and // whether they confined notifications to the in-app stream. type PushTargetResp struct { ExternalID string `json:"external_id"` Language string `json:"language"` NotificationsInAppOnly bool `json:"notifications_in_app_only"` } // PushTarget resolves a user id to their out-of-app Telegram routing data (the // gateway uses it to decide whether to deliver an event over platform push). func (c *Client) PushTarget(ctx context.Context, userID string) (PushTargetResp, error) { var out PushTargetResp err := c.do(ctx, http.MethodPost, "/api/v1/internal/push-target", "", "", map[string]string{"user_id": userID}, &out) return out, err } // GuestAuth provisions a guest account and mints a session. func (c *Client) GuestAuth(ctx context.Context) (SessionResp, error) { var out SessionResp err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/guest", "", "", struct{}{}, &out) return out, err } // EmailRequest asks the backend to mail a login code. func (c *Client) EmailRequest(ctx context.Context, email string) error { return c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/email/request", "", "", map[string]string{"email": email}, nil) } // EmailLogin verifies a login code and mints a session. func (c *Client) EmailLogin(ctx context.Context, email, code string) (SessionResp, error) { var out SessionResp err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/email/login", "", "", map[string]string{"email": email, "code": code}, &out) return out, err } // ResolveSession maps a token to its account id (gateway session-cache miss). func (c *Client) ResolveSession(ctx context.Context, token string) (string, error) { var out struct { UserID string `json:"user_id"` } err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/resolve", "", "", map[string]string{"token": token}, &out) return out.UserID, err } // Profile returns the authenticated account's profile. func (c *Client) Profile(ctx context.Context, userID string) (ProfileResp, error) { var out ProfileResp err := c.do(ctx, http.MethodGet, "/api/v1/user/profile", userID, "", nil, &out) return out, err } // SubmitPlay commits a placement on the player's turn. The tiles are addressed by alphabet // index (Stage 13). func (c *Client) SubmitPlay(ctx context.Context, userID, gameID, dir string, tiles []PlayTileJSON) (MoveResultResp, error) { var out MoveResultResp body := map[string]any{"dir": dir, "tiles": tiles} err := c.do(ctx, http.MethodPost, "/api/v1/user/games/"+url.PathEscape(gameID)+"/play", userID, "", body, &out) return out, err } // GameState returns the player's view of a game. When includeAlphabet is set the backend // embeds the variant's alphabet table (Stage 13); the client asks for it on a per-variant // cache miss only. func (c *Client) GameState(ctx context.Context, userID, gameID string, includeAlphabet bool) (StateResp, error) { var out StateResp path := "/api/v1/user/games/" + url.PathEscape(gameID) + "/state" if includeAlphabet { path += "?include_alphabet=true" } err := c.do(ctx, http.MethodGet, path, userID, "", nil, &out) return out, err } // Enqueue joins the auto-match pool for a variant. func (c *Client) Enqueue(ctx context.Context, userID, variant string) (MatchResp, error) { var out MatchResp err := c.do(ctx, http.MethodPost, "/api/v1/user/lobby/enqueue", userID, "", map[string]string{"variant": variant}, &out) return out, err } // Poll reports whether the caller has been paired since queueing. func (c *Client) Poll(ctx context.Context, userID string) (MatchResp, error) { var out MatchResp err := c.do(ctx, http.MethodGet, "/api/v1/user/lobby/poll", userID, "", nil, &out) return out, err } // ChatPost stores a chat message, forwarding the client IP for moderation. func (c *Client) ChatPost(ctx context.Context, userID, gameID, body, clientIP string) (ChatResp, error) { var out ChatResp err := c.do(ctx, http.MethodPost, "/api/v1/user/games/"+url.PathEscape(gameID)+"/chat", userID, clientIP, map[string]string{"body": body}, &out) return out, err } // HintResultResp is the top-ranked move plus the remaining hint budget. type HintResultResp struct { Move MoveRecordResp `json:"move"` HintsRemaining int `json:"hints_remaining"` } // EvalResultResp is an unlimited move preview. type EvalResultResp struct { Legal bool `json:"legal"` Score int `json:"score"` Words []string `json:"words"` } // WordCheckResp is a dictionary lookup outcome. type WordCheckResp struct { Word string `json:"word"` Legal bool `json:"legal"` } // HistoryResp is a game's decoded move journal. type HistoryResp struct { GameID string `json:"game_id"` Moves []MoveRecordResp `json:"moves"` } // GameListResp is the caller's games for the lobby. type GameListResp struct { Games []GameResp `json:"games"` } // ChatListResp is a game's chat history. type ChatListResp struct { Messages []ChatResp `json:"messages"` } func (c *Client) gamePath(gameID, suffix string) string { return "/api/v1/user/games/" + url.PathEscape(gameID) + suffix } // Pass forfeits the player's turn. func (c *Client) Pass(ctx context.Context, userID, gameID string) (MoveResultResp, error) { var out MoveResultResp err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/pass"), userID, "", struct{}{}, &out) return out, err } // Exchange swaps the chosen rack tiles back into the bag. Tiles are wire alphabet indices // (Stage 13; a blank is engine.BlankIndex). func (c *Client) Exchange(ctx context.Context, userID, gameID string, tiles []int) (MoveResultResp, error) { var out MoveResultResp err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/exchange"), userID, "", map[string]any{"tiles": tiles}, &out) return out, err } // Resign resigns the player from the game. func (c *Client) Resign(ctx context.Context, userID, gameID string) (MoveResultResp, error) { var out MoveResultResp err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/resign"), userID, "", struct{}{}, &out) return out, err } // Hint reveals the top-ranked move and spends a hint. func (c *Client) Hint(ctx context.Context, userID, gameID string) (HintResultResp, error) { var out HintResultResp err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/hint"), userID, "", struct{}{}, &out) return out, err } // Evaluate previews a tentative play's legality and score. The tiles are addressed by // alphabet index (Stage 13). func (c *Client) Evaluate(ctx context.Context, userID, gameID, dir string, tiles []PlayTileJSON) (EvalResultResp, error) { var out EvalResultResp err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/evaluate"), userID, "", map[string]any{"dir": dir, "tiles": tiles}, &out) return out, err } // CheckWord looks a word up in the game's pinned dictionary. The word is carried as // repeated ?idx= alphabet indices (Stage 13); the backend echoes the decoded concrete word. func (c *Client) CheckWord(ctx context.Context, userID, gameID string, word []int) (WordCheckResp, error) { var out WordCheckResp q := url.Values{} for _, x := range word { q.Add("idx", strconv.Itoa(x)) } err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/check_word")+"?"+q.Encode(), userID, "", nil, &out) return out, err } // Complaint disputes a word-check result. func (c *Client) Complaint(ctx context.Context, userID, gameID, word, note string) error { return c.do(ctx, http.MethodPost, c.gamePath(gameID, "/complaint"), userID, "", map[string]string{"word": word, "note": note}, nil) } // History returns a game's decoded move journal. func (c *Client) History(ctx context.Context, userID, gameID string) (HistoryResp, error) { var out HistoryResp err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/history"), userID, "", nil, &out) return out, err } // ChatList returns a game's chat history. func (c *Client) ChatList(ctx context.Context, userID, gameID string) (ChatListResp, error) { var out ChatListResp err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/chat"), userID, "", nil, &out) return out, err } // Nudge posts a nudge to the player whose turn is awaited. func (c *Client) Nudge(ctx context.Context, userID, gameID string) (ChatResp, error) { var out ChatResp err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/nudge"), userID, "", struct{}{}, &out) return out, err } // GamesList returns the caller's active and finished games. func (c *Client) GamesList(ctx context.Context, userID string) (GameListResp, error) { var out GameListResp err := c.do(ctx, http.MethodGet, "/api/v1/user/games", userID, "", nil, &out) return out, err }