feat(session): carry the bot service_language on the Session wire

Thread the Telegram bot's service language (en/ru) from the session mint response
through the gateway into the FlatBuffers Session, so the UI knows which bot the
player signed in through. handleTelegramAuth refreshes the account's service
language onto the response before minting (it was set after the fetched copy).
Empty for a non-Telegram login.
This commit is contained in:
Ilia Denisov
2026-06-15 15:49:26 +02:00
parent 711fe6e594
commit 6679260d0a
12 changed files with 61 additions and 17 deletions
+2
View File
@@ -20,6 +20,7 @@ type sessionResponse struct {
UserID string `json:"user_id"` UserID string `json:"user_id"`
IsGuest bool `json:"is_guest"` IsGuest bool `json:"is_guest"`
DisplayName string `json:"display_name"` DisplayName string `json:"display_name"`
ServiceLanguage string `json:"service_language"`
} }
// okResponse is a simple success acknowledgement. // okResponse is a simple success acknowledgement.
@@ -163,6 +164,7 @@ func sessionResponseFor(token string, acc account.Account) sessionResponse {
UserID: acc.ID.String(), UserID: acc.ID.String(),
IsGuest: acc.IsGuest, IsGuest: acc.IsGuest,
DisplayName: acc.DisplayName, DisplayName: acc.DisplayName,
ServiceLanguage: acc.ServiceLanguage,
} }
} }
+1
View File
@@ -48,6 +48,7 @@ func (s *Server) handleTelegramAuth(c *gin.Context) {
s.abortErr(c, err) s.abortErr(c, err)
return return
} }
acc.ServiceLanguage = req.ServiceLanguage // reflect this login's bot in the session response
s.mintSession(c, acc) s.mintSession(c, acc)
} }
+1
View File
@@ -18,6 +18,7 @@ type SessionResp struct {
UserID string `json:"user_id"` UserID string `json:"user_id"`
IsGuest bool `json:"is_guest"` IsGuest bool `json:"is_guest"`
DisplayName string `json:"display_name"` DisplayName string `json:"display_name"`
ServiceLanguage string `json:"service_language"`
} }
// ProfileResp is an account's own profile. // ProfileResp is an account's own profile.
+2
View File
@@ -36,6 +36,7 @@ func encodeSession(s backendclient.SessionResp, supportedLangs []string) []byte
token := b.CreateString(s.Token) token := b.CreateString(s.Token)
uid := b.CreateString(s.UserID) uid := b.CreateString(s.UserID)
name := b.CreateString(s.DisplayName) name := b.CreateString(s.DisplayName)
svcLang := b.CreateString(s.ServiceLanguage)
langs := buildSupportedLanguagesVector(b, supportedLangs) langs := buildSupportedLanguagesVector(b, supportedLangs)
fb.SessionStart(b) fb.SessionStart(b)
fb.SessionAddToken(b, token) fb.SessionAddToken(b, token)
@@ -43,6 +44,7 @@ func encodeSession(s backendclient.SessionResp, supportedLangs []string) []byte
fb.SessionAddIsGuest(b, s.IsGuest) fb.SessionAddIsGuest(b, s.IsGuest)
fb.SessionAddDisplayName(b, name) fb.SessionAddDisplayName(b, name)
fb.SessionAddSupportedLanguages(b, langs) fb.SessionAddSupportedLanguages(b, langs)
fb.SessionAddServiceLanguage(b, svcLang)
b.Finish(fb.SessionEnd(b)) b.Finish(fb.SessionEnd(b))
return b.FinishedBytes() return b.FinishedBytes()
} }
@@ -43,7 +43,7 @@ func TestTelegramAuthForwardsSeedFields(t *testing.T) {
t.Errorf("unexpected path %q", r.URL.Path) t.Errorf("unexpected path %q", r.URL.Path)
} }
_ = json.NewDecoder(r.Body).Decode(&gotBody) _ = 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() defer cleanup()
@@ -67,6 +67,11 @@ func TestTelegramAuthForwardsSeedFields(t *testing.T) {
if got := sessionLanguages(sess); len(got) != 1 || got[0] != "ru" { if got := sessionLanguages(sess); len(got) != 1 || got[0] != "ru" {
t.Errorf("session supported_languages = %v, want [ru]", got) 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; // 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. // 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" { if gotBody["external_id"] != "42" || gotBody["language_code"] != "ru" || gotBody["first_name"] != "Иван" || gotBody["service_language"] != "ru" {
+4
View File
@@ -123,6 +123,10 @@ table Session {
is_guest:bool; is_guest:bool;
display_name:string; display_name:string;
supported_languages:[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). // Ack is a simple success acknowledgement (e.g. an email-code request).
+12 -1
View File
@@ -94,8 +94,16 @@ func (rcv *Session) SupportedLanguagesLength() int {
return 0 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) { func SessionStart(builder *flatbuffers.Builder) {
builder.StartObject(5) builder.StartObject(6)
} }
func SessionAddToken(builder *flatbuffers.Builder, token flatbuffers.UOffsetT) { func SessionAddToken(builder *flatbuffers.Builder, token flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(token), 0) 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 { func SessionStartSupportedLanguagesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4) 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 { func SessionEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject() return builder.EndObject()
} }
+14 -2
View File
@@ -58,8 +58,15 @@ supportedLanguagesLength():number {
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; 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) { static startSession(builder:flatbuffers.Builder) {
builder.startObject(5); builder.startObject(6);
} }
static addToken(builder:flatbuffers.Builder, tokenOffset:flatbuffers.Offset) { static addToken(builder:flatbuffers.Builder, tokenOffset:flatbuffers.Offset) {
@@ -94,18 +101,23 @@ static startSupportedLanguagesVector(builder:flatbuffers.Builder, numElems:numbe
builder.startVector(4, numElems, 4); 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 { static endSession(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject(); const offset = builder.endObject();
return offset; 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.startSession(builder);
Session.addToken(builder, tokenOffset); Session.addToken(builder, tokenOffset);
Session.addUserId(builder, userIdOffset); Session.addUserId(builder, userIdOffset);
Session.addIsGuest(builder, isGuest); Session.addIsGuest(builder, isGuest);
Session.addDisplayName(builder, displayNameOffset); Session.addDisplayName(builder, displayNameOffset);
Session.addSupportedLanguages(builder, supportedLanguagesOffset); Session.addSupportedLanguages(builder, supportedLanguagesOffset);
Session.addServiceLanguage(builder, serviceLanguageOffset);
return Session.endSession(builder); return Session.endSession(builder);
} }
} }
+2 -1
View File
@@ -171,6 +171,7 @@ describe('codec', () => {
isGuest: true, isGuest: true,
displayName: 'Me', displayName: 'Me',
supportedLanguages: ['en', 'ru'], supportedLanguages: ['en', 'ru'],
serviceLanguage: '',
}); });
}); });
@@ -311,7 +312,7 @@ describe('codec', () => {
b.finish(fb.LinkResult.endLinkResult(b)); b.finish(fb.LinkResult.endLinkResult(b));
const r = decodeLinkResult(b.asUint8Array()); const r = decodeLinkResult(b.asUint8Array());
expect(r.status).toBe('merged'); 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', () => { it('decodes an Invitation with inviter and invitees', () => {
+1
View File
@@ -292,6 +292,7 @@ function sessionFromTable(t: fb.Session): Session {
isGuest: t.isGuest(), isGuest: t.isGuest(),
displayName: s(t.displayName()), displayName: s(t.displayName()),
supportedLanguages, supportedLanguages,
serviceLanguage: s(t.serviceLanguage()),
}; };
} }
+1
View File
@@ -25,6 +25,7 @@ export const SESSION: Session = {
displayName: 'You', displayName: 'You',
// Both languages by default, so the mock-driven UI offers every variant. // Both languages by default, so the mock-driven UI offers every variant.
supportedLanguages: ['en', 'ru'], supportedLanguages: ['en', 'ru'],
serviceLanguage: '',
}; };
export const PROFILE: Profile = { export const PROFILE: Profile = {
+3
View File
@@ -234,6 +234,9 @@ export interface Session {
// variants these languages support (en -> English; ru -> Russian + Эрудит). Empty // variants these languages support (en -> English; ru -> Russian + Эрудит). Empty
// means ungated (all variants). // means ungated (all variants).
supportedLanguages: string[]; 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 // LinkResult is the outcome of an account link/merge step. status is