diff --git a/backend/internal/server/dto.go b/backend/internal/server/dto.go index bf92a15..3f1b475 100644 --- a/backend/internal/server/dto.go +++ b/backend/internal/server/dto.go @@ -16,10 +16,11 @@ import ( // sessionResponse is the credential returned by every auth endpoint. type sessionResponse struct { - Token string `json:"token"` - UserID string `json:"user_id"` - IsGuest bool `json:"is_guest"` - DisplayName string `json:"display_name"` + Token string `json:"token"` + UserID string `json:"user_id"` + IsGuest bool `json:"is_guest"` + DisplayName string `json:"display_name"` + ServiceLanguage string `json:"service_language"` } // okResponse is a simple success acknowledgement. @@ -159,10 +160,11 @@ type errorBody struct { // sessionResponseFor builds the credential payload for a minted session. func sessionResponseFor(token string, acc account.Account) sessionResponse { return sessionResponse{ - Token: token, - UserID: acc.ID.String(), - IsGuest: acc.IsGuest, - DisplayName: acc.DisplayName, + Token: token, + UserID: acc.ID.String(), + IsGuest: acc.IsGuest, + DisplayName: acc.DisplayName, + ServiceLanguage: acc.ServiceLanguage, } } diff --git a/backend/internal/server/handlers_auth.go b/backend/internal/server/handlers_auth.go index 8a9b993..1fed516 100644 --- a/backend/internal/server/handlers_auth.go +++ b/backend/internal/server/handlers_auth.go @@ -48,6 +48,7 @@ func (s *Server) handleTelegramAuth(c *gin.Context) { s.abortErr(c, err) return } + acc.ServiceLanguage = req.ServiceLanguage // reflect this login's bot in the session response s.mintSession(c, acc) } diff --git a/gateway/internal/backendclient/api.go b/gateway/internal/backendclient/api.go index e1e034f..4c09e45 100644 --- a/gateway/internal/backendclient/api.go +++ b/gateway/internal/backendclient/api.go @@ -14,10 +14,11 @@ import ( // 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"` + Token string `json:"token"` + UserID string `json:"user_id"` + IsGuest bool `json:"is_guest"` + DisplayName string `json:"display_name"` + ServiceLanguage string `json:"service_language"` } // ProfileResp is an account's own profile. diff --git a/gateway/internal/transcode/encode.go b/gateway/internal/transcode/encode.go index 0a293f2..062fff4 100644 --- a/gateway/internal/transcode/encode.go +++ b/gateway/internal/transcode/encode.go @@ -36,6 +36,7 @@ func encodeSession(s backendclient.SessionResp, supportedLangs []string) []byte token := b.CreateString(s.Token) uid := b.CreateString(s.UserID) name := b.CreateString(s.DisplayName) + svcLang := b.CreateString(s.ServiceLanguage) langs := buildSupportedLanguagesVector(b, supportedLangs) fb.SessionStart(b) fb.SessionAddToken(b, token) @@ -43,6 +44,7 @@ func encodeSession(s backendclient.SessionResp, supportedLangs []string) []byte fb.SessionAddIsGuest(b, s.IsGuest) fb.SessionAddDisplayName(b, name) fb.SessionAddSupportedLanguages(b, langs) + fb.SessionAddServiceLanguage(b, svcLang) b.Finish(fb.SessionEnd(b)) return b.FinishedBytes() } diff --git a/gateway/internal/transcode/transcode_telegram_test.go b/gateway/internal/transcode/transcode_telegram_test.go index ff26edd..4849056 100644 --- a/gateway/internal/transcode/transcode_telegram_test.go +++ b/gateway/internal/transcode/transcode_telegram_test.go @@ -43,7 +43,7 @@ func TestTelegramAuthForwardsSeedFields(t *testing.T) { t.Errorf("unexpected path %q", r.URL.Path) } _ = json.NewDecoder(r.Body).Decode(&gotBody) - _, _ = w.Write([]byte(`{"token":"tok-tg","user_id":"u-tg","is_guest":false,"display_name":"Иван"}`)) + _, _ = w.Write([]byte(`{"token":"tok-tg","user_id":"u-tg","is_guest":false,"display_name":"Иван","service_language":"ru"}`)) }) defer cleanup() @@ -67,6 +67,11 @@ func TestTelegramAuthForwardsSeedFields(t *testing.T) { if got := sessionLanguages(sess); len(got) != 1 || got[0] != "ru" { t.Errorf("session supported_languages = %v, want [ru]", got) } + // The bot's service language rides the Session so the UI can build a share link to + // the same bot. + if got := string(sess.ServiceLanguage()); got != "ru" { + t.Errorf("session service_language = %q, want ru", got) + } // The validated launch fields are forwarded so the backend can seed a new account; // service_language is recorded to route the account's later out-of-app push. if gotBody["external_id"] != "42" || gotBody["language_code"] != "ru" || gotBody["first_name"] != "Иван" || gotBody["service_language"] != "ru" { diff --git a/pkg/fbs/scrabble.fbs b/pkg/fbs/scrabble.fbs index d451d79..b098ff0 100644 --- a/pkg/fbs/scrabble.fbs +++ b/pkg/fbs/scrabble.fbs @@ -123,6 +123,10 @@ table Session { is_guest:bool; display_name:string; supported_languages:[string]; + // service_language is the language tag (en/ru) of the Telegram bot the session was + // minted through; empty for a non-Telegram login. The UI uses it to build a share + // link that points at the same bot. + service_language:string; } // Ack is a simple success acknowledgement (e.g. an email-code request). diff --git a/pkg/fbs/scrabblefb/Session.go b/pkg/fbs/scrabblefb/Session.go index d1b2fc8..98a12fd 100644 --- a/pkg/fbs/scrabblefb/Session.go +++ b/pkg/fbs/scrabblefb/Session.go @@ -94,8 +94,16 @@ func (rcv *Session) SupportedLanguagesLength() int { return 0 } +func (rcv *Session) ServiceLanguage() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(14)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + func SessionStart(builder *flatbuffers.Builder) { - builder.StartObject(5) + builder.StartObject(6) } func SessionAddToken(builder *flatbuffers.Builder, token flatbuffers.UOffsetT) { builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(token), 0) @@ -115,6 +123,9 @@ func SessionAddSupportedLanguages(builder *flatbuffers.Builder, supportedLanguag func SessionStartSupportedLanguagesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { return builder.StartVector(4, numElems, 4) } +func SessionAddServiceLanguage(builder *flatbuffers.Builder, serviceLanguage flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(serviceLanguage), 0) +} func SessionEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } diff --git a/ui/src/gen/fbs/scrabblefb/session.ts b/ui/src/gen/fbs/scrabblefb/session.ts index bb33637..4b752fe 100644 --- a/ui/src/gen/fbs/scrabblefb/session.ts +++ b/ui/src/gen/fbs/scrabblefb/session.ts @@ -58,8 +58,15 @@ supportedLanguagesLength():number { return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; } +serviceLanguage():string|null +serviceLanguage(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +serviceLanguage(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 14); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + static startSession(builder:flatbuffers.Builder) { - builder.startObject(5); + builder.startObject(6); } static addToken(builder:flatbuffers.Builder, tokenOffset:flatbuffers.Offset) { @@ -94,18 +101,23 @@ static startSupportedLanguagesVector(builder:flatbuffers.Builder, numElems:numbe builder.startVector(4, numElems, 4); } +static addServiceLanguage(builder:flatbuffers.Builder, serviceLanguageOffset:flatbuffers.Offset) { + builder.addFieldOffset(5, serviceLanguageOffset, 0); +} + static endSession(builder:flatbuffers.Builder):flatbuffers.Offset { const offset = builder.endObject(); return offset; } -static createSession(builder:flatbuffers.Builder, tokenOffset:flatbuffers.Offset, userIdOffset:flatbuffers.Offset, isGuest:boolean, displayNameOffset:flatbuffers.Offset, supportedLanguagesOffset:flatbuffers.Offset):flatbuffers.Offset { +static createSession(builder:flatbuffers.Builder, tokenOffset:flatbuffers.Offset, userIdOffset:flatbuffers.Offset, isGuest:boolean, displayNameOffset:flatbuffers.Offset, supportedLanguagesOffset:flatbuffers.Offset, serviceLanguageOffset:flatbuffers.Offset):flatbuffers.Offset { Session.startSession(builder); Session.addToken(builder, tokenOffset); Session.addUserId(builder, userIdOffset); Session.addIsGuest(builder, isGuest); Session.addDisplayName(builder, displayNameOffset); Session.addSupportedLanguages(builder, supportedLanguagesOffset); + Session.addServiceLanguage(builder, serviceLanguageOffset); return Session.endSession(builder); } } diff --git a/ui/src/lib/codec.test.ts b/ui/src/lib/codec.test.ts index 5bd3516..f737ed9 100644 --- a/ui/src/lib/codec.test.ts +++ b/ui/src/lib/codec.test.ts @@ -171,6 +171,7 @@ describe('codec', () => { isGuest: true, displayName: 'Me', supportedLanguages: ['en', 'ru'], + serviceLanguage: '', }); }); @@ -311,7 +312,7 @@ describe('codec', () => { b.finish(fb.LinkResult.endLinkResult(b)); const r = decodeLinkResult(b.asUint8Array()); expect(r.status).toBe('merged'); - expect(r.session).toEqual({ token: 'tok-9', userId: 'a-1', isGuest: false, displayName: 'Kaya', supportedLanguages: [] }); + expect(r.session).toEqual({ token: 'tok-9', userId: 'a-1', isGuest: false, displayName: 'Kaya', supportedLanguages: [], serviceLanguage: '' }); }); it('decodes an Invitation with inviter and invitees', () => { diff --git a/ui/src/lib/codec.ts b/ui/src/lib/codec.ts index 4b8f916..d6537e0 100644 --- a/ui/src/lib/codec.ts +++ b/ui/src/lib/codec.ts @@ -292,6 +292,7 @@ function sessionFromTable(t: fb.Session): Session { isGuest: t.isGuest(), displayName: s(t.displayName()), supportedLanguages, + serviceLanguage: s(t.serviceLanguage()), }; } diff --git a/ui/src/lib/mock/data.ts b/ui/src/lib/mock/data.ts index 602ec13..f5af53c 100644 --- a/ui/src/lib/mock/data.ts +++ b/ui/src/lib/mock/data.ts @@ -25,6 +25,7 @@ export const SESSION: Session = { displayName: 'You', // Both languages by default, so the mock-driven UI offers every variant. supportedLanguages: ['en', 'ru'], + serviceLanguage: '', }; export const PROFILE: Profile = { diff --git a/ui/src/lib/model.ts b/ui/src/lib/model.ts index 5726802..d3dbfd4 100644 --- a/ui/src/lib/model.ts +++ b/ui/src/lib/model.ts @@ -234,6 +234,9 @@ export interface Session { // variants these languages support (en -> English; ru -> Russian + Эрудит). Empty // means ungated (all variants). supportedLanguages: string[]; + // serviceLanguage is the en/ru tag of the Telegram bot this session was minted + // through (empty for a non-Telegram login); used to share a link to the same bot. + serviceLanguage: string; } // LinkResult is the outcome of an account link/merge step. status is