2a6dc5a304
Add the WalletOrderRequest / WalletOrderResponse FlatBuffers messages and the wallet.order Connect op: the gateway decodes the order request, forwards it to the backend POST /wallet/order and returns the created order id plus the provider launch URL the client opens. Regenerate the committed Go and TS FlatBuffers code.
665 lines
23 KiB
Go
665 lines
23 KiB
Go
// Package transcode is the gateway's FlatBuffers<->REST bridge. Each message type
|
|
// maps to a handler that decodes the FlatBuffers request payload, calls the
|
|
// backend over REST, and encodes the FlatBuffers response. The registry is the
|
|
// authoritative message_type catalog; new operations are added here following the
|
|
// same vertical-slice pattern.
|
|
package transcode
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/url"
|
|
|
|
"scrabble/gateway/internal/backendclient"
|
|
"scrabble/gateway/internal/connector"
|
|
"scrabble/gateway/internal/vkauth"
|
|
"scrabble/gateway/internal/vkid"
|
|
fb "scrabble/pkg/fbs/scrabblefb"
|
|
)
|
|
|
|
// Message types in the vertical slice.
|
|
const (
|
|
MsgAuthTelegram = "auth.telegram"
|
|
MsgAuthVK = "auth.vk"
|
|
MsgAuthGuest = "auth.guest"
|
|
MsgAuthEmailReq = "auth.email.request"
|
|
MsgAuthEmailLogin = "auth.email.login"
|
|
MsgAuthEmailConfirmLink = "auth.email.confirm_link"
|
|
MsgProfileGet = "profile.get"
|
|
MsgBlockStatus = "account.block_status"
|
|
MsgGameSubmitPlay = "game.submit_play"
|
|
MsgGameState = "game.state"
|
|
MsgLobbyEnqueue = "lobby.enqueue"
|
|
MsgLobbyCancel = "lobby.cancel"
|
|
MsgLobbyPoll = "lobby.poll"
|
|
MsgChatPost = "chat.post"
|
|
MsgGamesList = "games.list"
|
|
MsgGamePass = "game.pass"
|
|
MsgGameExchange = "game.exchange"
|
|
MsgGameResign = "game.resign"
|
|
MsgGameHint = "game.hint"
|
|
MsgGameEvaluate = "game.evaluate"
|
|
MsgGameCheckWord = "game.check_word"
|
|
MsgGameComplaint = "game.complaint"
|
|
MsgGameHistory = "game.history"
|
|
MsgChatList = "chat.list"
|
|
MsgChatNudge = "chat.nudge"
|
|
MsgChatRead = "chat.read"
|
|
MsgDraftGet = "draft.get"
|
|
MsgDraftSave = "draft.save"
|
|
MsgGameHide = "game.hide"
|
|
MsgFeedbackSubmit = "feedback.submit"
|
|
MsgFeedbackGet = "feedback.get"
|
|
MsgFeedbackUnread = "feedback.unread"
|
|
MsgWalletGet = "wallet.get"
|
|
MsgWalletCatalog = "wallet.catalog"
|
|
MsgWalletBuy = "wallet.buy"
|
|
MsgWalletOrder = "wallet.order"
|
|
)
|
|
|
|
// Request is one decoded Execute call.
|
|
type Request struct {
|
|
Payload []byte
|
|
UserID string // resolved account id; empty for auth (unauthenticated) ops
|
|
ClientIP string
|
|
}
|
|
|
|
// Handler runs one operation and returns the FlatBuffers response payload.
|
|
type Handler func(ctx context.Context, req Request) ([]byte, error)
|
|
|
|
// Op is a registered message type and its policy flags.
|
|
type Op struct {
|
|
Handler Handler
|
|
// Auth marks an operation that requires a resolved session (X-User-ID).
|
|
Auth bool
|
|
// Email marks the costly email-code path that gets a stricter rate sub-limit.
|
|
Email bool
|
|
// NonGuest marks an operation a guest account may not perform; the gateway
|
|
// rejects it with the guest_forbidden result code after resolving the session,
|
|
// before any backend call.
|
|
NonGuest bool
|
|
}
|
|
|
|
// Registry maps message types to their operations.
|
|
type Registry struct {
|
|
ops map[string]Op
|
|
}
|
|
|
|
// TelegramValidator validates Telegram credentials via the connector side-service:
|
|
// Mini App launch data (auth) and Login Widget data (linking).
|
|
// *connector.Client implements it; a nil value disables the telegram auth and
|
|
// telegram-link paths.
|
|
type TelegramValidator interface {
|
|
ValidateInitData(ctx context.Context, initData string) (connector.User, error)
|
|
ValidateLoginWidget(ctx context.Context, data string) (connector.User, error)
|
|
}
|
|
|
|
// NewRegistry builds the slice's message-type catalog over the backend client.
|
|
// The Telegram auth op is registered only when a validator is supplied (the
|
|
// connector is configured); otherwise auth.telegram is simply unknown. Optional ops
|
|
// (e.g. WithVKAuth) are applied last from opts.
|
|
func NewRegistry(backend *backendclient.Client, tg TelegramValidator, opts ...Option) *Registry {
|
|
r := &Registry{ops: make(map[string]Op)}
|
|
if tg != nil {
|
|
r.ops[MsgAuthTelegram] = Op{Handler: authTelegramHandler(backend, tg)}
|
|
}
|
|
r.ops[MsgAuthGuest] = Op{Handler: authGuestHandler(backend)}
|
|
r.ops[MsgAuthEmailReq] = Op{Handler: authEmailRequestHandler(backend), Email: true}
|
|
r.ops[MsgAuthEmailLogin] = Op{Handler: authEmailLoginHandler(backend), Email: true}
|
|
r.ops[MsgAuthEmailConfirmLink] = Op{Handler: authEmailConfirmLinkHandler(backend)}
|
|
r.ops[MsgProfileGet] = Op{Handler: profileHandler(backend), Auth: true}
|
|
r.ops[MsgWalletGet] = Op{Handler: walletHandler(backend), Auth: true}
|
|
r.ops[MsgWalletCatalog] = Op{Handler: walletCatalogHandler(backend), Auth: true}
|
|
r.ops[MsgWalletBuy] = Op{Handler: walletBuyHandler(backend), Auth: true}
|
|
r.ops[MsgWalletOrder] = Op{Handler: walletOrderHandler(backend), Auth: true}
|
|
r.ops[MsgBlockStatus] = Op{Handler: blockStatusHandler(backend), Auth: true}
|
|
r.ops[MsgGameSubmitPlay] = Op{Handler: submitPlayHandler(backend), Auth: true}
|
|
r.ops[MsgGameState] = Op{Handler: gameStateHandler(backend), Auth: true}
|
|
r.ops[MsgLobbyEnqueue] = Op{Handler: enqueueHandler(backend), Auth: true}
|
|
r.ops[MsgLobbyCancel] = Op{Handler: cancelHandler(backend), Auth: true}
|
|
r.ops[MsgLobbyPoll] = Op{Handler: pollHandler(backend), Auth: true}
|
|
r.ops[MsgChatPost] = Op{Handler: chatPostHandler(backend), Auth: true}
|
|
r.ops[MsgGamesList] = Op{Handler: gamesListHandler(backend), Auth: true}
|
|
r.ops[MsgGamePass] = Op{Handler: passHandler(backend), Auth: true}
|
|
r.ops[MsgGameExchange] = Op{Handler: exchangeHandler(backend), Auth: true}
|
|
r.ops[MsgGameResign] = Op{Handler: resignHandler(backend), Auth: true}
|
|
r.ops[MsgGameHint] = Op{Handler: hintHandler(backend), Auth: true}
|
|
r.ops[MsgGameEvaluate] = Op{Handler: evaluateHandler(backend), Auth: true}
|
|
r.ops[MsgGameCheckWord] = Op{Handler: checkWordHandler(backend), Auth: true}
|
|
r.ops[MsgGameComplaint] = Op{Handler: complaintHandler(backend), Auth: true}
|
|
r.ops[MsgGameHistory] = Op{Handler: historyHandler(backend), Auth: true}
|
|
r.ops[MsgChatList] = Op{Handler: chatListHandler(backend), Auth: true}
|
|
r.ops[MsgChatNudge] = Op{Handler: nudgeHandler(backend), Auth: true}
|
|
r.ops[MsgChatRead] = Op{Handler: markChatReadHandler(backend), Auth: true}
|
|
r.ops[MsgDraftGet] = Op{Handler: getDraftHandler(backend), Auth: true}
|
|
r.ops[MsgDraftSave] = Op{Handler: saveDraftHandler(backend), Auth: true}
|
|
r.ops[MsgGameHide] = Op{Handler: hideGameHandler(backend), Auth: true}
|
|
r.ops[MsgFeedbackSubmit] = Op{Handler: feedbackSubmitHandler(backend), Auth: true, NonGuest: true}
|
|
r.ops[MsgFeedbackGet] = Op{Handler: feedbackGetHandler(backend), Auth: true}
|
|
r.ops[MsgFeedbackUnread] = Op{Handler: feedbackUnreadHandler(backend), Auth: true}
|
|
registerSocialOps(r, backend)
|
|
registerLinkOps(r, backend, tg)
|
|
for _, opt := range opts {
|
|
opt(r, backend)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Option configures an optional registry operation at construction. It is kept out of
|
|
// NewRegistry's positional signature so existing call sites stay unaffected.
|
|
type Option func(r *Registry, backend *backendclient.Client)
|
|
|
|
// WithVKAuth registers the VK Mini App auth op (auth.vk), which verifies launch params
|
|
// in-process under the VK app secret. A blank secret leaves auth.vk unregistered, so
|
|
// the op is simply unknown wherever VK is not configured.
|
|
func WithVKAuth(secret string) Option {
|
|
return func(r *Registry, backend *backendclient.Client) {
|
|
if secret != "" {
|
|
r.ops[MsgAuthVK] = Op{Handler: authVKHandler(backend, secret)}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithVKLink registers the VK web-link ops (link.vk.confirm/merge), which complete a
|
|
// browser VK ID authorization via the server-side code exchange. A nil exchanger leaves
|
|
// them unregistered, so the ops are unknown wherever VK ID web login is not configured.
|
|
func WithVKLink(ex VKIDExchanger) Option {
|
|
return func(r *Registry, backend *backendclient.Client) {
|
|
registerVKLinkOps(r, backend, ex)
|
|
}
|
|
}
|
|
|
|
// Lookup returns the operation for messageType, and whether it is registered.
|
|
func (r *Registry) Lookup(messageType string) (Op, bool) {
|
|
op, ok := r.ops[messageType]
|
|
return op, ok
|
|
}
|
|
|
|
// DomainCode maps an error to a stable result code to surface in the Execute
|
|
// envelope, reporting false for an unexpected error the caller should treat as a
|
|
// transport-level internal failure.
|
|
func DomainCode(err error) (string, bool) {
|
|
var apiErr *backendclient.APIError
|
|
if errors.As(err, &apiErr) {
|
|
return apiErr.Code, true
|
|
}
|
|
if errors.Is(err, connector.ErrInvalidInitData) {
|
|
return "invalid_init_data", true
|
|
}
|
|
if errors.Is(err, vkauth.ErrInvalid) {
|
|
return "invalid_vk_params", true
|
|
}
|
|
if errors.Is(err, vkid.ErrInvalid) {
|
|
return "invalid_vk_id", true
|
|
}
|
|
if errors.Is(err, connector.ErrInvalidLoginWidget) {
|
|
return "invalid_login_widget", true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func authTelegramHandler(backend *backendclient.Client, tg TelegramValidator) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsTelegramLoginRequest(req.Payload, 0)
|
|
initData := string(in.InitData())
|
|
user, err := tg.ValidateInitData(ctx, initData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// start_param rides inside the signed initData validated just above, so the launch
|
|
// deep-link payload can be trusted here without a separate wire field; the backend
|
|
// uses it to seed a brand-new account's variant preferences.
|
|
startParam := ""
|
|
if q, perr := url.ParseQuery(initData); perr == nil {
|
|
startParam = q.Get("start_param")
|
|
}
|
|
sess, err := backend.TelegramAuth(ctx, user.ExternalID, user.LanguageCode, user.Username, user.FirstName, string(in.BrowserTz()), startParam, string(in.Subtype()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeSession(sess), nil
|
|
}
|
|
}
|
|
|
|
// authVKHandler verifies a VK Mini App launch in-process (HMAC over the signed vk_*
|
|
// params under the app secret) and provisions/finds the bound account. Unlike Telegram,
|
|
// VK omits the user's name from the signed params, so the client-supplied display_name
|
|
// rides the wire as a cosmetic seed for a brand-new account.
|
|
func authVKHandler(backend *backendclient.Client, secret string) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsVKLoginRequest(req.Payload, 0)
|
|
user, err := vkauth.Verify(string(in.Params()), secret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sess, err := backend.VKAuth(ctx, user.ExternalID, user.Language, string(in.DisplayName()), string(in.BrowserTz()), user.Subtype())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeSession(sess), nil
|
|
}
|
|
}
|
|
|
|
func authGuestHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
// The guest bootstrap historically carried no payload; the detected zone is
|
|
// optional, so an absent or empty one simply yields no time-zone seed (rather
|
|
// than panicking in GetRootAs* on a zero-length buffer).
|
|
var browserTz, subtype string
|
|
if len(req.Payload) > 0 {
|
|
g := fb.GetRootAsGuestLoginRequest(req.Payload, 0)
|
|
browserTz = string(g.BrowserTz())
|
|
subtype = string(g.Subtype())
|
|
}
|
|
sess, err := backend.GuestAuth(ctx, browserTz, subtype)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeSession(sess), nil
|
|
}
|
|
}
|
|
|
|
func authEmailRequestHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsEmailRequestRequest(req.Payload, 0)
|
|
if err := backend.EmailRequest(ctx, string(in.Email()), string(in.BrowserTz()), string(in.Language()), in.Pwa()); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeAck(true), nil
|
|
}
|
|
}
|
|
|
|
func authEmailLoginHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsEmailLoginRequest(req.Payload, 0)
|
|
sess, err := backend.EmailLogin(ctx, string(in.Email()), string(in.Code()), string(in.Subtype()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeSession(sess), nil
|
|
}
|
|
}
|
|
|
|
func authEmailConfirmLinkHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsEmailConfirmLinkRequest(req.Payload, 0)
|
|
res, err := backend.EmailConfirmLink(ctx, string(in.Token()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeConfirmLinkResult(res), nil
|
|
}
|
|
}
|
|
|
|
func profileHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
p, err := backend.Profile(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeProfile(p), nil
|
|
}
|
|
}
|
|
|
|
func walletHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
w, err := backend.Wallet(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeWallet(w), nil
|
|
}
|
|
}
|
|
|
|
func walletCatalogHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
cat, err := backend.Catalog(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeCatalog(cat), nil
|
|
}
|
|
}
|
|
|
|
func walletBuyHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsWalletBuyRequest(req.Payload, 0)
|
|
w, err := backend.WalletBuy(ctx, req.UserID, string(in.ProductId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeWallet(w), nil
|
|
}
|
|
}
|
|
|
|
func walletOrderHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsWalletOrderRequest(req.Payload, 0)
|
|
o, err := backend.WalletOrder(ctx, req.UserID, string(in.ProductId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeWalletOrder(o), nil
|
|
}
|
|
}
|
|
|
|
func blockStatusHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
bs, err := backend.BlockStatus(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeBlockStatus(bs), nil
|
|
}
|
|
}
|
|
|
|
func submitPlayHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsSubmitPlayRequest(req.Payload, 0)
|
|
res, err := backend.SubmitPlay(ctx, req.UserID, string(in.GameId()), decodeTiles(in))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeMoveResult(res), nil
|
|
}
|
|
}
|
|
|
|
func gameStateHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsStateRequest(req.Payload, 0)
|
|
st, err := backend.GameState(ctx, req.UserID, string(in.GameId()), in.IncludeAlphabet())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeState(st), nil
|
|
}
|
|
}
|
|
|
|
func enqueueHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsEnqueueRequest(req.Payload, 0)
|
|
m, err := backend.Enqueue(ctx, req.UserID, string(in.Variant()), in.MultipleWordsPerTurn(), in.VsAi())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeMatch(m), nil
|
|
}
|
|
}
|
|
|
|
func pollHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
m, err := backend.Poll(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeMatch(m), nil
|
|
}
|
|
}
|
|
|
|
// cancelHandler removes the caller from the auto-match pool. It carries no result;
|
|
// it echoes an empty (unmatched) Match so the client has a well-formed payload.
|
|
func cancelHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
if err := backend.Cancel(ctx, req.UserID); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeMatch(backendclient.MatchResp{}), nil
|
|
}
|
|
}
|
|
|
|
func chatPostHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsChatPostRequest(req.Payload, 0)
|
|
c, err := backend.ChatPost(ctx, req.UserID, string(in.GameId()), string(in.Body()), req.ClientIP)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeChat(c), nil
|
|
}
|
|
}
|
|
|
|
// decodeTiles reads the index-addressed tiles to place from a SubmitPlayRequest.
|
|
func decodeTiles(in *fb.SubmitPlayRequest) []backendclient.PlayTileJSON {
|
|
n := in.TilesLength()
|
|
tiles := make([]backendclient.PlayTileJSON, 0, n)
|
|
var t fb.PlayTile
|
|
for i := 0; i < n; i++ {
|
|
if in.Tiles(&t, i) {
|
|
tiles = append(tiles, backendclient.PlayTileJSON{
|
|
Row: int(t.Row()),
|
|
Col: int(t.Col()),
|
|
Letter: int(t.Letter()),
|
|
Blank: t.Blank(),
|
|
})
|
|
}
|
|
}
|
|
return tiles
|
|
}
|
|
|
|
// decodeEvalTiles reads the index-addressed tentative tiles from an EvalRequest.
|
|
func decodeEvalTiles(in *fb.EvalRequest) []backendclient.PlayTileJSON {
|
|
n := in.TilesLength()
|
|
tiles := make([]backendclient.PlayTileJSON, 0, n)
|
|
var t fb.PlayTile
|
|
for i := 0; i < n; i++ {
|
|
if in.Tiles(&t, i) {
|
|
tiles = append(tiles, backendclient.PlayTileJSON{
|
|
Row: int(t.Row()),
|
|
Col: int(t.Col()),
|
|
Letter: int(t.Letter()),
|
|
Blank: t.Blank(),
|
|
})
|
|
}
|
|
}
|
|
return tiles
|
|
}
|
|
|
|
// bytesToInts widens a FlatBuffers ubyte vector (an alphabet-index list) to []int for the
|
|
// backend JSON edge (rack-exchange tiles and the word-check query).
|
|
func bytesToInts(bs []byte) []int {
|
|
out := make([]int, len(bs))
|
|
for i, b := range bs {
|
|
out[i] = int(b)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func gamesListHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
res, err := backend.GamesList(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeGameList(res), nil
|
|
}
|
|
}
|
|
|
|
func passHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
|
res, err := backend.Pass(ctx, req.UserID, string(in.GameId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeMoveResult(res), nil
|
|
}
|
|
}
|
|
|
|
func resignHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
|
res, err := backend.Resign(ctx, req.UserID, string(in.GameId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeMoveResult(res), nil
|
|
}
|
|
}
|
|
|
|
func exchangeHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsExchangeRequest(req.Payload, 0)
|
|
res, err := backend.Exchange(ctx, req.UserID, string(in.GameId()), bytesToInts(in.TilesBytes()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeMoveResult(res), nil
|
|
}
|
|
}
|
|
|
|
func hintHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
|
res, err := backend.Hint(ctx, req.UserID, string(in.GameId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeHintResult(res), nil
|
|
}
|
|
}
|
|
|
|
func evaluateHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsEvalRequest(req.Payload, 0)
|
|
res, err := backend.Evaluate(ctx, req.UserID, string(in.GameId()), decodeEvalTiles(in))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeEvalResult(res), nil
|
|
}
|
|
}
|
|
|
|
func checkWordHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsCheckWordRequest(req.Payload, 0)
|
|
res, err := backend.CheckWord(ctx, req.UserID, string(in.GameId()), bytesToInts(in.WordBytes()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeWordCheck(res), nil
|
|
}
|
|
}
|
|
|
|
func complaintHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsComplaintRequest(req.Payload, 0)
|
|
if err := backend.Complaint(ctx, req.UserID, string(in.GameId()), string(in.Word()), string(in.Note())); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeAck(true), nil
|
|
}
|
|
}
|
|
|
|
func historyHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
|
res, err := backend.History(ctx, req.UserID, string(in.GameId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeHistory(res), nil
|
|
}
|
|
}
|
|
|
|
func chatListHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
|
res, err := backend.ChatList(ctx, req.UserID, string(in.GameId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeChatList(res), nil
|
|
}
|
|
}
|
|
|
|
func nudgeHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
|
res, err := backend.Nudge(ctx, req.UserID, string(in.GameId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeChat(res), nil
|
|
}
|
|
}
|
|
|
|
// markChatReadHandler acknowledges the caller has read the game's chat. It reuses
|
|
// GameActionRequest for the game id and echoes an Ack.
|
|
func markChatReadHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
|
if err := backend.MarkChatRead(ctx, req.UserID, string(in.GameId())); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeAck(true), nil
|
|
}
|
|
}
|
|
|
|
// getDraftHandler returns the player's saved composition. It reuses
|
|
// GameActionRequest for the game id and wraps the backend's raw JSON in a DraftView.
|
|
func getDraftHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
|
raw, err := backend.GetDraft(ctx, req.UserID, string(in.GameId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeDraftView(string(raw)), nil
|
|
}
|
|
}
|
|
|
|
// saveDraftHandler upserts the player's composition, forwarding the opaque JSON
|
|
// string verbatim. It echoes an empty DraftView as a well-formed acknowledgement.
|
|
func saveDraftHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsDraftRequest(req.Payload, 0)
|
|
if err := backend.SaveDraft(ctx, req.UserID, string(in.GameId()), json.RawMessage(in.Json())); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeDraftView(""), nil
|
|
}
|
|
}
|
|
|
|
// hideGameHandler hides a finished game from the caller's own list. It reuses
|
|
// GameActionRequest for the game id and echoes an Ack.
|
|
func hideGameHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
|
if err := backend.HideGame(ctx, req.UserID, string(in.GameId())); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeAck(true), nil
|
|
}
|
|
}
|
|
|
|
func feedbackSubmitHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsFeedbackSubmitRequest(req.Payload, 0)
|
|
if err := backend.FeedbackSubmit(ctx, req.UserID, string(in.Body()), in.AttachmentBytes(), string(in.AttachmentName()), string(in.Channel()), string(in.Version()), string(in.BrowserTz()), req.ClientIP); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeAck(true), nil
|
|
}
|
|
}
|
|
|
|
func feedbackGetHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
st, err := backend.FeedbackGet(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeFeedbackState(st), nil
|
|
}
|
|
}
|
|
|
|
func feedbackUnreadHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
u, err := backend.FeedbackUnread(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeFeedbackUnread(u), nil
|
|
}
|
|
}
|