// Package connector is the gateway's gRPC client for the Telegram connector // side-service: it validates Mini App initData and delivers out-of-app push. The // connector lives on the trusted internal network, so the connection uses insecure // (plaintext) transport credentials (ARCHITECTURE.md ยง12). package connector import ( "context" "errors" "fmt" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" telegramv1 "scrabble/pkg/proto/telegram/v1" ) // ErrInvalidInitData is returned by ValidateInitData when the connector rejects the // launch data (a gRPC InvalidArgument), letting the transcode layer surface a stable // result code. var ErrInvalidInitData = errors.New("connector: invalid telegram init data") // ErrInvalidLoginWidget is returned by ValidateLoginWidget when the connector // rejects the Login Widget data (a gRPC InvalidArgument). var ErrInvalidLoginWidget = errors.New("connector: invalid telegram login widget data") // User is a validated Mini App identity. type User struct { ExternalID string Username string FirstName string LanguageCode string } // Client wraps the connector's Telegram gRPC service. type Client struct { conn *grpc.ClientConn c telegramv1.TelegramClient } // New dials the connector gRPC endpoint. func New(addr string) (*Client, error) { conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithStatsHandler(otelgrpc.NewClientHandler()), ) if err != nil { return nil, fmt.Errorf("connector: dial %s: %w", addr, err) } return &Client{conn: conn, c: telegramv1.NewTelegramClient(conn)}, nil } // Close releases the gRPC connection. func (c *Client) Close() error { return c.conn.Close() } // ValidateInitData verifies Mini App launch data and returns the user identity, // mapping a connector InvalidArgument to ErrInvalidInitData. func (c *Client) ValidateInitData(ctx context.Context, initData string) (User, error) { resp, err := c.c.ValidateInitData(ctx, &telegramv1.ValidateInitDataRequest{InitData: initData}) if err != nil { if status.Code(err) == codes.InvalidArgument { return User{}, ErrInvalidInitData } return User{}, err } return User{ ExternalID: resp.GetExternalId(), Username: resp.GetUsername(), FirstName: resp.GetFirstName(), LanguageCode: resp.GetLanguageCode(), }, nil } // ValidateLoginWidget verifies Telegram Login Widget data and returns the user // identity, mapping a connector InvalidArgument to ErrInvalidLoginWidget. It backs // the link.telegram edge operation (Stage 11). func (c *Client) ValidateLoginWidget(ctx context.Context, data string) (User, error) { resp, err := c.c.ValidateLoginWidget(ctx, &telegramv1.ValidateLoginWidgetRequest{Data: data}) if err != nil { if status.Code(err) == codes.InvalidArgument { return User{}, ErrInvalidLoginWidget } return User{}, err } return User{ ExternalID: resp.GetExternalId(), Username: resp.GetUsername(), FirstName: resp.GetFirstName(), }, nil } // Notify delivers an out-of-app notification for a push event; delivered reports // whether a message was actually sent. func (c *Client) Notify(ctx context.Context, externalID, kind string, payload []byte, language string) (bool, error) { resp, err := c.c.Notify(ctx, &telegramv1.NotifyRequest{ ExternalId: externalID, Kind: kind, Payload: payload, Language: language, }) if err != nil { return false, err } return resp.GetDelivered(), nil }