feat(profile): carry linked identities in the profile (email, telegram, vk)
Add email / telegram_linked / vk_linked to the Profile (fbs table + regenerated Go/TS bindings, gateway ProfileResp + encodeProfile, backend DTO, UI model + decode). They are filled outside the pure projection — Server.profileResponse now reads the account's identities (like the banner seam) — and will drive the profile's Add / Unlink / change-email controls.
This commit is contained in:
@@ -45,9 +45,34 @@ type bannerTimingsDTO struct {
|
||||
func (s *Server) profileResponse(ctx context.Context, acc account.Account) profileResponse {
|
||||
r := profileResponseFor(acc)
|
||||
r.Banner = s.bannerFor(ctx, acc)
|
||||
s.fillLinkedIdentities(ctx, &r, acc.ID)
|
||||
return r
|
||||
}
|
||||
|
||||
// fillLinkedIdentities sets the profile's confirmed email address and platform-linked
|
||||
// flags from the account's identities, so the client offers the right link / unlink /
|
||||
// change-email controls. A read failure leaves them zero (no controls), logged as a
|
||||
// warning so the profile response still succeeds.
|
||||
func (s *Server) fillLinkedIdentities(ctx context.Context, r *profileResponse, accountID uuid.UUID) {
|
||||
ids, err := s.accounts.Identities(ctx, accountID)
|
||||
if err != nil {
|
||||
s.log.Warn("profile: identities read failed", zap.String("account", accountID.String()), zap.Error(err))
|
||||
return
|
||||
}
|
||||
for _, id := range ids {
|
||||
switch id.Kind {
|
||||
case account.KindEmail:
|
||||
if id.Confirmed {
|
||||
r.Email = id.ExternalID
|
||||
}
|
||||
case account.KindTelegram:
|
||||
r.TelegramLinked = true
|
||||
case account.KindVK:
|
||||
r.VkLinked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bannerFor builds the advertising-banner block for the account's profile, or
|
||||
// nil when the ads service is not configured or the viewer is not eligible to
|
||||
// see a banner. The message language follows the account's bot (service)
|
||||
|
||||
@@ -56,6 +56,13 @@ type profileResponse struct {
|
||||
// see the banner (a free account with an empty hint wallet and without the
|
||||
// no_banner role), absent otherwise. See banner.go.
|
||||
Banner *bannerDTO `json:"banner,omitempty"`
|
||||
// Email is the account's confirmed email address ("" when none); TelegramLinked and
|
||||
// VkLinked report whether a platform identity is attached. They drive the profile's
|
||||
// link / unlink / change-email controls, and are filled outside the pure projection
|
||||
// (they read the account's identities). See Server.profileResponse.
|
||||
Email string `json:"email"`
|
||||
TelegramLinked bool `json:"telegram_linked"`
|
||||
VkLinked bool `json:"vk_linked"`
|
||||
}
|
||||
|
||||
// tileDTO is one placed (or to-place) tile.
|
||||
|
||||
@@ -37,6 +37,11 @@ type ProfileResp struct {
|
||||
// Banner is the advertising-banner block, present only for a viewer eligible to
|
||||
// see the banner. The gateway forwards it verbatim into the Profile payload.
|
||||
Banner *BannerResp `json:"banner,omitempty"`
|
||||
// Email is the confirmed email ("" when none); TelegramLinked/VkLinked report an
|
||||
// attached platform identity — they drive the profile's link/unlink/change controls.
|
||||
Email string `json:"email"`
|
||||
TelegramLinked bool `json:"telegram_linked"`
|
||||
VkLinked bool `json:"vk_linked"`
|
||||
}
|
||||
|
||||
// BannerResp is the advertising-banner block of an eligible viewer's profile: the
|
||||
|
||||
@@ -76,6 +76,7 @@ func encodeProfile(p backendclient.ProfileResp) []byte {
|
||||
tz := b.CreateString(p.TimeZone)
|
||||
awayStart := b.CreateString(p.AwayStart)
|
||||
awayEnd := b.CreateString(p.AwayEnd)
|
||||
email := b.CreateString(p.Email)
|
||||
// Build the banner table (and its children) before opening Profile: FlatBuffers
|
||||
// forbids a nested table while another is under construction.
|
||||
var banner flatbuffers.UOffsetT
|
||||
@@ -96,6 +97,9 @@ func encodeProfile(p backendclient.ProfileResp) []byte {
|
||||
fb.ProfileAddAwayEnd(b, awayEnd)
|
||||
fb.ProfileAddNotificationsInAppOnly(b, p.NotificationsInAppOnly)
|
||||
fb.ProfileAddVariantPreferences(b, prefs)
|
||||
fb.ProfileAddEmail(b, email)
|
||||
fb.ProfileAddTelegramLinked(b, p.TelegramLinked)
|
||||
fb.ProfileAddVkLinked(b, p.VkLinked)
|
||||
if p.Banner != nil {
|
||||
fb.ProfileAddBanner(b, banner)
|
||||
}
|
||||
|
||||
@@ -224,6 +224,12 @@ table Profile {
|
||||
// variant_preferences is the set of game variants the player allows themselves to be
|
||||
// matched into (engine.Variant labels), Erudit-first; the New Game picker is gated by it.
|
||||
variant_preferences:[string];
|
||||
// email is the account's confirmed email address ("" when none); telegram_linked and
|
||||
// vk_linked report whether a platform identity is attached. They drive the profile's
|
||||
// link / unlink / change-email controls (all added trailing — backward-compatible).
|
||||
email:string;
|
||||
telegram_linked:bool;
|
||||
vk_linked:bool;
|
||||
}
|
||||
|
||||
// BlockStatus reports the caller's current manual block. The UI fetches it after any operation
|
||||
|
||||
@@ -179,8 +179,40 @@ func (rcv *Profile) VariantPreferencesLength() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (rcv *Profile) Email() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(30))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *Profile) TelegramLinked() bool {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(32))
|
||||
if o != 0 {
|
||||
return rcv._tab.GetBool(o + rcv._tab.Pos)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rcv *Profile) MutateTelegramLinked(n bool) bool {
|
||||
return rcv._tab.MutateBoolSlot(32, n)
|
||||
}
|
||||
|
||||
func (rcv *Profile) VkLinked() bool {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(34))
|
||||
if o != 0 {
|
||||
return rcv._tab.GetBool(o + rcv._tab.Pos)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rcv *Profile) MutateVkLinked(n bool) bool {
|
||||
return rcv._tab.MutateBoolSlot(34, n)
|
||||
}
|
||||
|
||||
func ProfileStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(13)
|
||||
builder.StartObject(16)
|
||||
}
|
||||
func ProfileAddUserId(builder *flatbuffers.Builder, userId flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(userId), 0)
|
||||
@@ -224,6 +256,15 @@ func ProfileAddVariantPreferences(builder *flatbuffers.Builder, variantPreferenc
|
||||
func ProfileStartVariantPreferencesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
|
||||
return builder.StartVector(4, numElems, 4)
|
||||
}
|
||||
func ProfileAddEmail(builder *flatbuffers.Builder, email flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(13, flatbuffers.UOffsetT(email), 0)
|
||||
}
|
||||
func ProfileAddTelegramLinked(builder *flatbuffers.Builder, telegramLinked bool) {
|
||||
builder.PrependBoolSlot(14, telegramLinked, false)
|
||||
}
|
||||
func ProfileAddVkLinked(builder *flatbuffers.Builder, vkLinked bool) {
|
||||
builder.PrependBoolSlot(15, vkLinked, false)
|
||||
}
|
||||
func ProfileEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
|
||||
@@ -107,8 +107,25 @@ variantPreferencesLength():number {
|
||||
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
|
||||
}
|
||||
|
||||
email():string|null
|
||||
email(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
email(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 30);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
telegramLinked():boolean {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 32);
|
||||
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
|
||||
}
|
||||
|
||||
vkLinked():boolean {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 34);
|
||||
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
|
||||
}
|
||||
|
||||
static startProfile(builder:flatbuffers.Builder) {
|
||||
builder.startObject(13);
|
||||
builder.startObject(16);
|
||||
}
|
||||
|
||||
static addUserId(builder:flatbuffers.Builder, userIdOffset:flatbuffers.Offset) {
|
||||
@@ -175,6 +192,18 @@ static startVariantPreferencesVector(builder:flatbuffers.Builder, numElems:numbe
|
||||
builder.startVector(4, numElems, 4);
|
||||
}
|
||||
|
||||
static addEmail(builder:flatbuffers.Builder, emailOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(13, emailOffset, 0);
|
||||
}
|
||||
|
||||
static addTelegramLinked(builder:flatbuffers.Builder, telegramLinked:boolean) {
|
||||
builder.addFieldInt8(14, +telegramLinked, +false);
|
||||
}
|
||||
|
||||
static addVkLinked(builder:flatbuffers.Builder, vkLinked:boolean) {
|
||||
builder.addFieldInt8(15, +vkLinked, +false);
|
||||
}
|
||||
|
||||
static endProfile(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
|
||||
@@ -354,6 +354,9 @@ export function decodeProfile(buf: Uint8Array): Profile {
|
||||
notificationsInAppOnly: p.notificationsInAppOnly(),
|
||||
variantPreferences: decodeVariantPreferences(p),
|
||||
banner: decodeBanner(p),
|
||||
email: s(p.email()),
|
||||
telegramLinked: p.telegramLinked(),
|
||||
vkLinked: p.vkLinked(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ export const PROFILE: Profile = {
|
||||
notificationsInAppOnly: true,
|
||||
// Every variant enabled, so the mock-driven UI offers the full New Game picker.
|
||||
variantPreferences: ['erudit_ru', 'scrabble_ru', 'scrabble_en'],
|
||||
email: 'you@example.com',
|
||||
telegramLinked: false,
|
||||
vkLinked: false,
|
||||
};
|
||||
|
||||
// Seed social/account data for the mock (pnpm start + Playwright). The mock profile
|
||||
|
||||
@@ -156,6 +156,11 @@ export interface Profile {
|
||||
variantPreferences: Variant[];
|
||||
/** The advertising-banner block, present only for a viewer eligible to see it. */
|
||||
banner?: Banner;
|
||||
/** The account's confirmed email address ("" when none). */
|
||||
email: string;
|
||||
/** Whether a Telegram / VK identity is attached — drives the Add / Unlink controls. */
|
||||
telegramLinked: boolean;
|
||||
vkLinked: boolean;
|
||||
}
|
||||
|
||||
/** Banner is the advertising-banner block of an eligible viewer's profile. */
|
||||
|
||||
Reference in New Issue
Block a user