fix for phone generation on email signup
This commit is contained in:
parent
cee960fea0
commit
3409f190b9
20 changed files with 457 additions and 54 deletions
|
|
@ -75,8 +75,14 @@ func TestEmailSignupChangePhoneRoutesCodeToDecodedEmail(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("ChangePhone: %v", err)
|
||||
}
|
||||
if result.User.Phone != newPhone {
|
||||
t.Fatalf("result.User.Phone = %q, want %q", result.User.Phone, newPhone)
|
||||
// Rebinding to a new email assigns a fresh short "888" display number
|
||||
// (see auth.Service.SignUp's identical treatment) rather than storing
|
||||
// the long wire value directly; SignupEmail carries the association.
|
||||
if result.User.Phone == newPhone || !domain.ValidPhone(result.User.Phone) || domain.IsEmailSignupPhone(result.User.Phone) {
|
||||
t.Fatalf("result.User.Phone = %q, want a short all-digit 888 display number distinct from the wire value %q", result.User.Phone, newPhone)
|
||||
}
|
||||
if result.User.SignupEmail != "newmail@owpengram.local" {
|
||||
t.Fatalf("result.User.SignupEmail = %q, want newmail@owpengram.local", result.User.SignupEmail)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,15 @@ func (s *Service) sendChangePhoneCodeByEmail(ctx context.Context, userID int64,
|
|||
if !ok {
|
||||
return "", domain.AuthCodeDelivery{}, domain.ErrPhoneNumberInvalid
|
||||
}
|
||||
// The wire value never equals any stored users.phone once assigned (see
|
||||
// ChangePhone), so the generic ByPhone occupied check above is a no-op
|
||||
// for this path; the real "is this email already someone else's
|
||||
// account" guard is by SignupEmail instead.
|
||||
if existing, found, err := s.users.ByEmail(ctx, email); err != nil {
|
||||
return "", domain.AuthCodeDelivery{}, err
|
||||
} else if found && existing.ID != userID {
|
||||
return "", domain.AuthCodeDelivery{}, domain.ErrPhoneNumberOccupied
|
||||
}
|
||||
if s.loginEmailSender == nil {
|
||||
return "", domain.AuthCodeDelivery{}, fmt.Errorf("email signup sender is not configured")
|
||||
}
|
||||
|
|
@ -162,7 +171,7 @@ func (s *Service) ChangePhone(ctx context.Context, userID int64, authKeyID, orig
|
|||
if date == 0 {
|
||||
date = int(time.Now().Unix())
|
||||
}
|
||||
result, err := s.phoneChanges.ChangePhone(ctx, domain.PhoneChangeRequest{
|
||||
req := domain.PhoneChangeRequest{
|
||||
UserID: userID,
|
||||
Phone: phone,
|
||||
Date: date,
|
||||
|
|
@ -171,7 +180,31 @@ func (s *Service) ChangePhone(ctx context.Context, userID int64, authKeyID, orig
|
|||
// echoes updateUserPhone back to the initiating device and suppresses the wrong session.
|
||||
ExcludeAuthKeyID: originRawAuthKeyID,
|
||||
ExcludeSessionID: sessionID,
|
||||
})
|
||||
}
|
||||
// Rebinding to a different email: the account keeps its short "888"
|
||||
// display number (or gets a fresh one), never the long wire value —
|
||||
// same reasoning as SignUp. ByPhone above is a no-op for this case since
|
||||
// the wire value never equals a stored users.phone; the real
|
||||
// already-bound-elsewhere guard is by SignupEmail.
|
||||
if s.emailSignupEnabled && domain.IsEmailSignupPhone(phone) {
|
||||
email, ok := domain.DecodeEmailPhone(phone)
|
||||
if !ok {
|
||||
return domain.PhoneChangeResult{}, domain.ErrPhoneNumberInvalid
|
||||
}
|
||||
email = domain.NormalizeEmailForPhone(email)
|
||||
if existing, found, err := s.users.ByEmail(ctx, email); err != nil {
|
||||
return domain.PhoneChangeResult{}, err
|
||||
} else if found && existing.ID != userID {
|
||||
return domain.PhoneChangeResult{}, domain.ErrPhoneNumberOccupied
|
||||
}
|
||||
displayPhone, err := s.assignEmailSignupDisplayPhone(ctx)
|
||||
if err != nil {
|
||||
return domain.PhoneChangeResult{}, err
|
||||
}
|
||||
req.Phone = displayPhone
|
||||
req.SignupEmail = email
|
||||
}
|
||||
result, err := s.phoneChanges.ChangePhone(ctx, req)
|
||||
if err != nil {
|
||||
return domain.PhoneChangeResult{}, err
|
||||
}
|
||||
|
|
@ -205,6 +238,30 @@ func (s *Service) phoneChangeCaller(ctx context.Context, userID int64, authKeyID
|
|||
return u, nil
|
||||
}
|
||||
|
||||
// maxEmailSignupPhoneAttempts bounds assignEmailSignupDisplayPhone's
|
||||
// collision-retry loop so a store failure can't spin forever.
|
||||
const maxEmailSignupPhoneAttempts = 20
|
||||
|
||||
// assignEmailSignupDisplayPhone generates a short "888" display number for
|
||||
// an email-signup account rebinding to a new email (see
|
||||
// domain.NewEmailSignupDisplayPhone / auth.Service's SignUp counterpart),
|
||||
// re-rolling on the astronomically unlikely collision with an existing
|
||||
// account's phone.
|
||||
func (s *Service) assignEmailSignupDisplayPhone(ctx context.Context) (string, error) {
|
||||
for i := 0; i < maxEmailSignupPhoneAttempts; i++ {
|
||||
candidate, err := domain.NewEmailSignupDisplayPhone()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, found, err := s.users.ByPhone(ctx, candidate); err != nil {
|
||||
return "", err
|
||||
} else if !found {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("assign email signup display phone: exhausted %d attempts", maxEmailSignupPhoneAttempts)
|
||||
}
|
||||
|
||||
func phoneChangeHash() (string, error) {
|
||||
var raw [8]byte
|
||||
if _, err := rand.Read(raw[:]); err != nil {
|
||||
|
|
|
|||
|
|
@ -47,14 +47,20 @@ func TestEmailSignupSendCodeRoutesFreshSignupToEmail(t *testing.T) {
|
|||
t.Fatalf("needSignUp = false, want true for a brand-new email-signup account")
|
||||
}
|
||||
|
||||
// SignUp itself is completely untouched by email-signup: same call, same
|
||||
// phone (the 888-encoded value), no email-specific parameter anywhere.
|
||||
// SignUp itself is called exactly like the plain-phone path (same phone
|
||||
// argument, no email-specific parameter anywhere), but the account it
|
||||
// creates gets a short, normal-looking "888" display number instead of
|
||||
// storing the long email-encoded wire value as its permanent phone; the
|
||||
// email->user association lives in SignupEmail instead.
|
||||
created, _, err := svc.SignUp(ctx, domain.Authorization{}, phone, hash, "New", "User")
|
||||
if err != nil {
|
||||
t.Fatalf("SignUp: %v", err)
|
||||
}
|
||||
if created.Phone != phone {
|
||||
t.Fatalf("created.Phone = %q, want %q", created.Phone, phone)
|
||||
if created.Phone == phone || !domain.ValidPhone(created.Phone) || domain.IsEmailSignupPhone(created.Phone) {
|
||||
t.Fatalf("created.Phone = %q, want a short all-digit 888 display number distinct from the wire value %q", created.Phone, phone)
|
||||
}
|
||||
if created.SignupEmail != "newuser@owpengram.local" {
|
||||
t.Fatalf("created.SignupEmail = %q, want newuser@owpengram.local", created.SignupEmail)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,8 +94,11 @@ func TestEmailSignupSignUpSucceedsWithLoginEmailRequireSetupAlsoOn(t *testing.T)
|
|||
if err != nil {
|
||||
t.Fatalf("SignUp: %v (this is the loop bug if it fails with ErrCodeInvalid)", err)
|
||||
}
|
||||
if created.Phone != phone {
|
||||
t.Fatalf("created.Phone = %q, want %q", created.Phone, phone)
|
||||
if created.Phone == phone || !domain.ValidPhone(created.Phone) || domain.IsEmailSignupPhone(created.Phone) {
|
||||
t.Fatalf("created.Phone = %q, want a short all-digit 888 display number distinct from the wire value %q", created.Phone, phone)
|
||||
}
|
||||
if created.SignupEmail != "requiresetup@owpengram.local" {
|
||||
t.Fatalf("created.SignupEmail = %q, want requiresetup@owpengram.local", created.SignupEmail)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -298,7 +298,7 @@ func (s *Service) CompletePasswordSignIn(ctx context.Context, authKeyID [8]byte)
|
|||
// deliberately skipped the welcome message while password_pending.
|
||||
if a, found, err := s.auths.ByAuthKey(ctx, authKeyID); err == nil && found {
|
||||
if u, found, err := s.users.ByID(ctx, a.UserID); err == nil && found {
|
||||
s.recordWelcomeMessage(ctx, u.ID, u.Phone)
|
||||
s.recordWelcomeMessage(ctx, u)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -345,10 +345,25 @@ func (s *Service) SendCode(ctx context.Context, phone string) (string, error) {
|
|||
return s.createPhoneCode(ctx, phone, issuedUserID)
|
||||
}
|
||||
|
||||
// currentPhoneOwner resolves the account currently identified by a wire
|
||||
// phone value. For email-signup phones (see domain.EncodeEmailPhone) the
|
||||
// account's real users.phone is a short, unrelated "888" display number
|
||||
// (domain.NewEmailSignupDisplayPhone) assigned at SignUp — the wire value
|
||||
// itself is never stored — so lookup goes through the decoded email and
|
||||
// User.SignupEmail instead of ByPhone. Every owner-drift/idempotency
|
||||
// invariant elsewhere in this file (issuedOwnerMatches, verifyLoginCode,
|
||||
// SignUp's occupied checks, ...) is expressed purely in terms of "the
|
||||
// account currentPhoneOwner resolves to", so fixing this one lookup keeps
|
||||
// all of them correct for email-signup phones with no further changes.
|
||||
func (s *Service) currentPhoneOwner(ctx context.Context, phone string) (domain.User, bool, error) {
|
||||
if s == nil || s.users == nil {
|
||||
return domain.User{}, false, fmt.Errorf("user store is not configured")
|
||||
}
|
||||
if s.emailSignupEnabled {
|
||||
if email, ok := domain.DecodeEmailPhone(phone); ok {
|
||||
return s.users.ByEmail(ctx, email)
|
||||
}
|
||||
}
|
||||
return s.users.ByPhone(ctx, phone)
|
||||
}
|
||||
|
||||
|
|
@ -906,7 +921,7 @@ func (s *Service) finishSignIn(ctx context.Context, auth domain.Authorization, e
|
|||
// 2FA accounts only really finish authorizing in CompletePasswordSignIn;
|
||||
// firing the welcome message here too would notify about an attempt that
|
||||
// never actually got past the password check.
|
||||
s.recordWelcomeMessage(ctx, existing.ID, existing.Phone)
|
||||
s.recordWelcomeMessage(ctx, existing)
|
||||
return existing, domain.Message{}, false, nil
|
||||
}
|
||||
|
||||
|
|
@ -993,6 +1008,23 @@ func (s *Service) SignUp(ctx context.Context, auth domain.Authorization, phone,
|
|||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
}
|
||||
// Email-signup accounts don't keep the long email-encoded wire value as
|
||||
// their permanent phone — that only ever needed to travel on the wire to
|
||||
// get here. Assign a short, normal-looking "888" number instead and
|
||||
// record the email separately (SignupEmail), which is what
|
||||
// currentPhoneOwner uses to find this account again on a later login.
|
||||
if s.emailSignupEnabled && domain.IsEmailSignupPhone(phone) {
|
||||
email, ok := domain.DecodeEmailPhone(phone)
|
||||
if !ok {
|
||||
return domain.User{}, domain.Message{}, ErrPhoneNumberInvalid
|
||||
}
|
||||
displayPhone, err := s.assignEmailSignupDisplayPhone(ctx)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.Message{}, err
|
||||
}
|
||||
newUser.Phone = displayPhone
|
||||
newUser.SignupEmail = domain.NormalizeEmailForPhone(email)
|
||||
}
|
||||
// 新账号默认赠送会员:到期时间 = 注册时刻 + N 个月(与迁移 0094 对存量
|
||||
// 账号的 backfill 同一语义)。premium 状态由下发路径按该时间即时派生。
|
||||
if s.premiumGrantMonths > 0 {
|
||||
|
|
@ -1021,7 +1053,7 @@ func (s *Service) SignUp(ctx context.Context, auth domain.Authorization, phone,
|
|||
return domain.User{}, domain.Message{}, err
|
||||
}
|
||||
}
|
||||
s.recordWelcomeMessage(ctx, u.ID, phone)
|
||||
s.recordWelcomeMessage(ctx, u)
|
||||
return u, loginMessage, nil
|
||||
}
|
||||
|
||||
|
|
@ -1329,11 +1361,11 @@ func (s *Service) recordLoginMessage(ctx context.Context, userID int64, code str
|
|||
// SignIn/SignInWithEmail), regardless of channel. Best-effort: a failure here
|
||||
// must never fail the sign-in itself, since unlike recordLoginMessage it
|
||||
// carries no secret the caller needs.
|
||||
func (s *Service) recordWelcomeMessage(ctx context.Context, userID int64, phone string) {
|
||||
func (s *Service) recordWelcomeMessage(ctx context.Context, u domain.User) {
|
||||
if s == nil || s.messages == nil || s.dialogs == nil {
|
||||
return
|
||||
}
|
||||
msg, err := domain.OfficialWelcomeMessage(userID, domain.SignInMethodLabel(phone), int(time.Now().Unix()))
|
||||
msg, err := domain.OfficialWelcomeMessage(u.ID, domain.SignInMethodLabel(u), int(time.Now().Unix()))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -1341,7 +1373,7 @@ func (s *Service) recordWelcomeMessage(ctx context.Context, userID int64, phone
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = s.dialogs.UpsertInbox(ctx, userID, domain.Dialog{
|
||||
_ = s.dialogs.UpsertInbox(ctx, u.ID, domain.Dialog{
|
||||
Peer: created.Peer,
|
||||
TopMessage: created.ID,
|
||||
TopMessageDate: created.Date,
|
||||
|
|
@ -1445,6 +1477,28 @@ func normalizePhone(phone string) string {
|
|||
return domain.NormalizePhone(phone)
|
||||
}
|
||||
|
||||
// assignEmailSignupDisplayPhone generates a short "888" display number for a
|
||||
// new email-signup account (see domain.NewEmailSignupDisplayPhone),
|
||||
// re-rolling on the astronomically unlikely collision with an existing
|
||||
// account's phone. maxEmailSignupPhoneAttempts bounds the loop so a store
|
||||
// failure can't spin forever.
|
||||
const maxEmailSignupPhoneAttempts = 20
|
||||
|
||||
func (s *Service) assignEmailSignupDisplayPhone(ctx context.Context) (string, error) {
|
||||
for i := 0; i < maxEmailSignupPhoneAttempts; i++ {
|
||||
candidate, err := domain.NewEmailSignupDisplayPhone()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, found, err := s.users.ByPhone(ctx, candidate); err != nil {
|
||||
return "", err
|
||||
} else if !found {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("assign email signup display phone: exhausted %d attempts", maxEmailSignupPhoneAttempts)
|
||||
}
|
||||
|
||||
func randomHex(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package domain
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EmailPhonePrefix marks a "phone number" as a synthetic identity encoding an
|
||||
// email address, not a real phone. It reuses Telegram's own +888 "Anonymous
|
||||
|
|
@ -24,11 +28,11 @@ const MaxEmailSignupPhoneLen = 200
|
|||
const emailPhoneEscape = 'q'
|
||||
|
||||
var emailPhoneEscapeEncode = map[rune]byte{
|
||||
'@': '0',
|
||||
'.': '1',
|
||||
'-': '2',
|
||||
'_': '3',
|
||||
'+': '4',
|
||||
'@': '0',
|
||||
'.': '1',
|
||||
'-': '2',
|
||||
'_': '3',
|
||||
'+': '4',
|
||||
emailPhoneEscape: '5',
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +139,33 @@ func NormalizeEmailForPhone(email string) string {
|
|||
return strings.ToLower(strings.TrimSpace(email))
|
||||
}
|
||||
|
||||
// emailSignupDisplayPhoneDigits is how many random digits follow the "888"
|
||||
// prefix in a NewEmailSignupDisplayPhone result, e.g. "888" + 8 digits =
|
||||
// "88812345678" (formats on-screen as something like "+888 1234 5678").
|
||||
const emailSignupDisplayPhoneDigits = 8
|
||||
|
||||
// NewEmailSignupDisplayPhone generates a short, all-digit "888" phone number
|
||||
// for an email-signup account's users.phone column. Unlike EncodeEmailPhone
|
||||
// this carries no information about the email — it is purely a
|
||||
// normal-looking display/identity number — so the caller must separately
|
||||
// persist the email->user association (see User.SignupEmail) for returning
|
||||
// logins to be found. Because the result is all digits, IsEmailSignupPhone
|
||||
// on it is always false: once assigned, it behaves exactly like a real phone
|
||||
// number everywhere else in the system (contacts, search, ByPhone lookups).
|
||||
func NewEmailSignupDisplayPhone() (string, error) {
|
||||
b := make([]byte, emailSignupDisplayPhoneDigits)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", fmt.Errorf("generate email signup display phone: %w", err)
|
||||
}
|
||||
var out strings.Builder
|
||||
out.Grow(len(EmailPhonePrefix) + emailSignupDisplayPhoneDigits)
|
||||
out.WriteString(EmailPhonePrefix)
|
||||
for _, v := range b {
|
||||
out.WriteByte('0' + v%10)
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
// IsEmailSignupPhone reports whether phone was produced by EncodeEmailPhone.
|
||||
// Every encoded value contains at least one letter (the mandatory '@'
|
||||
// escape's 'q' marker byte), which real, all-digit phone numbers — even
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package domain
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeDecodeEmailPhoneRoundTrip(t *testing.T) {
|
||||
for _, email := range []string{
|
||||
|
|
@ -62,3 +65,32 @@ func TestDecodeEmailPhoneRejectsNonEmailNumbers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEmailSignupDisplayPhoneLooksLikeARealPhoneNumber(t *testing.T) {
|
||||
seen := make(map[string]struct{})
|
||||
for i := 0; i < 200; i++ {
|
||||
phone, err := NewEmailSignupDisplayPhone()
|
||||
if err != nil {
|
||||
t.Fatalf("NewEmailSignupDisplayPhone: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(phone, EmailPhonePrefix) {
|
||||
t.Fatalf("phone %q missing %q prefix", phone, EmailPhonePrefix)
|
||||
}
|
||||
if !ValidPhone(phone) {
|
||||
t.Fatalf("phone %q fails ValidPhone", phone)
|
||||
}
|
||||
// Must be indistinguishable from a real phone number: no letters, so
|
||||
// IsEmailSignupPhone/DecodeEmailPhone never mistake it for a wire
|
||||
// email-signup value once it's assigned as an account's real phone.
|
||||
if IsEmailSignupPhone(phone) {
|
||||
t.Fatalf("IsEmailSignupPhone(%q) = true, want false (must look like a real number)", phone)
|
||||
}
|
||||
if _, ok := DecodeEmailPhone(phone); ok {
|
||||
t.Fatalf("DecodeEmailPhone(%q) unexpectedly succeeded", phone)
|
||||
}
|
||||
seen[phone] = struct{}{}
|
||||
}
|
||||
if len(seen) < 190 {
|
||||
t.Fatalf("only %d distinct values out of 200 draws, generator looks non-random", len(seen))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ type PhoneChangeRequest struct {
|
|||
Date int
|
||||
ExcludeAuthKeyID [8]byte
|
||||
ExcludeSessionID int64
|
||||
// SignupEmail, when non-empty, is written to users.signup_email in the
|
||||
// same transaction as Phone. Only email-signup phone changes set this
|
||||
// (see account.Service.ChangePhone); ordinary phone-number changes leave
|
||||
// it as the zero value and the column untouched.
|
||||
SignupEmail string
|
||||
}
|
||||
|
||||
type PhoneChangeResult struct {
|
||||
|
|
|
|||
|
|
@ -22,9 +22,14 @@ func (c PeerColor) Empty() bool {
|
|||
// User 是一个账号。第一阶段仅保留登录链路必须字段;
|
||||
// access_hash 为任何 InputUser 校验所必须,不可省。
|
||||
type User struct {
|
||||
ID int64
|
||||
AccessHash int64
|
||||
Phone string
|
||||
ID int64
|
||||
AccessHash int64
|
||||
Phone string
|
||||
// SignupEmail is set only for email-signup accounts (see
|
||||
// domain.NewEmailSignupDisplayPhone): it is the durable email->user
|
||||
// reverse lookup key, since Phone itself no longer encodes the email.
|
||||
// Empty for every ordinary phone-number account.
|
||||
SignupEmail string
|
||||
FirstName string
|
||||
LastName string
|
||||
About string
|
||||
|
|
|
|||
|
|
@ -30,10 +30,12 @@ func OfficialWelcomeMessage(userID int64, method string, date int) (Message, err
|
|||
}
|
||||
|
||||
// SignInMethodLabel returns the human-readable method name embedded in
|
||||
// OfficialWelcomeMessage, derived from whether phone is an email-signup
|
||||
// synthetic number (see EncodeEmailPhone) or a real phone number.
|
||||
func SignInMethodLabel(phone string) string {
|
||||
if IsEmailSignupPhone(phone) {
|
||||
// OfficialWelcomeMessage. Email-signup accounts are identified by
|
||||
// SignupEmail (see NewEmailSignupDisplayPhone) rather than by their stored
|
||||
// Phone, which — once assigned — is an ordinary-looking short number that
|
||||
// carries no information about the signup method.
|
||||
func SignInMethodLabel(u User) string {
|
||||
if u.SignupEmail != "" {
|
||||
return "email"
|
||||
}
|
||||
return "phone number"
|
||||
|
|
|
|||
|
|
@ -42,7 +42,11 @@ func (s *PhoneChangeStore) ChangePhone(ctx context.Context, req domain.PhoneChan
|
|||
}
|
||||
}
|
||||
currentPhone := u.Phone
|
||||
currentSignupEmail := u.SignupEmail
|
||||
u.Phone = req.Phone
|
||||
if req.SignupEmail != "" {
|
||||
u.SignupEmail = req.SignupEmail
|
||||
}
|
||||
s.users.byID[req.UserID] = u
|
||||
|
||||
date := req.Date
|
||||
|
|
@ -62,6 +66,7 @@ func (s *PhoneChangeStore) ChangePhone(ctx context.Context, req domain.PhoneChan
|
|||
if err != nil {
|
||||
// 保持内存替身与 PG 的 user+event 原子可见语义。
|
||||
u.Phone = currentPhone
|
||||
u.SignupEmail = currentSignupEmail
|
||||
s.users.byID[req.UserID] = u
|
||||
s.users.mu.Unlock()
|
||||
return domain.PhoneChangeResult{}, err
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package memory
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -72,6 +73,23 @@ func (s *UserStore) ByPhone(_ context.Context, phone string) (domain.User, bool,
|
|||
return domain.User{}, false, nil
|
||||
}
|
||||
|
||||
// ByEmail looks up an email-signup account by its signup_email (see
|
||||
// domain.NewEmailSignupDisplayPhone). Mirrors postgres.UserStore.ByEmail.
|
||||
func (s *UserStore) ByEmail(_ context.Context, email string) (domain.User, bool, error) {
|
||||
email = strings.ToLower(strings.TrimSpace(email))
|
||||
if email == "" {
|
||||
return domain.User{}, false, nil
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, u := range s.byID {
|
||||
if u.SignupEmail != "" && strings.ToLower(u.SignupEmail) == email {
|
||||
return u, true, nil
|
||||
}
|
||||
}
|
||||
return domain.User{}, false, nil
|
||||
}
|
||||
|
||||
func (s *UserStore) ByPhones(_ context.Context, phones []string) ([]domain.User, error) {
|
||||
if len(phones) == 0 {
|
||||
return nil, nil
|
||||
|
|
@ -377,6 +395,14 @@ func (s *UserStore) Create(_ context.Context, u domain.User) (domain.User, error
|
|||
}
|
||||
}
|
||||
}
|
||||
signupEmail := strings.ToLower(strings.TrimSpace(u.SignupEmail))
|
||||
if signupEmail != "" {
|
||||
for _, existing := range s.byID {
|
||||
if existing.SignupEmail != "" && strings.ToLower(existing.SignupEmail) == signupEmail {
|
||||
return domain.User{}, fmt.Errorf("create user: signup email occupied")
|
||||
}
|
||||
}
|
||||
}
|
||||
u.ID = s.nextID
|
||||
s.nextID++
|
||||
s.byID[u.ID] = u
|
||||
|
|
|
|||
|
|
@ -64,9 +64,14 @@ func (s *PhoneChangeStore) ChangePhone(ctx context.Context, req domain.PhoneChan
|
|||
return domain.PhoneChangeResult{User: userFromModel(row)}, nil
|
||||
}
|
||||
|
||||
row, err := qtx.UpdateUserPhone(ctx, sqlcgen.UpdateUserPhoneParams{ID: req.UserID, Phone: req.Phone})
|
||||
var row sqlcgen.User
|
||||
if req.SignupEmail != "" {
|
||||
row, err = qtx.UpdateUserPhoneAndSignupEmail(ctx, sqlcgen.UpdateUserPhoneAndSignupEmailParams{ID: req.UserID, Phone: req.Phone, SignupEmail: req.SignupEmail})
|
||||
} else {
|
||||
row, err = qtx.UpdateUserPhone(ctx, sqlcgen.UpdateUserPhoneParams{ID: req.UserID, Phone: req.Phone})
|
||||
}
|
||||
if err != nil {
|
||||
if isUniqueConstraint(err, "users_phone_unique_idx") {
|
||||
if isUniqueConstraint(err, "users_phone_unique_idx") || isUniqueConstraint(err, "users_signup_email_lower_unique_idx") {
|
||||
return domain.PhoneChangeResult{}, domain.ErrPhoneNumberOccupied
|
||||
}
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ ORDER BY id;
|
|||
-- name: GetUserByPhone :one
|
||||
SELECT * FROM users WHERE phone = $1;
|
||||
|
||||
-- name: GetUserBySignupEmail :one
|
||||
SELECT * FROM users WHERE lower(signup_email) = lower($1) AND signup_email <> '';
|
||||
|
||||
-- name: GetUsersByPhones :many
|
||||
SELECT *
|
||||
FROM users
|
||||
|
|
@ -99,8 +102,8 @@ ORDER BY contact DESC, rank, id
|
|||
LIMIT sqlc.arg(limit_count);
|
||||
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO users (access_hash, phone, first_name, last_name, username, country_code, premium_expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
INSERT INTO users (access_hash, phone, signup_email, first_name, last_name, username, country_code, premium_expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserUsername :one
|
||||
|
|
@ -132,6 +135,14 @@ SET phone = sqlc.arg(phone)::text,
|
|||
WHERE id = sqlc.arg(id)::bigint
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserPhoneAndSignupEmail :one
|
||||
UPDATE users
|
||||
SET phone = sqlc.arg(phone)::text,
|
||||
signup_email = sqlc.arg(signup_email)::text,
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg(id)::bigint
|
||||
RETURNING *;
|
||||
|
||||
-- name: SetUserPremiumUntil :one
|
||||
UPDATE users
|
||||
SET premium_expires_at = sqlc.narg(premium_expires_at)::timestamptz,
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ func (q *Queries) InsertBot(ctx context.Context, arg InsertBotParams) error {
|
|||
const insertBotUser = `-- name: InsertBotUser :one
|
||||
INSERT INTO users (access_hash, phone, first_name, last_name, username, country_code, is_bot, bot_info_version)
|
||||
VALUES ($1, '', $2, '', $3, '', TRUE, 1)
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type InsertBotUserParams struct {
|
||||
|
|
@ -209,6 +209,7 @@ func (q *Queries) InsertBotUser(ctx context.Context, arg InsertBotUserParams) (U
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1579,6 +1579,7 @@ type User struct {
|
|||
BirthdayMonth int32
|
||||
BirthdayYear int32
|
||||
PersonalChannelID int64
|
||||
SignupEmail string
|
||||
}
|
||||
|
||||
type UserBusinessProfile struct {
|
||||
|
|
|
|||
|
|
@ -12,14 +12,15 @@ import (
|
|||
)
|
||||
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (access_hash, phone, first_name, last_name, username, country_code, premium_expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
INSERT INTO users (access_hash, phone, signup_email, first_name, last_name, username, country_code, premium_expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
AccessHash int64
|
||||
Phone string
|
||||
SignupEmail string
|
||||
FirstName string
|
||||
LastName string
|
||||
Username string
|
||||
|
|
@ -31,6 +32,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
|||
row := q.db.QueryRow(ctx, createUser,
|
||||
arg.AccessHash,
|
||||
arg.Phone,
|
||||
arg.SignupEmail,
|
||||
arg.FirstName,
|
||||
arg.LastName,
|
||||
arg.Username,
|
||||
|
|
@ -68,12 +70,13 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id FROM users WHERE id = $1
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email FROM users WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||
|
|
@ -109,12 +112,13 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByPhone = `-- name: GetUserByPhone :one
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id FROM users WHERE phone = $1
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email FROM users WHERE phone = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByPhone(ctx context.Context, phone string) (User, error) {
|
||||
|
|
@ -150,12 +154,55 @@ func (q *Queries) GetUserByPhone(ctx context.Context, phone string) (User, error
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserBySignupEmail = `-- name: GetUserBySignupEmail :one
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email FROM users WHERE lower(signup_email) = lower($1) AND signup_email <> ''
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserBySignupEmail(ctx context.Context, lower string) (User, error) {
|
||||
row := q.db.QueryRow(ctx, getUserBySignupEmail, lower)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.AccessHash,
|
||||
&i.Phone,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Username,
|
||||
&i.CountryCode,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Verified,
|
||||
&i.Support,
|
||||
&i.About,
|
||||
&i.LastSeenAt,
|
||||
&i.DefaultHistoryTtlPeriod,
|
||||
&i.IsBot,
|
||||
&i.BotInfoVersion,
|
||||
&i.PremiumExpiresAt,
|
||||
&i.EmojiStatusDocumentID,
|
||||
&i.EmojiStatusUntil,
|
||||
&i.ColorSet,
|
||||
&i.Color,
|
||||
&i.ColorBackgroundEmojiID,
|
||||
&i.ProfileColorSet,
|
||||
&i.ProfileColor,
|
||||
&i.ProfileColorBackgroundEmojiID,
|
||||
&i.BirthdayDay,
|
||||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id FROM users WHERE lower(username) = lower($1) AND username <> ''
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email FROM users WHERE lower(username) = lower($1) AND username <> ''
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByUsername(ctx context.Context, lower string) (User, error) {
|
||||
|
|
@ -191,12 +238,13 @@ func (q *Queries) GetUserByUsername(ctx context.Context, lower string) (User, er
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUsersByIDs = `-- name: GetUsersByIDs :many
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
FROM users
|
||||
WHERE id = ANY($1::bigint[])
|
||||
ORDER BY id
|
||||
|
|
@ -241,6 +289,7 @@ func (q *Queries) GetUsersByIDs(ctx context.Context, ids []int64) ([]User, error
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -253,7 +302,7 @@ func (q *Queries) GetUsersByIDs(ctx context.Context, ids []int64) ([]User, error
|
|||
}
|
||||
|
||||
const getUsersByPhones = `-- name: GetUsersByPhones :many
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
FROM users
|
||||
WHERE phone = ANY($1::text[])
|
||||
ORDER BY id
|
||||
|
|
@ -298,6 +347,7 @@ func (q *Queries) GetUsersByPhones(ctx context.Context, phones []string) ([]User
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -480,7 +530,7 @@ UPDATE users
|
|||
SET premium_expires_at = $1::timestamptz,
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type SetUserPremiumUntilParams struct {
|
||||
|
|
@ -521,6 +571,7 @@ func (q *Queries) SetUserPremiumUntil(ctx context.Context, arg SetUserPremiumUnt
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -530,7 +581,7 @@ UPDATE users
|
|||
SET verified = $1::boolean,
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type SetUserVerifiedParams struct {
|
||||
|
|
@ -571,6 +622,7 @@ func (q *Queries) SetUserVerified(ctx context.Context, arg SetUserVerifiedParams
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -586,7 +638,7 @@ WHERE id IN (
|
|||
ORDER BY premium_expires_at
|
||||
LIMIT $2::int
|
||||
)
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type SweepExpiredPremiumParams struct {
|
||||
|
|
@ -633,6 +685,7 @@ func (q *Queries) SweepExpiredPremium(ctx context.Context, arg SweepExpiredPremi
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -651,7 +704,7 @@ SET birthday_day = $1::int,
|
|||
birthday_year = $3::int,
|
||||
updated_at = now()
|
||||
WHERE id = $4::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type UpdateUserBirthdayParams struct {
|
||||
|
|
@ -699,6 +752,7 @@ func (q *Queries) UpdateUserBirthday(ctx context.Context, arg UpdateUserBirthday
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -710,7 +764,7 @@ SET color_set = $1::boolean,
|
|||
color_background_emoji_id = $3::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = $4::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type UpdateUserColorParams struct {
|
||||
|
|
@ -758,6 +812,7 @@ func (q *Queries) UpdateUserColor(ctx context.Context, arg UpdateUserColorParams
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -768,7 +823,7 @@ SET emoji_status_document_id = $1::bigint,
|
|||
emoji_status_until = $2::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = $3::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type UpdateUserEmojiStatusParams struct {
|
||||
|
|
@ -810,6 +865,7 @@ func (q *Queries) UpdateUserEmojiStatus(ctx context.Context, arg UpdateUserEmoji
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -836,7 +892,7 @@ UPDATE users
|
|||
SET personal_channel_id = $1::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type UpdateUserPersonalChannelParams struct {
|
||||
|
|
@ -877,6 +933,7 @@ func (q *Queries) UpdateUserPersonalChannel(ctx context.Context, arg UpdateUserP
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -886,7 +943,7 @@ UPDATE users
|
|||
SET phone = $1::text,
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type UpdateUserPhoneParams struct {
|
||||
|
|
@ -927,6 +984,60 @@ func (q *Queries) UpdateUserPhone(ctx context.Context, arg UpdateUserPhoneParams
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUserPhoneAndSignupEmail = `-- name: UpdateUserPhoneAndSignupEmail :one
|
||||
UPDATE users
|
||||
SET phone = $1::text,
|
||||
signup_email = $2::text,
|
||||
updated_at = now()
|
||||
WHERE id = $3::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type UpdateUserPhoneAndSignupEmailParams struct {
|
||||
Phone string
|
||||
SignupEmail string
|
||||
ID int64
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUserPhoneAndSignupEmail(ctx context.Context, arg UpdateUserPhoneAndSignupEmailParams) (User, error) {
|
||||
row := q.db.QueryRow(ctx, updateUserPhoneAndSignupEmail, arg.Phone, arg.SignupEmail, arg.ID)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.AccessHash,
|
||||
&i.Phone,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Username,
|
||||
&i.CountryCode,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Verified,
|
||||
&i.Support,
|
||||
&i.About,
|
||||
&i.LastSeenAt,
|
||||
&i.DefaultHistoryTtlPeriod,
|
||||
&i.IsBot,
|
||||
&i.BotInfoVersion,
|
||||
&i.PremiumExpiresAt,
|
||||
&i.EmojiStatusDocumentID,
|
||||
&i.EmojiStatusUntil,
|
||||
&i.ColorSet,
|
||||
&i.Color,
|
||||
&i.ColorBackgroundEmojiID,
|
||||
&i.ProfileColorSet,
|
||||
&i.ProfileColor,
|
||||
&i.ProfileColorBackgroundEmojiID,
|
||||
&i.BirthdayDay,
|
||||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -938,7 +1049,7 @@ SET first_name = $2,
|
|||
about = $4,
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type UpdateUserProfileParams struct {
|
||||
|
|
@ -986,6 +1097,7 @@ func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfilePa
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -997,7 +1109,7 @@ SET profile_color_set = $1::boolean,
|
|||
profile_color_background_emoji_id = $3::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = $4::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type UpdateUserProfileColorParams struct {
|
||||
|
|
@ -1045,6 +1157,7 @@ func (q *Queries) UpdateUserProfileColor(ctx context.Context, arg UpdateUserProf
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -1054,7 +1167,7 @@ UPDATE users
|
|||
SET username = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, signup_email
|
||||
`
|
||||
|
||||
type UpdateUserUsernameParams struct {
|
||||
|
|
@ -1095,6 +1208,7 @@ func (q *Queries) UpdateUserUsername(ctx context.Context, arg UpdateUserUsername
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.SignupEmail,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,24 @@ func (s *UserStore) ByPhone(ctx context.Context, phone string) (domain.User, boo
|
|||
return userFromModel(row), true, nil
|
||||
}
|
||||
|
||||
// ByEmail looks up an email-signup account by its signup_email (see
|
||||
// domain.NewEmailSignupDisplayPhone). Ordinary phone accounts never match
|
||||
// since signup_email is '' for them and the index excludes empty values.
|
||||
func (s *UserStore) ByEmail(ctx context.Context, email string) (domain.User, bool, error) {
|
||||
email = strings.TrimSpace(email)
|
||||
if email == "" {
|
||||
return domain.User{}, false, nil
|
||||
}
|
||||
row, err := s.q.GetUserBySignupEmail(ctx, email)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.User{}, false, nil
|
||||
}
|
||||
return domain.User{}, false, fmt.Errorf("get user by signup email: %w", err)
|
||||
}
|
||||
return userFromModel(row), true, nil
|
||||
}
|
||||
|
||||
func (s *UserStore) ByPhones(ctx context.Context, phones []string) ([]domain.User, error) {
|
||||
filtered := make([]string, 0, len(phones))
|
||||
for _, phone := range phones {
|
||||
|
|
@ -262,6 +280,7 @@ func (s *UserStore) Create(ctx context.Context, u domain.User) (domain.User, err
|
|||
row, err := qtx.CreateUser(ctx, sqlcgen.CreateUserParams{
|
||||
AccessHash: u.AccessHash,
|
||||
Phone: u.Phone,
|
||||
SignupEmail: u.SignupEmail,
|
||||
FirstName: u.FirstName,
|
||||
LastName: u.LastName,
|
||||
Username: u.Username,
|
||||
|
|
@ -448,6 +467,7 @@ func userFromModel(r sqlcgen.User) domain.User {
|
|||
ID: r.ID,
|
||||
AccessHash: r.AccessHash,
|
||||
Phone: r.Phone,
|
||||
SignupEmail: r.SignupEmail,
|
||||
FirstName: r.FirstName,
|
||||
LastName: r.LastName,
|
||||
About: r.About,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ type UserStore interface {
|
|||
ByIDs(ctx context.Context, ids []int64) ([]domain.User, error)
|
||||
ByPhone(ctx context.Context, phone string) (domain.User, bool, error)
|
||||
ByPhones(ctx context.Context, phones []string) ([]domain.User, error)
|
||||
// ByEmail looks up an email-signup account by its signup_email (see
|
||||
// domain.NewEmailSignupDisplayPhone). Ordinary phone accounts never
|
||||
// match: signup_email is empty for them.
|
||||
ByEmail(ctx context.Context, email string) (domain.User, bool, error)
|
||||
ByUsername(ctx context.Context, username string) (domain.User, bool, error)
|
||||
Search(ctx context.Context, currentUserID int64, query, phoneQuery string, limit int) (domain.UserSearchResult, error)
|
||||
UpdateProfile(ctx context.Context, userID int64, firstName, lastName, about string) (domain.User, error)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue