168 lines
5.9 KiB
Go
168 lines
5.9 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/iamxvbaba/td/tg"
|
|
"github.com/iamxvbaba/td/tgerr"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/postresponse"
|
|
)
|
|
|
|
type accountDeletionService interface {
|
|
DeleteAccount(ctx context.Context, userID int64, authKeyID [8]byte, reason string, password *domain.PasswordCheck, now time.Time) (domain.AccountDeleteOutcome, error)
|
|
SendConfirmPhoneCode(ctx context.Context, userID int64, authKeyID [8]byte, sessionID int64, hash string) (string, domain.AuthCodeDelivery, error)
|
|
ConfirmPhone(ctx context.Context, userID int64, authKeyID [8]byte, phoneCodeHash, code string, now time.Time) ([]domain.Authorization, error)
|
|
ResendConfirmPhoneCode(ctx context.Context, userID int64, authKeyID [8]byte, sessionID int64, phone, oldHash string) (string, domain.AuthCodeDelivery, bool, error)
|
|
CancelConfirmPhoneCode(ctx context.Context, userID int64, authKeyID [8]byte, phone, hash string) (bool, error)
|
|
}
|
|
|
|
func (r *Router) accountDeletionSvc() (accountDeletionService, bool) {
|
|
svc, ok := r.deps.Account.(accountDeletionService)
|
|
return svc, ok
|
|
}
|
|
|
|
func (r *Router) onAccountDeleteAccount(ctx context.Context, req *tg.AccountDeleteAccountRequest) (bool, error) {
|
|
userID, authorized, passwordPending, err := r.currentOrPendingPasswordUserID(ctx)
|
|
if err != nil {
|
|
return false, internalErr()
|
|
}
|
|
if userID == 0 || (!authorized && !passwordPending) {
|
|
return false, authKeyUnregisteredErr()
|
|
}
|
|
svc, ok := r.accountDeletionSvc()
|
|
if !ok {
|
|
return false, internalErr()
|
|
}
|
|
authKeyID, ok := AuthKeyIDFrom(ctx)
|
|
if !ok || authKeyID == ([8]byte{}) {
|
|
return false, authKeyUnregisteredErr()
|
|
}
|
|
var password *domain.PasswordCheck
|
|
if check, present := req.GetPassword(); present {
|
|
converted := domainPasswordCheck(check)
|
|
password = &converted
|
|
}
|
|
outcome, err := svc.DeleteAccount(ctx, userID, authKeyID, req.Reason, password, time.Now().UTC())
|
|
if err != nil {
|
|
return false, accountDeletionErr(err)
|
|
}
|
|
if outcome.Kind == domain.AccountDeleteDelayed {
|
|
wait := outcome.WaitSeconds
|
|
if wait < 1 {
|
|
wait = 1
|
|
}
|
|
return false, tgerr.New(420, fmt.Sprintf("2FA_CONFIRM_WAIT_%d", wait))
|
|
}
|
|
r.finishDeletedAccountAuthorizations(ctx, userID, outcome.Deletion.RevokedAuthorizations)
|
|
r.invalidateDeletedUserProjectionFacts(userID)
|
|
// A tombstone changes this target for every viewer. Flushing once is bounded
|
|
// and avoids four full-cache predicate scans; the PostgreSQL user_deleted
|
|
// event performs the same coarse invalidation on other instances.
|
|
r.flushRPCProjectionCache()
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onAccountSendConfirmPhoneCode(ctx context.Context, req *tg.AccountSendConfirmPhoneCodeRequest) (tg.AuthSentCodeClass, error) {
|
|
userID, authorized, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if !authorized || userID == 0 {
|
|
return nil, authKeyUnregisteredErr()
|
|
}
|
|
svc, ok := r.accountDeletionSvc()
|
|
if !ok {
|
|
return nil, internalErr()
|
|
}
|
|
authKeyID, _ := AuthKeyIDFrom(ctx)
|
|
sessionID, _ := SessionIDFrom(ctx)
|
|
hash, delivery, err := svc.SendConfirmPhoneCode(ctx, userID, authKeyID, sessionID, req.Hash)
|
|
if err != nil {
|
|
return nil, accountDeletionErr(err)
|
|
}
|
|
return tgSMSSentCode(hash, delivery.Length), nil
|
|
}
|
|
|
|
func (r *Router) onAccountConfirmPhone(ctx context.Context, req *tg.AccountConfirmPhoneRequest) (bool, error) {
|
|
userID, authorized, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return false, internalErr()
|
|
}
|
|
if !authorized || userID == 0 {
|
|
return false, authKeyUnregisteredErr()
|
|
}
|
|
svc, ok := r.accountDeletionSvc()
|
|
if !ok {
|
|
return false, internalErr()
|
|
}
|
|
authKeyID, _ := AuthKeyIDFrom(ctx)
|
|
revoked, err := svc.ConfirmPhone(ctx, userID, authKeyID, req.PhoneCodeHash, req.PhoneCode, time.Now().UTC())
|
|
if err != nil {
|
|
return false, accountDeletionErr(err)
|
|
}
|
|
r.finishDeletedAccountAuthorizations(ctx, userID, revoked)
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) finishDeletedAccountAuthorizations(ctx context.Context, userID int64, revoked []domain.Authorization) {
|
|
current, _ := AuthKeyIDFrom(ctx)
|
|
for _, authorization := range revoked {
|
|
a := authorization
|
|
finish := func() {
|
|
r.discardSecretChatsForAuthKey(context.Background(), businessAuthKeyInt64(a.AuthKeyID), userID)
|
|
r.revokeAuthKeySessions(a.AuthKeyID)
|
|
}
|
|
if a.AuthKeyID == current {
|
|
if postresponse.Register(ctx, finish) {
|
|
continue
|
|
}
|
|
}
|
|
finish()
|
|
}
|
|
}
|
|
|
|
// NotifyModerationAccountDeletion completes the runtime half of the durable
|
|
// moderation tombstone. Moderation runs off-request, so every revoked auth key
|
|
// can be disconnected immediately; reconnects then observe the missing durable
|
|
// authorization and the auth service's tombstone guard.
|
|
func (r *Router) NotifyModerationAccountDeletion(ctx context.Context, result domain.AccountDeletionResult) {
|
|
if r == nil || !result.Changed || result.User.ID == 0 {
|
|
return
|
|
}
|
|
r.finishDeletedAccountAuthorizations(ctx, result.User.ID, result.RevokedAuthorizations)
|
|
r.invalidateDeletedUserProjectionFacts(result.User.ID)
|
|
r.flushRPCProjectionCache()
|
|
}
|
|
|
|
func (r *Router) invalidateDeletedUserProjectionFacts(userID int64) {
|
|
if r == nil || userID == 0 || r.deps.UserProjectionFacts == nil {
|
|
return
|
|
}
|
|
r.deps.UserProjectionFacts.InvalidateAccountFreezeFact(userID)
|
|
}
|
|
|
|
func accountDeletionErr(err error) error {
|
|
switch {
|
|
case errors.Is(err, domain.ErrPasswordHashInvalid), errors.Is(err, domain.ErrSRPIDInvalid), errors.Is(err, domain.ErrSRPPasswordChanged):
|
|
return passwordErr(err)
|
|
case errors.Is(err, domain.ErrAccountDeletionHashInvalid), errors.Is(err, domain.ErrAccountDeletionNotPending):
|
|
return tgerr.New(400, "HASH_INVALID")
|
|
case errors.Is(err, domain.ErrPhoneCodeEmpty):
|
|
return phoneCodeEmptyErr()
|
|
case errors.Is(err, domain.ErrPhoneCodeInvalid):
|
|
return phoneCodeInvalidErr()
|
|
case errors.Is(err, domain.ErrPhoneCodeExpired):
|
|
return phoneCodeExpiredErr()
|
|
case errors.Is(err, domain.ErrAccountDeletionForbidden):
|
|
return botMethodInvalidErr()
|
|
case errors.Is(err, domain.ErrAccountDeleted):
|
|
return authKeyUnregisteredErr()
|
|
default:
|
|
return internalErr()
|
|
}
|
|
}
|