owpengram-server/internal/rpc/telegram_login.go

434 lines
15 KiB
Go

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)
}