option('days') ?? config('auth.prune_bot_registrations.after_days', 14)); if ($days < 7) { $this->error('Die Frist muss mindestens 7 Tage betragen.'); return self::FAILURE; } $candidates = $this->candidates($days)->limit((int) $this->option('limit'))->get(); if ($candidates->isEmpty()) { $this->info('Keine Bot-Registrierungen zu löschen.'); return self::SUCCESS; } if ($this->option('dry-run')) { $this->table( ['ID', 'Angelegt', 'Name', 'E-Mail', 'Zufallsname'], $candidates->map(fn (User $user): array => [ $user->id, $user->created_at?->format('d.m.Y H:i'), Str::limit((string) $user->name, 30), $this->maskEmail((string) $user->email), GibberishDetector::isRandomPersonName((string) $user->name) ? 'ja' : '', ])->all(), ); $this->info(sprintf( 'Probelauf: %d Konten älter als %d Tage würden gelöscht, davon %d mit Zufallsnamen.', $candidates->count(), $days, $candidates->filter(fn (User $user): bool => GibberishDetector::isRandomPersonName((string) $user->name))->count(), )); return self::SUCCESS; } $deleted = 0; $failed = 0; foreach ($candidates as $user) { try { DB::transaction(fn () => $user->forceDelete()); $deleted++; } catch (\Throwable $exception) { $failed++; Log::channel('spam')->warning('Bot-Registrierung konnte nicht gelöscht werden.', [ 'user_id' => $user->id, 'error' => $exception->getMessage(), ]); } } Log::channel('spam')->info('Bot-Registrierungen gelöscht.', ['deleted' => $deleted, 'failed' => $failed, 'days' => $days]); $this->info("Bot-Registrierungen gelöscht: {$deleted}".($failed > 0 ? ", {$failed} fehlgeschlagen (siehe spam-Log)" : '').'.'); return $failed > 0 ? self::FAILURE : self::SUCCESS; } /** * @return Builder */ private function candidates(int $days): Builder { $query = User::query() ->whereNull('legacy_id') ->whereNull('email_verified_at') ->whereNull('last_login_at') ->whereNull('oauth_provider') ->where('is_active', false) ->where('is_super_admin', false) ->where('created_at', '<', now()->subDays($days)) ->whereDoesntHave('roles') ->whereNotExists(fn (QueryBuilder $query) => $query ->from('personal_access_tokens') ->whereColumn('personal_access_tokens.tokenable_id', 'users.id') ->where('personal_access_tokens.tokenable_type', (new User)->getMorphClass())); foreach (self::USAGE_TABLES as $table) { $query->whereNotExists(fn (QueryBuilder $sub) => $sub ->from($table) ->whereColumn($table.'.user_id', 'users.id')); } return $query->orderBy('id'); } private function maskEmail(string $email): string { [$local, $domain] = array_pad(explode('@', $email, 2), 2, ''); return Str::substr($local, 0, 2).'***@'.$domain; } }