package rpc import ( "context" "errors" "math" "net/url" "strconv" "strings" "time" "github.com/iamxvbaba/td/tg" "github.com/iamxvbaba/td/tgerr" "telesrv/internal/domain" ) func telegramLoginOAuthInvalidErr() error { return tgerr.New(500, "OAUTH_REQUEST_INVALID") } func telegramLoginURLExpiredErr() error { return tgerr.New(400, "URL_EXPIRED") } func telegramLoginURLInvalidErr() error { return tgerr.New(400, "URL_INVALID") } func telegramLoginHashInvalidErr() error { return tgerr.New(400, "HASH_INVALID") } func telegramLoginRPCError(err error) error { switch { case errors.Is(err, domain.ErrTelegramLoginRequestExpired): return telegramLoginURLExpiredErr() case errors.Is(err, domain.ErrTelegramLoginURLInvalid): return telegramLoginURLInvalidErr() case errors.Is(err, domain.ErrTelegramLoginMatchCodeInvalid), errors.Is(err, domain.ErrTelegramLoginRequestInvalid), errors.Is(err, domain.ErrTelegramLoginRequestConflict), errors.Is(err, domain.ErrTelegramLoginClientDisabled), errors.Is(err, domain.ErrTelegramLoginOriginNotAllowed), errors.Is(err, domain.ErrTelegramLoginRedirectNotAllowed), errors.Is(err, domain.ErrTelegramLoginScopeInvalid), errors.Is(err, domain.ErrTelegramLoginAuthorizationsTooMany): return telegramLoginOAuthInvalidErr() default: return internalErr() } } func (r *Router) requireTelegramLoginUser(ctx context.Context) (int64, error) { userID, _, err := r.currentUserID(ctx) if err != nil || userID <= 0 || r.deps.TelegramLogin == nil || r.deps.Users == nil { return 0, internalErr() } self, err := r.deps.Users.Self(ctx, userID) if err != nil || self.Bot || self.Deleted { return 0, telegramLoginOAuthInvalidErr() } return userID, nil } func (r *Router) telegramLoginRequestResult(ctx context.Context, viewerUserID int64, request domain.TelegramLoginRequest, deepLink string) (tg.URLAuthResultClass, error) { switch request.Status { case domain.TelegramLoginRequestApproved: if request.AuthorizedUserID != viewerUserID { return nil, telegramLoginOAuthInvalidErr() } return r.telegramLoginAcceptedResult(ctx, request, deepLink) case domain.TelegramLoginRequestPending: // Continue below. case domain.TelegramLoginRequestDeclined, domain.TelegramLoginRequestExpired: return nil, telegramLoginURLExpiredErr() default: return nil, telegramLoginOAuthInvalidErr() } bot, found, err := r.deps.Users.ByID(ctx, viewerUserID, request.BotUserID) if err != nil { return nil, internalErr() } if !found || !bot.Bot || bot.Deleted { return nil, telegramLoginOAuthInvalidErr() } botTL := r.withBotProfileFlags(ctx, r.tgUser(bot)) out := &tg.URLAuthResultRequest{ RequestWriteAccess: request.Requests(domain.TelegramLoginScopeBotAccess), RequestPhoneNumber: request.Requests(domain.TelegramLoginScopePhone), MatchCodesFirst: request.MatchCodesFirst, IsApp: request.IsApp, Bot: botTL, Domain: request.Domain, } // OAuth requests carry the complete device tuple. Keep the four fields on // their shared flag together so old exact-layer codecs never see a partial // conditional shape. if request.Browser != "" && request.Platform != "" && request.IP != "" && request.Region != "" { out.SetBrowser(request.Browser) out.SetPlatform(request.Platform) out.SetIP(request.IP) out.SetRegion(request.Region) } if len(request.MatchCodes) > 0 { out.SetMatchCodes(append([]string(nil), request.MatchCodes...)) } if request.UserIDHint > 0 { out.SetUserIDHint(request.UserIDHint) } if request.IsApp && request.VerifiedAppName != "" { out.SetVerifiedAppName(request.VerifiedAppName) } return out, nil } func (r *Router) telegramLoginAcceptedResult(ctx context.Context, request domain.TelegramLoginRequest, deepLink string) (tg.URLAuthResultClass, error) { accepted := &tg.URLAuthResultAccepted{} switch { case request.Source == domain.TelegramLoginRequestNative && request.IsApp: redirectURL, err := r.deps.TelegramLogin.FinalizeRedirectByDeepLink(ctx, deepLink) if err != nil { return nil, telegramLoginRPCError(err) } accepted.SetURL(redirectURL) case request.Source == domain.TelegramLoginRequestMiniApp: resultURL, err := r.deps.TelegramLogin.FinalizeInAppRedirectByDeepLink(ctx, deepLink) if err != nil { return nil, telegramLoginRPCError(err) } accepted.SetURL(resultURL) } return accepted, nil } func (r *Router) onMessagesRequestURLAuth(ctx context.Context, req *tg.MessagesRequestURLAuthRequest) (tg.URLAuthResultClass, error) { userID, err := r.requireTelegramLoginUser(ctx) if err != nil { return nil, err } _, hasPeer := req.GetPeer() urlValue, hasURL := req.GetURL() _, hasOrigin := req.GetInAppOrigin() if hasPeer == hasURL || (!hasPeer && strings.TrimSpace(urlValue) == "") || hasOrigin && !hasURL { return nil, telegramLoginOAuthInvalidErr() } if hasPeer { button, peer, err := r.telegramLoginButtonFromMessage(ctx, userID, req.Peer, req.MsgID, req.ButtonID) if err != nil { return nil, err } u, err := url.Parse(button.URL) if err != nil || u.Hostname() == "" { return nil, telegramLoginURLInvalidErr() } request := domain.TelegramLoginRequest{ BotUserID: button.LoginBotUserID, Source: domain.TelegramLoginRequestMessageButton, ResponseType: "legacy_url", RedirectURI: button.URL, Domain: u.Hostname(), Scopes: []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile}, PeerType: peer.Type, PeerID: peer.ID, MessageID: req.MsgID, ButtonID: req.ButtonID, Status: domain.TelegramLoginRequestPending, } if button.RequestWriteAccess { request.Scopes = append(request.Scopes, domain.TelegramLoginScopeBotAccess) } return r.telegramLoginRequestResult(ctx, userID, request, "") } if hasOrigin && req.InAppOrigin == "" { return nil, telegramLoginURLInvalidErr() } request, err := r.deps.TelegramLogin.RequestByDeepLinkForOrigin(ctx, urlValue, req.InAppOrigin) if err != nil { return nil, telegramLoginRPCError(err) } return r.telegramLoginRequestResult(ctx, userID, request, urlValue) } func (r *Router) onMessagesAcceptURLAuth(ctx context.Context, req *tg.MessagesAcceptURLAuthRequest) (tg.URLAuthResultClass, error) { userID, err := r.requireTelegramLoginUser(ctx) if err != nil { return nil, err } _, hasPeer := req.GetPeer() deepLink, hasURL := req.GetURL() matchCode, hasMatchCode := req.GetMatchCode() if hasPeer == hasURL || (!hasPeer && strings.TrimSpace(deepLink) == "") || (hasMatchCode && matchCode == "") { return nil, telegramLoginOAuthInvalidErr() } if hasPeer { if hasMatchCode || req.SharePhoneNumber { return nil, telegramLoginOAuthInvalidErr() } button, peer, err := r.telegramLoginButtonFromMessage(ctx, userID, req.Peer, req.MsgID, req.ButtonID) if err != nil { return nil, err } if r.deps.Bots == nil { return nil, internalErr() } profile, found, err := r.deps.Bots.BotInfo(ctx, button.LoginBotUserID) if err != nil { return nil, internalErr() } if !found || profile.TokenSecret == "" { return nil, telegramLoginOAuthInvalidErr() } self, err := r.deps.Users.Self(ctx, userID) if err != nil { return nil, internalErr() } identity := r.telegramLoginIdentity(self) result, err := r.deps.TelegramLogin.AuthorizeMessageButton(ctx, domain.TelegramLoginMessageButtonAuthorization{ UserID: userID, BotUserID: button.LoginBotUserID, BotToken: domain.FormatBotToken(button.LoginBotUserID, profile.TokenSecret), URL: button.URL, RequestWriteAccess: button.RequestWriteAccess, WriteAllowed: req.WriteAllowed, Peer: peer, MessageID: req.MsgID, ButtonID: req.ButtonID, Browser: "Telegram", Platform: "Telegram Client", IP: "Unknown IP", Region: "Unknown region", Identity: identity, }) if err != nil { return nil, telegramLoginRPCError(err) } accepted := &tg.URLAuthResultAccepted{} accepted.SetURL(result.URL) return accepted, nil } request, err := r.deps.TelegramLogin.RequestByDeepLink(ctx, deepLink) if err != nil { return nil, telegramLoginRPCError(err) } if request.Status == domain.TelegramLoginRequestApproved { if request.AuthorizedUserID == userID { return r.telegramLoginAcceptedResult(ctx, request, deepLink) } return nil, telegramLoginOAuthInvalidErr() } if request.Status != domain.TelegramLoginRequestPending { return nil, telegramLoginURLExpiredErr() } self, err := r.deps.Users.Self(ctx, userID) if err != nil { return nil, internalErr() } identity := r.telegramLoginIdentity(self) approved, _, err := r.deps.TelegramLogin.Approve(ctx, deepLink, identity, req.WriteAllowed, req.SharePhoneNumber, matchCode) if err != nil { return nil, telegramLoginRPCError(err) } if approved.AuthorizedUserID != userID { return nil, telegramLoginOAuthInvalidErr() } return r.telegramLoginAcceptedResult(ctx, approved, deepLink) } func (r *Router) telegramLoginIdentity(self domain.User) domain.TelegramLoginIdentitySnapshot { identity := domain.TelegramLoginIdentitySnapshot{ UserID: self.ID, Name: strings.TrimSpace(strings.TrimSpace(self.FirstName) + " " + strings.TrimSpace(self.LastName)), GivenName: self.FirstName, FamilyName: self.LastName, PreferredUsername: self.Username, PhoneNumber: self.Phone, } if strings.TrimSpace(r.cfg.PublicBaseURL) != "" && self.Username != "" && self.PhotoID > 0 { identity.Picture = strings.TrimSuffix(r.cfg.PublicBaseURL, "/") + "/_public/avatar/" + url.PathEscape(self.Username) + "/" + strconv.FormatInt(self.PhotoID, 10) } return identity } func (r *Router) telegramLoginButtonFromMessage(ctx context.Context, userID int64, inputPeer tg.InputPeerClass, messageID, buttonID int) (domain.MarkupButton, domain.Peer, error) { if messageID <= 0 || messageID > domain.MaxMessageBoxID || buttonID < 0 { return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() } peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inputPeer) if err != nil { return domain.MarkupButton{}, domain.Peer{}, err } var markup *domain.MessageReplyMarkup switch peer.Type { case domain.PeerTypeUser: message, found, err := r.lookupOwnerMessage(ctx, userID, messageID) if err != nil { return domain.MarkupButton{}, domain.Peer{}, internalErr() } if !found || message.Peer != peer { return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() } markup = message.ReplyMarkup case domain.PeerTypeChannel: if r.deps.Channels == nil { return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() } history, err := r.deps.Channels.GetMessages(ctx, userID, peer.ID, []int{messageID}) if err != nil || len(history.Messages) != 1 || history.Messages[0].ID != messageID { return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() } markup = history.Messages[0].ReplyMarkup default: return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() } if markup == nil || markup.Kind() != domain.MessageReplyMarkupInline { return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() } for _, row := range markup.Inline { for _, button := range row { if button.Type == domain.MarkupButtonLoginURL && button.ButtonID == buttonID && button.LoginBotUserID > 0 { return button, peer, nil } } } return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() } func (r *Router) onMessagesDeclineURLAuth(ctx context.Context, deepLink string) (bool, error) { userID, err := r.requireTelegramLoginUser(ctx) if err != nil { return false, err } if strings.TrimSpace(deepLink) == "" { return false, telegramLoginURLInvalidErr() } request, err := r.deps.TelegramLogin.RequestByDeepLink(ctx, deepLink) if err != nil { return false, telegramLoginRPCError(err) } if request.Status == domain.TelegramLoginRequestDeclined { return true, nil } if request.Status != domain.TelegramLoginRequestPending { return false, telegramLoginOAuthInvalidErr() } if _, err := r.deps.TelegramLogin.Decline(ctx, deepLink, userID); err != nil { return false, telegramLoginRPCError(err) } return true, nil } func (r *Router) onMessagesCheckURLAuthMatchCode(ctx context.Context, deepLink, matchCode string) (bool, error) { if _, err := r.requireTelegramLoginUser(ctx); err != nil { return false, err } if strings.TrimSpace(deepLink) == "" || matchCode == "" { return false, telegramLoginURLInvalidErr() } ok, err := r.deps.TelegramLogin.CheckMatchCode(ctx, deepLink, matchCode) if err != nil { return false, telegramLoginRPCError(err) } return ok, nil } func (r *Router) onAccountGetWebAuthorizations(ctx context.Context) (*tg.AccountWebAuthorizations, error) { if r.deps.TelegramLogin == nil { if _, _, err := r.currentUserID(ctx); err != nil { return nil, internalErr() } return &tg.AccountWebAuthorizations{Authorizations: []tg.WebAuthorization{}, Users: []tg.UserClass{}}, nil } userID, err := r.requireTelegramLoginUser(ctx) if err != nil { return nil, err } authorizations, err := r.deps.TelegramLogin.ListWebAuthorizations(ctx, userID) if err != nil { return nil, internalErr() } result := &tg.AccountWebAuthorizations{ Authorizations: make([]tg.WebAuthorization, 0, len(authorizations)), Users: []tg.UserClass{}, } botIDs := make([]int64, 0, len(authorizations)) seenBots := make(map[int64]struct{}, len(authorizations)) for _, authorization := range authorizations { result.Authorizations = append(result.Authorizations, tg.WebAuthorization{ Hash: authorization.Hash, BotID: authorization.BotUserID, Domain: authorization.Domain, Browser: authorization.Browser, Platform: authorization.Platform, DateCreated: telegramLoginUnixInt(authorization.CreatedAt), DateActive: telegramLoginUnixInt(authorization.LastActiveAt), IP: authorization.IP, Region: authorization.Region, }) if _, duplicate := seenBots[authorization.BotUserID]; !duplicate { seenBots[authorization.BotUserID] = struct{}{} botIDs = append(botIDs, authorization.BotUserID) } } if len(botIDs) > 0 { bots, err := r.deps.Users.ByIDs(ctx, userID, botIDs) if err != nil { return nil, internalErr() } for _, bot := range bots { if bot.Bot && !bot.Deleted { result.Users = append(result.Users, r.withBotProfileFlags(ctx, r.tgUser(bot))) } } } return result, nil } func (r *Router) onAccountResetWebAuthorization(ctx context.Context, hash int64) (bool, error) { if r.deps.TelegramLogin == nil { if _, _, err := r.currentUserID(ctx); err != nil { return false, internalErr() } return true, nil } userID, err := r.requireTelegramLoginUser(ctx) if err != nil { return false, err } if hash == 0 { return false, telegramLoginHashInvalidErr() } if err := r.deps.TelegramLogin.RevokeWebAuthorization(ctx, userID, hash); err != nil { if errors.Is(err, domain.ErrTelegramLoginWebAuthHashInvalid) { return false, telegramLoginHashInvalidErr() } return false, internalErr() } return true, nil } func (r *Router) onAccountResetWebAuthorizations(ctx context.Context) (bool, error) { if r.deps.TelegramLogin == nil { if _, _, err := r.currentUserID(ctx); err != nil { return false, internalErr() } return true, nil } userID, err := r.requireTelegramLoginUser(ctx) if err != nil { return false, err } if _, err := r.deps.TelegramLogin.RevokeAllWebAuthorizations(ctx, userID); err != nil { return false, internalErr() } return true, nil } func telegramLoginUnixInt(value time.Time) int { unix := value.Unix() if unix < 0 { return 0 } if unix > math.MaxInt32 { return math.MaxInt32 } return int(unix) }