Compare commits
No commits in common. "fff8b431ab269e47eb1b13c94873eefb0b54e7be" and "3ca8ef1a16c35b35895e23139677016a9e8a7828" have entirely different histories.
fff8b431ab
...
3ca8ef1a16
32 changed files with 44 additions and 750 deletions
|
|
@ -1143,7 +1143,6 @@ func run(logger *zap.Logger) error {
|
||||||
contacts.WithPhotoProvider(cachedPhotos),
|
contacts.WithPhotoProvider(cachedPhotos),
|
||||||
contacts.WithPrivacyEvaluator(privacyService),
|
contacts.WithPrivacyEvaluator(privacyService),
|
||||||
contacts.WithAccountFreezeProvider(userProjectionFacts),
|
contacts.WithAccountFreezeProvider(userProjectionFacts),
|
||||||
contacts.WithAccountRestrictionProvider(userProjectionFacts),
|
|
||||||
contacts.WithReadModelVersions(readModelVersionStore),
|
contacts.WithReadModelVersions(readModelVersionStore),
|
||||||
contacts.WithHideThirdPartyVerification(cfg.HideThirdPartyVerification),
|
contacts.WithHideThirdPartyVerification(cfg.HideThirdPartyVerification),
|
||||||
)
|
)
|
||||||
|
|
@ -1371,7 +1370,7 @@ func run(logger *zap.Logger) error {
|
||||||
passkeyapp.WithAllowedOrigins(cfg.PasskeyAllowedOrigins))
|
passkeyapp.WithAllowedOrigins(cfg.PasskeyAllowedOrigins))
|
||||||
// 自定义云主题(Create a New Theme):主题目录与每用户已安装列表均持久化到 postgres。
|
// 自定义云主题(Create a New Theme):主题目录与每用户已安装列表均持久化到 postgres。
|
||||||
themeService := themesapp.NewService(postgres.NewThemeStore(pool))
|
themeService := themesapp.NewService(postgres.NewThemeStore(pool))
|
||||||
usersService := users.NewService(userStore, users.WithBaseUserCache(userCache), users.WithContactStore(contactStore), users.WithPhotoProvider(cachedPhotos), users.WithPrivacyEvaluator(privacyService), users.WithAccountFreezeProvider(userProjectionFacts), users.WithAccountRestrictionProvider(userProjectionFacts), users.WithHideThirdPartyVerification(cfg.HideThirdPartyVerification), users.WithReservedUsernames(cfg.ReservedUsernames))
|
usersService := users.NewService(userStore, users.WithBaseUserCache(userCache), users.WithContactStore(contactStore), users.WithPhotoProvider(cachedPhotos), users.WithPrivacyEvaluator(privacyService), users.WithAccountFreezeProvider(userProjectionFacts), users.WithHideThirdPartyVerification(cfg.HideThirdPartyVerification), users.WithReservedUsernames(cfg.ReservedUsernames))
|
||||||
privacyService.ConfigureReadModels(usersService, channelStore)
|
privacyService.ConfigureReadModels(usersService, channelStore)
|
||||||
aiComposeService := aiapp.NewService(aiComposeStore, newAIComposeOptions(cfg, rateLimiter, usersService.PremiumActive, logger)...)
|
aiComposeService := aiapp.NewService(aiComposeStore, newAIComposeOptions(cfg, rateLimiter, usersService.PremiumActive, logger)...)
|
||||||
botsService.SetAIChatGenerator(aiComposeService)
|
botsService.SetAIChatGenerator(aiComposeService)
|
||||||
|
|
@ -1380,7 +1379,6 @@ func run(logger *zap.Logger) error {
|
||||||
dialogs.WithPhotoProvider(cachedPhotos),
|
dialogs.WithPhotoProvider(cachedPhotos),
|
||||||
dialogs.WithPrivacyEvaluator(privacyService),
|
dialogs.WithPrivacyEvaluator(privacyService),
|
||||||
dialogs.WithAccountFreezeProvider(userProjectionFacts),
|
dialogs.WithAccountFreezeProvider(userProjectionFacts),
|
||||||
dialogs.WithAccountRestrictionProvider(userProjectionFacts),
|
|
||||||
dialogs.WithPremiumChecker(usersService.PremiumActive),
|
dialogs.WithPremiumChecker(usersService.PremiumActive),
|
||||||
dialogs.WithReadModelVersions(readModelVersionStore),
|
dialogs.WithReadModelVersions(readModelVersionStore),
|
||||||
dialogs.WithDialogHydrationCaches(
|
dialogs.WithDialogHydrationCaches(
|
||||||
|
|
@ -1428,7 +1426,6 @@ func run(logger *zap.Logger) error {
|
||||||
messageapp.WithPhotoProvider(cachedPhotos),
|
messageapp.WithPhotoProvider(cachedPhotos),
|
||||||
messageapp.WithPrivacyEvaluator(privacyService),
|
messageapp.WithPrivacyEvaluator(privacyService),
|
||||||
messageapp.WithAccountFreezeProvider(userProjectionFacts),
|
messageapp.WithAccountFreezeProvider(userProjectionFacts),
|
||||||
messageapp.WithAccountRestrictionProvider(userProjectionFacts),
|
|
||||||
messageapp.WithReadModelVersions(readModelVersionStore),
|
messageapp.WithReadModelVersions(readModelVersionStore),
|
||||||
messageapp.WithBotResponder(botsService),
|
messageapp.WithBotResponder(botsService),
|
||||||
messageapp.WithSendPermissionChecker(adminService),
|
messageapp.WithSendPermissionChecker(adminService),
|
||||||
|
|
@ -1624,7 +1621,6 @@ func run(logger *zap.Logger) error {
|
||||||
AppUpdates: appUpdateResolver,
|
AppUpdates: appUpdateResolver,
|
||||||
AccountFreeze: userProjectionFacts,
|
AccountFreeze: userProjectionFacts,
|
||||||
AccountFreezeNotifications: adminService,
|
AccountFreezeNotifications: adminService,
|
||||||
AccountRestriction: userProjectionFacts,
|
|
||||||
AICompose: aiComposeService,
|
AICompose: aiComposeService,
|
||||||
Ephemeral: ephemeralService,
|
Ephemeral: ephemeralService,
|
||||||
EphemeralPush: ephemeralStore,
|
EphemeralPush: ephemeralStore,
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
DROP TABLE IF EXISTS account_message_restrictions;
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
CREATE TABLE IF NOT EXISTS account_message_restrictions (
|
|
||||||
user_id bigint PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
restricted boolean NOT NULL DEFAULT false,
|
|
||||||
version bigint NOT NULL DEFAULT 0,
|
|
||||||
restricted_since timestamptz,
|
|
||||||
restricted_until timestamptz,
|
|
||||||
reason text NOT NULL DEFAULT '',
|
|
||||||
actor text NOT NULL DEFAULT '',
|
|
||||||
command_id text NOT NULL DEFAULT '',
|
|
||||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
||||||
CONSTRAINT account_message_restrictions_user_id_check CHECK (user_id > 0),
|
|
||||||
CONSTRAINT account_message_restrictions_shape_check CHECK (
|
|
||||||
(restricted AND restricted_since IS NOT NULL
|
|
||||||
AND (restricted_until IS NULL OR restricted_until > restricted_since)
|
|
||||||
AND (restricted_until IS NULL OR restricted_until <= to_timestamp(2147483647)))
|
|
||||||
OR
|
|
||||||
(NOT restricted AND restricted_since IS NULL AND restricted_until IS NULL)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS account_message_restrictions_restricted_idx
|
|
||||||
ON account_message_restrictions (user_id)
|
|
||||||
WHERE restricted;
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
ALTER TABLE public.moderation_actions
|
|
||||||
DROP CONSTRAINT moderation_actions_kind_check;
|
|
||||||
|
|
||||||
ALTER TABLE public.moderation_actions
|
|
||||||
ADD CONSTRAINT moderation_actions_kind_check CHECK (kind IN (
|
|
||||||
'mark_scam', 'mark_fake', 'clear_peer_flags', 'freeze_account',
|
|
||||||
'unfreeze_account', 'delete_private_message',
|
|
||||||
'delete_channel_message', 'delete_account'
|
|
||||||
));
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
ALTER TABLE public.moderation_actions
|
|
||||||
DROP CONSTRAINT moderation_actions_kind_check;
|
|
||||||
|
|
||||||
ALTER TABLE public.moderation_actions
|
|
||||||
ADD CONSTRAINT moderation_actions_kind_check CHECK (kind IN (
|
|
||||||
'mark_scam', 'mark_fake', 'clear_peer_flags', 'freeze_account',
|
|
||||||
'unfreeze_account', 'restrict_account', 'unrestrict_account',
|
|
||||||
'delete_private_message', 'delete_channel_message', 'delete_account'
|
|
||||||
));
|
|
||||||
|
|
@ -24,7 +24,6 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ActionSetAccountFrozen = "account.set_frozen"
|
ActionSetAccountFrozen = "account.set_frozen"
|
||||||
ActionSetAccountRestricted = "account.set_restricted"
|
|
||||||
ActionGrantPremium = "account.grant_premium"
|
ActionGrantPremium = "account.grant_premium"
|
||||||
ActionSetVerified = "account.set_verified"
|
ActionSetVerified = "account.set_verified"
|
||||||
ActionSetUserFlags = "account.set_flags"
|
ActionSetUserFlags = "account.set_flags"
|
||||||
|
|
@ -180,12 +179,6 @@ type CommandRepository interface {
|
||||||
type RestrictionStore interface {
|
type RestrictionStore interface {
|
||||||
GetAccountFreeze(ctx context.Context, userID int64) (domain.AccountFreeze, bool, error)
|
GetAccountFreeze(ctx context.Context, userID int64) (domain.AccountFreeze, bool, error)
|
||||||
SetAccountFreeze(ctx context.Context, freeze domain.AccountFreeze) (domain.AccountFreeze, error)
|
SetAccountFreeze(ctx context.Context, freeze domain.AccountFreeze) (domain.AccountFreeze, error)
|
||||||
GetAccountRestriction(ctx context.Context, userID int64) (domain.AccountRestriction, bool, error)
|
|
||||||
SetAccountRestriction(ctx context.Context, restriction domain.AccountRestriction) (domain.AccountRestriction, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type accountRestrictionBatchStore interface {
|
|
||||||
GetAccountRestrictions(ctx context.Context, userIDs []int64) (map[int64]domain.AccountRestriction, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type accountFreezeBatchStore interface {
|
type accountFreezeBatchStore interface {
|
||||||
|
|
@ -832,13 +825,6 @@ type SetAccountFrozenRequest struct {
|
||||||
AppealURL string `json:"freeze_appeal_url,omitempty"`
|
AppealURL string `json:"freeze_appeal_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SetAccountRestrictedRequest struct {
|
|
||||||
CommandMeta
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
Restricted bool `json:"restricted"`
|
|
||||||
Until time.Time `json:"restricted_until,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GrantPremiumRequest struct {
|
type GrantPremiumRequest struct {
|
||||||
CommandMeta
|
CommandMeta
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
|
|
@ -1286,134 +1272,6 @@ func (s *Service) SetAccountFrozen(ctx context.Context, req SetAccountFrozenRequ
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountRestriction returns the durable narrower spam-restriction state. A
|
|
||||||
// missing row is the only non-restricted default.
|
|
||||||
func (s *Service) AccountRestriction(ctx context.Context, userID int64) (domain.AccountRestriction, bool, error) {
|
|
||||||
if s == nil || s.restrictions == nil || userID == 0 {
|
|
||||||
return domain.AccountRestriction{}, false, nil
|
|
||||||
}
|
|
||||||
restriction, found, err := s.restrictions.GetAccountRestriction(ctx, userID)
|
|
||||||
if err != nil || !found {
|
|
||||||
return restriction, found, err
|
|
||||||
}
|
|
||||||
if err := validateAccountRestriction(restriction); err != nil {
|
|
||||||
return domain.AccountRestriction{}, false, fmt.Errorf("invalid durable account restriction for user %d: %w", userID, err)
|
|
||||||
}
|
|
||||||
return restriction, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) AccountRestrictions(ctx context.Context, userIDs []int64) (map[int64]domain.AccountRestriction, error) {
|
|
||||||
out := make(map[int64]domain.AccountRestriction)
|
|
||||||
if s == nil || s.restrictions == nil || len(userIDs) == 0 {
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
ids := uniqueFreezeUserIDs(userIDs)
|
|
||||||
if batch, ok := s.restrictions.(accountRestrictionBatchStore); ok {
|
|
||||||
const batchSize = 1000
|
|
||||||
for start := 0; start < len(ids); start += batchSize {
|
|
||||||
end := min(start+batchSize, len(ids))
|
|
||||||
items, err := batch.GetAccountRestrictions(ctx, ids[start:end])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for id, restriction := range items {
|
|
||||||
if err := validateAccountRestriction(restriction); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid durable account restriction for user %d: %w", id, err)
|
|
||||||
}
|
|
||||||
if restriction.Restricted {
|
|
||||||
out[id] = restriction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
for _, id := range ids {
|
|
||||||
restriction, found, err := s.AccountRestriction(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if found && restriction.Restricted {
|
|
||||||
out[id] = restriction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateAccountRestriction(restriction domain.AccountRestriction) error {
|
|
||||||
if !restriction.Restricted {
|
|
||||||
if !restriction.Since.IsZero() || !restriction.Until.IsZero() {
|
|
||||||
return fmt.Errorf("inactive restriction retains client-visible state")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if restriction.Since.IsZero() || restriction.Since.Unix() <= 0 {
|
|
||||||
return fmt.Errorf("active restriction has invalid since")
|
|
||||||
}
|
|
||||||
if !restriction.Until.IsZero() && (!restriction.Until.After(restriction.Since) || restriction.Until.Unix() > math.MaxInt32) {
|
|
||||||
return fmt.Errorf("active restriction has invalid until")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SetAccountRestricted(ctx context.Context, req SetAccountRestrictedRequest) (CommandResult, error) {
|
|
||||||
if req.UserID <= 0 {
|
|
||||||
return CommandResult{}, fmt.Errorf("user_id is required")
|
|
||||||
}
|
|
||||||
if s == nil || s.restrictions == nil {
|
|
||||||
return CommandResult{}, fmt.Errorf("admin restriction store is not configured")
|
|
||||||
}
|
|
||||||
now := s.now().UTC()
|
|
||||||
if req.Restricted && !req.Until.IsZero() && req.Until.Unix() > math.MaxInt32 {
|
|
||||||
return CommandResult{}, fmt.Errorf("restricted_until must be a valid int32 Unix timestamp")
|
|
||||||
}
|
|
||||||
return s.runCommand(ctx, req.CommandMeta, ActionSetAccountRestricted, req.UserID, domain.Peer{}, req, func() (CommandResult, error) {
|
|
||||||
if req.Restricted && !req.Until.IsZero() && !req.Until.After(now) {
|
|
||||||
return CommandResult{}, fmt.Errorf("restricted_until must be in the future")
|
|
||||||
}
|
|
||||||
prev, found, err := s.restrictions.GetAccountRestriction(ctx, req.UserID)
|
|
||||||
if err != nil {
|
|
||||||
return CommandResult{}, err
|
|
||||||
}
|
|
||||||
next := domain.AccountRestriction{
|
|
||||||
UserID: req.UserID,
|
|
||||||
Restricted: req.Restricted,
|
|
||||||
Reason: req.Reason,
|
|
||||||
Actor: req.Actor,
|
|
||||||
CommandID: req.CommandID,
|
|
||||||
}
|
|
||||||
if req.Restricted {
|
|
||||||
next.Since = now
|
|
||||||
if found && prev.Restricted {
|
|
||||||
next.Since = prev.Since
|
|
||||||
}
|
|
||||||
next.Until = req.Until.UTC()
|
|
||||||
}
|
|
||||||
wouldChange := !found || prev.Restricted != next.Restricted ||
|
|
||||||
!prev.Since.Equal(next.Since) || !prev.Until.Equal(next.Until)
|
|
||||||
details := map[string]any{
|
|
||||||
"previous_restricted": found && prev.Restricted,
|
|
||||||
"new_restricted": req.Restricted,
|
|
||||||
"would_change": wouldChange,
|
|
||||||
}
|
|
||||||
if req.Restricted {
|
|
||||||
details["restricted_since"] = next.Since.Format(time.RFC3339)
|
|
||||||
if !next.Until.IsZero() {
|
|
||||||
details["restricted_until"] = next.Until.Format(time.RFC3339)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if req.DryRun {
|
|
||||||
return CommandResult{Message: "dry-run completed", Details: details}, nil
|
|
||||||
}
|
|
||||||
updated, err := s.restrictions.SetAccountRestriction(ctx, next)
|
|
||||||
if err != nil {
|
|
||||||
return CommandResult{}, err
|
|
||||||
}
|
|
||||||
details["updated_at"] = updated.UpdatedAt.UTC().Format(time.RFC3339)
|
|
||||||
details["version"] = updated.Version
|
|
||||||
return CommandResult{Message: "account restriction updated", Details: details}, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GrantPremium(ctx context.Context, req GrantPremiumRequest) (CommandResult, error) {
|
func (s *Service) GrantPremium(ctx context.Context, req GrantPremiumRequest) (CommandResult, error) {
|
||||||
if req.UserID <= 0 {
|
if req.UserID <= 0 {
|
||||||
return CommandResult{}, fmt.Errorf("user_id is required")
|
return CommandResult{}, fmt.Errorf("user_id is required")
|
||||||
|
|
|
||||||
|
|
@ -576,10 +576,8 @@ func (f *fakeBotService) AdminExportBotToken(_ context.Context, botUserID int64)
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeRestrictionStore struct {
|
type fakeRestrictionStore struct {
|
||||||
items map[int64]domain.AccountFreeze
|
items map[int64]domain.AccountFreeze
|
||||||
setCalls int
|
setCalls int
|
||||||
restrictionItems map[int64]domain.AccountRestriction
|
|
||||||
restrictionSetCall int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeRestrictionStore) GetAccountFreeze(_ context.Context, userID int64) (domain.AccountFreeze, bool, error) {
|
func (f *fakeRestrictionStore) GetAccountFreeze(_ context.Context, userID int64) (domain.AccountFreeze, bool, error) {
|
||||||
|
|
@ -601,25 +599,6 @@ func (f *fakeRestrictionStore) SetAccountFreeze(_ context.Context, r domain.Acco
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeRestrictionStore) GetAccountRestriction(_ context.Context, userID int64) (domain.AccountRestriction, bool, error) {
|
|
||||||
if f.restrictionItems == nil {
|
|
||||||
return domain.AccountRestriction{}, false, nil
|
|
||||||
}
|
|
||||||
r, ok := f.restrictionItems[userID]
|
|
||||||
return r, ok, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeRestrictionStore) SetAccountRestriction(_ context.Context, r domain.AccountRestriction) (domain.AccountRestriction, error) {
|
|
||||||
if f.restrictionItems == nil {
|
|
||||||
f.restrictionItems = map[int64]domain.AccountRestriction{}
|
|
||||||
}
|
|
||||||
f.restrictionSetCall++
|
|
||||||
r.Version = f.restrictionItems[r.UserID].Version + 1
|
|
||||||
r.UpdatedAt = fixedNow()
|
|
||||||
f.restrictionItems[r.UserID] = r
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeBatchRestrictionStore struct {
|
type fakeBatchRestrictionStore struct {
|
||||||
fakeRestrictionStore
|
fakeRestrictionStore
|
||||||
requests [][]int64
|
requests [][]int64
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ type Config struct {
|
||||||
type Service interface {
|
type Service interface {
|
||||||
AccountAvatar(ctx context.Context, userID int64) ([]byte, string, bool, error)
|
AccountAvatar(ctx context.Context, userID int64) ([]byte, string, bool, error)
|
||||||
SetAccountFrozen(ctx context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error)
|
SetAccountFrozen(ctx context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error)
|
||||||
SetAccountRestricted(ctx context.Context, req admin.SetAccountRestrictedRequest) (admin.CommandResult, error)
|
|
||||||
GrantPremium(ctx context.Context, req admin.GrantPremiumRequest) (admin.CommandResult, error)
|
GrantPremium(ctx context.Context, req admin.GrantPremiumRequest) (admin.CommandResult, error)
|
||||||
SetVerified(ctx context.Context, req admin.SetVerifiedRequest) (admin.CommandResult, error)
|
SetVerified(ctx context.Context, req admin.SetVerifiedRequest) (admin.CommandResult, error)
|
||||||
SetUserFlags(ctx context.Context, req admin.SetUserFlagsRequest) (admin.CommandResult, error)
|
SetUserFlags(ctx context.Context, req admin.SetUserFlagsRequest) (admin.CommandResult, error)
|
||||||
|
|
@ -183,7 +182,6 @@ func (s *Server) routes() http.Handler {
|
||||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||||
})
|
})
|
||||||
mux.HandleFunc("POST /v1/accounts/set-frozen", s.authenticated(s.handleSetAccountFrozen))
|
mux.HandleFunc("POST /v1/accounts/set-frozen", s.authenticated(s.handleSetAccountFrozen))
|
||||||
mux.HandleFunc("POST /v1/accounts/set-restricted", s.authenticated(s.handleSetAccountRestricted))
|
|
||||||
mux.HandleFunc("GET /v1/accounts/{id}/avatar", s.authenticated(s.handleAccountAvatar))
|
mux.HandleFunc("GET /v1/accounts/{id}/avatar", s.authenticated(s.handleAccountAvatar))
|
||||||
mux.HandleFunc("POST /v1/accounts/grant-premium", s.authenticated(s.handleGrantPremium))
|
mux.HandleFunc("POST /v1/accounts/grant-premium", s.authenticated(s.handleGrantPremium))
|
||||||
mux.HandleFunc("POST /v1/accounts/set-verified", s.authenticated(s.handleSetVerified))
|
mux.HandleFunc("POST /v1/accounts/set-verified", s.authenticated(s.handleSetVerified))
|
||||||
|
|
@ -311,15 +309,6 @@ func (s *Server) handleSetAccountFrozen(w http.ResponseWriter, r *http.Request)
|
||||||
writeCommandResult(w, result, err)
|
writeCommandResult(w, result, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleSetAccountRestricted(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req admin.SetAccountRestrictedRequest
|
|
||||||
if !decodeJSON(w, r, &req) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result, err := s.svc.SetAccountRestricted(r.Context(), req)
|
|
||||||
writeCommandResult(w, result, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleGrantPremium(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGrantPremium(w http.ResponseWriter, r *http.Request) {
|
||||||
var req admin.GrantPremiumRequest
|
var req admin.GrantPremiumRequest
|
||||||
if !decodeJSON(w, r, &req) {
|
if !decodeJSON(w, r, &req) {
|
||||||
|
|
|
||||||
|
|
@ -300,10 +300,6 @@ func (s *captureFreezeService) SetAccountFrozen(_ context.Context, req admin.Set
|
||||||
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
|
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fakeService) SetAccountRestricted(_ context.Context, req admin.SetAccountRestrictedRequest) (admin.CommandResult, error) {
|
|
||||||
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fakeService) SetAccountFrozen(_ context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error) {
|
func (fakeService) SetAccountFrozen(_ context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error) {
|
||||||
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
|
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,14 @@ type phonePrivacyService interface {
|
||||||
|
|
||||||
// Service 提供通讯录查询。
|
// Service 提供通讯录查询。
|
||||||
type Service struct {
|
type Service struct {
|
||||||
contacts store.ContactStore
|
contacts store.ContactStore
|
||||||
users store.UserStore
|
users store.UserStore
|
||||||
photos userprojection.ProfilePhotoProvider
|
photos userprojection.ProfilePhotoProvider
|
||||||
privacy phonePrivacyService
|
privacy phonePrivacyService
|
||||||
freezes userprojection.AccountFreezeProvider
|
freezes userprojection.AccountFreezeProvider
|
||||||
restrictions userprojection.AccountRestrictionProvider
|
projector *userprojection.Projector
|
||||||
projector *userprojection.Projector
|
versions store.ReadModelVersionStore
|
||||||
versions store.ReadModelVersionStore
|
cache *contactListReadModelCache
|
||||||
cache *contactListReadModelCache
|
|
||||||
// hideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
// hideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
||||||
// while true, Search never returns @marksbot (domain.VerifierBotUserID),
|
// while true, Search never returns @marksbot (domain.VerifierBotUserID),
|
||||||
// so the account is unreachable for a client that doesn't already know it.
|
// so the account is unreachable for a client that doesn't already know it.
|
||||||
|
|
@ -60,11 +59,6 @@ func WithAccountFreezeProvider(p userprojection.AccountFreezeProvider) Option {
|
||||||
return func(s *Service) { s.freezes = p }
|
return func(s *Service) { s.freezes = p }
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAccountRestrictionProvider injects the account spam-restriction reader.
|
|
||||||
func WithAccountRestrictionProvider(p userprojection.AccountRestrictionProvider) Option {
|
|
||||||
return func(s *Service) { s.restrictions = p }
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithHideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
// WithHideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
||||||
// while true, Search filters @marksbot out of every result set.
|
// while true, Search filters @marksbot out of every result set.
|
||||||
func WithHideThirdPartyVerification(hidden bool) Option {
|
func WithHideThirdPartyVerification(hidden bool) Option {
|
||||||
|
|
@ -107,7 +101,6 @@ func (s *Service) rebuildProjector() {
|
||||||
userprojection.WithPhotoProvider(s.photos),
|
userprojection.WithPhotoProvider(s.photos),
|
||||||
userprojection.WithPrivacyEvaluator(s.privacy),
|
userprojection.WithPrivacyEvaluator(s.privacy),
|
||||||
userprojection.WithAccountFreezeProvider(s.freezes),
|
userprojection.WithAccountFreezeProvider(s.freezes),
|
||||||
userprojection.WithAccountRestrictionProvider(s.restrictions),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,15 +126,6 @@ func (s *Service) GetContacts(ctx context.Context, userID int64, hash int64) (do
|
||||||
return list, false, nil
|
return list, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsContact reports whether contactUserID is in userID's own contact list.
|
|
||||||
func (s *Service) IsContact(ctx context.Context, userID, contactUserID int64) (bool, error) {
|
|
||||||
if s == nil || s.contacts == nil || userID == 0 || contactUserID == 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
_, found, err := s.contacts.Get(ctx, userID, contactUserID)
|
|
||||||
return found, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) AddContact(ctx context.Context, userID int64, input domain.ContactInput) (domain.Contact, error) {
|
func (s *Service) AddContact(ctx context.Context, userID int64, input domain.ContactInput) (domain.Contact, error) {
|
||||||
if s == nil || s.contacts == nil || userID == 0 || input.ContactUserID == 0 || input.ContactUserID == userID {
|
if s == nil || s.contacts == nil || userID == 0 || input.ContactUserID == 0 || input.ContactUserID == userID {
|
||||||
return domain.Contact{}, ErrContactIDInvalid
|
return domain.Contact{}, ErrContactIDInvalid
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ type Service struct {
|
||||||
photos userprojection.ProfilePhotoProvider
|
photos userprojection.ProfilePhotoProvider
|
||||||
privacy userprojection.PrivacyEvaluator
|
privacy userprojection.PrivacyEvaluator
|
||||||
freezes userprojection.AccountFreezeProvider
|
freezes userprojection.AccountFreezeProvider
|
||||||
restrictions userprojection.AccountRestrictionProvider
|
|
||||||
premium PremiumChecker
|
premium PremiumChecker
|
||||||
projector *userprojection.Projector
|
projector *userprojection.Projector
|
||||||
versions store.ReadModelVersionStore
|
versions store.ReadModelVersionStore
|
||||||
|
|
@ -64,11 +63,6 @@ func WithAccountFreezeProvider(p userprojection.AccountFreezeProvider) Option {
|
||||||
return func(s *Service) { s.freezes = p }
|
return func(s *Service) { s.freezes = p }
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAccountRestrictionProvider injects the account spam-restriction reader.
|
|
||||||
func WithAccountRestrictionProvider(p userprojection.AccountRestrictionProvider) Option {
|
|
||||||
return func(s *Service) { s.restrictions = p }
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithReadModelVersions enables durable version-token backed peer dialog caching.
|
// WithReadModelVersions enables durable version-token backed peer dialog caching.
|
||||||
func WithReadModelVersions(v store.ReadModelVersionStore) Option {
|
func WithReadModelVersions(v store.ReadModelVersionStore) Option {
|
||||||
return func(s *Service) { s.versions = v }
|
return func(s *Service) { s.versions = v }
|
||||||
|
|
@ -138,7 +132,6 @@ func (s *Service) rebuildProjector() {
|
||||||
userprojection.WithPhotoProvider(s.photos),
|
userprojection.WithPhotoProvider(s.photos),
|
||||||
userprojection.WithPrivacyEvaluator(s.privacy),
|
userprojection.WithPrivacyEvaluator(s.privacy),
|
||||||
userprojection.WithAccountFreezeProvider(s.freezes),
|
userprojection.WithAccountFreezeProvider(s.freezes),
|
||||||
userprojection.WithAccountRestrictionProvider(s.restrictions),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,14 @@ import (
|
||||||
|
|
||||||
// Service 提供消息历史、搜索与已读业务。
|
// Service 提供消息历史、搜索与已读业务。
|
||||||
type Service struct {
|
type Service struct {
|
||||||
messages store.MessageStore
|
messages store.MessageStore
|
||||||
dialogs store.DialogStore
|
dialogs store.DialogStore
|
||||||
contacts store.ContactStore
|
contacts store.ContactStore
|
||||||
photos userprojection.ProfilePhotoProvider
|
photos userprojection.ProfilePhotoProvider
|
||||||
privacy userprojection.PrivacyEvaluator
|
privacy userprojection.PrivacyEvaluator
|
||||||
freezes userprojection.AccountFreezeProvider
|
freezes userprojection.AccountFreezeProvider
|
||||||
restrictions userprojection.AccountRestrictionProvider
|
versions store.ReadModelVersionStore
|
||||||
versions store.ReadModelVersionStore
|
projector *userprojection.Projector
|
||||||
projector *userprojection.Projector
|
|
||||||
// viewerProjectionComplete is true only when every viewer-scoped user
|
// viewerProjectionComplete is true only when every viewer-scoped user
|
||||||
// overlay used by the shared RPC Users service is configured here too.
|
// overlay used by the shared RPC Users service is configured here too.
|
||||||
// A partially configured service may still project the dependencies it has,
|
// A partially configured service may still project the dependencies it has,
|
||||||
|
|
@ -68,11 +67,6 @@ func WithAccountFreezeProvider(p userprojection.AccountFreezeProvider) Option {
|
||||||
return func(s *Service) { s.freezes = p }
|
return func(s *Service) { s.freezes = p }
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAccountRestrictionProvider injects the account spam-restriction reader.
|
|
||||||
func WithAccountRestrictionProvider(p userprojection.AccountRestrictionProvider) Option {
|
|
||||||
return func(s *Service) { s.restrictions = p }
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithBotResponder 启用服务端内置 bot(BotFather)对私聊消息的自动应答。
|
// WithBotResponder 启用服务端内置 bot(BotFather)对私聊消息的自动应答。
|
||||||
func WithBotResponder(r BotResponder) Option {
|
func WithBotResponder(r BotResponder) Option {
|
||||||
return func(s *Service) { s.botResponder = r }
|
return func(s *Service) { s.botResponder = r }
|
||||||
|
|
@ -102,7 +96,6 @@ func NewService(messages store.MessageStore, dialogs store.DialogStore, opts ...
|
||||||
userprojection.WithPhotoProvider(s.photos),
|
userprojection.WithPhotoProvider(s.photos),
|
||||||
userprojection.WithPrivacyEvaluator(s.privacy),
|
userprojection.WithPrivacyEvaluator(s.privacy),
|
||||||
userprojection.WithAccountFreezeProvider(s.freezes),
|
userprojection.WithAccountFreezeProvider(s.freezes),
|
||||||
userprojection.WithAccountRestrictionProvider(s.restrictions),
|
|
||||||
)
|
)
|
||||||
s.viewerProjectionComplete = s.contacts != nil && s.photos != nil && s.privacy != nil && s.freezes != nil
|
s.viewerProjectionComplete = s.contacts != nil && s.photos != nil && s.privacy != nil && s.freezes != nil
|
||||||
return s
|
return s
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import (
|
||||||
|
|
||||||
type moderationAdminActions interface {
|
type moderationAdminActions interface {
|
||||||
SetAccountFrozen(ctx context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error)
|
SetAccountFrozen(ctx context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error)
|
||||||
SetAccountRestricted(ctx context.Context, req admin.SetAccountRestrictedRequest) (admin.CommandResult, error)
|
|
||||||
SetUserFlags(ctx context.Context, req admin.SetUserFlagsRequest) (admin.CommandResult, error)
|
SetUserFlags(ctx context.Context, req admin.SetUserFlagsRequest) (admin.CommandResult, error)
|
||||||
SetChannelFlags(ctx context.Context, req admin.SetChannelFlagsRequest) (admin.CommandResult, error)
|
SetChannelFlags(ctx context.Context, req admin.SetChannelFlagsRequest) (admin.CommandResult, error)
|
||||||
DeletePrivateMessages(ctx context.Context, req admin.DeletePrivateMessagesRequest) (admin.CommandResult, error)
|
DeletePrivateMessages(ctx context.Context, req admin.DeletePrivateMessagesRequest) (admin.CommandResult, error)
|
||||||
|
|
@ -100,10 +99,6 @@ type freezeAccountActionPayload struct {
|
||||||
AppealURL string `json:"appeal_url,omitempty"`
|
AppealURL string `json:"appeal_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type restrictAccountActionPayload struct {
|
|
||||||
Until time.Time `json:"until,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type deletePrivateMessageActionPayload struct {
|
type deletePrivateMessageActionPayload struct {
|
||||||
OwnerUserID int64 `json:"owner_user_id"`
|
OwnerUserID int64 `json:"owner_user_id"`
|
||||||
IDs []int `json:"ids"`
|
IDs []int `json:"ids"`
|
||||||
|
|
@ -186,23 +181,6 @@ func (e *ActionExecutor) Execute(ctx context.Context, detail domain.ModerationCa
|
||||||
Until: payload.Until, AppealURL: payload.AppealURL,
|
Until: payload.Until, AppealURL: payload.AppealURL,
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
case domain.ModerationActionRestrictAccount, domain.ModerationActionUnrestrictAccount:
|
|
||||||
if e.admin == nil || detail.Case.Target.Type != domain.PeerTypeUser {
|
|
||||||
return domain.ErrModerationActionInvalid
|
|
||||||
}
|
|
||||||
var payload restrictAccountActionPayload
|
|
||||||
if err := decodeStrictActionPayload(action.Payload, &payload); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
restricted := action.Kind == domain.ModerationActionRestrictAccount
|
|
||||||
if !restricted && !payload.Until.IsZero() {
|
|
||||||
return domain.ErrModerationActionInvalid
|
|
||||||
}
|
|
||||||
_, err := e.admin.SetAccountRestricted(ctx, admin.SetAccountRestrictedRequest{
|
|
||||||
CommandMeta: meta, UserID: detail.Case.Target.ID, Restricted: restricted,
|
|
||||||
Until: payload.Until,
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
case domain.ModerationActionDeletePrivateMessage:
|
case domain.ModerationActionDeletePrivateMessage:
|
||||||
if e.admin == nil || detail.Case.Target.Type != domain.PeerTypeUser {
|
if e.admin == nil || detail.Case.Target.Type != domain.PeerTypeUser {
|
||||||
return domain.ErrModerationActionInvalid
|
return domain.ErrModerationActionInvalid
|
||||||
|
|
@ -274,7 +252,6 @@ func (s *Service) validateDecisionActions(ctx context.Context, detail domain.Mod
|
||||||
seen := make(map[domain.ModerationActionKind]struct{}, len(actions))
|
seen := make(map[domain.ModerationActionKind]struct{}, len(actions))
|
||||||
flagActions := 0
|
flagActions := 0
|
||||||
freezeActions := 0
|
freezeActions := 0
|
||||||
restrictActions := 0
|
|
||||||
hasDeleteAccount := false
|
hasDeleteAccount := false
|
||||||
for _, action := range actions {
|
for _, action := range actions {
|
||||||
if _, duplicate := seen[action.Kind]; duplicate {
|
if _, duplicate := seen[action.Kind]; duplicate {
|
||||||
|
|
@ -301,18 +278,6 @@ func (s *Service) validateDecisionActions(ctx context.Context, detail domain.Mod
|
||||||
(!payload.Until.IsZero() || payload.AppealURL != "") {
|
(!payload.Until.IsZero() || payload.AppealURL != "") {
|
||||||
return domain.ErrModerationActionInvalid
|
return domain.ErrModerationActionInvalid
|
||||||
}
|
}
|
||||||
case domain.ModerationActionRestrictAccount, domain.ModerationActionUnrestrictAccount:
|
|
||||||
restrictActions++
|
|
||||||
if detail.Case.Target.Type != domain.PeerTypeUser {
|
|
||||||
return domain.ErrModerationActionInvalid
|
|
||||||
}
|
|
||||||
var payload restrictAccountActionPayload
|
|
||||||
if err := decodeStrictActionPayload(action.Payload, &payload); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if action.Kind == domain.ModerationActionUnrestrictAccount && !payload.Until.IsZero() {
|
|
||||||
return domain.ErrModerationActionInvalid
|
|
||||||
}
|
|
||||||
case domain.ModerationActionDeletePrivateMessage:
|
case domain.ModerationActionDeletePrivateMessage:
|
||||||
var payload deletePrivateMessageActionPayload
|
var payload deletePrivateMessageActionPayload
|
||||||
if err := decodeStrictActionPayload(action.Payload, &payload); err != nil {
|
if err := decodeStrictActionPayload(action.Payload, &payload); err != nil {
|
||||||
|
|
@ -346,7 +311,7 @@ func (s *Service) validateDecisionActions(ctx context.Context, detail domain.Mod
|
||||||
return domain.ErrModerationActionInvalid
|
return domain.ErrModerationActionInvalid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if flagActions > 1 || freezeActions > 1 || restrictActions > 1 ||
|
if flagActions > 1 || freezeActions > 1 ||
|
||||||
(hasDeleteAccount && len(actions) != 1) {
|
(hasDeleteAccount && len(actions) != 1) {
|
||||||
return domain.ErrModerationActionInvalid
|
return domain.ErrModerationActionInvalid
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type captureModerationAdmin struct {
|
type captureModerationAdmin struct {
|
||||||
userFlags []admin.SetUserFlagsRequest
|
userFlags []admin.SetUserFlagsRequest
|
||||||
frozen []admin.SetAccountFrozenRequest
|
frozen []admin.SetAccountFrozenRequest
|
||||||
restricted []admin.SetAccountRestrictedRequest
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type captureModerationAccountDeleter struct {
|
type captureModerationAccountDeleter struct {
|
||||||
|
|
@ -43,11 +42,6 @@ func (a *captureModerationAdmin) SetAccountFrozen(_ context.Context, req admin.S
|
||||||
return admin.CommandResult{}, nil
|
return admin.CommandResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *captureModerationAdmin) SetAccountRestricted(_ context.Context, req admin.SetAccountRestrictedRequest) (admin.CommandResult, error) {
|
|
||||||
a.restricted = append(a.restricted, req)
|
|
||||||
return admin.CommandResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *captureModerationAdmin) SetUserFlags(_ context.Context, req admin.SetUserFlagsRequest) (admin.CommandResult, error) {
|
func (a *captureModerationAdmin) SetUserFlags(_ context.Context, req admin.SetUserFlagsRequest) (admin.CommandResult, error) {
|
||||||
a.userFlags = append(a.userFlags, req)
|
a.userFlags = append(a.userFlags, req)
|
||||||
return admin.CommandResult{}, nil
|
return admin.CommandResult{}, nil
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ func (s *Service) ReviewAppeal(ctx context.Context, request domain.ModerationDec
|
||||||
func validateAppealRemedyActions(detail domain.ModerationCaseDetail, actions []domain.ModerationActionDraft) error {
|
func validateAppealRemedyActions(detail domain.ModerationCaseDetail, actions []domain.ModerationActionDraft) error {
|
||||||
history := append([]domain.ModerationAction(nil), detail.Actions...)
|
history := append([]domain.ModerationAction(nil), detail.Actions...)
|
||||||
sort.Slice(history, func(i, j int) bool { return history[i].ID < history[j].ID })
|
sort.Slice(history, func(i, j int) bool { return history[i].ID < history[j].ID })
|
||||||
var flagsActive, freezeActive, restrictActive, irreversible bool
|
var flagsActive, freezeActive, irreversible bool
|
||||||
for _, action := range history {
|
for _, action := range history {
|
||||||
if action.Status != domain.ModerationActionSucceeded {
|
if action.Status != domain.ModerationActionSucceeded {
|
||||||
continue
|
continue
|
||||||
|
|
@ -148,10 +148,6 @@ func validateAppealRemedyActions(detail domain.ModerationCaseDetail, actions []d
|
||||||
freezeActive = true
|
freezeActive = true
|
||||||
case domain.ModerationActionUnfreezeAccount:
|
case domain.ModerationActionUnfreezeAccount:
|
||||||
freezeActive = false
|
freezeActive = false
|
||||||
case domain.ModerationActionRestrictAccount:
|
|
||||||
restrictActive = true
|
|
||||||
case domain.ModerationActionUnrestrictAccount:
|
|
||||||
restrictActive = false
|
|
||||||
case domain.ModerationActionDeletePrivateMessage,
|
case domain.ModerationActionDeletePrivateMessage,
|
||||||
domain.ModerationActionDeleteChannelMessage,
|
domain.ModerationActionDeleteChannelMessage,
|
||||||
domain.ModerationActionDeleteAccount:
|
domain.ModerationActionDeleteAccount:
|
||||||
|
|
@ -161,16 +157,13 @@ func validateAppealRemedyActions(detail domain.ModerationCaseDetail, actions []d
|
||||||
if irreversible {
|
if irreversible {
|
||||||
return domain.ErrModerationActionInvalid
|
return domain.ErrModerationActionInvalid
|
||||||
}
|
}
|
||||||
expected := make(map[domain.ModerationActionKind]bool, 3)
|
expected := make(map[domain.ModerationActionKind]bool, 2)
|
||||||
if flagsActive {
|
if flagsActive {
|
||||||
expected[domain.ModerationActionClearPeerFlags] = true
|
expected[domain.ModerationActionClearPeerFlags] = true
|
||||||
}
|
}
|
||||||
if freezeActive {
|
if freezeActive {
|
||||||
expected[domain.ModerationActionUnfreezeAccount] = true
|
expected[domain.ModerationActionUnfreezeAccount] = true
|
||||||
}
|
}
|
||||||
if restrictActive {
|
|
||||||
expected[domain.ModerationActionUnrestrictAccount] = true
|
|
||||||
}
|
|
||||||
if len(actions) != len(expected) {
|
if len(actions) != len(expected) {
|
||||||
return domain.ErrModerationActionInvalid
|
return domain.ErrModerationActionInvalid
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,21 +14,14 @@ type accountFreezeFact struct {
|
||||||
found bool
|
found bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type accountRestrictionFact struct {
|
|
||||||
value domain.AccountRestriction
|
|
||||||
found bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// DurableUserProjectionFacts caches only viewer-independent durable overlays.
|
// DurableUserProjectionFacts caches only viewer-independent durable overlays.
|
||||||
// Contact/privacy/presence decisions remain outside and are evaluated after
|
// Contact/privacy/presence decisions remain outside and are evaluated after
|
||||||
// these facts are loaded.
|
// these facts are loaded.
|
||||||
type DurableUserProjectionFacts struct {
|
type DurableUserProjectionFacts struct {
|
||||||
freezes AccountFreezeProvider
|
freezes AccountFreezeProvider
|
||||||
restrictions AccountRestrictionProvider
|
versions store.ReadModelVersionStore
|
||||||
versions store.ReadModelVersionStore
|
|
||||||
|
|
||||||
freezeCache *readmodelcache.Cache[int64, accountFreezeFact]
|
freezeCache *readmodelcache.Cache[int64, accountFreezeFact]
|
||||||
restrictionCache *readmodelcache.Cache[int64, accountRestrictionFact]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDurableUserProjectionFacts(
|
func NewDurableUserProjectionFacts(
|
||||||
|
|
@ -36,20 +29,12 @@ func NewDurableUserProjectionFacts(
|
||||||
versions store.ReadModelVersionStore,
|
versions store.ReadModelVersionStore,
|
||||||
maxEntries int,
|
maxEntries int,
|
||||||
) *DurableUserProjectionFacts {
|
) *DurableUserProjectionFacts {
|
||||||
// freezes optionally also implements AccountRestrictionProvider (the
|
|
||||||
// production admin.Service does); callers that only need freeze facts,
|
|
||||||
// such as tests, are unaffected.
|
|
||||||
restrictions, _ := freezes.(AccountRestrictionProvider)
|
|
||||||
return &DurableUserProjectionFacts{
|
return &DurableUserProjectionFacts{
|
||||||
freezes: freezes,
|
freezes: freezes,
|
||||||
restrictions: restrictions,
|
versions: versions,
|
||||||
versions: versions,
|
|
||||||
freezeCache: readmodelcache.New[int64, accountFreezeFact](readmodelcache.Config[int64, accountFreezeFact]{
|
freezeCache: readmodelcache.New[int64, accountFreezeFact](readmodelcache.Config[int64, accountFreezeFact]{
|
||||||
MaxEntries: maxEntries,
|
MaxEntries: maxEntries,
|
||||||
}),
|
}),
|
||||||
restrictionCache: readmodelcache.New[int64, accountRestrictionFact](readmodelcache.Config[int64, accountRestrictionFact]{
|
|
||||||
MaxEntries: maxEntries,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,62 +94,6 @@ func (f *DurableUserProjectionFacts) AccountFreeze(ctx context.Context, userID i
|
||||||
return value, found, nil
|
return value, found, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *DurableUserProjectionFacts) AccountRestrictions(ctx context.Context, userIDs []int64) (map[int64]domain.AccountRestriction, error) {
|
|
||||||
out := make(map[int64]domain.AccountRestriction)
|
|
||||||
ids := uniqueDurableFactUserIDs(userIDs)
|
|
||||||
if f == nil || f.restrictions == nil || len(ids) == 0 {
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
// Reuses the same user_visibility read model version as freeze facts:
|
|
||||||
// SetAccountRestriction bumps it on write, so a shared hash keeps both
|
|
||||||
// caches correctly invalidated without a separate model.
|
|
||||||
hashes, err := f.factHashes(ctx, readmodel.ModelUserVisibility, ids)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
loaded, err := f.restrictionCache.GetOrLoadBatch(ctx, ids,
|
|
||||||
func(userID int64) (int64, bool) {
|
|
||||||
hash := hashes[userID]
|
|
||||||
return hash, f.versions != nil && hash != 0
|
|
||||||
},
|
|
||||||
func(ctx context.Context, missing []int64) (map[int64]accountRestrictionFact, error) {
|
|
||||||
values, err := f.restrictions.AccountRestrictions(ctx, missing)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
entries := make(map[int64]accountRestrictionFact, len(missing))
|
|
||||||
for _, userID := range missing {
|
|
||||||
entry := accountRestrictionFact{}
|
|
||||||
if value, ok := values[userID]; ok {
|
|
||||||
entry = accountRestrictionFact{value: value, found: true}
|
|
||||||
}
|
|
||||||
entries[userID] = entry
|
|
||||||
}
|
|
||||||
return entries, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for userID, entry := range loaded {
|
|
||||||
if entry.found {
|
|
||||||
out[userID] = entry.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *DurableUserProjectionFacts) AccountRestriction(ctx context.Context, userID int64) (domain.AccountRestriction, bool, error) {
|
|
||||||
if userID == 0 {
|
|
||||||
return domain.AccountRestriction{}, false, nil
|
|
||||||
}
|
|
||||||
items, err := f.AccountRestrictions(ctx, []int64{userID})
|
|
||||||
if err != nil {
|
|
||||||
return domain.AccountRestriction{}, false, err
|
|
||||||
}
|
|
||||||
value, found := items[userID]
|
|
||||||
return value, found, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *DurableUserProjectionFacts) factHashes(ctx context.Context, model string, userIDs []int64) (map[int64]int64, error) {
|
func (f *DurableUserProjectionFacts) factHashes(ctx context.Context, model string, userIDs []int64) (map[int64]int64, error) {
|
||||||
out := make(map[int64]int64, len(userIDs))
|
out := make(map[int64]int64, len(userIDs))
|
||||||
if f == nil || f.versions == nil {
|
if f == nil || f.versions == nil {
|
||||||
|
|
@ -190,16 +119,9 @@ func (f *DurableUserProjectionFacts) InvalidateAccountFreezeFact(userID int64) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *DurableUserProjectionFacts) InvalidateAccountRestrictionFact(userID int64) {
|
|
||||||
if f != nil && userID != 0 {
|
|
||||||
f.restrictionCache.Invalidate(userID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *DurableUserProjectionFacts) FlushUserProjectionFactReadModel() {
|
func (f *DurableUserProjectionFacts) FlushUserProjectionFactReadModel() {
|
||||||
if f != nil {
|
if f != nil {
|
||||||
f.freezeCache.Flush()
|
f.freezeCache.Flush()
|
||||||
f.restrictionCache.Flush()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,13 +31,6 @@ type AccountFreezeProvider interface {
|
||||||
AccountFreezes(ctx context.Context, userIDs []int64) (map[int64]domain.AccountFreeze, error)
|
AccountFreezes(ctx context.Context, userIDs []int64) (map[int64]domain.AccountFreeze, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountRestrictionProvider returns durable narrower spam-restriction facts
|
|
||||||
// for a bounded batch. Like AccountFreezeProvider, the projector only exposes
|
|
||||||
// them to viewers other than the restricted user.
|
|
||||||
type AccountRestrictionProvider interface {
|
|
||||||
AccountRestrictions(ctx context.Context, userIDs []int64) (map[int64]domain.AccountRestriction, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchPrivacyEvaluator 批量评估多 owner 对单 viewer 的可见性,消除 projectBatch / fan-out
|
// BatchPrivacyEvaluator 批量评估多 owner 对单 viewer 的可见性,消除 projectBatch / fan-out
|
||||||
// 投影里 per-user 3×CanSee 的 N+1。可选:实现了它的 evaluator(privacy.Service)会被
|
// 投影里 per-user 3×CanSee 的 N+1。可选:实现了它的 evaluator(privacy.Service)会被
|
||||||
// projectBatch 优先用批量预取,否则回退逐 CanSee。结果必须与逐 CanSee 字节等价。
|
// projectBatch 优先用批量预取,否则回退逐 CanSee。结果必须与逐 CanSee 字节等价。
|
||||||
|
|
@ -63,11 +56,10 @@ var privacyProjectionKeys = []domain.PrivacyKey{
|
||||||
// Projector builds the current viewer's user view for RPC response payloads.
|
// Projector builds the current viewer's user view for RPC response payloads.
|
||||||
// It intentionally stays in app/domain types; tg.* conversion remains in rpc.
|
// It intentionally stays in app/domain types; tg.* conversion remains in rpc.
|
||||||
type Projector struct {
|
type Projector struct {
|
||||||
contacts store.ContactStore
|
contacts store.ContactStore
|
||||||
photos ProfilePhotoProvider
|
photos ProfilePhotoProvider
|
||||||
privacy PrivacyEvaluator
|
privacy PrivacyEvaluator
|
||||||
freezes AccountFreezeProvider
|
freezes AccountFreezeProvider
|
||||||
restrictions AccountRestrictionProvider
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option configures a Projector.
|
// Option configures a Projector.
|
||||||
|
|
@ -93,12 +85,6 @@ func WithAccountFreezeProvider(provider AccountFreezeProvider) Option {
|
||||||
return func(p *Projector) { p.freezes = provider }
|
return func(p *Projector) { p.freezes = provider }
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAccountRestrictionProvider enables viewer-scoped restricted-account
|
|
||||||
// visibility (the narrower spam restriction).
|
|
||||||
func WithAccountRestrictionProvider(provider AccountRestrictionProvider) Option {
|
|
||||||
return func(p *Projector) { p.restrictions = provider }
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a user projector.
|
// New creates a user projector.
|
||||||
func New(opts ...Option) *Projector {
|
func New(opts ...Option) *Projector {
|
||||||
p := &Projector{}
|
p := &Projector{}
|
||||||
|
|
@ -114,7 +100,7 @@ func (p *Projector) ForViewer(ctx context.Context, viewerUserID int64, users []d
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
return projectBatch(ctx, p.contacts, p.photos, p.privacy, p.freezes, p.restrictions, viewerUserID, users)
|
return projectBatch(ctx, p.contacts, p.photos, p.privacy, p.freezes, viewerUserID, users)
|
||||||
}
|
}
|
||||||
|
|
||||||
// One applies ForViewer to a single user.
|
// One applies ForViewer to a single user.
|
||||||
|
|
@ -163,7 +149,6 @@ func (p *Projector) ForViewers(ctx context.Context, viewerUserIDs []int64, users
|
||||||
personalRefsByViewer map[int64]map[int64]domain.ProfilePhotoRef
|
personalRefsByViewer map[int64]map[int64]domain.ProfilePhotoRef
|
||||||
matrix map[int64]map[int64]map[domain.PrivacyKey]bool
|
matrix map[int64]map[int64]map[domain.PrivacyKey]bool
|
||||||
freezes map[int64]domain.AccountFreeze
|
freezes map[int64]domain.AccountFreeze
|
||||||
restrictions map[int64]domain.AccountRestriction
|
|
||||||
)
|
)
|
||||||
g, gctx := errgroup.WithContext(ctx)
|
g, gctx := errgroup.WithContext(ctx)
|
||||||
// 1) 共享头像:profile/fallback 一次批量,跨全部 viewer 复用。
|
// 1) 共享头像:profile/fallback 一次批量,跨全部 viewer 复用。
|
||||||
|
|
@ -201,13 +186,6 @@ func (p *Projector) ForViewers(ctx context.Context, viewerUserIDs []int64, users
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if p.restrictions != nil && len(ids) > 0 {
|
|
||||||
g.Go(func() error {
|
|
||||||
var err error
|
|
||||||
restrictions, err = p.restrictions.AccountRestrictions(gctx, ids)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := g.Wait(); err != nil {
|
if err := g.Wait(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +223,6 @@ func (p *Projector) ForViewers(ctx context.Context, viewerUserIDs []int64, users
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pj = applyAccountFreezeProjection(pj, viewer, freezes[u.ID])
|
pj = applyAccountFreezeProjection(pj, viewer, freezes[u.ID])
|
||||||
pj = applyAccountRestrictionProjection(pj, viewer, restrictions[u.ID])
|
|
||||||
cache[u.ID] = pj
|
cache[u.ID] = pj
|
||||||
projected[i] = pj
|
projected[i] = pj
|
||||||
}
|
}
|
||||||
|
|
@ -386,7 +363,7 @@ func One(ctx context.Context, contacts store.ContactStore, viewerUserID int64, u
|
||||||
return projected[0], nil
|
return projected[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func projectBatch(ctx context.Context, contacts store.ContactStore, photos ProfilePhotoProvider, privacy PrivacyEvaluator, freezesProvider AccountFreezeProvider, restrictionsProvider AccountRestrictionProvider, viewerUserID int64, users []domain.User) ([]domain.User, error) {
|
func projectBatch(ctx context.Context, contacts store.ContactStore, photos ProfilePhotoProvider, privacy PrivacyEvaluator, freezesProvider AccountFreezeProvider, viewerUserID int64, users []domain.User) ([]domain.User, error) {
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
@ -400,7 +377,6 @@ func projectBatch(ctx context.Context, contacts store.ContactStore, photos Profi
|
||||||
contactsByID map[int64]domain.Contact
|
contactsByID map[int64]domain.Contact
|
||||||
visibility map[int64]map[domain.PrivacyKey]bool
|
visibility map[int64]map[domain.PrivacyKey]bool
|
||||||
freezes map[int64]domain.AccountFreeze
|
freezes map[int64]domain.AccountFreeze
|
||||||
restrictions map[int64]domain.AccountRestriction
|
|
||||||
)
|
)
|
||||||
// 这些预取查询互不依赖(头像 profile/fallback、联系人 GetMany/PersonalPhotos、privacy 可见性),
|
// 这些预取查询互不依赖(头像 profile/fallback、联系人 GetMany/PersonalPhotos、privacy 可见性),
|
||||||
// 并发执行把 ~6 次串行 round-trip 收敛成一波;每个 goroutine 只写自己那一个变量,组装循环在
|
// 并发执行把 ~6 次串行 round-trip 收敛成一波;每个 goroutine 只写自己那一个变量,组装循环在
|
||||||
|
|
@ -473,16 +449,6 @@ func projectBatch(ctx context.Context, contacts store.ContactStore, photos Profi
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if restrictionsProvider != nil && len(ids) > 0 {
|
|
||||||
g.Go(func() error {
|
|
||||||
m, err := restrictionsProvider.AccountRestrictions(gctx, ids)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
restrictions = m
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := g.Wait(); err != nil {
|
if err := g.Wait(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -514,7 +480,6 @@ func projectBatch(ctx context.Context, contacts store.ContactStore, photos Profi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
projected = applyAccountFreezeProjection(projected, viewerUserID, freezes[u.ID])
|
projected = applyAccountFreezeProjection(projected, viewerUserID, freezes[u.ID])
|
||||||
projected = applyAccountRestrictionProjection(projected, viewerUserID, restrictions[u.ID])
|
|
||||||
cache[u.ID] = projected
|
cache[u.ID] = projected
|
||||||
out[i] = projected
|
out[i] = projected
|
||||||
}
|
}
|
||||||
|
|
@ -523,8 +488,6 @@ func projectBatch(ctx context.Context, contacts store.ContactStore, photos Profi
|
||||||
|
|
||||||
func applyAccountFreezeProjection(user domain.User, viewerUserID int64, freeze domain.AccountFreeze) domain.User {
|
func applyAccountFreezeProjection(user domain.User, viewerUserID int64, freeze domain.AccountFreeze) domain.User {
|
||||||
// Base users and self users must never retain a viewer-scoped restriction.
|
// Base users and self users must never retain a viewer-scoped restriction.
|
||||||
// This runs first in the projection chain, so it is the one place that
|
|
||||||
// establishes the nil baseline; applyAccountRestrictionProjection appends.
|
|
||||||
user.RestrictionReasons = nil
|
user.RestrictionReasons = nil
|
||||||
if user.Deleted || viewerUserID == 0 || user.ID == 0 || user.ID == viewerUserID || !freeze.Frozen {
|
if user.Deleted || viewerUserID == 0 || user.ID == 0 || user.ID == viewerUserID || !freeze.Frozen {
|
||||||
return user
|
return user
|
||||||
|
|
@ -533,14 +496,6 @@ func applyAccountFreezeProjection(user domain.User, viewerUserID int64, freeze d
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyAccountRestrictionProjection(user domain.User, viewerUserID int64, restriction domain.AccountRestriction) domain.User {
|
|
||||||
if user.Deleted || viewerUserID == 0 || user.ID == 0 || user.ID == viewerUserID || !restriction.Restricted {
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
user.RestrictionReasons = append(user.RestrictionReasons, domain.AccountRestrictedRestrictionReasons()...)
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
func prefetchPrivacyVisibility(ctx context.Context, privacy PrivacyEvaluator, viewerUserID int64, users []domain.User) (map[int64]map[domain.PrivacyKey]bool, error) {
|
func prefetchPrivacyVisibility(ctx context.Context, privacy PrivacyEvaluator, viewerUserID int64, users []domain.User) (map[int64]map[domain.PrivacyKey]bool, error) {
|
||||||
if privacy == nil || viewerUserID == 0 {
|
if privacy == nil || viewerUserID == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,13 @@ type ProfilePhotoProvider = userprojection.ProfilePhotoProvider
|
||||||
|
|
||||||
// Service 提供用户查询。
|
// Service 提供用户查询。
|
||||||
type Service struct {
|
type Service struct {
|
||||||
users store.UserStore
|
users store.UserStore
|
||||||
cache store.UserCache
|
cache store.UserCache
|
||||||
contacts store.ContactStore
|
contacts store.ContactStore
|
||||||
photos ProfilePhotoProvider
|
photos ProfilePhotoProvider
|
||||||
privacy userprojection.PrivacyEvaluator
|
privacy userprojection.PrivacyEvaluator
|
||||||
freezes userprojection.AccountFreezeProvider
|
freezes userprojection.AccountFreezeProvider
|
||||||
restrictions userprojection.AccountRestrictionProvider
|
projector *userprojection.Projector
|
||||||
projector *userprojection.Projector
|
|
||||||
// hideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
// hideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
||||||
// while true, ResolveUsername never resolves @marksbot
|
// while true, ResolveUsername never resolves @marksbot
|
||||||
// (domain.VerifierBotUserID), so a client cannot discover it by username.
|
// (domain.VerifierBotUserID), so a client cannot discover it by username.
|
||||||
|
|
@ -98,11 +97,6 @@ func WithAccountFreezeProvider(p userprojection.AccountFreezeProvider) Option {
|
||||||
return func(s *Service) { s.freezes = p }
|
return func(s *Service) { s.freezes = p }
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAccountRestrictionProvider injects the account spam-restriction reader.
|
|
||||||
func WithAccountRestrictionProvider(p userprojection.AccountRestrictionProvider) Option {
|
|
||||||
return func(s *Service) { s.restrictions = p }
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithHideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
// WithHideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
||||||
// while true, ResolveUsername treats @marksbot as not found.
|
// while true, ResolveUsername treats @marksbot as not found.
|
||||||
func WithHideThirdPartyVerification(hidden bool) Option {
|
func WithHideThirdPartyVerification(hidden bool) Option {
|
||||||
|
|
@ -141,7 +135,6 @@ func NewService(users store.UserStore, opts ...Option) *Service {
|
||||||
userprojection.WithPhotoProvider(s.photos),
|
userprojection.WithPhotoProvider(s.photos),
|
||||||
userprojection.WithPrivacyEvaluator(s.privacy),
|
userprojection.WithPrivacyEvaluator(s.privacy),
|
||||||
userprojection.WithAccountFreezeProvider(s.freezes),
|
userprojection.WithAccountFreezeProvider(s.freezes),
|
||||||
userprojection.WithAccountRestrictionProvider(s.restrictions),
|
|
||||||
)
|
)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,21 +53,3 @@ type AccountFreezeNotification struct {
|
||||||
Frozen bool
|
Frozen bool
|
||||||
Attempts int
|
Attempts int
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountRestriction is the durable, narrower spam sanction: unlike
|
|
||||||
// AccountFreeze it does not make the account read-only. A restricted account
|
|
||||||
// keeps every existing membership and conversation; it can only not join new
|
|
||||||
// channels/groups and not start new conversations with non-contacts. Until,
|
|
||||||
// when set, is when the restriction auto-lifts; a zero Until means it stays
|
|
||||||
// in effect until an admin clears it.
|
|
||||||
type AccountRestriction struct {
|
|
||||||
UserID int64
|
|
||||||
Restricted bool
|
|
||||||
Version int64
|
|
||||||
Since time.Time
|
|
||||||
Until time.Time
|
|
||||||
Reason string
|
|
||||||
Actor string
|
|
||||||
CommandID string
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -111,8 +111,6 @@ const (
|
||||||
ModerationActionClearPeerFlags ModerationActionKind = "clear_peer_flags"
|
ModerationActionClearPeerFlags ModerationActionKind = "clear_peer_flags"
|
||||||
ModerationActionFreezeAccount ModerationActionKind = "freeze_account"
|
ModerationActionFreezeAccount ModerationActionKind = "freeze_account"
|
||||||
ModerationActionUnfreezeAccount ModerationActionKind = "unfreeze_account"
|
ModerationActionUnfreezeAccount ModerationActionKind = "unfreeze_account"
|
||||||
ModerationActionRestrictAccount ModerationActionKind = "restrict_account"
|
|
||||||
ModerationActionUnrestrictAccount ModerationActionKind = "unrestrict_account"
|
|
||||||
ModerationActionDeletePrivateMessage ModerationActionKind = "delete_private_message"
|
ModerationActionDeletePrivateMessage ModerationActionKind = "delete_private_message"
|
||||||
ModerationActionDeleteChannelMessage ModerationActionKind = "delete_channel_message"
|
ModerationActionDeleteChannelMessage ModerationActionKind = "delete_channel_message"
|
||||||
ModerationActionDeleteAccount ModerationActionKind = "delete_account"
|
ModerationActionDeleteAccount ModerationActionKind = "delete_account"
|
||||||
|
|
@ -123,7 +121,6 @@ func (k ModerationActionKind) Valid() bool {
|
||||||
case ModerationActionMarkScam, ModerationActionMarkFake,
|
case ModerationActionMarkScam, ModerationActionMarkFake,
|
||||||
ModerationActionClearPeerFlags, ModerationActionFreezeAccount,
|
ModerationActionClearPeerFlags, ModerationActionFreezeAccount,
|
||||||
ModerationActionUnfreezeAccount,
|
ModerationActionUnfreezeAccount,
|
||||||
ModerationActionRestrictAccount, ModerationActionUnrestrictAccount,
|
|
||||||
ModerationActionDeletePrivateMessage,
|
ModerationActionDeletePrivateMessage,
|
||||||
ModerationActionDeleteChannelMessage,
|
ModerationActionDeleteChannelMessage,
|
||||||
ModerationActionDeleteAccount:
|
ModerationActionDeleteAccount:
|
||||||
|
|
@ -162,9 +159,8 @@ func (s ModerationActionStatus) Valid() bool {
|
||||||
type ModerationSanctionFamily string
|
type ModerationSanctionFamily string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ModerationSanctionPeerFlags ModerationSanctionFamily = "peer_flags"
|
ModerationSanctionPeerFlags ModerationSanctionFamily = "peer_flags"
|
||||||
ModerationSanctionAccountFreeze ModerationSanctionFamily = "account_freeze"
|
ModerationSanctionAccountFreeze ModerationSanctionFamily = "account_freeze"
|
||||||
ModerationSanctionAccountRestrict ModerationSanctionFamily = "account_restrict"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (k ModerationActionKind) SanctionFamily() (ModerationSanctionFamily, bool) {
|
func (k ModerationActionKind) SanctionFamily() (ModerationSanctionFamily, bool) {
|
||||||
|
|
@ -174,8 +170,6 @@ func (k ModerationActionKind) SanctionFamily() (ModerationSanctionFamily, bool)
|
||||||
return ModerationSanctionPeerFlags, true
|
return ModerationSanctionPeerFlags, true
|
||||||
case ModerationActionFreezeAccount, ModerationActionUnfreezeAccount:
|
case ModerationActionFreezeAccount, ModerationActionUnfreezeAccount:
|
||||||
return ModerationSanctionAccountFreeze, true
|
return ModerationSanctionAccountFreeze, true
|
||||||
case ModerationActionRestrictAccount, ModerationActionUnrestrictAccount:
|
|
||||||
return ModerationSanctionAccountRestrict, true
|
|
||||||
default:
|
default:
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -189,14 +189,6 @@ func AccountFrozenRestrictionReasons() []UserRestrictionReason {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AccountRestrictedRestrictionReasons() []UserRestrictionReason {
|
|
||||||
return []UserRestrictionReason{{
|
|
||||||
Platform: "all",
|
|
||||||
Reason: "spam-restricted",
|
|
||||||
Text: "This account has been restricted for suspected spam.",
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PremiumActiveAt 报告用户在 now(Unix 秒)时刻是否为有效会员。
|
// PremiumActiveAt 报告用户在 now(Unix 秒)时刻是否为有效会员。
|
||||||
// bot 永不为会员(官方语义;授予路径同样排除 bot,这里是双保险)。
|
// bot 永不为会员(官方语义;授予路径同样排除 bot,这里是双保险)。
|
||||||
func (u User) PremiumActiveAt(now int64) bool {
|
func (u User) PremiumActiveAt(now int64) bool {
|
||||||
|
|
|
||||||
|
|
@ -550,12 +550,6 @@ type AccountFreezeService interface {
|
||||||
AccountFreeze(ctx context.Context, userID int64) (domain.AccountFreeze, bool, error)
|
AccountFreeze(ctx context.Context, userID int64) (domain.AccountFreeze, bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountRestrictionService exposes the narrower spam-restriction fact used
|
|
||||||
// by the central RPC join gate and the private-message send path.
|
|
||||||
type AccountRestrictionService interface {
|
|
||||||
AccountRestriction(ctx context.Context, userID int64) (domain.AccountRestriction, bool, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountFreezeNotificationService owns the durable non-PTS notification
|
// AccountFreezeNotificationService owns the durable non-PTS notification
|
||||||
// queue. It is intentionally separate from AccountFreezeService so hot
|
// queue. It is intentionally separate from AccountFreezeService so hot
|
||||||
// read-only gates can use a versioned fact cache without disabling queue
|
// read-only gates can use a versioned fact cache without disabling queue
|
||||||
|
|
@ -613,10 +607,6 @@ type UserEmojiStatusUpdatesService interface {
|
||||||
type ContactsService interface {
|
type ContactsService interface {
|
||||||
GetContacts(ctx context.Context, userID int64, hash int64) (domain.ContactList, bool, error)
|
GetContacts(ctx context.Context, userID int64, hash int64) (domain.ContactList, bool, error)
|
||||||
ContactIDs(ctx context.Context, userID int64, hash int64) ([]int, bool, error)
|
ContactIDs(ctx context.Context, userID int64, hash int64) ([]int, bool, error)
|
||||||
// IsContact reports whether peerUserID is in userID's own contact list.
|
|
||||||
// Used by the account-restriction send gate, which is a one-directional
|
|
||||||
// check unrelated to the recipient's privacy settings.
|
|
||||||
IsContact(ctx context.Context, userID, peerUserID int64) (bool, error)
|
|
||||||
AddContact(ctx context.Context, userID int64, input domain.ContactInput) (domain.Contact, error)
|
AddContact(ctx context.Context, userID int64, input domain.ContactInput) (domain.Contact, error)
|
||||||
AcceptContact(ctx context.Context, userID, contactUserID int64) (domain.Contact, error)
|
AcceptContact(ctx context.Context, userID, contactUserID int64) (domain.Contact, error)
|
||||||
ImportContacts(ctx context.Context, userID int64, inputs []domain.ContactInput) (domain.ImportContactsResult, error)
|
ImportContacts(ctx context.Context, userID int64, inputs []domain.ContactInput) (domain.ImportContactsResult, error)
|
||||||
|
|
@ -1159,7 +1149,6 @@ type Deps struct {
|
||||||
AppUpdates updatecdn.Resolver
|
AppUpdates updatecdn.Resolver
|
||||||
AccountFreeze AccountFreezeService
|
AccountFreeze AccountFreezeService
|
||||||
AccountFreezeNotifications AccountFreezeNotificationService
|
AccountFreezeNotifications AccountFreezeNotificationService
|
||||||
AccountRestriction AccountRestrictionService
|
|
||||||
AICompose AIComposeService
|
AICompose AIComposeService
|
||||||
Ephemeral EphemeralService
|
Ephemeral EphemeralService
|
||||||
EphemeralPush store.EphemeralPushBroker
|
EphemeralPush store.EphemeralPushBroker
|
||||||
|
|
|
||||||
|
|
@ -128,14 +128,6 @@ func mediaEmptyErr() error { return tgerr.New(400, "MEDIA_EMPTY") }
|
||||||
func frozenMethodInvalidErr() error { return tgerr.New(420, "FROZEN_METHOD_INVALID") }
|
func frozenMethodInvalidErr() error { return tgerr.New(420, "FROZEN_METHOD_INVALID") }
|
||||||
func frozenParticipantMissingErr() error { return tgerr.New(400, "FROZEN_PARTICIPANT_MISSING") }
|
func frozenParticipantMissingErr() error { return tgerr.New(400, "FROZEN_PARTICIPANT_MISSING") }
|
||||||
|
|
||||||
// restrictedMethodInvalidErr covers a spam-restricted account attempting to
|
|
||||||
// join a new channel/group (public join or private invite link).
|
|
||||||
func restrictedMethodInvalidErr() error { return tgerr.New(420, "USER_RESTRICTED") }
|
|
||||||
|
|
||||||
// restrictedNoncontactErr covers a spam-restricted account attempting to
|
|
||||||
// start a new conversation with a peer that is not in its contacts.
|
|
||||||
func restrictedNoncontactErr() error { return tgerr.New(403, "USER_RESTRICTED_NONCONTACT") }
|
|
||||||
|
|
||||||
func photoInvalidErr() error { return tgerr.New(400, "PHOTO_INVALID") }
|
func photoInvalidErr() error { return tgerr.New(400, "PHOTO_INVALID") }
|
||||||
|
|
||||||
func stickersetInvalidErr() error { return tgerr.New(406, "STICKERSET_INVALID") }
|
func stickersetInvalidErr() error { return tgerr.New(406, "STICKERSET_INVALID") }
|
||||||
|
|
|
||||||
|
|
@ -77,67 +77,6 @@ func frozenMethodRequiresWriteGate(method string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// restrictedAlwaysBlockedMethods covers the narrower spam restriction: unlike
|
|
||||||
// freeze it does not touch reads or existing-membership actions, it only
|
|
||||||
// blocks starting a NEW channel/group membership (public join or private
|
|
||||||
// invite link). Non-contact messaging is peer-dependent and is gated
|
|
||||||
// separately in the send path, not here.
|
|
||||||
var restrictedAlwaysBlockedMethods = map[string]struct{}{
|
|
||||||
"channels.joinChannel": {},
|
|
||||||
"messages.importChatInvite": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) checkRestrictedRPC(ctx context.Context, method string) error {
|
|
||||||
if r == nil || r.deps.AccountRestriction == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, blocked := restrictedAlwaysBlockedMethods[method]; !blocked {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
userID, authorized := UserIDFrom(ctx)
|
|
||||||
if !authorized || userID == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
restriction, found, err := r.deps.AccountRestriction.AccountRestriction(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return internalErr()
|
|
||||||
}
|
|
||||||
if found && restriction.Restricted {
|
|
||||||
return restrictedMethodInvalidErr()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureNotRestrictedFromMessaging enforces the peer-dependent half of the
|
|
||||||
// spam restriction: a restricted sender may still message existing contacts,
|
|
||||||
// but not start a new conversation with a peer outside their own contact
|
|
||||||
// list. Unlike checkRestrictedRPC this cannot be a method-name gate, since the
|
|
||||||
// same method (messages.sendMessage) is allowed or blocked depending on who
|
|
||||||
// the peer is.
|
|
||||||
func (r *Router) ensureNotRestrictedFromMessaging(ctx context.Context, senderUserID, recipientUserID int64) error {
|
|
||||||
if r == nil || r.deps.AccountRestriction == nil || senderUserID == 0 || recipientUserID == 0 || senderUserID == recipientUserID {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
restriction, found, err := r.deps.AccountRestriction.AccountRestriction(ctx, senderUserID)
|
|
||||||
if err != nil {
|
|
||||||
return internalErr()
|
|
||||||
}
|
|
||||||
if !found || !restriction.Restricted {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if r.deps.Contacts == nil {
|
|
||||||
return restrictedNoncontactErr()
|
|
||||||
}
|
|
||||||
isContact, err := r.deps.Contacts.IsContact(ctx, senderUserID, recipientUserID)
|
|
||||||
if err != nil {
|
|
||||||
return internalErr()
|
|
||||||
}
|
|
||||||
if isContact {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return restrictedNoncontactErr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) checkFrozenRPC(ctx context.Context, method string) error {
|
func (r *Router) checkFrozenRPC(ctx context.Context, method string) error {
|
||||||
if r == nil || r.deps.AccountFreeze == nil || !frozenMethodRequiresWriteGate(method) {
|
if r == nil || r.deps.AccountFreeze == nil || !frozenMethodRequiresWriteGate(method) {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -309,9 +309,6 @@ func (r *Router) DispatchAdmitted(
|
||||||
if err := r.checkFrozenRPC(ctx, method); err != nil {
|
if err := r.checkFrozenRPC(ctx, method); err != nil {
|
||||||
return nil, method, err
|
return nil, method, err
|
||||||
}
|
}
|
||||||
if err := r.checkRestrictedRPC(ctx, method); err != nil {
|
|
||||||
return nil, method, err
|
|
||||||
}
|
|
||||||
if profileKnown && profileEvidenceFresh {
|
if profileKnown && profileEvidenceFresh {
|
||||||
r.maybeMarkSessionReceivesUpdates(ctx)
|
r.maybeMarkSessionReceivesUpdates(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,9 +150,6 @@ func (r *Router) onMessagesForwardMessages(ctx context.Context, req *tg.Messages
|
||||||
if err := r.ensurePrivateContactAllowed(ctx, userID, toPeer.ID, req.AllowPaidStars, len(absentIndexes)); err != nil {
|
if err := r.ensurePrivateContactAllowed(ctx, userID, toPeer.ID, req.AllowPaidStars, len(absentIndexes)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := r.ensureNotRestrictedFromMessaging(ctx, userID, toPeer.ID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
absentIDs := make([]int, len(absentIndexes))
|
absentIDs := make([]int, len(absentIndexes))
|
||||||
absentRandomIDs := make([]int64, len(absentIndexes))
|
absentRandomIDs := make([]int64, len(absentIndexes))
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,6 @@ func (r *Router) onMessagesSendMessage(ctx context.Context, req *tg.MessagesSend
|
||||||
sendErr = peerIDInvalidErr()
|
sendErr = peerIDInvalidErr()
|
||||||
return nil, sendErr
|
return nil, sendErr
|
||||||
}
|
}
|
||||||
if peer.Type == domain.PeerTypeUser {
|
|
||||||
if err := r.ensureNotRestrictedFromMessaging(ctx, userID, peer.ID); err != nil {
|
|
||||||
sendErr = err
|
|
||||||
return nil, sendErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
idempotencyFingerprint, err := sendMessageIdempotencyFingerprint(req)
|
idempotencyFingerprint, err := sendMessageIdempotencyFingerprint(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendErr = internalErr()
|
sendErr = internalErr()
|
||||||
|
|
|
||||||
|
|
@ -809,9 +809,6 @@ func (r *Router) dispatch(ctx context.Context, b *bin.Buffer, depth int, meta *r
|
||||||
if err := r.checkFrozenRPC(ctx, tlTypeName(id)); err != nil {
|
if err := r.checkFrozenRPC(ctx, tlTypeName(id)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := r.checkRestrictedRPC(ctx, tlTypeName(id)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// 任何未包 invokeWithoutUpdates 的已登录 RPC 都把当前 session 视为 updates
|
// 任何未包 invokeWithoutUpdates 的已登录 RPC 都把当前 session 视为 updates
|
||||||
// 接收者。仅靠 updates.getState/getDifference 置位会漏掉 DrKLO 热恢复:
|
// 接收者。仅靠 updates.getState/getDifference 置位会漏掉 DrKLO 热恢复:
|
||||||
// 它重连后不重建同步基线(pts 在进程内存里),只发普通业务请求,置位
|
// 它重连后不重建同步基线(pts 在进程内存里),只发普通业务请求,置位
|
||||||
|
|
|
||||||
|
|
@ -131,9 +131,6 @@ func (r *Router) sendOutgoing(ctx context.Context, userID int64, peer domain.Pee
|
||||||
if err := r.ensurePrivateContactAllowed(ctx, userID, peer.ID, p.allowPaidStars, 1); err != nil {
|
if err := r.ensurePrivateContactAllowed(ctx, userID, peer.ID, p.allowPaidStars, 1); err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
if err := r.ensureNotRestrictedFromMessaging(ctx, userID, peer.ID); err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
if err := r.ensureVoiceMessagesAllowed(ctx, userID, peer, p.media != nil && p.media.HasUnreadPayload()); err != nil {
|
if err := r.ensureVoiceMessagesAllowed(ctx, userID, peer, p.media != nil && p.media.HasUnreadPayload()); err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
@ -547,9 +544,6 @@ func (r *Router) onMessagesSendMultiMedia(ctx context.Context, req *tg.MessagesS
|
||||||
if err := r.ensurePrivateContactAllowed(ctx, userID, peer.ID, req.AllowPaidStars, absentCount); err != nil {
|
if err := r.ensurePrivateContactAllowed(ctx, userID, peer.ID, req.AllowPaidStars, absentCount); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := r.ensureNotRestrictedFromMessaging(ctx, userID, peer.ID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pendingMedia := make([]tg.InputMediaClass, 0, absentCount)
|
pendingMedia := make([]tg.InputMediaClass, 0, absentCount)
|
||||||
for i, item := range req.MultiMedia {
|
for i, item := range req.MultiMedia {
|
||||||
|
|
|
||||||
|
|
@ -954,10 +954,6 @@ func (s *ModerationReportStore) memoryCaseOwnsRemedySanctionsLocked(item domain.
|
||||||
if latest.Kind != domain.ModerationActionFreezeAccount {
|
if latest.Kind != domain.ModerationActionFreezeAccount {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case domain.ModerationActionUnrestrictAccount:
|
|
||||||
if latest.Kind != domain.ModerationActionRestrictAccount {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -972,8 +968,6 @@ func memoryModerationActionsValid(target domain.Peer, actions []domain.Moderatio
|
||||||
domain.ModerationActionClearPeerFlags:
|
domain.ModerationActionClearPeerFlags:
|
||||||
case domain.ModerationActionFreezeAccount,
|
case domain.ModerationActionFreezeAccount,
|
||||||
domain.ModerationActionUnfreezeAccount,
|
domain.ModerationActionUnfreezeAccount,
|
||||||
domain.ModerationActionRestrictAccount,
|
|
||||||
domain.ModerationActionUnrestrictAccount,
|
|
||||||
domain.ModerationActionDeletePrivateMessage,
|
domain.ModerationActionDeletePrivateMessage,
|
||||||
domain.ModerationActionDeleteAccount:
|
domain.ModerationActionDeleteAccount:
|
||||||
if target.Type != domain.PeerTypeUser {
|
if target.Type != domain.PeerTypeUser {
|
||||||
|
|
|
||||||
|
|
@ -303,130 +303,6 @@ func scanAccountFreeze(row accountFreezeScanner) (domain.AccountFreeze, error) {
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AdminStore) GetAccountRestriction(ctx context.Context, userID int64) (domain.AccountRestriction, bool, error) {
|
|
||||||
row := s.db.QueryRow(ctx, `
|
|
||||||
SELECT user_id, restricted, version, restricted_since, restricted_until, reason, actor, command_id, updated_at
|
|
||||||
FROM account_message_restrictions
|
|
||||||
WHERE user_id = $1`, userID)
|
|
||||||
r, err := scanAccountRestriction(row)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.AccountRestriction{}, false, nil
|
|
||||||
}
|
|
||||||
return domain.AccountRestriction{}, false, fmt.Errorf("get account restriction: %w", err)
|
|
||||||
}
|
|
||||||
return r, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AdminStore) GetAccountRestrictions(ctx context.Context, userIDs []int64) (map[int64]domain.AccountRestriction, error) {
|
|
||||||
out := make(map[int64]domain.AccountRestriction)
|
|
||||||
if s == nil || s.db == nil || len(userIDs) == 0 {
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
rows, err := s.db.Query(ctx, `
|
|
||||||
SELECT user_id, restricted, version, restricted_since, restricted_until, reason, actor, command_id, updated_at
|
|
||||||
FROM account_message_restrictions
|
|
||||||
WHERE user_id = ANY($1::bigint[]) AND restricted = true`, userIDs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get account restrictions: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
restriction, err := scanAccountRestriction(rows)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("scan account restriction: %w", err)
|
|
||||||
}
|
|
||||||
out[restriction.UserID] = restriction
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("iterate account restrictions: %w", err)
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AdminStore) SetAccountRestriction(ctx context.Context, restriction domain.AccountRestriction) (domain.AccountRestriction, error) {
|
|
||||||
beginner, ok := s.db.(txBeginner)
|
|
||||||
if !ok {
|
|
||||||
return setAccountRestrictionRow(ctx, s.db, restriction)
|
|
||||||
}
|
|
||||||
tx, err := beginner.Begin(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return domain.AccountRestriction{}, fmt.Errorf("begin set account restriction: %w", err)
|
|
||||||
}
|
|
||||||
committed := false
|
|
||||||
defer func() {
|
|
||||||
if !committed {
|
|
||||||
_ = tx.Rollback(ctx)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
out, err := setAccountRestrictionRow(ctx, tx, restriction)
|
|
||||||
if err != nil {
|
|
||||||
return domain.AccountRestriction{}, err
|
|
||||||
}
|
|
||||||
// Reuses the same user_visibility read model as account freeze so the RPC
|
|
||||||
// gate and user projection caches invalidate on the same signal.
|
|
||||||
if _, err := tx.Exec(ctx, `SELECT telesrv_bump_read_model_version('user_visibility', 0, 'user', $1)`, out.UserID); err != nil {
|
|
||||||
return domain.AccountRestriction{}, fmt.Errorf("bump restricted user visibility: %w", err)
|
|
||||||
}
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
|
||||||
return domain.AccountRestriction{}, fmt.Errorf("commit set account restriction: %w", err)
|
|
||||||
}
|
|
||||||
committed = true
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setAccountRestrictionRow(ctx context.Context, db sqlcgen.DBTX, restriction domain.AccountRestriction) (domain.AccountRestriction, error) {
|
|
||||||
var since, until any
|
|
||||||
if restriction.Restricted {
|
|
||||||
since = restriction.Since
|
|
||||||
if !restriction.Until.IsZero() {
|
|
||||||
until = restriction.Until
|
|
||||||
}
|
|
||||||
}
|
|
||||||
row := db.QueryRow(ctx, `
|
|
||||||
INSERT INTO account_message_restrictions (
|
|
||||||
user_id, restricted, restricted_since, restricted_until, reason, actor, command_id, updated_at
|
|
||||||
)
|
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,now())
|
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
|
||||||
restricted = EXCLUDED.restricted,
|
|
||||||
restricted_since = EXCLUDED.restricted_since,
|
|
||||||
restricted_until = EXCLUDED.restricted_until,
|
|
||||||
reason = EXCLUDED.reason,
|
|
||||||
actor = EXCLUDED.actor,
|
|
||||||
command_id = EXCLUDED.command_id,
|
|
||||||
version = account_message_restrictions.version + 1,
|
|
||||||
updated_at = now()
|
|
||||||
RETURNING user_id, restricted, version, restricted_since, restricted_until, reason, actor, command_id, updated_at`,
|
|
||||||
restriction.UserID, restriction.Restricted, since, until, restriction.Reason, restriction.Actor, restriction.CommandID,
|
|
||||||
)
|
|
||||||
out, err := scanAccountRestriction(row)
|
|
||||||
if err != nil {
|
|
||||||
return domain.AccountRestriction{}, fmt.Errorf("set account restriction: %w", err)
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanAccountRestriction(row accountFreezeScanner) (domain.AccountRestriction, error) {
|
|
||||||
var r domain.AccountRestriction
|
|
||||||
var since, until pgtype.Timestamptz
|
|
||||||
var updated time.Time
|
|
||||||
if err := row.Scan(
|
|
||||||
&r.UserID, &r.Restricted, &r.Version, &since, &until,
|
|
||||||
&r.Reason, &r.Actor, &r.CommandID, &updated,
|
|
||||||
); err != nil {
|
|
||||||
return domain.AccountRestriction{}, err
|
|
||||||
}
|
|
||||||
if since.Valid {
|
|
||||||
r.Since = since.Time
|
|
||||||
}
|
|
||||||
if until.Valid {
|
|
||||||
r.Until = until.Time
|
|
||||||
}
|
|
||||||
r.UpdatedAt = updated
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func enqueueAccountFreezeNotifications(ctx context.Context, tx pgx.Tx, freeze domain.AccountFreeze) error {
|
func enqueueAccountFreezeNotifications(ctx context.Context, tx pgx.Tx, freeze domain.AccountFreeze) error {
|
||||||
const maxAccountFreezeNotificationAudience = 4096
|
const maxAccountFreezeNotificationAudience = 4096
|
||||||
_, err := tx.Exec(ctx, `
|
_, err := tx.Exec(ctx, `
|
||||||
|
|
|
||||||
|
|
@ -969,10 +969,6 @@ LIMIT 1`, string(current.Target.Type), current.Target.ID,
|
||||||
if domain.ModerationActionKind(kind) != domain.ModerationActionFreezeAccount {
|
if domain.ModerationActionKind(kind) != domain.ModerationActionFreezeAccount {
|
||||||
return domain.ErrModerationActionConflict
|
return domain.ErrModerationActionConflict
|
||||||
}
|
}
|
||||||
case domain.ModerationActionUnrestrictAccount:
|
|
||||||
if domain.ModerationActionKind(kind) != domain.ModerationActionRestrictAccount {
|
|
||||||
return domain.ErrModerationActionConflict
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return domain.ErrModerationActionConflict
|
return domain.ErrModerationActionConflict
|
||||||
}
|
}
|
||||||
|
|
@ -993,11 +989,6 @@ func moderationSanctionKinds(family domain.ModerationSanctionFamily) []string {
|
||||||
string(domain.ModerationActionFreezeAccount),
|
string(domain.ModerationActionFreezeAccount),
|
||||||
string(domain.ModerationActionUnfreezeAccount),
|
string(domain.ModerationActionUnfreezeAccount),
|
||||||
}
|
}
|
||||||
case domain.ModerationSanctionAccountRestrict:
|
|
||||||
return []string{
|
|
||||||
string(domain.ModerationActionRestrictAccount),
|
|
||||||
string(domain.ModerationActionUnrestrictAccount),
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -1011,8 +1002,6 @@ func validateModerationActionsForTarget(target domain.Peer, actions []domain.Mod
|
||||||
// Both users and channels support protocol scam/fake flags.
|
// Both users and channels support protocol scam/fake flags.
|
||||||
case domain.ModerationActionFreezeAccount,
|
case domain.ModerationActionFreezeAccount,
|
||||||
domain.ModerationActionUnfreezeAccount,
|
domain.ModerationActionUnfreezeAccount,
|
||||||
domain.ModerationActionRestrictAccount,
|
|
||||||
domain.ModerationActionUnrestrictAccount,
|
|
||||||
domain.ModerationActionDeletePrivateMessage,
|
domain.ModerationActionDeletePrivateMessage,
|
||||||
domain.ModerationActionDeleteAccount:
|
domain.ModerationActionDeleteAccount:
|
||||||
if target.Type != domain.PeerTypeUser {
|
if target.Type != domain.PeerTypeUser {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue