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"`
IsGuest bool `json:"is_guest"`
DisplayName string `json:"display_name"`
ServiceLanguage string `json:"service_language"`
}
// okResponse is a simple success acknowledgement.
@@ -163,6 +164,7 @@ func sessionResponseFor(token string, acc account.Account) sessionResponse {
UserID: acc.ID.String(),
IsGuest: acc.IsGuest,
DisplayName: acc.DisplayName,
ServiceLanguage: acc.ServiceLanguage,
}
}
+1
View File
@@ -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)
}
+1
View File
@@ -18,6 +18,7 @@ type SessionResp struct {
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.
+2
View File
@@ -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()
}
@@ -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" {
+4
View File
@@ -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).
+12 -1
View File
@@ -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()
}
+14 -2
View File
@@ -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);
}
}
+2 -1
View File
@@ -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', () => {
+1
View File
@@ -292,6 +292,7 @@ function sessionFromTable(t: fb.Session): Session {
isGuest: t.isGuest(),
displayName: s(t.displayName()),
supportedLanguages,
serviceLanguage: s(t.serviceLanguage()),
};
}
+1
View File
@@ -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 = {
+3
View File
@@ -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