52f898ca6f
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 11s
Tests · UI / test (push) Successful in 20s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
Link an email (confirm-code) or Telegram (web Login Widget) to the current account; if the identity already has its own account, merge the two into the one in use (the current account is primary, except a guest initiator whose durable counterpart wins). The merge runs in one transaction (internal/accountmerge): stats + hint wallet summed, paid_account ORed, identities/games/chat/complaints transferred, friends/blocks de-duplicated, the secondary kept as a merged_into tombstone so a shared finished game's no-cascade FKs hold; a shared active game blocks the merge. - migration 00009: accounts.paid_account, merged_into, merged_at (+ jetgen) - internal/link orchestrator; session.RevokeAllForAccount on merge - connector ValidateLoginWidget RPC + loginwidget HMAC validator - edge ops link.email.request/confirm/merge, link.telegram.confirm/merge; supersedes the Stage 8 email.bind.* surface (request never reveals 'taken' before the code is verified, so a probe cannot enumerate addresses) - UI Profile link section + irreversible-merge dialog; Telegram web sign-in - focused regression tests (merge core, guest inversion, active-game refusal, finished-shared-game kept), gateway transcode + connector + UI codec/e2e - docs: PLAN, ARCHITECTURE 3/4/9, FUNCTIONAL(+ru), module READMEs
346 lines
11 KiB
Go
346 lines
11 KiB
Go
package transcode
|
|
|
|
import (
|
|
flatbuffers "github.com/google/flatbuffers/go"
|
|
|
|
"scrabble/gateway/internal/backendclient"
|
|
fb "scrabble/pkg/fbs/scrabblefb"
|
|
)
|
|
|
|
// The encoders build the FlatBuffers response payloads from the backend's typed
|
|
// responses. FlatBuffers is built bottom-up: every string and child vector is
|
|
// created before the table that references it, and no two tables/vectors are
|
|
// under construction at once.
|
|
|
|
// encodeSession builds a Session payload.
|
|
func encodeSession(s backendclient.SessionResp) []byte {
|
|
b := flatbuffers.NewBuilder(128)
|
|
token := b.CreateString(s.Token)
|
|
uid := b.CreateString(s.UserID)
|
|
name := b.CreateString(s.DisplayName)
|
|
fb.SessionStart(b)
|
|
fb.SessionAddToken(b, token)
|
|
fb.SessionAddUserId(b, uid)
|
|
fb.SessionAddIsGuest(b, s.IsGuest)
|
|
fb.SessionAddDisplayName(b, name)
|
|
b.Finish(fb.SessionEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// encodeAck builds an Ack payload.
|
|
func encodeAck(ok bool) []byte {
|
|
b := flatbuffers.NewBuilder(16)
|
|
fb.AckStart(b)
|
|
fb.AckAddOk(b, ok)
|
|
b.Finish(fb.AckEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// encodeProfile builds a Profile payload.
|
|
func encodeProfile(p backendclient.ProfileResp) []byte {
|
|
b := flatbuffers.NewBuilder(192)
|
|
uid := b.CreateString(p.UserID)
|
|
name := b.CreateString(p.DisplayName)
|
|
lang := b.CreateString(p.PreferredLanguage)
|
|
tz := b.CreateString(p.TimeZone)
|
|
awayStart := b.CreateString(p.AwayStart)
|
|
awayEnd := b.CreateString(p.AwayEnd)
|
|
fb.ProfileStart(b)
|
|
fb.ProfileAddUserId(b, uid)
|
|
fb.ProfileAddDisplayName(b, name)
|
|
fb.ProfileAddPreferredLanguage(b, lang)
|
|
fb.ProfileAddTimeZone(b, tz)
|
|
fb.ProfileAddHintBalance(b, int32(p.HintBalance))
|
|
fb.ProfileAddBlockChat(b, p.BlockChat)
|
|
fb.ProfileAddBlockFriendRequests(b, p.BlockFriendRequests)
|
|
fb.ProfileAddIsGuest(b, p.IsGuest)
|
|
fb.ProfileAddAwayStart(b, awayStart)
|
|
fb.ProfileAddAwayEnd(b, awayEnd)
|
|
fb.ProfileAddNotificationsInAppOnly(b, p.NotificationsInAppOnly)
|
|
b.Finish(fb.ProfileEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// encodeLinkResult builds a LinkResult payload (Stage 11). A switched-session token
|
|
// (a guest initiator whose durable counterpart won) is carried as a nested Session
|
|
// for the client to adopt; it is omitted otherwise.
|
|
func encodeLinkResult(r backendclient.LinkResultResp) []byte {
|
|
b := flatbuffers.NewBuilder(256)
|
|
status := b.CreateString(r.Status)
|
|
secID := b.CreateString(r.SecondaryUserID)
|
|
secName := b.CreateString(r.SecondaryName)
|
|
hasSession := r.Token != "" && r.Profile != nil
|
|
var sess flatbuffers.UOffsetT
|
|
if hasSession {
|
|
token := b.CreateString(r.Token)
|
|
uid := b.CreateString(r.Profile.UserID)
|
|
name := b.CreateString(r.Profile.DisplayName)
|
|
fb.SessionStart(b)
|
|
fb.SessionAddToken(b, token)
|
|
fb.SessionAddUserId(b, uid)
|
|
fb.SessionAddIsGuest(b, r.Profile.IsGuest)
|
|
fb.SessionAddDisplayName(b, name)
|
|
sess = fb.SessionEnd(b)
|
|
}
|
|
fb.LinkResultStart(b)
|
|
fb.LinkResultAddStatus(b, status)
|
|
fb.LinkResultAddSecondaryUserId(b, secID)
|
|
fb.LinkResultAddSecondaryDisplayName(b, secName)
|
|
fb.LinkResultAddSecondaryGames(b, int32(r.SecondaryGames))
|
|
fb.LinkResultAddSecondaryFriends(b, int32(r.SecondaryFriends))
|
|
if hasSession {
|
|
fb.LinkResultAddSession(b, sess)
|
|
}
|
|
b.Finish(fb.LinkResultEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// encodeMoveResult builds a MoveResult payload.
|
|
func encodeMoveResult(r backendclient.MoveResultResp) []byte {
|
|
b := flatbuffers.NewBuilder(512)
|
|
move := buildMoveRecord(b, r.Move)
|
|
game := buildGameView(b, r.Game)
|
|
fb.MoveResultStart(b)
|
|
fb.MoveResultAddMove(b, move)
|
|
fb.MoveResultAddGame(b, game)
|
|
b.Finish(fb.MoveResultEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// encodeState builds a StateView payload.
|
|
func encodeState(s backendclient.StateResp) []byte {
|
|
b := flatbuffers.NewBuilder(512)
|
|
game := buildGameView(b, s.Game)
|
|
rack := buildStringVector(b, s.Rack, fb.StateViewStartRackVector)
|
|
fb.StateViewStart(b)
|
|
fb.StateViewAddGame(b, game)
|
|
fb.StateViewAddSeat(b, int32(s.Seat))
|
|
fb.StateViewAddRack(b, rack)
|
|
fb.StateViewAddBagLen(b, int32(s.BagLen))
|
|
fb.StateViewAddHintsRemaining(b, int32(s.HintsRemaining))
|
|
b.Finish(fb.StateViewEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// encodeMatch builds a MatchResult payload.
|
|
func encodeMatch(m backendclient.MatchResp) []byte {
|
|
b := flatbuffers.NewBuilder(512)
|
|
matched := m.Matched && m.Game != nil
|
|
var game flatbuffers.UOffsetT
|
|
if matched {
|
|
game = buildGameView(b, *m.Game)
|
|
}
|
|
fb.MatchResultStart(b)
|
|
fb.MatchResultAddMatched(b, matched)
|
|
if matched {
|
|
fb.MatchResultAddGame(b, game)
|
|
}
|
|
b.Finish(fb.MatchResultEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// buildChatMessage builds a ChatMessage table and returns its offset.
|
|
func buildChatMessage(b *flatbuffers.Builder, c backendclient.ChatResp) flatbuffers.UOffsetT {
|
|
id := b.CreateString(c.ID)
|
|
gid := b.CreateString(c.GameID)
|
|
sid := b.CreateString(c.SenderID)
|
|
kind := b.CreateString(c.Kind)
|
|
body := b.CreateString(c.Body)
|
|
fb.ChatMessageStart(b)
|
|
fb.ChatMessageAddId(b, id)
|
|
fb.ChatMessageAddGameId(b, gid)
|
|
fb.ChatMessageAddSenderId(b, sid)
|
|
fb.ChatMessageAddKind(b, kind)
|
|
fb.ChatMessageAddBody(b, body)
|
|
fb.ChatMessageAddCreatedAtUnix(b, c.CreatedAtUnix)
|
|
return fb.ChatMessageEnd(b)
|
|
}
|
|
|
|
// encodeChat builds a ChatMessage payload.
|
|
func encodeChat(c backendclient.ChatResp) []byte {
|
|
b := flatbuffers.NewBuilder(192)
|
|
b.Finish(buildChatMessage(b, c))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// encodeHintResult builds a HintResult payload.
|
|
func encodeHintResult(r backendclient.HintResultResp) []byte {
|
|
b := flatbuffers.NewBuilder(512)
|
|
move := buildMoveRecord(b, r.Move)
|
|
fb.HintResultStart(b)
|
|
fb.HintResultAddMove(b, move)
|
|
fb.HintResultAddHintsRemaining(b, int32(r.HintsRemaining))
|
|
b.Finish(fb.HintResultEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// encodeEvalResult builds an EvalResult payload.
|
|
func encodeEvalResult(r backendclient.EvalResultResp) []byte {
|
|
b := flatbuffers.NewBuilder(256)
|
|
words := buildStringVector(b, r.Words, fb.EvalResultStartWordsVector)
|
|
fb.EvalResultStart(b)
|
|
fb.EvalResultAddLegal(b, r.Legal)
|
|
fb.EvalResultAddScore(b, int32(r.Score))
|
|
fb.EvalResultAddWords(b, words)
|
|
b.Finish(fb.EvalResultEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// encodeWordCheck builds a WordCheckResult payload.
|
|
func encodeWordCheck(r backendclient.WordCheckResp) []byte {
|
|
b := flatbuffers.NewBuilder(64)
|
|
word := b.CreateString(r.Word)
|
|
fb.WordCheckResultStart(b)
|
|
fb.WordCheckResultAddWord(b, word)
|
|
fb.WordCheckResultAddLegal(b, r.Legal)
|
|
b.Finish(fb.WordCheckResultEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// encodeHistory builds a History payload (the decoded move journal).
|
|
func encodeHistory(r backendclient.HistoryResp) []byte {
|
|
b := flatbuffers.NewBuilder(1024)
|
|
moveOffs := make([]flatbuffers.UOffsetT, len(r.Moves))
|
|
for i, m := range r.Moves {
|
|
moveOffs[i] = buildMoveRecord(b, m)
|
|
}
|
|
fb.HistoryStartMovesVector(b, len(moveOffs))
|
|
for i := len(moveOffs) - 1; i >= 0; i-- {
|
|
b.PrependUOffsetT(moveOffs[i])
|
|
}
|
|
moves := b.EndVector(len(moveOffs))
|
|
gid := b.CreateString(r.GameID)
|
|
fb.HistoryStart(b)
|
|
fb.HistoryAddGameId(b, gid)
|
|
fb.HistoryAddMoves(b, moves)
|
|
b.Finish(fb.HistoryEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// encodeGameList builds a GameList payload (the caller's games).
|
|
func encodeGameList(r backendclient.GameListResp) []byte {
|
|
b := flatbuffers.NewBuilder(1024)
|
|
gameOffs := make([]flatbuffers.UOffsetT, len(r.Games))
|
|
for i, g := range r.Games {
|
|
gameOffs[i] = buildGameView(b, g)
|
|
}
|
|
fb.GameListStartGamesVector(b, len(gameOffs))
|
|
for i := len(gameOffs) - 1; i >= 0; i-- {
|
|
b.PrependUOffsetT(gameOffs[i])
|
|
}
|
|
games := b.EndVector(len(gameOffs))
|
|
fb.GameListStart(b)
|
|
fb.GameListAddGames(b, games)
|
|
b.Finish(fb.GameListEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// encodeChatList builds a ChatList payload (a game's chat history).
|
|
func encodeChatList(r backendclient.ChatListResp) []byte {
|
|
b := flatbuffers.NewBuilder(512)
|
|
msgOffs := make([]flatbuffers.UOffsetT, len(r.Messages))
|
|
for i, m := range r.Messages {
|
|
msgOffs[i] = buildChatMessage(b, m)
|
|
}
|
|
fb.ChatListStartMessagesVector(b, len(msgOffs))
|
|
for i := len(msgOffs) - 1; i >= 0; i-- {
|
|
b.PrependUOffsetT(msgOffs[i])
|
|
}
|
|
msgs := b.EndVector(len(msgOffs))
|
|
fb.ChatListStart(b)
|
|
fb.ChatListAddMessages(b, msgs)
|
|
b.Finish(fb.ChatListEnd(b))
|
|
return b.FinishedBytes()
|
|
}
|
|
|
|
// buildGameView builds a GameView table and returns its offset.
|
|
func buildGameView(b *flatbuffers.Builder, g backendclient.GameResp) flatbuffers.UOffsetT {
|
|
seatOffs := make([]flatbuffers.UOffsetT, len(g.Seats))
|
|
for i, s := range g.Seats {
|
|
aid := b.CreateString(s.AccountID)
|
|
dname := b.CreateString(s.DisplayName)
|
|
fb.SeatViewStart(b)
|
|
fb.SeatViewAddSeat(b, int32(s.Seat))
|
|
fb.SeatViewAddAccountId(b, aid)
|
|
fb.SeatViewAddScore(b, int32(s.Score))
|
|
fb.SeatViewAddHintsUsed(b, int32(s.HintsUsed))
|
|
fb.SeatViewAddIsWinner(b, s.IsWinner)
|
|
fb.SeatViewAddDisplayName(b, dname)
|
|
seatOffs[i] = fb.SeatViewEnd(b)
|
|
}
|
|
fb.GameViewStartSeatsVector(b, len(seatOffs))
|
|
for i := len(seatOffs) - 1; i >= 0; i-- {
|
|
b.PrependUOffsetT(seatOffs[i])
|
|
}
|
|
seats := b.EndVector(len(seatOffs))
|
|
|
|
id := b.CreateString(g.ID)
|
|
variant := b.CreateString(g.Variant)
|
|
dictVer := b.CreateString(g.DictVersion)
|
|
status := b.CreateString(g.Status)
|
|
endReason := b.CreateString(g.EndReason)
|
|
|
|
fb.GameViewStart(b)
|
|
fb.GameViewAddId(b, id)
|
|
fb.GameViewAddVariant(b, variant)
|
|
fb.GameViewAddDictVersion(b, dictVer)
|
|
fb.GameViewAddStatus(b, status)
|
|
fb.GameViewAddPlayers(b, int32(g.Players))
|
|
fb.GameViewAddToMove(b, int32(g.ToMove))
|
|
fb.GameViewAddTurnTimeoutSecs(b, int32(g.TurnTimeoutSecs))
|
|
fb.GameViewAddMoveCount(b, int32(g.MoveCount))
|
|
fb.GameViewAddEndReason(b, endReason)
|
|
fb.GameViewAddSeats(b, seats)
|
|
return fb.GameViewEnd(b)
|
|
}
|
|
|
|
// buildMoveRecord builds a MoveRecord table and returns its offset.
|
|
func buildMoveRecord(b *flatbuffers.Builder, m backendclient.MoveRecordResp) flatbuffers.UOffsetT {
|
|
tileOffs := make([]flatbuffers.UOffsetT, len(m.Tiles))
|
|
for i, t := range m.Tiles {
|
|
letter := b.CreateString(t.Letter)
|
|
fb.TileRecordStart(b)
|
|
fb.TileRecordAddRow(b, int32(t.Row))
|
|
fb.TileRecordAddCol(b, int32(t.Col))
|
|
fb.TileRecordAddLetter(b, letter)
|
|
fb.TileRecordAddBlank(b, t.Blank)
|
|
tileOffs[i] = fb.TileRecordEnd(b)
|
|
}
|
|
fb.MoveRecordStartTilesVector(b, len(tileOffs))
|
|
for i := len(tileOffs) - 1; i >= 0; i-- {
|
|
b.PrependUOffsetT(tileOffs[i])
|
|
}
|
|
tiles := b.EndVector(len(tileOffs))
|
|
|
|
words := buildStringVector(b, m.Words, fb.MoveRecordStartWordsVector)
|
|
|
|
action := b.CreateString(m.Action)
|
|
dir := b.CreateString(m.Dir)
|
|
fb.MoveRecordStart(b)
|
|
fb.MoveRecordAddPlayer(b, int32(m.Player))
|
|
fb.MoveRecordAddAction(b, action)
|
|
fb.MoveRecordAddDir(b, dir)
|
|
fb.MoveRecordAddMainRow(b, int32(m.MainRow))
|
|
fb.MoveRecordAddMainCol(b, int32(m.MainCol))
|
|
fb.MoveRecordAddTiles(b, tiles)
|
|
fb.MoveRecordAddWords(b, words)
|
|
fb.MoveRecordAddCount(b, int32(m.Count))
|
|
fb.MoveRecordAddScore(b, int32(m.Score))
|
|
fb.MoveRecordAddTotal(b, int32(m.Total))
|
|
return fb.MoveRecordEnd(b)
|
|
}
|
|
|
|
// buildStringVector builds a vector of strings using the table-specific
|
|
// StartXVector function and returns the vector offset.
|
|
func buildStringVector(b *flatbuffers.Builder, items []string, start func(*flatbuffers.Builder, int) flatbuffers.UOffsetT) flatbuffers.UOffsetT {
|
|
offs := make([]flatbuffers.UOffsetT, len(items))
|
|
for i, s := range items {
|
|
offs[i] = b.CreateString(s)
|
|
}
|
|
start(b, len(offs))
|
|
for i := len(offs) - 1; i >= 0; i-- {
|
|
b.PrependUOffsetT(offs[i])
|
|
}
|
|
return b.EndVector(len(offs))
|
|
}
|