chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -19,22 +18,17 @@ const (
|
|||
passwordResetRetry = 24 * time.Hour
|
||||
)
|
||||
|
||||
// EmailUnconfirmedError reports the dev recovery-code length expected by TDesktop.
|
||||
type EmailUnconfirmedError struct {
|
||||
Length int
|
||||
}
|
||||
|
||||
func (e EmailUnconfirmedError) Error() string {
|
||||
if e.Length <= 0 {
|
||||
return "email unconfirmed"
|
||||
}
|
||||
return fmt.Sprintf("email unconfirmed: %d", e.Length)
|
||||
}
|
||||
|
||||
// Service 提供账号安全配置查询。
|
||||
type Service struct {
|
||||
passwords store.PasswordStore
|
||||
reactions store.AccountReactionSettingsStore
|
||||
passwords store.PasswordStore
|
||||
reactions store.AccountReactionSettingsStore
|
||||
settings store.AccountSettingsStore
|
||||
notify store.NotifySettingsStore
|
||||
stickers store.StickerCollectionStore
|
||||
savedMusic store.SavedMusicStore
|
||||
business store.BusinessAutomationStore
|
||||
// users 仅用于登录邮箱的 phone→user 解析(sendCode 检测 / login-setup / reset 走 phone)。
|
||||
users store.UserStore
|
||||
}
|
||||
|
||||
// ServiceOption 调整 account 服务依赖。
|
||||
|
|
@ -47,6 +41,48 @@ func WithReactionSettings(reactions store.AccountReactionSettingsStore) ServiceO
|
|||
}
|
||||
}
|
||||
|
||||
// WithAccountSettings 注入账号级单例设置(全局隐私/TTL/敏感内容/注册通知)持久化。
|
||||
func WithAccountSettings(settings store.AccountSettingsStore) ServiceOption {
|
||||
return func(s *Service) {
|
||||
s.settings = settings
|
||||
}
|
||||
}
|
||||
|
||||
// WithNotifySettings 注入 per-scope 通知设置持久化。
|
||||
func WithNotifySettings(notify store.NotifySettingsStore) ServiceOption {
|
||||
return func(s *Service) {
|
||||
s.notify = notify
|
||||
}
|
||||
}
|
||||
|
||||
// WithStickerCollections 注入个人贴纸/GIF 集合持久化(faved/recent/gif)。
|
||||
func WithStickerCollections(stickers store.StickerCollectionStore) ServiceOption {
|
||||
return func(s *Service) {
|
||||
s.stickers = stickers
|
||||
}
|
||||
}
|
||||
|
||||
// WithSavedMusic 注入账号级 profile music 列表持久化。
|
||||
func WithSavedMusic(savedMusic store.SavedMusicStore) ServiceOption {
|
||||
return func(s *Service) {
|
||||
s.savedMusic = savedMusic
|
||||
}
|
||||
}
|
||||
|
||||
// WithBusinessAutomation 注入账号级 Business Profile/Quick Replies/Chat Links 持久化。
|
||||
func WithBusinessAutomation(business store.BusinessAutomationStore) ServiceOption {
|
||||
return func(s *Service) {
|
||||
s.business = business
|
||||
}
|
||||
}
|
||||
|
||||
// WithUsers 注入用户读存储,供登录邮箱的 phone→user 解析使用。
|
||||
func WithUsers(users store.UserStore) ServiceOption {
|
||||
return func(s *Service) {
|
||||
s.users = users
|
||||
}
|
||||
}
|
||||
|
||||
// NewService 创建 account 服务。
|
||||
func NewService(passwords store.PasswordStore, opts ...ServiceOption) *Service {
|
||||
s := &Service{passwords: passwords}
|
||||
|
|
@ -56,6 +92,42 @@ func NewService(passwords store.PasswordStore, opts ...ServiceOption) *Service {
|
|||
return s
|
||||
}
|
||||
|
||||
// SaveMusic adds, removes, or reorders a song in the current user's profile music list.
|
||||
func (s *Service) SaveMusic(ctx context.Context, userID int64, req domain.SaveMusicRequest) (bool, error) {
|
||||
if userID == 0 || req.Document.ID == 0 || !req.Document.IsMusic() {
|
||||
return false, domain.ErrDocumentInvalid
|
||||
}
|
||||
if s == nil || s.savedMusic == nil {
|
||||
return true, nil
|
||||
}
|
||||
req.UserID = userID
|
||||
return true, s.savedMusic.SaveMusic(ctx, req)
|
||||
}
|
||||
|
||||
// ListSavedMusicIDs returns the full ordered id list for account.getSavedMusicIds.
|
||||
func (s *Service) ListSavedMusicIDs(ctx context.Context, userID int64, limit int) ([]int64, error) {
|
||||
if s == nil || s.savedMusic == nil || userID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.savedMusic.ListSavedMusicIDs(ctx, userID, limit)
|
||||
}
|
||||
|
||||
// ListSavedMusic returns an ordered saved/profile music page.
|
||||
func (s *Service) ListSavedMusic(ctx context.Context, userID int64, offset, limit int) (domain.SavedMusicList, error) {
|
||||
if s == nil || s.savedMusic == nil || userID == 0 {
|
||||
return domain.SavedMusicList{UserID: userID}, nil
|
||||
}
|
||||
return s.savedMusic.ListSavedMusic(ctx, userID, offset, limit)
|
||||
}
|
||||
|
||||
// GetSavedMusicByIDs refreshes file references for songs still present in the user's list.
|
||||
func (s *Service) GetSavedMusicByIDs(ctx context.Context, userID int64, ids []int64) (domain.SavedMusicList, error) {
|
||||
if s == nil || s.savedMusic == nil || userID == 0 || len(ids) == 0 {
|
||||
return domain.SavedMusicList{UserID: userID}, nil
|
||||
}
|
||||
return s.savedMusic.GetSavedMusicByIDs(ctx, userID, ids)
|
||||
}
|
||||
|
||||
// GetPassword 返回当前账号 2FA 配置。未登录或无记录时返回持久化策略的默认 no-password 配置。
|
||||
func (s *Service) GetPassword(ctx context.Context, userID int64) (domain.PasswordSettings, error) {
|
||||
if s == nil || s.passwords == nil || userID == 0 {
|
||||
|
|
@ -110,6 +182,9 @@ func normalizePasswordSettings(settings domain.PasswordSettings) domain.Password
|
|||
if settings.RecoveryEmail != "" {
|
||||
settings.HasRecovery = true
|
||||
}
|
||||
// login_email_pattern 始终从已确认的登录邮箱派生,与 2FA 恢复邮箱 RecoveryEmail
|
||||
// 解耦(历史实现曾把恢复邮箱掩码误写进此字段,导致客户端把恢复邮箱当成登录邮箱显示)。
|
||||
settings.LoginEmailPattern = emailPattern(settings.LoginEmail)
|
||||
return settings
|
||||
}
|
||||
|
||||
|
|
@ -197,7 +272,6 @@ func (s *Service) UpdatePasswordSettings(ctx context.Context, userID int64, chec
|
|||
}
|
||||
settings.RecoveryEmail = email
|
||||
settings.HasRecovery = email != ""
|
||||
settings.LoginEmailPattern = emailPattern(email)
|
||||
settings.EmailUnconfirmedPattern = ""
|
||||
}
|
||||
settings.SecureRandom = randomBytesOrDefault(passwordHashSize, settings.SecureRandom)
|
||||
|
|
@ -378,15 +452,98 @@ func randomInt64() (int64, error) {
|
|||
}
|
||||
|
||||
func emailPattern(email string) string {
|
||||
if email == "" {
|
||||
return ""
|
||||
return domain.MaskEmail(email)
|
||||
}
|
||||
|
||||
// validLoginEmail 是登录邮箱的最小校验:非空且含 '@'。开发环境不做更严格的 RFC 校验。
|
||||
func validLoginEmail(email string) bool {
|
||||
return email != "" && strings.Contains(email, "@")
|
||||
}
|
||||
|
||||
// SetLoginEmail 为已登录用户写入登录邮箱(authed 的 emailVerifyPurposeLoginChange)。
|
||||
// 账号无 2FA 也可设置:account_passwords 行可在 has_password=false 下仅承载登录邮箱。
|
||||
func (s *Service) SetLoginEmail(ctx context.Context, userID int64, email string) error {
|
||||
if s == nil || s.passwords == nil || userID == 0 {
|
||||
return domain.ErrEmailInvalid
|
||||
}
|
||||
at := strings.Index(email, "@")
|
||||
if at <= 1 {
|
||||
return email
|
||||
email = strings.TrimSpace(email)
|
||||
if !validLoginEmail(email) {
|
||||
return domain.ErrEmailInvalid
|
||||
}
|
||||
name := email[:at]
|
||||
return name[:1] + "***" + name[len(name)-1:] + email[at:]
|
||||
settings, err := s.GetPasswordWithoutRefresh(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.LoginEmail = email
|
||||
settings.LoginEmailPattern = emailPattern(email)
|
||||
return s.passwords.Save(ctx, userID, settings)
|
||||
}
|
||||
|
||||
// SetLoginEmailByPhone 为某手机号对应的账号写入登录邮箱(登录流程中的
|
||||
// emailVerifyPurposeLoginSetup,此时尚未鉴权,只能凭 phone 定位用户)。
|
||||
func (s *Service) SetLoginEmailByPhone(ctx context.Context, phone, email string) error {
|
||||
userID, found, err := s.userIDByPhone(ctx, phone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return domain.ErrEmailInvalid
|
||||
}
|
||||
return s.SetLoginEmail(ctx, userID, email)
|
||||
}
|
||||
|
||||
// LoginEmail 返回已登录用户的登录邮箱原始地址(用于 verifyEmail 回显 emailVerified.email)。
|
||||
func (s *Service) LoginEmail(ctx context.Context, userID int64) (string, bool, error) {
|
||||
if s == nil || s.passwords == nil || userID == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
settings, found, err := s.passwords.GetByUser(ctx, userID)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if !found || settings.LoginEmail == "" {
|
||||
return "", false, nil
|
||||
}
|
||||
return settings.LoginEmail, true, nil
|
||||
}
|
||||
|
||||
// LoginEmailByPhone 按手机号返回登录邮箱原始地址(供 auth.sendCode 检测是否改投邮箱、
|
||||
// login-setup 回显、reset 回显使用)。
|
||||
func (s *Service) LoginEmailByPhone(ctx context.Context, phone string) (string, bool, error) {
|
||||
userID, found, err := s.userIDByPhone(ctx, phone)
|
||||
if err != nil || !found {
|
||||
return "", false, err
|
||||
}
|
||||
return s.LoginEmail(ctx, userID)
|
||||
}
|
||||
|
||||
// ClearLoginEmailByPhone 清除某手机号账号的登录邮箱(auth.resetLoginEmail)。
|
||||
func (s *Service) ClearLoginEmailByPhone(ctx context.Context, phone string) error {
|
||||
userID, found, err := s.userIDByPhone(ctx, phone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
settings, found, err := s.passwords.GetByUser(ctx, userID)
|
||||
if err != nil || !found {
|
||||
return err
|
||||
}
|
||||
settings.LoginEmail = ""
|
||||
settings.LoginEmailPattern = ""
|
||||
return s.passwords.Save(ctx, userID, settings)
|
||||
}
|
||||
|
||||
func (s *Service) userIDByPhone(ctx context.Context, phone string) (int64, bool, error) {
|
||||
if s == nil || s.users == nil {
|
||||
return 0, false, nil
|
||||
}
|
||||
u, found, err := s.users.ByPhone(ctx, domain.NormalizePhone(phone))
|
||||
if err != nil || !found {
|
||||
return 0, false, err
|
||||
}
|
||||
return u.ID, true, nil
|
||||
}
|
||||
|
||||
// GetReactionSettings returns account-level reaction preferences.
|
||||
|
|
@ -420,7 +577,7 @@ func (s *Service) SetDefaultReaction(ctx context.Context, userID int64, reaction
|
|||
if err != nil {
|
||||
return domain.AccountReactionSettings{}, err
|
||||
}
|
||||
if reaction.Type == "" || reaction.Emoticon == "" {
|
||||
if !reaction.Valid() {
|
||||
reaction = domain.DefaultAccountReactionSettings().DefaultReaction
|
||||
}
|
||||
settings.DefaultReaction = reaction
|
||||
|
|
@ -445,10 +602,157 @@ func (s *Service) saveReactionSettings(ctx context.Context, userID int64, settin
|
|||
return settings, s.reactions.SaveReactionSettings(ctx, userID, settings)
|
||||
}
|
||||
|
||||
// GetAccountSettings 返回账号级单例设置(未持久化时回落默认)。
|
||||
func (s *Service) GetAccountSettings(ctx context.Context, userID int64) (domain.AccountSettings, error) {
|
||||
if s == nil || s.settings == nil || userID == 0 {
|
||||
return domain.DefaultAccountSettings(), nil
|
||||
}
|
||||
settings, found, err := s.settings.GetAccountSettings(ctx, userID)
|
||||
if err != nil {
|
||||
return domain.AccountSettings{}, err
|
||||
}
|
||||
if !found {
|
||||
return domain.DefaultAccountSettings(), nil
|
||||
}
|
||||
settings.AccountTTLDays = settings.NormalizedTTLDays()
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// SetGlobalPrivacy 持久化账号全局隐私开关,返回合并后的完整设置。
|
||||
func (s *Service) SetGlobalPrivacy(ctx context.Context, userID int64, privacy domain.GlobalPrivacy) (domain.AccountSettings, error) {
|
||||
settings, err := s.GetAccountSettings(ctx, userID)
|
||||
if err != nil {
|
||||
return domain.AccountSettings{}, err
|
||||
}
|
||||
if privacy.NoncontactPeersPaidStars < 0 {
|
||||
privacy.NoncontactPeersPaidStars = 0
|
||||
}
|
||||
settings.GlobalPrivacy = privacy
|
||||
return s.saveAccountSettings(ctx, userID, settings)
|
||||
}
|
||||
|
||||
// SetAccountTTL 持久化账号自毁期限(钳制 >0)。
|
||||
func (s *Service) SetAccountTTL(ctx context.Context, userID int64, days int) (domain.AccountSettings, error) {
|
||||
settings, err := s.GetAccountSettings(ctx, userID)
|
||||
if err != nil {
|
||||
return domain.AccountSettings{}, err
|
||||
}
|
||||
settings.AccountTTLDays = days
|
||||
settings.AccountTTLDays = settings.NormalizedTTLDays()
|
||||
return s.saveAccountSettings(ctx, userID, settings)
|
||||
}
|
||||
|
||||
// SetSensitiveContent 持久化敏感内容查看开关。
|
||||
func (s *Service) SetSensitiveContent(ctx context.Context, userID int64, enabled bool) (domain.AccountSettings, error) {
|
||||
settings, err := s.GetAccountSettings(ctx, userID)
|
||||
if err != nil {
|
||||
return domain.AccountSettings{}, err
|
||||
}
|
||||
settings.SensitiveContentEnabled = enabled
|
||||
return s.saveAccountSettings(ctx, userID, settings)
|
||||
}
|
||||
|
||||
// SetContactSignUpSilent 持久化“联系人注册时是否静音通知”。
|
||||
func (s *Service) SetContactSignUpSilent(ctx context.Context, userID int64, silent bool) (domain.AccountSettings, error) {
|
||||
settings, err := s.GetAccountSettings(ctx, userID)
|
||||
if err != nil {
|
||||
return domain.AccountSettings{}, err
|
||||
}
|
||||
settings.ContactSignUpSilent = silent
|
||||
return s.saveAccountSettings(ctx, userID, settings)
|
||||
}
|
||||
|
||||
func (s *Service) saveAccountSettings(ctx context.Context, userID int64, settings domain.AccountSettings) (domain.AccountSettings, error) {
|
||||
settings.AccountTTLDays = settings.NormalizedTTLDays()
|
||||
if settings.GlobalPrivacy.NoncontactPeersPaidStars < 0 {
|
||||
settings.GlobalPrivacy.NoncontactPeersPaidStars = 0
|
||||
}
|
||||
if s == nil || s.settings == nil || userID == 0 {
|
||||
return settings, nil
|
||||
}
|
||||
return settings, s.settings.SaveAccountSettings(ctx, userID, settings)
|
||||
}
|
||||
|
||||
// GetNotifySettings 返回某作用域的通知设置(未配置返回零值=继承默认)。
|
||||
func (s *Service) GetNotifySettings(ctx context.Context, ownerUserID int64, scope domain.NotifyScope) (domain.PeerNotifySettings, error) {
|
||||
if s == nil || s.notify == nil || ownerUserID == 0 {
|
||||
return domain.PeerNotifySettings{}, nil
|
||||
}
|
||||
settings, _, err := s.notify.GetNotifySettings(ctx, ownerUserID, scope)
|
||||
if err != nil {
|
||||
return domain.PeerNotifySettings{}, err
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// SaveNotifySettings 持久化某作用域的通知设置。
|
||||
func (s *Service) SaveNotifySettings(ctx context.Context, ownerUserID int64, scope domain.NotifyScope, settings domain.PeerNotifySettings) error {
|
||||
if s == nil || s.notify == nil || ownerUserID == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.notify.SaveNotifySettings(ctx, ownerUserID, scope, settings)
|
||||
}
|
||||
|
||||
// ResetNotifySettings 清空该用户全部作用域的通知设置(恢复默认)。
|
||||
func (s *Service) ResetNotifySettings(ctx context.Context, ownerUserID int64) error {
|
||||
if s == nil || s.notify == nil || ownerUserID == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.notify.ResetNotifySettings(ctx, ownerUserID)
|
||||
}
|
||||
|
||||
// PeerNotifySettings 批量取一组 peer 的整-peer 通知设置(dialog 列表投影)。
|
||||
func (s *Service) PeerNotifySettings(ctx context.Context, ownerUserID int64, peers []domain.Peer) (map[domain.Peer]domain.PeerNotifySettings, error) {
|
||||
if s == nil || s.notify == nil || ownerUserID == 0 || len(peers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.notify.GetPeerNotifySettings(ctx, ownerUserID, peers)
|
||||
}
|
||||
|
||||
// AllPeerNotifySettings 一次取该用户全部整-peer 通知设置(per-user notify 缓存的加载源)。
|
||||
func (s *Service) AllPeerNotifySettings(ctx context.Context, ownerUserID int64) (map[domain.Peer]domain.PeerNotifySettings, error) {
|
||||
if s == nil || s.notify == nil || ownerUserID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.notify.AllPeerNotifySettings(ctx, ownerUserID)
|
||||
}
|
||||
|
||||
// ListNotifyExceptions 列出该用户全部 per-peer 非默认通知设置(getNotifyExceptions)。
|
||||
func (s *Service) ListNotifyExceptions(ctx context.Context, ownerUserID int64) ([]domain.NotifyException, error) {
|
||||
if s == nil || s.notify == nil || ownerUserID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.notify.ListNotifyExceptions(ctx, ownerUserID)
|
||||
}
|
||||
|
||||
// SaveStickerCollectionItem 收藏/最近/GIF 集合的加入或移除(最新置顶、按类别上界截断)。
|
||||
func (s *Service) SaveStickerCollectionItem(ctx context.Context, userID int64, kind domain.StickerCollectionKind, documentID int64, unsave bool, now int) error {
|
||||
if s == nil || s.stickers == nil || userID == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.stickers.SaveStickerCollectionItem(ctx, userID, kind, documentID, unsave, now, domain.MaxStickerCollectionItems(kind))
|
||||
}
|
||||
|
||||
// ListStickerCollection 取某类个人贴纸集合(最新在前)。
|
||||
func (s *Service) ListStickerCollection(ctx context.Context, userID int64, kind domain.StickerCollectionKind, limit int) ([]domain.StickerCollectionItem, error) {
|
||||
if s == nil || s.stickers == nil || userID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.stickers.ListStickerCollection(ctx, userID, kind, limit)
|
||||
}
|
||||
|
||||
// ClearStickerCollection 清空某类个人贴纸集合。
|
||||
func (s *Service) ClearStickerCollection(ctx context.Context, userID int64, kind domain.StickerCollectionKind) error {
|
||||
if s == nil || s.stickers == nil || userID == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.stickers.ClearStickerCollection(ctx, userID, kind)
|
||||
}
|
||||
|
||||
func normalizeReactionSettings(settings domain.AccountReactionSettings) domain.AccountReactionSettings {
|
||||
defaults := domain.DefaultAccountReactionSettings()
|
||||
settings.Notify = normalizeNotifySettings(settings.Notify)
|
||||
if settings.DefaultReaction.Type == "" || settings.DefaultReaction.Emoticon == "" {
|
||||
if !settings.DefaultReaction.Valid() {
|
||||
settings.DefaultReaction = defaults.DefaultReaction
|
||||
}
|
||||
settings.PaidPrivacy = normalizePaidPrivacy(settings.PaidPrivacy)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue