package backendclient import ( "context" "encoding/json" "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"` VariantPreferences []string `json:"variant_preferences"` // Banner is the advertising-banner block, present only for a viewer eligible to // see the banner. The gateway forwards it verbatim into the Profile payload. Banner *BannerResp `json:"banner,omitempty"` // Email is the confirmed email ("" when none); TelegramLinked/VkLinked report an // attached platform identity — they drive the profile's link/unlink/change controls. Email string `json:"email"` TelegramLinked bool `json:"telegram_linked"` VkLinked bool `json:"vk_linked"` // DictVersions is the current dictionary version per game variant, forwarded verbatim // into the Profile payload so an offline-capable client preloads the matching dawg. DictVersions []DictVersion `json:"dict_versions,omitempty"` } // DictVersion pairs a game variant's stable label with its current dictionary version. type DictVersion struct { Variant string `json:"variant"` Version string `json:"version"` } // BannerResp is the advertising-banner block of an eligible viewer's profile: the // campaigns to rotate and the global display timings the client rotator reads. type BannerResp struct { Campaigns []BannerCampaignResp `json:"campaigns"` Timings BannerTimingsResp `json:"timings"` } // BannerCampaignResp is one campaign in the rotation feed: a GCD-reduced show // weight and its messages, in display order, already resolved to one language. // The override_* colours are the optional per-campaign palette (empty when the // campaign keeps the neutral theme tokens); they ride through to the client // verbatim, mirroring the backend DTO. type BannerCampaignResp struct { Weight int `json:"weight"` Messages []string `json:"messages"` OverrideBg string `json:"override_bg,omitempty"` OverrideFg string `json:"override_fg,omitempty"` OverrideLink string `json:"override_link,omitempty"` OverrideBgDark string `json:"override_bg_dark,omitempty"` OverrideFgDark string `json:"override_fg_dark,omitempty"` OverrideLinkDark string `json:"override_link_dark,omitempty"` } // BannerTimingsResp mirrors the backend's global display timings. type BannerTimingsResp struct { HoldMs int `json:"hold_ms"` EdgePauseMs int `json:"edge_pause_ms"` ScrollPxPerSec int `json:"scroll_px_per_sec"` FadeOutMs int `json:"fade_out_ms"` GapMs int `json:"gap_ms"` FadeInMs int `json:"fade_in_ms"` } // LinkResultResp is the result of an account link/merge step. 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 (the move journal is kept 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. 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"` MultipleWordsPerTurn bool `json:"multiple_words_per_turn"` VsAI bool `json:"vs_ai"` MoveCount int `json:"move_count"` EndReason string `json:"end_reason"` LastActivityUnix int64 `json:"last_activity_unix"` Seats []SeatResp `json:"seats"` UnreadChat bool `json:"unread_chat"` UnreadMessages bool `json:"unread_messages"` } // MoveResultResp is the outcome of a committed move. Rack carries the actor's refilled rack as // wire alphabet indices and BagLen the bag size after the draw. type MoveResultResp struct { Move MoveRecordResp `json:"move"` Game GameResp `json:"game"` Rack []int `json:"rack"` BagLen int `json:"bag_len"` } // 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. 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; // 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"` WalletBalance int `json:"wallet_balance"` HintUnlockLeftSeconds int `json:"hint_unlock_left_seconds"` 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, its // time zone from browserTz (the client's detected "±HH:MM" UTC offset) and, from the // validated launch deep-link startParam, its variant preferences (first contact only). func (c *Client) TelegramAuth(ctx context.Context, externalID, languageCode, username, firstName, browserTz, startParam, subtype 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, "browser_tz": browserTz, "start_param": startParam, "subtype": subtype, }, &out) return out, err } // VKAuth provisions/finds the VK account and mints a session, seeding a brand-new // account's preferred language from languageCode (the vk_language hint), its display // name from displayName (read client-side via VKWebAppGetUserInfo, since VK omits the // name from the signed launch params) and its time zone from browserTz (the client's // detected "±HH:MM" UTC offset). All seeds apply on first contact only. func (c *Client) VKAuth(ctx context.Context, externalID, languageCode, displayName, browserTz, subtype string) (SessionResp, error) { var out SessionResp err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/vk", "", "", map[string]string{ "external_id": externalID, "language_code": languageCode, "display_name": displayName, "browser_tz": browserTz, "subtype": subtype, }, &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 } // ChatAccessResp is a user's moderated-chat write eligibility: ExternalID is their // Telegram identity (empty when they have none, so the gateway has nothing to gate), // Registered whether an account was found, and Eligible the final gate the bot applies // (registered and neither admin-suspended nor chat-muted). type ChatAccessResp struct { ExternalID string `json:"external_id"` Registered bool `json:"registered"` Eligible bool `json:"eligible"` } // ChatEligibility resolves a Telegram identity to its moderated-chat write // eligibility — the join path, when the bot sees a user enter the chat. func (c *Client) ChatEligibility(ctx context.Context, externalID string) (ChatAccessResp, error) { var out ChatAccessResp err := c.do(ctx, http.MethodPost, "/api/v1/internal/chat-access", "", "", map[string]string{"external_id": externalID}, &out) return out, err } // ChatAccessByUser resolves an account id to its Telegram identity and current // moderated-chat write eligibility — the change path, for a chat-access-changed event. func (c *Client) ChatAccessByUser(ctx context.Context, userID string) (ChatAccessResp, error) { var out ChatAccessResp err := c.do(ctx, http.MethodPost, "/api/v1/internal/chat-access", "", "", map[string]string{"user_id": userID}, &out) return out, err } // GuestAuth provisions a guest account and mints a session, seeding its time zone // from browserTz (the client's detected "±HH:MM" UTC offset). func (c *Client) GuestAuth(ctx context.Context, browserTz, subtype string) (SessionResp, error) { var out SessionResp err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/guest", "", "", map[string]string{"browser_tz": browserTz, "subtype": subtype}, &out) return out, err } // EmailRequest asks the backend to mail a login code, provisioning the account on // first contact; browserTz (the client's detected "±HH:MM" UTC offset) seeds the new // account's time zone, since the email account is created here, not at login. pwa marks a // request from an installed PWA, so the backend omits the one-tap confirm link from the email. func (c *Client) EmailRequest(ctx context.Context, email, browserTz, language string, pwa bool) error { return c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/email/request", "", "", map[string]any{"email": email, "browser_tz": browserTz, "language": language, "pwa": pwa}, nil) } // EmailLogin verifies a login code and mints a session. func (c *Client) EmailLogin(ctx context.Context, email, code, subtype 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, "subtype": subtype}, &out) return out, err } // ConfirmLinkResp is the backend's outcome of a one-tap deeplink confirmation: for a // login Session carries the minted credential; for a link Status is "confirmed" or // "merge_required". type ConfirmLinkResp struct { Purpose string `json:"purpose"` Status string `json:"status"` Session *SessionResp `json:"session,omitempty"` } // EmailConfirmLink verifies a one-tap deeplink token; the token is the authorization. func (c *Client) EmailConfirmLink(ctx context.Context, token string) (ConfirmLinkResp, error) { var out ConfirmLinkResp err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/email/confirm-link", "", "", map[string]string{"token": token}, &out) return out, err } // ResolveSession maps a token to its account id, guest flag, and trusted platform // (gateway session-cache miss). The guest flag lets the edge gate guest-forbidden // ops; the platform ("/", empty for an untrusted session) is injected // as X-Platform on the caller's backend requests. func (c *Client) ResolveSession(ctx context.Context, token string) (string, bool, string, error) { var out struct { UserID string `json:"user_id"` IsGuest bool `json:"is_guest"` PlatformKind string `json:"platform_kind"` PlatformSubtype string `json:"platform_subtype"` } err := c.do(ctx, http.MethodPost, "/api/v1/internal/sessions/resolve", "", "", map[string]string{"token": token}, &out) platform := "" if out.PlatformKind != "" { platform = out.PlatformKind + "/" + out.PlatformSubtype } return out.UserID, out.IsGuest, platform, 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 } // WalletSegmentResp is one chip balance in the wallet: the funding source, the chip count, and // whether it is spendable in the caller's current context. type WalletSegmentResp struct { Source string `json:"source"` Chips int `json:"chips"` Spendable bool `json:"spendable"` } // WalletResp is the caller's wallet: the context-visible chip segments and the context-applicable // benefits (no-ads term end as unix millis / forever flag, and the available hints). type WalletResp struct { Segments []WalletSegmentResp `json:"segments"` AdsForever bool `json:"ads_forever"` AdsPaidUntil int64 `json:"ads_paid_until_ms"` Hints int `json:"hints"` } // walletBuyBody is the chip-spend request body. type walletBuyBody struct { ProductID string `json:"product_id"` } // Wallet fetches the caller's wallet in their current execution context. func (c *Client) Wallet(ctx context.Context, userID string) (WalletResp, error) { var out WalletResp err := c.do(ctx, http.MethodGet, "/api/v1/user/wallet", userID, "", nil, &out) return out, err } // WalletBuy spends chips on a chip-priced value and returns the updated wallet. func (c *Client) WalletBuy(ctx context.Context, userID, productID string) (WalletResp, error) { var out WalletResp err := c.do(ctx, http.MethodPost, "/api/v1/user/wallet/buy", userID, "", walletBuyBody{ProductID: productID}, &out) return out, err } // walletOrderBody is the POST body of an order: the chip pack to fund. type walletOrderBody struct { ProductID string `json:"product_id"` } // WalletOrderResp is a created order: its id and the provider launch URL the client opens. type WalletOrderResp struct { OrderID string `json:"order_id"` RedirectURL string `json:"redirect_url"` } // WalletOrder opens a pending order to fund a chip pack and returns the provider launch URL. func (c *Client) WalletOrder(ctx context.Context, userID, productID string) (WalletOrderResp, error) { var out WalletOrderResp err := c.do(ctx, http.MethodPost, "/api/v1/user/wallet/order", userID, "", walletOrderBody{ProductID: productID}, &out) return out, err } // robokassaResultResp is the backend intake's reply: the body to echo back to Robokassa. type robokassaResultResp struct { Response string `json:"response"` } // RobokassaResult forwards a Robokassa Result callback's parameters to the backend intake (the // single writer, which verifies the signature and credits) and returns the body to echo to // Robokassa ("OK"). params carries the provider's raw form fields. func (c *Client) RobokassaResult(ctx context.Context, params map[string]string) (string, error) { var out robokassaResultResp err := c.do(ctx, http.MethodPost, "/api/v1/internal/payments/robokassa/result", "", "", params, &out) return out.Response, err } // CatalogAtomResp is one atom line of a storefront product: the value type it grants and quantity. type CatalogAtomResp struct { AtomType string `json:"atom_type"` Quantity int `json:"quantity"` } // CatalogProductResp is one storefront product for the caller's context: a chip-priced value // (chips set) or a chip pack priced in the context method (money_amount + money_currency). type CatalogProductResp struct { Kind string `json:"kind"` ProductID string `json:"product_id"` Title string `json:"title"` Chips int `json:"chips"` MoneyAmount int64 `json:"money_amount"` MoneyCurrency string `json:"money_currency"` Atoms []CatalogAtomResp `json:"atoms"` } // CatalogResp is the storefront: the products visible and purchasable in the caller's context. type CatalogResp struct { Products []CatalogProductResp `json:"products"` } // Catalog fetches the storefront in the caller's current execution context. func (c *Client) Catalog(ctx context.Context, userID string) (CatalogResp, error) { var out CatalogResp err := c.do(ctx, http.MethodGet, "/api/v1/user/wallet/catalog", userID, "", nil, &out) return out, err } // BlockStatusResp is the caller's current manual-block state. Until is an RFC3339 UTC instant for // a temporary block, empty for a permanent one or when not blocked; Reason is resolved to the // account's language, empty when none was cited. type BlockStatusResp struct { Blocked bool `json:"blocked"` Permanent bool `json:"permanent"` Until string `json:"until"` Reason string `json:"reason"` } // BlockStatus fetches the caller's current block. It is the one user endpoint a blocked account // can still reach (the backend's suspension gate exempts it), so the UI can render the blocked // screen. func (c *Client) BlockStatus(ctx context.Context, userID string) (BlockStatusResp, error) { var out BlockStatusResp err := c.do(ctx, http.MethodGet, "/api/v1/user/block-status", userID, "", nil, &out) return out, err } // SubmitPlay commits a placement on the player's turn. The tiles are addressed by alphabet // index. func (c *Client) SubmitPlay(ctx context.Context, userID, gameID string, tiles []PlayTileJSON) (MoveResultResp, error) { var out MoveResultResp body := map[string]any{"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; 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 starts a quick game for a variant under a per-turn word rule (multipleWords // true is standard Scrabble, false the single-word rule). vsAI true starts an honest-AI // game against a robot instead of human auto-match. func (c *Client) Enqueue(ctx context.Context, userID, variant string, multipleWords, vsAI bool) (MatchResp, error) { var out MatchResp err := c.do(ctx, http.MethodPost, "/api/v1/user/lobby/enqueue", userID, "", map[string]any{"variant": variant, "multiple_words_per_turn": multipleWords, "vs_ai": vsAI}, &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 } // Cancel removes the caller from the auto-match pool (idempotent; 204 No Content). func (c *Client) Cancel(ctx context.Context, userID string) error { return c.do(ctx, http.MethodPost, "/api/v1/user/lobby/cancel", userID, "", nil, nil) } // 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"` WalletBalance int `json:"wallet_balance"` } // EvalResultResp is an unlimited move preview. Dir is the orientation the backend // inferred ("H"/"V", empty when illegal); Words lists the words formed, main word first. type EvalResultResp struct { Legal bool `json:"legal"` Score int `json:"score"` Words []string `json:"words"` Dir string `json:"dir"` } // 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. AtGameLimit is true when the caller // has reached the simultaneous quick-game cap, so the lobby disables "New Game". type GameListResp struct { Games []GameResp `json:"games"` AtGameLimit bool `json:"at_game_limit"` } // 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 // (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 } // GetDraft returns the player's saved composition for a game as the backend's // raw JSON body. The gateway forwards it verbatim, never interpreting its shape. func (c *Client) GetDraft(ctx context.Context, userID, gameID string) (json.RawMessage, error) { var out json.RawMessage err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/draft"), userID, "", nil, &out) return out, err } // SaveDraft upserts the player's composition for a game. body is the client's // {rack_order, board_tiles} JSON, forwarded verbatim — a json.RawMessage marshals as-is, so // there is no double-encode. func (c *Client) SaveDraft(ctx context.Context, userID, gameID string, body json.RawMessage) error { return c.do(ctx, http.MethodPut, c.gamePath(gameID, "/draft"), userID, "", body, nil) } // HideGame hides a finished game from the caller's own games list. The action is // per-account and irreversible; the game stays visible to the other players. func (c *Client) HideGame(ctx context.Context, userID, gameID string) error { return c.do(ctx, http.MethodPost, c.gamePath(gameID, "/hide"), userID, "", struct{}{}, nil) } // Evaluate previews a tentative play's legality and score. The tiles are addressed by // alphabet index. func (c *Client) Evaluate(ctx context.Context, userID, gameID string, tiles []PlayTileJSON) (EvalResultResp, error) { var out EvalResultResp err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/evaluate"), userID, "", map[string]any{"tiles": tiles}, &out) return out, err } // DictBytes fetches the raw serialized dictionary for a (variant, version) pair, // backing the client-side local move preview. It returns the blob and the // backend's Cache-Control header, forwarded so the immutable blob is cached hard. func (c *Client) DictBytes(ctx context.Context, userID, variant, version string) ([]byte, string, error) { return c.getRaw(ctx, "/api/v1/user/dict/"+url.PathEscape(variant)+"/"+url.PathEscape(version), userID, "Cache-Control") } // CheckWord looks a word up in the game's pinned dictionary. The word is carried as // repeated ?idx= alphabet indices; 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 } // MarkChatRead acknowledges that the caller has read the game's chat (sent when they open // the move history or chat), so the backend clears their unread bits and records the // publish-to-read latency. func (c *Client) MarkChatRead(ctx context.Context, userID, gameID string) error { return c.do(ctx, http.MethodPost, c.gamePath(gameID, "/chat/read"), userID, "", struct{}{}, nil) } // 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 }