57c778f9b2
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s
Collapse the two per-language Telegram bots into one unified bot and
replace language-based variant gating with explicit per-user variant
preferences.
- Telegram: one bot; drop service_language and the supported_languages
set everywhere (DB, account, auth, FlatBuffers Session wire, gateway,
connector proto). The single bot renders chat and out-of-app push in
the recipient's preferred_language; remove the game-language push
routing override (notify Intent.Language / push Event.language).
- Preferences: new accounts.variant_preferences (text[], DB default
{erudit_ru}, CHECK non-empty + subset of the three variants). Gates
the New Game picker, vs-AI and the friend invitation the player
creates, enforced server-side (HTTP 400 otherwise); an invited friend
may still accept any variant. Edited on the Settings screen; variants
are Erudit-first everywhere.
- Admin: drop the per-bot language selectors (broadcast / send-to-user)
and the feedback channel_lang column/field.
- Env/CI: collapse TELEGRAM_BOT_TOKEN_{EN,RU}, TELEGRAM_GAME_CHANNEL_ID_{EN,RU},
VITE_TELEGRAM_LINK{,_EN,_RU} and VITE_TELEGRAM_GAME_CHANNEL_NAME_{EN,RU}
to single unsuffixed names; drop GATEWAY_DEFAULT_SUPPORTED_LANGUAGES.
- Docs updated (ARCHITECTURE, FUNCTIONAL + _ru, platform/telegram, gateway,
backend, ui, UI_DESIGN, PRERELEASE).
The migration squash is deferred to a follow-up PR.
302 lines
10 KiB
Go
302 lines
10 KiB
Go
package transcode
|
|
|
|
import (
|
|
"context"
|
|
|
|
"scrabble/gateway/internal/backendclient"
|
|
fb "scrabble/pkg/fbs/scrabblefb"
|
|
)
|
|
|
|
// Message types: friends (incl. the one-time code path), per-user blocks,
|
|
// friend-game invitations, profile editing + email binding, statistics and GCG
|
|
// export. All are authenticated. Registered by registerSocialOps from NewRegistry.
|
|
const (
|
|
MsgFriendsList = "friends.list"
|
|
MsgFriendsIncoming = "friends.incoming"
|
|
MsgFriendsOutgoing = "friends.outgoing"
|
|
MsgFriendRequest = "friends.request"
|
|
MsgFriendRespond = "friends.respond"
|
|
MsgFriendCancel = "friends.cancel"
|
|
MsgFriendUnfriend = "friends.unfriend"
|
|
MsgFriendCodeIssue = "friends.code.issue"
|
|
MsgFriendCodeRedeem = "friends.code.redeem"
|
|
MsgBlocksList = "blocks.list"
|
|
MsgBlockAdd = "blocks.add"
|
|
MsgBlockRemove = "blocks.remove"
|
|
MsgInvitationsList = "invitation.list"
|
|
MsgInvitationCreate = "invitation.create"
|
|
MsgInvitationAccept = "invitation.accept"
|
|
MsgInvitationDecline = "invitation.decline"
|
|
MsgInvitationCancel = "invitation.cancel"
|
|
MsgProfileUpdate = "profile.update"
|
|
MsgStatsGet = "stats.get"
|
|
MsgGameGCG = "game.gcg"
|
|
)
|
|
|
|
// registerSocialOps adds the social, account and history operations to the
|
|
// registry (all authenticated; the email-bind ops carry the costly-email flag).
|
|
func registerSocialOps(r *Registry, backend *backendclient.Client) {
|
|
r.ops[MsgFriendsList] = Op{Handler: friendsListHandler(backend), Auth: true}
|
|
r.ops[MsgFriendsIncoming] = Op{Handler: friendsIncomingHandler(backend), Auth: true}
|
|
r.ops[MsgFriendsOutgoing] = Op{Handler: friendsOutgoingHandler(backend), Auth: true}
|
|
r.ops[MsgFriendRequest] = Op{Handler: friendRequestHandler(backend), Auth: true}
|
|
r.ops[MsgFriendRespond] = Op{Handler: friendRespondHandler(backend), Auth: true}
|
|
r.ops[MsgFriendCancel] = Op{Handler: friendCancelHandler(backend), Auth: true}
|
|
r.ops[MsgFriendUnfriend] = Op{Handler: friendUnfriendHandler(backend), Auth: true}
|
|
r.ops[MsgFriendCodeIssue] = Op{Handler: friendCodeIssueHandler(backend), Auth: true}
|
|
r.ops[MsgFriendCodeRedeem] = Op{Handler: friendCodeRedeemHandler(backend), Auth: true}
|
|
r.ops[MsgBlocksList] = Op{Handler: blocksListHandler(backend), Auth: true}
|
|
r.ops[MsgBlockAdd] = Op{Handler: blockAddHandler(backend), Auth: true}
|
|
r.ops[MsgBlockRemove] = Op{Handler: blockRemoveHandler(backend), Auth: true}
|
|
r.ops[MsgInvitationsList] = Op{Handler: invitationsListHandler(backend), Auth: true}
|
|
r.ops[MsgInvitationCreate] = Op{Handler: invitationCreateHandler(backend), Auth: true}
|
|
r.ops[MsgInvitationAccept] = Op{Handler: invitationRespondHandler(backend, true), Auth: true}
|
|
r.ops[MsgInvitationDecline] = Op{Handler: invitationRespondHandler(backend, false), Auth: true}
|
|
r.ops[MsgInvitationCancel] = Op{Handler: invitationCancelHandler(backend), Auth: true}
|
|
r.ops[MsgProfileUpdate] = Op{Handler: profileUpdateHandler(backend), Auth: true}
|
|
r.ops[MsgStatsGet] = Op{Handler: statsHandler(backend), Auth: true}
|
|
r.ops[MsgGameGCG] = Op{Handler: gcgHandler(backend), Auth: true}
|
|
}
|
|
|
|
// --- friends ---
|
|
|
|
func friendsListHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
res, err := backend.ListFriends(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeFriendList(res), nil
|
|
}
|
|
}
|
|
|
|
func friendsIncomingHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
res, err := backend.ListIncoming(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeIncomingList(res), nil
|
|
}
|
|
}
|
|
|
|
func friendsOutgoingHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
res, err := backend.ListOutgoing(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeOutgoingList(res), nil
|
|
}
|
|
}
|
|
|
|
func friendRequestHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsTargetRequest(req.Payload, 0)
|
|
// game_id is set only by an in-game request, so a disguised-robot opponent is recorded
|
|
// per-game; it is empty for any non-game path.
|
|
if err := backend.SendFriendRequest(ctx, req.UserID, string(in.AccountId()), string(in.GameId())); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeAck(true), nil
|
|
}
|
|
}
|
|
|
|
func friendRespondHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsFriendRespondRequest(req.Payload, 0)
|
|
if err := backend.RespondFriendRequest(ctx, req.UserID, string(in.RequesterId()), in.Accept()); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeAck(true), nil
|
|
}
|
|
}
|
|
|
|
func friendCancelHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsTargetRequest(req.Payload, 0)
|
|
if err := backend.CancelFriendRequest(ctx, req.UserID, string(in.AccountId())); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeAck(true), nil
|
|
}
|
|
}
|
|
|
|
func friendUnfriendHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsTargetRequest(req.Payload, 0)
|
|
if err := backend.Unfriend(ctx, req.UserID, string(in.AccountId())); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeAck(true), nil
|
|
}
|
|
}
|
|
|
|
func friendCodeIssueHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
res, err := backend.IssueFriendCode(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeFriendCode(res), nil
|
|
}
|
|
}
|
|
|
|
func friendCodeRedeemHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsRedeemCodeRequest(req.Payload, 0)
|
|
res, err := backend.RedeemFriendCode(ctx, req.UserID, string(in.Code()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeRedeemResult(res), nil
|
|
}
|
|
}
|
|
|
|
// --- blocks ---
|
|
|
|
func blocksListHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
res, err := backend.ListBlocks(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeBlockList(res), nil
|
|
}
|
|
}
|
|
|
|
func blockAddHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsTargetRequest(req.Payload, 0)
|
|
// game_id is set only by an in-game block, so a disguised-robot opponent is recorded
|
|
// per-game; it is empty for a settings-screen block.
|
|
if err := backend.Block(ctx, req.UserID, string(in.AccountId()), string(in.GameId())); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeAck(true), nil
|
|
}
|
|
}
|
|
|
|
func blockRemoveHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsTargetRequest(req.Payload, 0)
|
|
if err := backend.Unblock(ctx, req.UserID, string(in.AccountId())); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeAck(true), nil
|
|
}
|
|
}
|
|
|
|
// --- invitations ---
|
|
|
|
func invitationsListHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
res, err := backend.ListInvitations(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeInvitationList(res), nil
|
|
}
|
|
}
|
|
|
|
func invitationCreateHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsCreateInvitationRequest(req.Payload, 0)
|
|
params := backendclient.InvitationParams{
|
|
InviteeIDs: decodeInviteeIDs(in),
|
|
Variant: string(in.Variant()),
|
|
TurnTimeoutSecs: int(in.TurnTimeoutSecs()),
|
|
HintsAllowed: in.HintsAllowed(),
|
|
HintsPerPlayer: int(in.HintsPerPlayer()),
|
|
DropoutTiles: string(in.DropoutTiles()),
|
|
|
|
MultipleWordsPerTurn: in.MultipleWordsPerTurn(),
|
|
}
|
|
res, err := backend.CreateInvitation(ctx, req.UserID, params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeInvitation(res), nil
|
|
}
|
|
}
|
|
|
|
func invitationRespondHandler(backend *backendclient.Client, accept bool) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsInvitationActionRequest(req.Payload, 0)
|
|
res, err := backend.RespondInvitation(ctx, req.UserID, string(in.InvitationId()), accept)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeInvitation(res), nil
|
|
}
|
|
}
|
|
|
|
func invitationCancelHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsInvitationActionRequest(req.Payload, 0)
|
|
if err := backend.CancelInvitation(ctx, req.UserID, string(in.InvitationId())); err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeAck(true), nil
|
|
}
|
|
}
|
|
|
|
// --- profile, email, stats, gcg ---
|
|
|
|
func profileUpdateHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsUpdateProfileRequest(req.Payload, 0)
|
|
prefs := make([]string, in.VariantPreferencesLength())
|
|
for i := range prefs {
|
|
prefs[i] = string(in.VariantPreferences(i))
|
|
}
|
|
p := backendclient.ProfileResp{
|
|
DisplayName: string(in.DisplayName()),
|
|
PreferredLanguage: string(in.PreferredLanguage()),
|
|
TimeZone: string(in.TimeZone()),
|
|
AwayStart: string(in.AwayStart()),
|
|
AwayEnd: string(in.AwayEnd()),
|
|
BlockChat: in.BlockChat(),
|
|
BlockFriendRequests: in.BlockFriendRequests(),
|
|
NotificationsInAppOnly: in.NotificationsInAppOnly(),
|
|
VariantPreferences: prefs,
|
|
}
|
|
out, err := backend.UpdateProfile(ctx, req.UserID, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeProfile(out), nil
|
|
}
|
|
}
|
|
|
|
func statsHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
res, err := backend.Stats(ctx, req.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeStats(res), nil
|
|
}
|
|
}
|
|
|
|
func gcgHandler(backend *backendclient.Client) Handler {
|
|
return func(ctx context.Context, req Request) ([]byte, error) {
|
|
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
|
|
res, err := backend.ExportGCG(ctx, req.UserID, string(in.GameId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encodeGcg(res), nil
|
|
}
|
|
}
|
|
|
|
// decodeInviteeIDs reads the invitee id vector from a CreateInvitationRequest.
|
|
func decodeInviteeIDs(in *fb.CreateInvitationRequest) []string {
|
|
n := in.InviteeIdsLength()
|
|
out := make([]string, 0, n)
|
|
for i := range n {
|
|
out = append(out, string(in.InviteeIds(i)))
|
|
}
|
|
return out
|
|
}
|