Stage 11: account linking & merge (email + Telegram Login Widget)
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 11s
Tests · UI / test (push) Successful in 20s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s

Link an email (confirm-code) or Telegram (web Login Widget) to the current
account; if the identity already has its own account, merge the two into the
one in use (the current account is primary, except a guest initiator whose
durable counterpart wins). The merge runs in one transaction
(internal/accountmerge): stats + hint wallet summed, paid_account ORed,
identities/games/chat/complaints transferred, friends/blocks de-duplicated,
the secondary kept as a merged_into tombstone so a shared finished game's
no-cascade FKs hold; a shared active game blocks the merge.

- migration 00009: accounts.paid_account, merged_into, merged_at (+ jetgen)
- internal/link orchestrator; session.RevokeAllForAccount on merge
- connector ValidateLoginWidget RPC + loginwidget HMAC validator
- edge ops link.email.request/confirm/merge, link.telegram.confirm/merge;
  supersedes the Stage 8 email.bind.* surface (request never reveals 'taken'
  before the code is verified, so a probe cannot enumerate addresses)
- UI Profile link section + irreversible-merge dialog; Telegram web sign-in
- focused regression tests (merge core, guest inversion, active-game refusal,
  finished-shared-game kept), gateway transcode + connector + UI codec/e2e
- docs: PLAN, ARCHITECTURE 3/4/9, FUNCTIONAL(+ru), module READMEs
This commit is contained in:
Ilia Denisov
2026-06-04 11:15:14 +02:00
parent 3a640a17a4
commit 52f898ca6f
68 changed files with 3331 additions and 369 deletions
+2
View File
@@ -16,6 +16,8 @@ fbs/scrabblefb/ # committed generated Go for the schema
- **`proto/push/v1`** is the single gRPC server-stream the backend exposes and
the gateway subscribes to (`Event{user_id, kind, payload, event_id}`); the
`payload` is an opaque FlatBuffers body the gateway forwards verbatim.
- **`proto/telegram/v1`** is the Telegram connector's RPC contract (Stage 9; Stage 11
added `ValidateLoginWidget` for the web Login Widget sign-in).
- **`fbs`** holds the client↔gateway request/response and event payloads as
FlatBuffers tables. The backend encodes the push payloads from these types; the
gateway transcodes the rest to and from the backend's JSON; the UI generates
+29 -5
View File
@@ -272,18 +272,42 @@ table UpdateProfileRequest {
notifications_in_app_only:bool = true;
}
// EmailBindRequest asks the backend to send a confirm-code binding email to the
// caller's account.
table EmailBindRequest {
// --- account linking & merge (Stage 11, authenticated) ---
// LinkEmailRequest mails a confirm-code to email for a later link or merge. The
// code is always sent (no pre-send "taken" signal), so a probe cannot enumerate
// registered addresses.
table LinkEmailRequest {
email:string;
}
// EmailConfirmRequest verifies the code and binds the email (returns Profile).
table EmailConfirmRequest {
// LinkEmailConfirm carries the email and its confirm code, for both the confirm
// (preview) and the explicit merge step.
table LinkEmailConfirm {
email:string;
code:string;
}
// LinkTelegramRequest carries Telegram Login Widget data (a URL query string) for
// attaching a Telegram identity to the current account.
table LinkTelegramRequest {
data:string;
}
// LinkResult is the unified result of a confirm or merge step. status is "linked"
// (bound to the caller), "merge_required" (the identity belongs to another account —
// the secondary_* fields summarise it for the irreversible confirmation), or
// "merged" (done). session is present only when the active account switched (a guest
// initiator whose durable counterpart won) — the client adopts it.
table LinkResult {
status:string;
secondary_user_id:string;
secondary_display_name:string;
secondary_games:int;
secondary_friends:int;
session:Session;
}
// StatsView is a durable account's lifetime statistics (games-played and win-rate
// are derived client-side).
table StatsView {
-71
View File
@@ -1,71 +0,0 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type EmailConfirmRequest struct {
_tab flatbuffers.Table
}
func GetRootAsEmailConfirmRequest(buf []byte, offset flatbuffers.UOffsetT) *EmailConfirmRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &EmailConfirmRequest{}
x.Init(buf, n+offset)
return x
}
func FinishEmailConfirmRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsEmailConfirmRequest(buf []byte, offset flatbuffers.UOffsetT) *EmailConfirmRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &EmailConfirmRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedEmailConfirmRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *EmailConfirmRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *EmailConfirmRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *EmailConfirmRequest) Email() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *EmailConfirmRequest) Code() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func EmailConfirmRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func EmailConfirmRequestAddEmail(builder *flatbuffers.Builder, email flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(email), 0)
}
func EmailConfirmRequestAddCode(builder *flatbuffers.Builder, code flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(code), 0)
}
func EmailConfirmRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+71
View File
@@ -0,0 +1,71 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type LinkEmailConfirm struct {
_tab flatbuffers.Table
}
func GetRootAsLinkEmailConfirm(buf []byte, offset flatbuffers.UOffsetT) *LinkEmailConfirm {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &LinkEmailConfirm{}
x.Init(buf, n+offset)
return x
}
func FinishLinkEmailConfirmBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsLinkEmailConfirm(buf []byte, offset flatbuffers.UOffsetT) *LinkEmailConfirm {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &LinkEmailConfirm{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedLinkEmailConfirmBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *LinkEmailConfirm) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *LinkEmailConfirm) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *LinkEmailConfirm) Email() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *LinkEmailConfirm) Code() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func LinkEmailConfirmStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func LinkEmailConfirmAddEmail(builder *flatbuffers.Builder, email flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(email), 0)
}
func LinkEmailConfirmAddCode(builder *flatbuffers.Builder, code flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(code), 0)
}
func LinkEmailConfirmEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
@@ -6,42 +6,42 @@ import (
flatbuffers "github.com/google/flatbuffers/go"
)
type EmailBindRequest struct {
type LinkEmailRequest struct {
_tab flatbuffers.Table
}
func GetRootAsEmailBindRequest(buf []byte, offset flatbuffers.UOffsetT) *EmailBindRequest {
func GetRootAsLinkEmailRequest(buf []byte, offset flatbuffers.UOffsetT) *LinkEmailRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &EmailBindRequest{}
x := &LinkEmailRequest{}
x.Init(buf, n+offset)
return x
}
func FinishEmailBindRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
func FinishLinkEmailRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsEmailBindRequest(buf []byte, offset flatbuffers.UOffsetT) *EmailBindRequest {
func GetSizePrefixedRootAsLinkEmailRequest(buf []byte, offset flatbuffers.UOffsetT) *LinkEmailRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &EmailBindRequest{}
x := &LinkEmailRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedEmailBindRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
func FinishSizePrefixedLinkEmailRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *EmailBindRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
func (rcv *LinkEmailRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *EmailBindRequest) Table() flatbuffers.Table {
func (rcv *LinkEmailRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *EmailBindRequest) Email() []byte {
func (rcv *LinkEmailRequest) Email() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
@@ -49,12 +49,12 @@ func (rcv *EmailBindRequest) Email() []byte {
return nil
}
func EmailBindRequestStart(builder *flatbuffers.Builder) {
func LinkEmailRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func EmailBindRequestAddEmail(builder *flatbuffers.Builder, email flatbuffers.UOffsetT) {
func LinkEmailRequestAddEmail(builder *flatbuffers.Builder, email flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(email), 0)
}
func EmailBindRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
func LinkEmailRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+128
View File
@@ -0,0 +1,128 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type LinkResult struct {
_tab flatbuffers.Table
}
func GetRootAsLinkResult(buf []byte, offset flatbuffers.UOffsetT) *LinkResult {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &LinkResult{}
x.Init(buf, n+offset)
return x
}
func FinishLinkResultBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsLinkResult(buf []byte, offset flatbuffers.UOffsetT) *LinkResult {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &LinkResult{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedLinkResultBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *LinkResult) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *LinkResult) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *LinkResult) Status() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *LinkResult) SecondaryUserId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *LinkResult) SecondaryDisplayName() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *LinkResult) SecondaryGames() int32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.GetInt32(o + rcv._tab.Pos)
}
return 0
}
func (rcv *LinkResult) MutateSecondaryGames(n int32) bool {
return rcv._tab.MutateInt32Slot(10, n)
}
func (rcv *LinkResult) SecondaryFriends() int32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
return rcv._tab.GetInt32(o + rcv._tab.Pos)
}
return 0
}
func (rcv *LinkResult) MutateSecondaryFriends(n int32) bool {
return rcv._tab.MutateInt32Slot(12, n)
}
func (rcv *LinkResult) Session(obj *Session) *Session {
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
if o != 0 {
x := rcv._tab.Indirect(o + rcv._tab.Pos)
if obj == nil {
obj = new(Session)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func LinkResultStart(builder *flatbuffers.Builder) {
builder.StartObject(6)
}
func LinkResultAddStatus(builder *flatbuffers.Builder, status flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(status), 0)
}
func LinkResultAddSecondaryUserId(builder *flatbuffers.Builder, secondaryUserId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(secondaryUserId), 0)
}
func LinkResultAddSecondaryDisplayName(builder *flatbuffers.Builder, secondaryDisplayName flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(secondaryDisplayName), 0)
}
func LinkResultAddSecondaryGames(builder *flatbuffers.Builder, secondaryGames int32) {
builder.PrependInt32Slot(3, secondaryGames, 0)
}
func LinkResultAddSecondaryFriends(builder *flatbuffers.Builder, secondaryFriends int32) {
builder.PrependInt32Slot(4, secondaryFriends, 0)
}
func LinkResultAddSession(builder *flatbuffers.Builder, session flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(session), 0)
}
func LinkResultEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+60
View File
@@ -0,0 +1,60 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type LinkTelegramRequest struct {
_tab flatbuffers.Table
}
func GetRootAsLinkTelegramRequest(buf []byte, offset flatbuffers.UOffsetT) *LinkTelegramRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &LinkTelegramRequest{}
x.Init(buf, n+offset)
return x
}
func FinishLinkTelegramRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsLinkTelegramRequest(buf []byte, offset flatbuffers.UOffsetT) *LinkTelegramRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &LinkTelegramRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedLinkTelegramRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *LinkTelegramRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *LinkTelegramRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *LinkTelegramRequest) Data() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func LinkTelegramRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func LinkTelegramRequestAddData(builder *flatbuffers.Builder, data flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(data), 0)
}
func LinkTelegramRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+158 -36
View File
@@ -148,6 +148,115 @@ func (x *ValidateInitDataResponse) GetLanguageCode() string {
return ""
}
// ValidateLoginWidgetRequest carries the Login Widget result serialized as a URL
// query string (the widget fields plus the hash, e.g. "auth_date=...&id=...&hash=...").
type ValidateLoginWidgetRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Data string `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ValidateLoginWidgetRequest) Reset() {
*x = ValidateLoginWidgetRequest{}
mi := &file_telegram_v1_telegram_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ValidateLoginWidgetRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ValidateLoginWidgetRequest) ProtoMessage() {}
func (x *ValidateLoginWidgetRequest) ProtoReflect() protoreflect.Message {
mi := &file_telegram_v1_telegram_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ValidateLoginWidgetRequest.ProtoReflect.Descriptor instead.
func (*ValidateLoginWidgetRequest) Descriptor() ([]byte, []int) {
return file_telegram_v1_telegram_proto_rawDescGZIP(), []int{2}
}
func (x *ValidateLoginWidgetRequest) GetData() string {
if x != nil {
return x.Data
}
return ""
}
// ValidateLoginWidgetResponse is the validated identity. external_id is the
// Telegram user id used as the identities external_id. The Login Widget carries no
// language_code (unlike Mini App initData).
type ValidateLoginWidgetResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ExternalId string `protobuf:"bytes,1,opt,name=external_id,json=externalId,proto3" json:"external_id,omitempty"`
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
FirstName string `protobuf:"bytes,3,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ValidateLoginWidgetResponse) Reset() {
*x = ValidateLoginWidgetResponse{}
mi := &file_telegram_v1_telegram_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ValidateLoginWidgetResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ValidateLoginWidgetResponse) ProtoMessage() {}
func (x *ValidateLoginWidgetResponse) ProtoReflect() protoreflect.Message {
mi := &file_telegram_v1_telegram_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ValidateLoginWidgetResponse.ProtoReflect.Descriptor instead.
func (*ValidateLoginWidgetResponse) Descriptor() ([]byte, []int) {
return file_telegram_v1_telegram_proto_rawDescGZIP(), []int{3}
}
func (x *ValidateLoginWidgetResponse) GetExternalId() string {
if x != nil {
return x.ExternalId
}
return ""
}
func (x *ValidateLoginWidgetResponse) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *ValidateLoginWidgetResponse) GetFirstName() string {
if x != nil {
return x.FirstName
}
return ""
}
// NotifyRequest addresses a push event to one recipient. kind is the backend push
// catalog kind (your_turn, nudge, match_found, notify); payload is the FlatBuffers
// scrabblefb.* body for that kind; language (en/ru) selects the message template.
@@ -163,7 +272,7 @@ type NotifyRequest struct {
func (x *NotifyRequest) Reset() {
*x = NotifyRequest{}
mi := &file_telegram_v1_telegram_proto_msgTypes[2]
mi := &file_telegram_v1_telegram_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -175,7 +284,7 @@ func (x *NotifyRequest) String() string {
func (*NotifyRequest) ProtoMessage() {}
func (x *NotifyRequest) ProtoReflect() protoreflect.Message {
mi := &file_telegram_v1_telegram_proto_msgTypes[2]
mi := &file_telegram_v1_telegram_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -188,7 +297,7 @@ func (x *NotifyRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use NotifyRequest.ProtoReflect.Descriptor instead.
func (*NotifyRequest) Descriptor() ([]byte, []int) {
return file_telegram_v1_telegram_proto_rawDescGZIP(), []int{2}
return file_telegram_v1_telegram_proto_rawDescGZIP(), []int{4}
}
func (x *NotifyRequest) GetExternalId() string {
@@ -230,7 +339,7 @@ type NotifyResponse struct {
func (x *NotifyResponse) Reset() {
*x = NotifyResponse{}
mi := &file_telegram_v1_telegram_proto_msgTypes[3]
mi := &file_telegram_v1_telegram_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -242,7 +351,7 @@ func (x *NotifyResponse) String() string {
func (*NotifyResponse) ProtoMessage() {}
func (x *NotifyResponse) ProtoReflect() protoreflect.Message {
mi := &file_telegram_v1_telegram_proto_msgTypes[3]
mi := &file_telegram_v1_telegram_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -255,7 +364,7 @@ func (x *NotifyResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use NotifyResponse.ProtoReflect.Descriptor instead.
func (*NotifyResponse) Descriptor() ([]byte, []int) {
return file_telegram_v1_telegram_proto_rawDescGZIP(), []int{3}
return file_telegram_v1_telegram_proto_rawDescGZIP(), []int{5}
}
func (x *NotifyResponse) GetDelivered() bool {
@@ -276,7 +385,7 @@ type SendToUserRequest struct {
func (x *SendToUserRequest) Reset() {
*x = SendToUserRequest{}
mi := &file_telegram_v1_telegram_proto_msgTypes[4]
mi := &file_telegram_v1_telegram_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -288,7 +397,7 @@ func (x *SendToUserRequest) String() string {
func (*SendToUserRequest) ProtoMessage() {}
func (x *SendToUserRequest) ProtoReflect() protoreflect.Message {
mi := &file_telegram_v1_telegram_proto_msgTypes[4]
mi := &file_telegram_v1_telegram_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -301,7 +410,7 @@ func (x *SendToUserRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SendToUserRequest.ProtoReflect.Descriptor instead.
func (*SendToUserRequest) Descriptor() ([]byte, []int) {
return file_telegram_v1_telegram_proto_rawDescGZIP(), []int{4}
return file_telegram_v1_telegram_proto_rawDescGZIP(), []int{6}
}
func (x *SendToUserRequest) GetExternalId() string {
@@ -328,7 +437,7 @@ type SendToGameChannelRequest struct {
func (x *SendToGameChannelRequest) Reset() {
*x = SendToGameChannelRequest{}
mi := &file_telegram_v1_telegram_proto_msgTypes[5]
mi := &file_telegram_v1_telegram_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -340,7 +449,7 @@ func (x *SendToGameChannelRequest) String() string {
func (*SendToGameChannelRequest) ProtoMessage() {}
func (x *SendToGameChannelRequest) ProtoReflect() protoreflect.Message {
mi := &file_telegram_v1_telegram_proto_msgTypes[5]
mi := &file_telegram_v1_telegram_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -353,7 +462,7 @@ func (x *SendToGameChannelRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SendToGameChannelRequest.ProtoReflect.Descriptor instead.
func (*SendToGameChannelRequest) Descriptor() ([]byte, []int) {
return file_telegram_v1_telegram_proto_rawDescGZIP(), []int{5}
return file_telegram_v1_telegram_proto_rawDescGZIP(), []int{7}
}
func (x *SendToGameChannelRequest) GetText() string {
@@ -373,7 +482,7 @@ type SendResponse struct {
func (x *SendResponse) Reset() {
*x = SendResponse{}
mi := &file_telegram_v1_telegram_proto_msgTypes[6]
mi := &file_telegram_v1_telegram_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -385,7 +494,7 @@ func (x *SendResponse) String() string {
func (*SendResponse) ProtoMessage() {}
func (x *SendResponse) ProtoReflect() protoreflect.Message {
mi := &file_telegram_v1_telegram_proto_msgTypes[6]
mi := &file_telegram_v1_telegram_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -398,7 +507,7 @@ func (x *SendResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SendResponse.ProtoReflect.Descriptor instead.
func (*SendResponse) Descriptor() ([]byte, []int) {
return file_telegram_v1_telegram_proto_rawDescGZIP(), []int{6}
return file_telegram_v1_telegram_proto_rawDescGZIP(), []int{8}
}
func (x *SendResponse) GetDelivered() bool {
@@ -421,7 +530,15 @@ const file_telegram_v1_telegram_proto_rawDesc = "" +
"\busername\x18\x02 \x01(\tR\busername\x12\x1d\n" +
"\n" +
"first_name\x18\x03 \x01(\tR\tfirstName\x12#\n" +
"\rlanguage_code\x18\x04 \x01(\tR\flanguageCode\"z\n" +
"\rlanguage_code\x18\x04 \x01(\tR\flanguageCode\"0\n" +
"\x1aValidateLoginWidgetRequest\x12\x12\n" +
"\x04data\x18\x01 \x01(\tR\x04data\"y\n" +
"\x1bValidateLoginWidgetResponse\x12\x1f\n" +
"\vexternal_id\x18\x01 \x01(\tR\n" +
"externalId\x12\x1a\n" +
"\busername\x18\x02 \x01(\tR\busername\x12\x1d\n" +
"\n" +
"first_name\x18\x03 \x01(\tR\tfirstName\"z\n" +
"\rNotifyRequest\x12\x1f\n" +
"\vexternal_id\x18\x01 \x01(\tR\n" +
"externalId\x12\x12\n" +
@@ -437,9 +554,10 @@ const file_telegram_v1_telegram_proto_rawDesc = "" +
"\x18SendToGameChannelRequest\x12\x12\n" +
"\x04text\x18\x01 \x01(\tR\x04text\",\n" +
"\fSendResponse\x12\x1c\n" +
"\tdelivered\x18\x01 \x01(\bR\tdelivered2\x96\x03\n" +
"\tdelivered\x18\x01 \x01(\bR\tdelivered2\x92\x04\n" +
"\bTelegram\x12q\n" +
"\x10ValidateInitData\x12-.scrabble.telegram.v1.ValidateInitDataRequest\x1a..scrabble.telegram.v1.ValidateInitDataResponse\x12S\n" +
"\x10ValidateInitData\x12-.scrabble.telegram.v1.ValidateInitDataRequest\x1a..scrabble.telegram.v1.ValidateInitDataResponse\x12z\n" +
"\x13ValidateLoginWidget\x120.scrabble.telegram.v1.ValidateLoginWidgetRequest\x1a1.scrabble.telegram.v1.ValidateLoginWidgetResponse\x12S\n" +
"\x06Notify\x12#.scrabble.telegram.v1.NotifyRequest\x1a$.scrabble.telegram.v1.NotifyResponse\x12Y\n" +
"\n" +
"SendToUser\x12'.scrabble.telegram.v1.SendToUserRequest\x1a\".scrabble.telegram.v1.SendResponse\x12g\n" +
@@ -457,27 +575,31 @@ func file_telegram_v1_telegram_proto_rawDescGZIP() []byte {
return file_telegram_v1_telegram_proto_rawDescData
}
var file_telegram_v1_telegram_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_telegram_v1_telegram_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_telegram_v1_telegram_proto_goTypes = []any{
(*ValidateInitDataRequest)(nil), // 0: scrabble.telegram.v1.ValidateInitDataRequest
(*ValidateInitDataResponse)(nil), // 1: scrabble.telegram.v1.ValidateInitDataResponse
(*NotifyRequest)(nil), // 2: scrabble.telegram.v1.NotifyRequest
(*NotifyResponse)(nil), // 3: scrabble.telegram.v1.NotifyResponse
(*SendToUserRequest)(nil), // 4: scrabble.telegram.v1.SendToUserRequest
(*SendToGameChannelRequest)(nil), // 5: scrabble.telegram.v1.SendToGameChannelRequest
(*SendResponse)(nil), // 6: scrabble.telegram.v1.SendResponse
(*ValidateInitDataRequest)(nil), // 0: scrabble.telegram.v1.ValidateInitDataRequest
(*ValidateInitDataResponse)(nil), // 1: scrabble.telegram.v1.ValidateInitDataResponse
(*ValidateLoginWidgetRequest)(nil), // 2: scrabble.telegram.v1.ValidateLoginWidgetRequest
(*ValidateLoginWidgetResponse)(nil), // 3: scrabble.telegram.v1.ValidateLoginWidgetResponse
(*NotifyRequest)(nil), // 4: scrabble.telegram.v1.NotifyRequest
(*NotifyResponse)(nil), // 5: scrabble.telegram.v1.NotifyResponse
(*SendToUserRequest)(nil), // 6: scrabble.telegram.v1.SendToUserRequest
(*SendToGameChannelRequest)(nil), // 7: scrabble.telegram.v1.SendToGameChannelRequest
(*SendResponse)(nil), // 8: scrabble.telegram.v1.SendResponse
}
var file_telegram_v1_telegram_proto_depIdxs = []int32{
0, // 0: scrabble.telegram.v1.Telegram.ValidateInitData:input_type -> scrabble.telegram.v1.ValidateInitDataRequest
2, // 1: scrabble.telegram.v1.Telegram.Notify:input_type -> scrabble.telegram.v1.NotifyRequest
4, // 2: scrabble.telegram.v1.Telegram.SendToUser:input_type -> scrabble.telegram.v1.SendToUserRequest
5, // 3: scrabble.telegram.v1.Telegram.SendToGameChannel:input_type -> scrabble.telegram.v1.SendToGameChannelRequest
1, // 4: scrabble.telegram.v1.Telegram.ValidateInitData:output_type -> scrabble.telegram.v1.ValidateInitDataResponse
3, // 5: scrabble.telegram.v1.Telegram.Notify:output_type -> scrabble.telegram.v1.NotifyResponse
6, // 6: scrabble.telegram.v1.Telegram.SendToUser:output_type -> scrabble.telegram.v1.SendResponse
6, // 7: scrabble.telegram.v1.Telegram.SendToGameChannel:output_type -> scrabble.telegram.v1.SendResponse
4, // [4:8] is the sub-list for method output_type
0, // [0:4] is the sub-list for method input_type
2, // 1: scrabble.telegram.v1.Telegram.ValidateLoginWidget:input_type -> scrabble.telegram.v1.ValidateLoginWidgetRequest
4, // 2: scrabble.telegram.v1.Telegram.Notify:input_type -> scrabble.telegram.v1.NotifyRequest
6, // 3: scrabble.telegram.v1.Telegram.SendToUser:input_type -> scrabble.telegram.v1.SendToUserRequest
7, // 4: scrabble.telegram.v1.Telegram.SendToGameChannel:input_type -> scrabble.telegram.v1.SendToGameChannelRequest
1, // 5: scrabble.telegram.v1.Telegram.ValidateInitData:output_type -> scrabble.telegram.v1.ValidateInitDataResponse
3, // 6: scrabble.telegram.v1.Telegram.ValidateLoginWidget:output_type -> scrabble.telegram.v1.ValidateLoginWidgetResponse
5, // 7: scrabble.telegram.v1.Telegram.Notify:output_type -> scrabble.telegram.v1.NotifyResponse
8, // 8: scrabble.telegram.v1.Telegram.SendToUser:output_type -> scrabble.telegram.v1.SendResponse
8, // 9: scrabble.telegram.v1.Telegram.SendToGameChannel:output_type -> scrabble.telegram.v1.SendResponse
5, // [5:10] is the sub-list for method output_type
0, // [0:5] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
@@ -494,7 +616,7 @@ func file_telegram_v1_telegram_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_telegram_v1_telegram_proto_rawDesc), len(file_telegram_v1_telegram_proto_rawDesc)),
NumEnums: 0,
NumMessages: 7,
NumMessages: 9,
NumExtensions: 0,
NumServices: 1,
},
+20
View File
@@ -20,6 +20,11 @@ service Telegram {
// the authenticated user. The gateway calls it during the auth.telegram edge
// operation, then provisions the session through the backend internal API.
rpc ValidateInitData(ValidateInitDataRequest) returns (ValidateInitDataResponse);
// ValidateLoginWidget verifies Telegram Login Widget authorization data (the web
// sign-in flow, HMAC under SHA-256(bot_token)) and returns the authenticated
// user. The gateway calls it during the link.telegram edge operation to attach a
// Telegram identity to an existing account (Stage 11).
rpc ValidateLoginWidget(ValidateLoginWidgetRequest) returns (ValidateLoginWidgetResponse);
// Notify delivers an out-of-app notification for a backend push event. The
// gateway calls it only for a recipient with no live in-app stream (so the
// platform push never duplicates in-app delivery). The connector renders a
@@ -50,6 +55,21 @@ message ValidateInitDataResponse {
string language_code = 4;
}
// ValidateLoginWidgetRequest carries the Login Widget result serialized as a URL
// query string (the widget fields plus the hash, e.g. "auth_date=...&id=...&hash=...").
message ValidateLoginWidgetRequest {
string data = 1;
}
// ValidateLoginWidgetResponse is the validated identity. external_id is the
// Telegram user id used as the identities external_id. The Login Widget carries no
// language_code (unlike Mini App initData).
message ValidateLoginWidgetResponse {
string external_id = 1;
string username = 2;
string first_name = 3;
}
// NotifyRequest addresses a push event to one recipient. kind is the backend push
// catalog kind (your_turn, nudge, match_found, notify); payload is the FlatBuffers
// scrabblefb.* body for that kind; language (en/ru) selects the message template.
+50 -4
View File
@@ -30,10 +30,11 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
Telegram_ValidateInitData_FullMethodName = "/scrabble.telegram.v1.Telegram/ValidateInitData"
Telegram_Notify_FullMethodName = "/scrabble.telegram.v1.Telegram/Notify"
Telegram_SendToUser_FullMethodName = "/scrabble.telegram.v1.Telegram/SendToUser"
Telegram_SendToGameChannel_FullMethodName = "/scrabble.telegram.v1.Telegram/SendToGameChannel"
Telegram_ValidateInitData_FullMethodName = "/scrabble.telegram.v1.Telegram/ValidateInitData"
Telegram_ValidateLoginWidget_FullMethodName = "/scrabble.telegram.v1.Telegram/ValidateLoginWidget"
Telegram_Notify_FullMethodName = "/scrabble.telegram.v1.Telegram/Notify"
Telegram_SendToUser_FullMethodName = "/scrabble.telegram.v1.Telegram/SendToUser"
Telegram_SendToGameChannel_FullMethodName = "/scrabble.telegram.v1.Telegram/SendToGameChannel"
)
// TelegramClient is the client API for Telegram service.
@@ -46,6 +47,11 @@ type TelegramClient interface {
// the authenticated user. The gateway calls it during the auth.telegram edge
// operation, then provisions the session through the backend internal API.
ValidateInitData(ctx context.Context, in *ValidateInitDataRequest, opts ...grpc.CallOption) (*ValidateInitDataResponse, error)
// ValidateLoginWidget verifies Telegram Login Widget authorization data (the web
// sign-in flow, HMAC under SHA-256(bot_token)) and returns the authenticated
// user. The gateway calls it during the link.telegram edge operation to attach a
// Telegram identity to an existing account (Stage 11).
ValidateLoginWidget(ctx context.Context, in *ValidateLoginWidgetRequest, opts ...grpc.CallOption) (*ValidateLoginWidgetResponse, error)
// Notify delivers an out-of-app notification for a backend push event. The
// gateway calls it only for a recipient with no live in-app stream (so the
// platform push never duplicates in-app delivery). The connector renders a
@@ -79,6 +85,16 @@ func (c *telegramClient) ValidateInitData(ctx context.Context, in *ValidateInitD
return out, nil
}
func (c *telegramClient) ValidateLoginWidget(ctx context.Context, in *ValidateLoginWidgetRequest, opts ...grpc.CallOption) (*ValidateLoginWidgetResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ValidateLoginWidgetResponse)
err := c.cc.Invoke(ctx, Telegram_ValidateLoginWidget_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *telegramClient) Notify(ctx context.Context, in *NotifyRequest, opts ...grpc.CallOption) (*NotifyResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(NotifyResponse)
@@ -119,6 +135,11 @@ type TelegramServer interface {
// the authenticated user. The gateway calls it during the auth.telegram edge
// operation, then provisions the session through the backend internal API.
ValidateInitData(context.Context, *ValidateInitDataRequest) (*ValidateInitDataResponse, error)
// ValidateLoginWidget verifies Telegram Login Widget authorization data (the web
// sign-in flow, HMAC under SHA-256(bot_token)) and returns the authenticated
// user. The gateway calls it during the link.telegram edge operation to attach a
// Telegram identity to an existing account (Stage 11).
ValidateLoginWidget(context.Context, *ValidateLoginWidgetRequest) (*ValidateLoginWidgetResponse, error)
// Notify delivers an out-of-app notification for a backend push event. The
// gateway calls it only for a recipient with no live in-app stream (so the
// platform push never duplicates in-app delivery). The connector renders a
@@ -145,6 +166,9 @@ type UnimplementedTelegramServer struct{}
func (UnimplementedTelegramServer) ValidateInitData(context.Context, *ValidateInitDataRequest) (*ValidateInitDataResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ValidateInitData not implemented")
}
func (UnimplementedTelegramServer) ValidateLoginWidget(context.Context, *ValidateLoginWidgetRequest) (*ValidateLoginWidgetResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ValidateLoginWidget not implemented")
}
func (UnimplementedTelegramServer) Notify(context.Context, *NotifyRequest) (*NotifyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Notify not implemented")
}
@@ -193,6 +217,24 @@ func _Telegram_ValidateInitData_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler)
}
func _Telegram_ValidateLoginWidget_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ValidateLoginWidgetRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TelegramServer).ValidateLoginWidget(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Telegram_ValidateLoginWidget_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TelegramServer).ValidateLoginWidget(ctx, req.(*ValidateLoginWidgetRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Telegram_Notify_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(NotifyRequest)
if err := dec(in); err != nil {
@@ -258,6 +300,10 @@ var Telegram_ServiceDesc = grpc.ServiceDesc{
MethodName: "ValidateInitData",
Handler: _Telegram_ValidateInitData_Handler,
},
{
MethodName: "ValidateLoginWidget",
Handler: _Telegram_ValidateLoginWidget_Handler,
},
{
MethodName: "Notify",
Handler: _Telegram_Notify_Handler,