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
2
deploy/migrations/0088_users_signup_email.down.sql
Normal file
2
deploy/migrations/0088_users_signup_email.down.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
DROP INDEX IF EXISTS users_signup_email_lower_unique_idx;
|
||||||
|
ALTER TABLE public.users DROP COLUMN signup_email;
|
||||||
13
deploy/migrations/0088_users_signup_email.up.sql
Normal file
13
deploy/migrations/0088_users_signup_email.up.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
-- Email-signup accounts (see internal/domain.EncodeEmailPhone) now get a
|
||||||
|
-- short, normal-looking random "888" phone number in users.phone instead of
|
||||||
|
-- storing the email-derived string there directly (that string only ever
|
||||||
|
-- travels on the wire during auth.sendCode/signIn/signUp, exactly like a real
|
||||||
|
-- phone number would). signup_email is the durable email->user reverse
|
||||||
|
-- lookup that replaces the old "phone IS the email encoding" identity: it is
|
||||||
|
-- what SendCode/SignIn use to find a returning email-signup account by the
|
||||||
|
-- (deterministic) wire value decoded back to an email.
|
||||||
|
ALTER TABLE public.users ADD COLUMN signup_email character varying(200) NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
-- Case-insensitive, ignores the empty default so real phone-number accounts
|
||||||
|
-- (the overwhelming majority) never touch this index.
|
||||||
|
CREATE UNIQUE INDEX users_signup_email_lower_unique_idx ON public.users (lower(signup_email)) WHERE signup_email <> '';
|
||||||
|
|
@ -75,8 +75,14 @@ func TestEmailSignupChangePhoneRoutesCodeToDecodedEmail(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ChangePhone: %v", err)
|
t.Fatalf("ChangePhone: %v", err)
|
||||||
}
|
}
|
||||||
if result.User.Phone != newPhone {
|
// Rebinding to a new email assigns a fresh short "888" display number
|
||||||
t.Fatalf("result.User.Phone = %q, want %q", result.User.Phone, newPhone)
|
// (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 {
|
if !ok {
|
||||||
return "", domain.AuthCodeDelivery{}, domain.ErrPhoneNumberInvalid
|
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 {
|
if s.loginEmailSender == nil {
|
||||||
return "", domain.AuthCodeDelivery{}, fmt.Errorf("email signup sender is not configured")
|
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 {
|
if date == 0 {
|
||||||
date = int(time.Now().Unix())
|
date = int(time.Now().Unix())
|
||||||
}
|
}
|
||||||
result, err := s.phoneChanges.ChangePhone(ctx, domain.PhoneChangeRequest{
|
req := domain.PhoneChangeRequest{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Phone: phone,
|
Phone: phone,
|
||||||
Date: date,
|
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.
|
// echoes updateUserPhone back to the initiating device and suppresses the wrong session.
|
||||||
ExcludeAuthKeyID: originRawAuthKeyID,
|
ExcludeAuthKeyID: originRawAuthKeyID,
|
||||||
ExcludeSessionID: sessionID,
|
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 {
|
if err != nil {
|
||||||
return domain.PhoneChangeResult{}, err
|
return domain.PhoneChangeResult{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -205,6 +238,30 @@ func (s *Service) phoneChangeCaller(ctx context.Context, userID int64, authKeyID
|
||||||
return u, nil
|
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) {
|
func phoneChangeHash() (string, error) {
|
||||||
var raw [8]byte
|
var raw [8]byte
|
||||||
if _, err := rand.Read(raw[:]); err != nil {
|
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")
|
t.Fatalf("needSignUp = false, want true for a brand-new email-signup account")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignUp itself is completely untouched by email-signup: same call, same
|
// SignUp itself is called exactly like the plain-phone path (same phone
|
||||||
// phone (the 888-encoded value), no email-specific parameter anywhere.
|
// 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")
|
created, _, err := svc.SignUp(ctx, domain.Authorization{}, phone, hash, "New", "User")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("SignUp: %v", err)
|
t.Fatalf("SignUp: %v", err)
|
||||||
}
|
}
|
||||||
if created.Phone != phone {
|
if created.Phone == phone || !domain.ValidPhone(created.Phone) || domain.IsEmailSignupPhone(created.Phone) {
|
||||||
t.Fatalf("created.Phone = %q, want %q", created.Phone, 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 {
|
if err != nil {
|
||||||
t.Fatalf("SignUp: %v (this is the loop bug if it fails with ErrCodeInvalid)", err)
|
t.Fatalf("SignUp: %v (this is the loop bug if it fails with ErrCodeInvalid)", err)
|
||||||
}
|
}
|
||||||
if created.Phone != phone {
|
if created.Phone == phone || !domain.ValidPhone(created.Phone) || domain.IsEmailSignupPhone(created.Phone) {
|
||||||
t.Fatalf("created.Phone = %q, want %q", created.Phone, 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.
|
// deliberately skipped the welcome message while password_pending.
|
||||||
if a, found, err := s.auths.ByAuthKey(ctx, authKeyID); err == nil && found {
|
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 {
|
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
|
return nil
|
||||||
|
|
@ -345,10 +345,25 @@ func (s *Service) SendCode(ctx context.Context, phone string) (string, error) {
|
||||||
return s.createPhoneCode(ctx, phone, issuedUserID)
|
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) {
|
func (s *Service) currentPhoneOwner(ctx context.Context, phone string) (domain.User, bool, error) {
|
||||||
if s == nil || s.users == nil {
|
if s == nil || s.users == nil {
|
||||||
return domain.User{}, false, fmt.Errorf("user store is not configured")
|
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)
|
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;
|
// 2FA accounts only really finish authorizing in CompletePasswordSignIn;
|
||||||
// firing the welcome message here too would notify about an attempt that
|
// firing the welcome message here too would notify about an attempt that
|
||||||
// never actually got past the password check.
|
// never actually got past the password check.
|
||||||
s.recordWelcomeMessage(ctx, existing.ID, existing.Phone)
|
s.recordWelcomeMessage(ctx, existing)
|
||||||
return existing, domain.Message{}, false, nil
|
return existing, domain.Message{}, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -993,6 +1008,23 @@ func (s *Service) SignUp(ctx context.Context, auth domain.Authorization, phone,
|
||||||
FirstName: firstName,
|
FirstName: firstName,
|
||||||
LastName: lastName,
|
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 对存量
|
// 新账号默认赠送会员:到期时间 = 注册时刻 + N 个月(与迁移 0094 对存量
|
||||||
// 账号的 backfill 同一语义)。premium 状态由下发路径按该时间即时派生。
|
// 账号的 backfill 同一语义)。premium 状态由下发路径按该时间即时派生。
|
||||||
if s.premiumGrantMonths > 0 {
|
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
|
return domain.User{}, domain.Message{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.recordWelcomeMessage(ctx, u.ID, phone)
|
s.recordWelcomeMessage(ctx, u)
|
||||||
return u, loginMessage, nil
|
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
|
// SignIn/SignInWithEmail), regardless of channel. Best-effort: a failure here
|
||||||
// must never fail the sign-in itself, since unlike recordLoginMessage it
|
// must never fail the sign-in itself, since unlike recordLoginMessage it
|
||||||
// carries no secret the caller needs.
|
// 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 {
|
if s == nil || s.messages == nil || s.dialogs == nil {
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1341,7 +1373,7 @@ func (s *Service) recordWelcomeMessage(ctx context.Context, userID int64, phone
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = s.dialogs.UpsertInbox(ctx, userID, domain.Dialog{
|
_ = s.dialogs.UpsertInbox(ctx, u.ID, domain.Dialog{
|
||||||
Peer: created.Peer,
|
Peer: created.Peer,
|
||||||
TopMessage: created.ID,
|
TopMessage: created.ID,
|
||||||
TopMessageDate: created.Date,
|
TopMessageDate: created.Date,
|
||||||
|
|
@ -1445,6 +1477,28 @@ func normalizePhone(phone string) string {
|
||||||
return domain.NormalizePhone(phone)
|
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) {
|
func randomHex(n int) (string, error) {
|
||||||
b := make([]byte, n)
|
b := make([]byte, n)
|
||||||
if _, err := rand.Read(b); err != nil {
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// EmailPhonePrefix marks a "phone number" as a synthetic identity encoding an
|
// EmailPhonePrefix marks a "phone number" as a synthetic identity encoding an
|
||||||
// email address, not a real phone. It reuses Telegram's own +888 "Anonymous
|
// email address, not a real phone. It reuses Telegram's own +888 "Anonymous
|
||||||
|
|
@ -135,6 +139,33 @@ func NormalizeEmailForPhone(email string) string {
|
||||||
return strings.ToLower(strings.TrimSpace(email))
|
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.
|
// IsEmailSignupPhone reports whether phone was produced by EncodeEmailPhone.
|
||||||
// Every encoded value contains at least one letter (the mandatory '@'
|
// Every encoded value contains at least one letter (the mandatory '@'
|
||||||
// escape's 'q' marker byte), which real, all-digit phone numbers — even
|
// escape's 'q' marker byte), which real, all-digit phone numbers — even
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestEncodeDecodeEmailPhoneRoundTrip(t *testing.T) {
|
func TestEncodeDecodeEmailPhoneRoundTrip(t *testing.T) {
|
||||||
for _, email := range []string{
|
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
|
Date int
|
||||||
ExcludeAuthKeyID [8]byte
|
ExcludeAuthKeyID [8]byte
|
||||||
ExcludeSessionID int64
|
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 {
|
type PhoneChangeResult struct {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@ type User struct {
|
||||||
ID int64
|
ID int64
|
||||||
AccessHash int64
|
AccessHash int64
|
||||||
Phone string
|
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
|
FirstName string
|
||||||
LastName string
|
LastName string
|
||||||
About 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
|
// SignInMethodLabel returns the human-readable method name embedded in
|
||||||
// OfficialWelcomeMessage, derived from whether phone is an email-signup
|
// OfficialWelcomeMessage. Email-signup accounts are identified by
|
||||||
// synthetic number (see EncodeEmailPhone) or a real phone number.
|
// SignupEmail (see NewEmailSignupDisplayPhone) rather than by their stored
|
||||||
func SignInMethodLabel(phone string) string {
|
// Phone, which — once assigned — is an ordinary-looking short number that
|
||||||
if IsEmailSignupPhone(phone) {
|
// carries no information about the signup method.
|
||||||
|
func SignInMethodLabel(u User) string {
|
||||||
|
if u.SignupEmail != "" {
|
||||||
return "email"
|
return "email"
|
||||||
}
|
}
|
||||||
return "phone number"
|
return "phone number"
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,11 @@ func (s *PhoneChangeStore) ChangePhone(ctx context.Context, req domain.PhoneChan
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentPhone := u.Phone
|
currentPhone := u.Phone
|
||||||
|
currentSignupEmail := u.SignupEmail
|
||||||
u.Phone = req.Phone
|
u.Phone = req.Phone
|
||||||
|
if req.SignupEmail != "" {
|
||||||
|
u.SignupEmail = req.SignupEmail
|
||||||
|
}
|
||||||
s.users.byID[req.UserID] = u
|
s.users.byID[req.UserID] = u
|
||||||
|
|
||||||
date := req.Date
|
date := req.Date
|
||||||
|
|
@ -62,6 +66,7 @@ func (s *PhoneChangeStore) ChangePhone(ctx context.Context, req domain.PhoneChan
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 保持内存替身与 PG 的 user+event 原子可见语义。
|
// 保持内存替身与 PG 的 user+event 原子可见语义。
|
||||||
u.Phone = currentPhone
|
u.Phone = currentPhone
|
||||||
|
u.SignupEmail = currentSignupEmail
|
||||||
s.users.byID[req.UserID] = u
|
s.users.byID[req.UserID] = u
|
||||||
s.users.mu.Unlock()
|
s.users.mu.Unlock()
|
||||||
return domain.PhoneChangeResult{}, err
|
return domain.PhoneChangeResult{}, err
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package memory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
@ -72,6 +73,23 @@ func (s *UserStore) ByPhone(_ context.Context, phone string) (domain.User, bool,
|
||||||
return domain.User{}, false, nil
|
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) {
|
func (s *UserStore) ByPhones(_ context.Context, phones []string) ([]domain.User, error) {
|
||||||
if len(phones) == 0 {
|
if len(phones) == 0 {
|
||||||
return nil, nil
|
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
|
u.ID = s.nextID
|
||||||
s.nextID++
|
s.nextID++
|
||||||
s.byID[u.ID] = u
|
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
|
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 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
|
return domain.PhoneChangeResult{}, domain.ErrPhoneNumberOccupied
|
||||||
}
|
}
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ ORDER BY id;
|
||||||
-- name: GetUserByPhone :one
|
-- name: GetUserByPhone :one
|
||||||
SELECT * FROM users WHERE phone = $1;
|
SELECT * FROM users WHERE phone = $1;
|
||||||
|
|
||||||
|
-- name: GetUserBySignupEmail :one
|
||||||
|
SELECT * FROM users WHERE lower(signup_email) = lower($1) AND signup_email <> '';
|
||||||
|
|
||||||
-- name: GetUsersByPhones :many
|
-- name: GetUsersByPhones :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM users
|
FROM users
|
||||||
|
|
@ -99,8 +102,8 @@ ORDER BY contact DESC, rank, id
|
||||||
LIMIT sqlc.arg(limit_count);
|
LIMIT sqlc.arg(limit_count);
|
||||||
|
|
||||||
-- name: CreateUser :one
|
-- name: CreateUser :one
|
||||||
INSERT INTO users (access_hash, phone, first_name, last_name, username, country_code, premium_expires_at)
|
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)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: UpdateUserUsername :one
|
-- name: UpdateUserUsername :one
|
||||||
|
|
@ -132,6 +135,14 @@ SET phone = sqlc.arg(phone)::text,
|
||||||
WHERE id = sqlc.arg(id)::bigint
|
WHERE id = sqlc.arg(id)::bigint
|
||||||
RETURNING *;
|
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
|
-- name: SetUserPremiumUntil :one
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET premium_expires_at = sqlc.narg(premium_expires_at)::timestamptz,
|
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
|
const insertBotUser = `-- name: InsertBotUser :one
|
||||||
INSERT INTO users (access_hash, phone, first_name, last_name, username, country_code, is_bot, bot_info_version)
|
INSERT INTO users (access_hash, phone, first_name, last_name, username, country_code, is_bot, bot_info_version)
|
||||||
VALUES ($1, '', $2, '', $3, '', TRUE, 1)
|
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 {
|
type InsertBotUserParams struct {
|
||||||
|
|
@ -209,6 +209,7 @@ func (q *Queries) InsertBotUser(ctx context.Context, arg InsertBotUserParams) (U
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1579,6 +1579,7 @@ type User struct {
|
||||||
BirthdayMonth int32
|
BirthdayMonth int32
|
||||||
BirthdayYear int32
|
BirthdayYear int32
|
||||||
PersonalChannelID int64
|
PersonalChannelID int64
|
||||||
|
SignupEmail string
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserBusinessProfile struct {
|
type UserBusinessProfile struct {
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const createUser = `-- name: CreateUser :one
|
const createUser = `-- name: CreateUser :one
|
||||||
INSERT INTO users (access_hash, phone, first_name, last_name, username, country_code, premium_expires_at)
|
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)
|
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
|
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 {
|
type CreateUserParams struct {
|
||||||
AccessHash int64
|
AccessHash int64
|
||||||
Phone string
|
Phone string
|
||||||
|
SignupEmail string
|
||||||
FirstName string
|
FirstName string
|
||||||
LastName string
|
LastName string
|
||||||
Username string
|
Username string
|
||||||
|
|
@ -31,6 +32,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
||||||
row := q.db.QueryRow(ctx, createUser,
|
row := q.db.QueryRow(ctx, createUser,
|
||||||
arg.AccessHash,
|
arg.AccessHash,
|
||||||
arg.Phone,
|
arg.Phone,
|
||||||
|
arg.SignupEmail,
|
||||||
arg.FirstName,
|
arg.FirstName,
|
||||||
arg.LastName,
|
arg.LastName,
|
||||||
arg.Username,
|
arg.Username,
|
||||||
|
|
@ -68,12 +70,13 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserByID = `-- name: GetUserByID :one
|
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) {
|
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.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserByPhone = `-- name: GetUserByPhone :one
|
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) {
|
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.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&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
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
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) {
|
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.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUsersByIDs = `-- name: GetUsersByIDs :many
|
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
|
FROM users
|
||||||
WHERE id = ANY($1::bigint[])
|
WHERE id = ANY($1::bigint[])
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
|
|
@ -241,6 +289,7 @@ func (q *Queries) GetUsersByIDs(ctx context.Context, ids []int64) ([]User, error
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -253,7 +302,7 @@ func (q *Queries) GetUsersByIDs(ctx context.Context, ids []int64) ([]User, error
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUsersByPhones = `-- name: GetUsersByPhones :many
|
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
|
FROM users
|
||||||
WHERE phone = ANY($1::text[])
|
WHERE phone = ANY($1::text[])
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
|
|
@ -298,6 +347,7 @@ func (q *Queries) GetUsersByPhones(ctx context.Context, phones []string) ([]User
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -480,7 +530,7 @@ UPDATE users
|
||||||
SET premium_expires_at = $1::timestamptz,
|
SET premium_expires_at = $1::timestamptz,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $2::bigint
|
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 {
|
type SetUserPremiumUntilParams struct {
|
||||||
|
|
@ -521,6 +571,7 @@ func (q *Queries) SetUserPremiumUntil(ctx context.Context, arg SetUserPremiumUnt
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -530,7 +581,7 @@ UPDATE users
|
||||||
SET verified = $1::boolean,
|
SET verified = $1::boolean,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $2::bigint
|
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 {
|
type SetUserVerifiedParams struct {
|
||||||
|
|
@ -571,6 +622,7 @@ func (q *Queries) SetUserVerified(ctx context.Context, arg SetUserVerifiedParams
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -586,7 +638,7 @@ WHERE id IN (
|
||||||
ORDER BY premium_expires_at
|
ORDER BY premium_expires_at
|
||||||
LIMIT $2::int
|
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 {
|
type SweepExpiredPremiumParams struct {
|
||||||
|
|
@ -633,6 +685,7 @@ func (q *Queries) SweepExpiredPremium(ctx context.Context, arg SweepExpiredPremi
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -651,7 +704,7 @@ SET birthday_day = $1::int,
|
||||||
birthday_year = $3::int,
|
birthday_year = $3::int,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $4::bigint
|
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 {
|
type UpdateUserBirthdayParams struct {
|
||||||
|
|
@ -699,6 +752,7 @@ func (q *Queries) UpdateUserBirthday(ctx context.Context, arg UpdateUserBirthday
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -710,7 +764,7 @@ SET color_set = $1::boolean,
|
||||||
color_background_emoji_id = $3::bigint,
|
color_background_emoji_id = $3::bigint,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $4::bigint
|
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 {
|
type UpdateUserColorParams struct {
|
||||||
|
|
@ -758,6 +812,7 @@ func (q *Queries) UpdateUserColor(ctx context.Context, arg UpdateUserColorParams
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -768,7 +823,7 @@ SET emoji_status_document_id = $1::bigint,
|
||||||
emoji_status_until = $2::bigint,
|
emoji_status_until = $2::bigint,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $3::bigint
|
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 {
|
type UpdateUserEmojiStatusParams struct {
|
||||||
|
|
@ -810,6 +865,7 @@ func (q *Queries) UpdateUserEmojiStatus(ctx context.Context, arg UpdateUserEmoji
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -836,7 +892,7 @@ UPDATE users
|
||||||
SET personal_channel_id = $1::bigint,
|
SET personal_channel_id = $1::bigint,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $2::bigint
|
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 {
|
type UpdateUserPersonalChannelParams struct {
|
||||||
|
|
@ -877,6 +933,7 @@ func (q *Queries) UpdateUserPersonalChannel(ctx context.Context, arg UpdateUserP
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -886,7 +943,7 @@ UPDATE users
|
||||||
SET phone = $1::text,
|
SET phone = $1::text,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $2::bigint
|
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 {
|
type UpdateUserPhoneParams struct {
|
||||||
|
|
@ -927,6 +984,60 @@ func (q *Queries) UpdateUserPhone(ctx context.Context, arg UpdateUserPhoneParams
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&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
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -938,7 +1049,7 @@ SET first_name = $2,
|
||||||
about = $4,
|
about = $4,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $1
|
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 {
|
type UpdateUserProfileParams struct {
|
||||||
|
|
@ -986,6 +1097,7 @@ func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfilePa
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -997,7 +1109,7 @@ SET profile_color_set = $1::boolean,
|
||||||
profile_color_background_emoji_id = $3::bigint,
|
profile_color_background_emoji_id = $3::bigint,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $4::bigint
|
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 {
|
type UpdateUserProfileColorParams struct {
|
||||||
|
|
@ -1045,6 +1157,7 @@ func (q *Queries) UpdateUserProfileColor(ctx context.Context, arg UpdateUserProf
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -1054,7 +1167,7 @@ UPDATE users
|
||||||
SET username = $2,
|
SET username = $2,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $1
|
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 {
|
type UpdateUserUsernameParams struct {
|
||||||
|
|
@ -1095,6 +1208,7 @@ func (q *Queries) UpdateUserUsername(ctx context.Context, arg UpdateUserUsername
|
||||||
&i.BirthdayMonth,
|
&i.BirthdayMonth,
|
||||||
&i.BirthdayYear,
|
&i.BirthdayYear,
|
||||||
&i.PersonalChannelID,
|
&i.PersonalChannelID,
|
||||||
|
&i.SignupEmail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,24 @@ func (s *UserStore) ByPhone(ctx context.Context, phone string) (domain.User, boo
|
||||||
return userFromModel(row), true, nil
|
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) {
|
func (s *UserStore) ByPhones(ctx context.Context, phones []string) ([]domain.User, error) {
|
||||||
filtered := make([]string, 0, len(phones))
|
filtered := make([]string, 0, len(phones))
|
||||||
for _, phone := range 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{
|
row, err := qtx.CreateUser(ctx, sqlcgen.CreateUserParams{
|
||||||
AccessHash: u.AccessHash,
|
AccessHash: u.AccessHash,
|
||||||
Phone: u.Phone,
|
Phone: u.Phone,
|
||||||
|
SignupEmail: u.SignupEmail,
|
||||||
FirstName: u.FirstName,
|
FirstName: u.FirstName,
|
||||||
LastName: u.LastName,
|
LastName: u.LastName,
|
||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
|
|
@ -448,6 +467,7 @@ func userFromModel(r sqlcgen.User) domain.User {
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
AccessHash: r.AccessHash,
|
AccessHash: r.AccessHash,
|
||||||
Phone: r.Phone,
|
Phone: r.Phone,
|
||||||
|
SignupEmail: r.SignupEmail,
|
||||||
FirstName: r.FirstName,
|
FirstName: r.FirstName,
|
||||||
LastName: r.LastName,
|
LastName: r.LastName,
|
||||||
About: r.About,
|
About: r.About,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,10 @@ type UserStore interface {
|
||||||
ByIDs(ctx context.Context, ids []int64) ([]domain.User, error)
|
ByIDs(ctx context.Context, ids []int64) ([]domain.User, error)
|
||||||
ByPhone(ctx context.Context, phone string) (domain.User, bool, error)
|
ByPhone(ctx context.Context, phone string) (domain.User, bool, error)
|
||||||
ByPhones(ctx context.Context, phones []string) ([]domain.User, 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)
|
ByUsername(ctx context.Context, username string) (domain.User, bool, error)
|
||||||
Search(ctx context.Context, currentUserID int64, query, phoneQuery string, limit int) (domain.UserSearchResult, 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)
|
UpdateProfile(ctx context.Context, userID int64, firstName, lastName, about string) (domain.User, error)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue