You've already forked Atomcms-edit
Initial commit
This commit is contained in:
Executable
+362
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\AlertChannel;
|
||||
use App\Enums\AlertSeverity;
|
||||
use App\Enums\AlertType;
|
||||
use App\Models\Miscellaneous\AlertLog;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Throwable;
|
||||
|
||||
class AlertService
|
||||
{
|
||||
private bool $emailEnabled;
|
||||
|
||||
private bool $discordEnabled;
|
||||
|
||||
private string $discordWebhookUrl;
|
||||
|
||||
private string $alertEmail;
|
||||
|
||||
private readonly string $siteName;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->emailEnabled = (bool) setting('alert_email_enabled', false);
|
||||
$this->discordEnabled = (bool) setting('alert_discord_enabled', false);
|
||||
$this->discordWebhookUrl = setting('alert_discord_webhook_url', '');
|
||||
$this->alertEmail = setting('alert_email_address', '');
|
||||
$this->siteName = setting('hotel_name', config('app.name', 'Atom CMS'));
|
||||
}
|
||||
|
||||
public function send(
|
||||
AlertType $type,
|
||||
string $message,
|
||||
array $context = [],
|
||||
AlertChannel $channel = AlertChannel::BOTH,
|
||||
): void {
|
||||
$this->logAlert($type, $message, $context);
|
||||
|
||||
$channels = $this->resolveChannels($channel);
|
||||
|
||||
foreach ($channels as $ch) {
|
||||
match ($ch) {
|
||||
AlertChannel::EMAIL => $this->sendEmail($type, $message, $context),
|
||||
AlertChannel::DISCORD => $this->sendDiscord($type, $message, $context),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public function sendCritical(AlertType $type, string $message, array $context = []): void
|
||||
{
|
||||
$this->send($type, $message, $context, AlertChannel::BOTH);
|
||||
}
|
||||
|
||||
public function sendEmulatorOffline(string $error = ''): void
|
||||
{
|
||||
$this->send(
|
||||
AlertType::EMULATOR_OFFLINE,
|
||||
'De emulator is offline of niet bereikbaar via RCON.',
|
||||
['error' => $error],
|
||||
);
|
||||
}
|
||||
|
||||
public function sendEmulatorOnline(): void
|
||||
{
|
||||
$this->send(
|
||||
AlertType::EMULATOR_ONLINE,
|
||||
'De emulator is weer online en verbonden via RCON.',
|
||||
[],
|
||||
AlertChannel::BOTH,
|
||||
);
|
||||
}
|
||||
|
||||
public function sendDDoSDetected(array $details): void
|
||||
{
|
||||
$message = sprintf(
|
||||
'Mogelijke DDoS aanval gedetecteerd! %d requests in %d seconden van %d unieke IPs.',
|
||||
$details['total_requests'] ?? 0,
|
||||
$details['time_window'] ?? 0,
|
||||
$details['unique_ips'] ?? 0,
|
||||
);
|
||||
|
||||
$this->send(AlertType::DDOS_DETECTED, $message, $details);
|
||||
}
|
||||
|
||||
public function sendCriticalError(string $error, ?Throwable $exception = null): void
|
||||
{
|
||||
$context = [
|
||||
'error' => $error,
|
||||
'exception_class' => $exception instanceof Throwable ? $exception::class : null,
|
||||
'file' => $exception?->getFile(),
|
||||
'line' => $exception?->getLine(),
|
||||
'trace' => $exception instanceof Throwable ? $exception->getTraceAsString() : null,
|
||||
];
|
||||
|
||||
$this->send(AlertType::CRITICAL_ERROR, $error, $context);
|
||||
}
|
||||
|
||||
public function sendQueueFailed(string $job, array $details = []): void
|
||||
{
|
||||
$this->send(
|
||||
AlertType::QUEUE_FAILED,
|
||||
"Queue job gefaald: {$job}",
|
||||
array_merge(['job' => $job], $details),
|
||||
);
|
||||
}
|
||||
|
||||
public function sendHighErrorRate(int $errorCount, int $threshold, int $timeWindow): void
|
||||
{
|
||||
$this->send(
|
||||
AlertType::HIGH_ERROR_RATE,
|
||||
"Hoge error rate gedetecteerd: {$errorCount} errors in {$timeWindow} minuten (drempel: {$threshold})",
|
||||
['error_count' => $errorCount, 'threshold' => $threshold, 'time_window' => $timeWindow],
|
||||
);
|
||||
}
|
||||
|
||||
public function sendEmulatorUpdate(string $version, string $details = ''): void
|
||||
{
|
||||
$this->send(
|
||||
AlertType::EMULATOR_UPDATE,
|
||||
"Emulator succesvol geüpdatet naar versie {$version}",
|
||||
['version' => $version, 'details' => $details],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
}
|
||||
|
||||
public function sendSqlUpdate(int $count, string $details = ''): void
|
||||
{
|
||||
$this->send(
|
||||
AlertType::SQL_UPDATE,
|
||||
"{$count} SQL update(s) succesvol toegepast",
|
||||
['count' => $count, 'details' => $details],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
}
|
||||
|
||||
private function resolveChannels(AlertChannel $preferred): array
|
||||
{
|
||||
$channels = [];
|
||||
|
||||
if (($preferred === AlertChannel::BOTH || $preferred === AlertChannel::EMAIL) && ($this->emailEnabled && ($this->alertEmail !== '' && $this->alertEmail !== '0'))) {
|
||||
$channels[] = AlertChannel::EMAIL;
|
||||
}
|
||||
|
||||
if (($preferred === AlertChannel::BOTH || $preferred === AlertChannel::DISCORD) && ($this->discordEnabled && ($this->discordWebhookUrl !== '' && $this->discordWebhookUrl !== '0'))) {
|
||||
$channels[] = AlertChannel::DISCORD;
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
private function sendEmail(AlertType $type, string $message, array $context): void
|
||||
{
|
||||
if (! $this->emailEnabled || ($this->alertEmail === '' || $this->alertEmail === '0')) {
|
||||
Log::warning('Email alert skipped: not enabled or no email configured', [
|
||||
'emailEnabled' => $this->emailEnabled,
|
||||
'alertEmail' => $this->alertEmail,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$severity = $type->getSeverity();
|
||||
|
||||
Mail::raw($this->buildEmailBody($type, $message, $context), function ($mail) use ($type, $severity) {
|
||||
$mail->from(config('mail.from.address', 'noreply@' . request()->getHost()), $this->siteName)
|
||||
->to($this->alertEmail)
|
||||
->subject(sprintf(
|
||||
'[%s] %s - %s',
|
||||
config('app.env', 'Production'),
|
||||
strtoupper($severity->value),
|
||||
$type->getLabel(),
|
||||
));
|
||||
});
|
||||
|
||||
Log::info('Alert email sent successfully', [
|
||||
'to' => $this->alertEmail,
|
||||
'type' => $type->value,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to send alert email: ' . $e->getMessage(), [
|
||||
'to' => $this->alertEmail,
|
||||
'exception' => $e,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildEmailBody(AlertType $type, string $message, array $context): string
|
||||
{
|
||||
$severity = $type->getSeverity();
|
||||
$timestamp = now()->format('Y-m-d H:i:s T');
|
||||
|
||||
$body = "{$severity->getEmoji()} ALERT: {$type->getLabel()}\n";
|
||||
$body .= "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n";
|
||||
$body .= "Bericht: {$message}\n";
|
||||
$body .= "Tijd: {$timestamp}\n";
|
||||
$body .= "Server: {$this->siteName}\n";
|
||||
$body .= 'Environment: ' . config('app.env', 'Production') . "\n";
|
||||
$body .= 'Severity: ' . ucfirst($severity->value) . "\n\n";
|
||||
|
||||
if ($context !== []) {
|
||||
$body .= "Details:\n";
|
||||
foreach ($context as $key => $value) {
|
||||
if ($key === 'trace' && is_string($value)) {
|
||||
$body .= " - {$key}: [Zie logs voor volledige trace]\n";
|
||||
} else {
|
||||
$body .= " - {$key}: " . (is_array($value) ? json_encode($value) : (string) $value) . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$body .= "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
|
||||
|
||||
return $body . "Dit is een automatisch gegenereerd bericht van {$this->siteName}.\n";
|
||||
}
|
||||
|
||||
private function sendDiscord(AlertType $type, string $message, array $context): void
|
||||
{
|
||||
if (! $this->discordEnabled || ($this->discordWebhookUrl === '' || $this->discordWebhookUrl === '0')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$severity = $type->getSeverity();
|
||||
$color = $this->getDiscordColor($severity);
|
||||
|
||||
$embed = [
|
||||
'title' => "{$severity->getEmoji()} {$type->getLabel()}",
|
||||
'description' => $message,
|
||||
'color' => $color,
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'Server',
|
||||
'value' => $this->siteName,
|
||||
'inline' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'Environment',
|
||||
'value' => config('app.env', 'Production'),
|
||||
'inline' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'Tijd',
|
||||
'value' => now()->format('Y-m-d H:i:s T'),
|
||||
'inline' => true,
|
||||
],
|
||||
],
|
||||
'footer' => [
|
||||
'text' => 'Atom CMS Alert System',
|
||||
],
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
];
|
||||
|
||||
if ($context !== []) {
|
||||
$details = [];
|
||||
foreach (array_slice($context, 0, 5, true) as $key => $value) {
|
||||
if (in_array($key, ['trace', 'stack_trace'])) {
|
||||
continue;
|
||||
}
|
||||
$details[] = [
|
||||
'name' => ucfirst(str_replace('_', ' ', $key)),
|
||||
'value' => mb_substr((string) (is_array($value) ? json_encode($value) : $value), 0, 1024),
|
||||
'inline' => false,
|
||||
];
|
||||
}
|
||||
$embed['fields'] = array_merge($embed['fields'], $details);
|
||||
}
|
||||
|
||||
Http::post($this->discordWebhookUrl, [
|
||||
'username' => 'Atom CMS Alerts',
|
||||
'embeds' => [$embed],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to send Discord alert: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function getDiscordColor(AlertSeverity $severity): int
|
||||
{
|
||||
return match ($severity) {
|
||||
AlertSeverity::INFO => 3447003,
|
||||
AlertSeverity::WARNING => 16776960,
|
||||
AlertSeverity::ERROR => 15158332,
|
||||
AlertSeverity::CRITICAL => 10038562,
|
||||
};
|
||||
}
|
||||
|
||||
private function logAlert(AlertType $type, string $message, array $context = []): void
|
||||
{
|
||||
try {
|
||||
AlertLog::create([
|
||||
'type' => $type->value,
|
||||
'severity' => $type->getSeverity()->value,
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
'sent_via_email' => $this->emailEnabled && ($this->alertEmail !== '' && $this->alertEmail !== '0'),
|
||||
'sent_via_discord' => $this->discordEnabled && ($this->discordWebhookUrl !== '' && $this->discordWebhookUrl !== '0'),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to log alert: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function testAlert(): array
|
||||
{
|
||||
Cache::forget('website_settings');
|
||||
$this->refreshSettings();
|
||||
|
||||
$results = [];
|
||||
|
||||
if ($this->emailEnabled && ($this->alertEmail !== '' && $this->alertEmail !== '0')) {
|
||||
try {
|
||||
$this->send(
|
||||
AlertType::EMULATOR_ONLINE,
|
||||
'Dit is een testmelding van het Atom CMS Alert Systeem.',
|
||||
['test' => true],
|
||||
AlertChannel::EMAIL,
|
||||
);
|
||||
$results['email'] = 'success';
|
||||
} catch (\Exception $e) {
|
||||
$results['email'] = 'failed: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->discordEnabled && ($this->discordWebhookUrl !== '' && $this->discordWebhookUrl !== '0')) {
|
||||
try {
|
||||
$this->send(
|
||||
AlertType::EMULATOR_ONLINE,
|
||||
'Dit is een testmelding van het Atom CMS Alert Systeem.',
|
||||
['test' => true],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
$results['discord'] = 'success';
|
||||
} catch (\Exception $e) {
|
||||
$results['discord'] = 'failed: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function refreshSettings(): void
|
||||
{
|
||||
$this->emailEnabled = (bool) setting('alert_email_enabled', false);
|
||||
$this->discordEnabled = (bool) setting('alert_discord_enabled', false);
|
||||
$this->discordWebhookUrl = setting('alert_discord_webhook_url', '');
|
||||
$this->alertEmail = setting('alert_email_address', '');
|
||||
}
|
||||
|
||||
public static function clearCache(): void
|
||||
{
|
||||
Cache::forget('alert_settings');
|
||||
}
|
||||
}
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Articles;
|
||||
|
||||
use App\Models\Articles\WebsiteArticle;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
readonly class ArticleService
|
||||
{
|
||||
public function getArticles(bool $paginate = false, int $perPage = 8): array|Collection|LengthAwarePaginator
|
||||
{
|
||||
$query = WebsiteArticle::query()->with(['user' => function ($query) {
|
||||
$query->select('id', 'username', 'look');
|
||||
}])->orderByDesc('id');
|
||||
|
||||
return $paginate ? $query->paginate($perPage) : $query->get();
|
||||
}
|
||||
|
||||
public function fetchArticle(string $slug): WebsiteArticle
|
||||
{
|
||||
return WebsiteArticle::query()->where('slug', '=', $slug)->firstOrFail();
|
||||
}
|
||||
}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Articles;
|
||||
|
||||
use App\Models\Articles\WebsiteArticle;
|
||||
use App\Models\Articles\WebsiteArticleComment;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
readonly class CommentService
|
||||
{
|
||||
public function store(string $comment, WebsiteArticle $article): mixed
|
||||
{
|
||||
if ($article->userHasReachedArticleCommentLimit()) {
|
||||
return redirect()->back()->withErrors([
|
||||
'message' => __('You can only comment :amount times per article', ['amount' => setting('max_comment_per_article')]),
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $article->can_comment) {
|
||||
return redirect()->back()->withErrors([
|
||||
'message' => __('This article has been locked from receiving comments'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $article->comments()->create([
|
||||
'user_id' => Auth::id(),
|
||||
'comment' => $comment,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(WebsiteArticleComment $comment): bool|RedirectResponse|null
|
||||
{
|
||||
if (! $comment->canBeDeleted()) {
|
||||
return redirect()->back()->withErrors([
|
||||
'message' => __('You can only delete your own comments'),
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $comment->delete()) {
|
||||
return redirect()->back()->withErrors([
|
||||
'message' => __('An error occurred while deleting the comment'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $comment->delete();
|
||||
}
|
||||
}
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Articles;
|
||||
|
||||
use App\Models\Articles\WebsiteArticle;
|
||||
use App\Models\Articles\WebsiteArticleReaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
readonly class ReactionService
|
||||
{
|
||||
public function toggleReaction(WebsiteArticle $article, User $user, Request $request): array
|
||||
{
|
||||
$reaction = $request->get('reaction');
|
||||
|
||||
if (! is_string($reaction) || ! in_array($reaction, config('habbo.reactions'))) {
|
||||
return ['success' => false];
|
||||
}
|
||||
|
||||
$existingReaction = WebsiteArticleReaction::getReaction($article->id, $user->id, $reaction);
|
||||
|
||||
if ($existingReaction instanceof WebsiteArticleReaction) {
|
||||
$existingReaction->update(['active' => ! $existingReaction->active]);
|
||||
} else {
|
||||
$article->reactions()->create([
|
||||
'reaction' => $reaction,
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'added' => $existingReaction->active ?? true,
|
||||
'username' => $user->username,
|
||||
];
|
||||
}
|
||||
}
|
||||
Executable
+724
@@ -0,0 +1,724 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class AutoDetectService
|
||||
{
|
||||
private static ?self $instance = null;
|
||||
|
||||
private array $cache = [];
|
||||
|
||||
public static function getInstance(): self
|
||||
{
|
||||
return self::$instance ??= new self;
|
||||
}
|
||||
|
||||
// ─── Emulator ──────────────────────────────────────────────
|
||||
|
||||
public function detectEmulatorJarPath(): string
|
||||
{
|
||||
return $this->cache['emulator_jar_path'] ??= $this->doDetectEmulatorJarPath();
|
||||
}
|
||||
|
||||
private function doDetectEmulatorJarPath(): string
|
||||
{
|
||||
// 1. Check setting
|
||||
$setting = $this->getSetting('emulator_jar_path');
|
||||
if ($setting && $this->exists($setting)) {
|
||||
return $setting;
|
||||
}
|
||||
|
||||
// 2. Check well-known directories
|
||||
$dirNames = ['Emulator', 'emulator', 'emulator-source', 'arcturus', 'morningstar', 'hotel', 'habbo-emulator', 'nitro-emulator'];
|
||||
$found = $this->smartSearch($dirNames, ['*.jar'], ['/var/www', '/opt', '/home', '/root', '/srv', base_path()]);
|
||||
if ($found) {
|
||||
return $found;
|
||||
}
|
||||
|
||||
// 3. Find any directory containing a JAR
|
||||
$result = Process::timeout(10)->run(
|
||||
'find /var/www /opt /home /root /srv -maxdepth 4 -name "*.jar" -type f 2>/dev/null | head -1',
|
||||
);
|
||||
if ($result->successful()) {
|
||||
$jarFile = trim($result->output());
|
||||
if ($jarFile !== '' && str_ends_with($jarFile, '.jar')) {
|
||||
return dirname($jarFile);
|
||||
}
|
||||
}
|
||||
|
||||
return '/root/emulator';
|
||||
}
|
||||
|
||||
public function detectEmulatorSourcePath(): string
|
||||
{
|
||||
return $this->cache['emulator_source_path'] ??= $this->doDetectEmulatorSourcePath();
|
||||
}
|
||||
|
||||
private function doDetectEmulatorSourcePath(): string
|
||||
{
|
||||
$setting = $this->getSetting('emulator_source_path');
|
||||
if ($setting && $this->exists($setting)) {
|
||||
return $setting;
|
||||
}
|
||||
|
||||
$dirNames = ['emulator-source', 'emulator', 'arcturus-source', 'morningstar-source', 'Arcturus-Emulator', 'Morningstar', 'hotel-source'];
|
||||
$found = $this->smartSearch($dirNames, ['pom.xml', 'build.gradle', 'src'], ['/var/www', '/opt', '/home', '/root', '/srv', base_path()]);
|
||||
if ($found) {
|
||||
return $found;
|
||||
}
|
||||
|
||||
return '/var/www/emulator-source';
|
||||
}
|
||||
|
||||
public function detectEmulatorServiceName(): string
|
||||
{
|
||||
return $this->cache['emulator_service'] ??= $this->doDetectEmulatorServiceName();
|
||||
}
|
||||
|
||||
private function doDetectEmulatorServiceName(): string
|
||||
{
|
||||
$setting = $this->getSetting('emulator_service_name');
|
||||
if ($setting && $setting !== '' && $setting !== '0') {
|
||||
// Verify it actually exists
|
||||
$check = Process::timeout(5)->run(
|
||||
'systemctl list-unit-files ' . escapeshellarg($setting) . '.service 2>/dev/null | grep -q ' . escapeshellarg($setting) . ' && echo found',
|
||||
);
|
||||
if ($check->successful() && trim($check->output()) === 'found') {
|
||||
return $setting;
|
||||
}
|
||||
}
|
||||
|
||||
// Try common service names
|
||||
$possibleServices = [
|
||||
'emulator', 'arcturus', 'morningstar', 'arcturus-emulator',
|
||||
'habbo', 'hotel', 'game', 'nitro-emulator', 'hotel-server',
|
||||
'arcturus-emu', 'morningstar-emu',
|
||||
];
|
||||
|
||||
foreach ($possibleServices as $service) {
|
||||
$check = Process::timeout(5)->run(
|
||||
'systemctl list-unit-files ' . escapeshellarg($service) . '.service 2>/dev/null | grep -q ' . escapeshellarg($service) . ' && echo found',
|
||||
);
|
||||
if ($check->successful() && trim($check->output()) === 'found') {
|
||||
return $service;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check running services
|
||||
$running = Process::timeout(5)->run('systemctl list-units --type=service --state=running --no-legend 2>/dev/null');
|
||||
if ($running->successful()) {
|
||||
$lines = explode("\n", $running->output());
|
||||
foreach ($lines as $line) {
|
||||
foreach ($possibleServices as $service) {
|
||||
if (str_contains($line, $service)) {
|
||||
return $service;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any Java process that looks like an emulator
|
||||
$javaProc = Process::timeout(5)->run("ps aux | grep -i 'java.*jar\\|arcturus\\|morningstar\\|emulator' | grep -v grep | head -1");
|
||||
if ($javaProc->successful() && trim($javaProc->output()) !== '') {
|
||||
$output = trim($javaProc->output());
|
||||
// Try to extract service name from systemd
|
||||
if (preg_match('/systemd.*--name[=\s]+(\S+)/', $output, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return 'emulator';
|
||||
}
|
||||
|
||||
public function detectEmulatorJavaProcess(): ?array
|
||||
{
|
||||
$result = Process::timeout(5)->run(
|
||||
"ps aux | grep -i '[j]ava.*jar' | head -1",
|
||||
);
|
||||
|
||||
if (! $result->successful() || trim($result->output()) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$line = trim($result->output());
|
||||
$parts = preg_split('/\s+/', $line);
|
||||
|
||||
return [
|
||||
'pid' => $parts[1] ?? null,
|
||||
'full_command' => $line,
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Nitro Client / Renderer ───────────────────────────────
|
||||
|
||||
public function detectNitroClientPath(): string
|
||||
{
|
||||
return $this->cache['nitro_client_path'] ??= $this->doDetectNitroClientPath();
|
||||
}
|
||||
|
||||
private function doDetectNitroClientPath(): string
|
||||
{
|
||||
$setting = $this->getSetting('nitro_client_path');
|
||||
if ($setting && $this->exists($setting)) {
|
||||
return $setting;
|
||||
}
|
||||
|
||||
$dirNames = ['nitro-client', 'nitro-client-v3', 'nitro-V3', 'nitro-V3-client', 'Nitro-V3', 'Nitro_Client', 'nitro', 'client'];
|
||||
$found = $this->smartSearch($dirNames, ['package.json', 'src'], ['/var/www', '/opt', '/home', '/root', base_path(), base_path() . '/..']);
|
||||
if ($found) {
|
||||
return $found;
|
||||
}
|
||||
|
||||
return base_path() . '/nitro-client';
|
||||
}
|
||||
|
||||
public function detectNitroRendererPath(): string
|
||||
{
|
||||
return $this->cache['nitro_renderer_path'] ??= $this->doDetectNitroRendererPath();
|
||||
}
|
||||
|
||||
private function doDetectNitroRendererPath(): string
|
||||
{
|
||||
$setting = $this->getSetting('nitro_renderer_path');
|
||||
if ($setting && $this->exists($setting)) {
|
||||
return $setting;
|
||||
}
|
||||
|
||||
$dirNames = ['nitro-renderer', 'nitro-render-v3', 'Nitro_Render_V3', 'Nitro-Render-V3', 'nitro-render', 'renderer'];
|
||||
$found = $this->smartSearch($dirNames, ['package.json', 'src'], ['/var/www', '/opt', '/home', '/root', base_path(), base_path() . '/..']);
|
||||
if ($found) {
|
||||
return $found;
|
||||
}
|
||||
|
||||
return base_path() . '/nitro-renderer';
|
||||
}
|
||||
|
||||
public function detectNitroWebroot(): string
|
||||
{
|
||||
return $this->cache['nitro_webroot'] ??= $this->doDetectNitroWebroot();
|
||||
}
|
||||
|
||||
private function doDetectNitroWebroot(): string
|
||||
{
|
||||
$setting = $this->getSetting('nitro_webroot');
|
||||
if ($setting && $this->exists($setting)) {
|
||||
return $setting;
|
||||
}
|
||||
|
||||
// Check nginx config for root directives
|
||||
$nginxRoot = $this->detectNginxWebroot();
|
||||
if ($nginxRoot) {
|
||||
return $nginxRoot;
|
||||
}
|
||||
|
||||
$possiblePaths = [
|
||||
'/var/www/Client', '/var/www/client', '/var/www/Nitro', '/var/www/nitro',
|
||||
'/var/www/html/Client', '/var/www/html/client',
|
||||
'/var/www/atomcms/public/Client', '/var/www/atomcms/public/nitro',
|
||||
];
|
||||
|
||||
foreach ($possiblePaths as $path) {
|
||||
if ($this->exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return '/var/www/Client';
|
||||
}
|
||||
|
||||
public function detectNitroBuildPath(): string
|
||||
{
|
||||
return $this->cache['nitro_build_path'] ??= $this->doDetectNitroBuildPath();
|
||||
}
|
||||
|
||||
private function doDetectNitroBuildPath(): string
|
||||
{
|
||||
$setting = $this->getSetting('nitro_build_path');
|
||||
if ($setting && $this->exists($setting)) {
|
||||
return $setting;
|
||||
}
|
||||
|
||||
$clientPath = $this->detectNitroClientPath();
|
||||
$distPath = $clientPath . '/dist';
|
||||
if ($this->exists($distPath)) {
|
||||
return $distPath;
|
||||
}
|
||||
|
||||
$possiblePaths = [
|
||||
'/var/www/nitro-client/dist', '/var/www/nitro-V3/dist',
|
||||
'/var/www/atomcms/nitro-client/dist',
|
||||
];
|
||||
|
||||
foreach ($possiblePaths as $path) {
|
||||
if ($this->exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return $clientPath . '/dist';
|
||||
}
|
||||
|
||||
// ─── Gamedata ──────────────────────────────────────────────
|
||||
|
||||
public function detectGamedataPath(): string
|
||||
{
|
||||
return $this->cache['gamedata_path'] ??= $this->doDetectGamedataPath();
|
||||
}
|
||||
|
||||
private function doDetectGamedataPath(): string
|
||||
{
|
||||
// Always scan first to detect actual case on disk
|
||||
$result = Process::timeout(5)->run('ls -1 /var/www/ 2>/dev/null | grep -i "^gamedata$"');
|
||||
if ($result->successful() && trim($result->output()) !== '') {
|
||||
$actualDir = trim($result->output());
|
||||
$path = '/var/www/' . $actualDir;
|
||||
if ($this->exists($path)) {
|
||||
// Save detected path to setting for future use
|
||||
$this->saveSetting('gamedata_path', $path);
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to setting if scan didn't find it
|
||||
$setting = $this->getSetting('gamedata_path');
|
||||
if ($setting && $this->exists($setting)) {
|
||||
return $setting;
|
||||
}
|
||||
|
||||
// Last resort: check common paths
|
||||
$possiblePaths = [
|
||||
'/var/www/atomcms/public/gamedata', '/var/www/html/gamedata',
|
||||
'/opt/gamedata', '/root/gamedata',
|
||||
];
|
||||
|
||||
foreach ($possiblePaths as $path) {
|
||||
if ($this->exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return '/var/www/gamedata';
|
||||
}
|
||||
|
||||
public function detectFurnitureIconsPath(): string
|
||||
{
|
||||
$setting = $this->getSetting('furniture_icons_path');
|
||||
if ($setting && $this->exists($setting)) {
|
||||
return $setting;
|
||||
}
|
||||
|
||||
$gamedata = $this->detectGamedataPath();
|
||||
$iconsPath = $gamedata . '/icons';
|
||||
|
||||
if ($this->exists($iconsPath)) {
|
||||
return $iconsPath;
|
||||
}
|
||||
|
||||
return '/var/www/Gamedata/icons';
|
||||
}
|
||||
|
||||
public function detectCatalogIconsPath(): string
|
||||
{
|
||||
$setting = $this->getSetting('catalog_icons_path');
|
||||
if ($setting && $this->exists($setting)) {
|
||||
return $setting;
|
||||
}
|
||||
|
||||
$gamedata = $this->detectGamedataPath();
|
||||
$cataloguePath = $gamedata . '/catalogue';
|
||||
|
||||
if ($this->exists($cataloguePath)) {
|
||||
return $cataloguePath;
|
||||
}
|
||||
|
||||
return '/var/www/Gamedata/catalogue';
|
||||
}
|
||||
|
||||
// ─── Nginx ─────────────────────────────────────────────────
|
||||
|
||||
public function detectNginxConfigPath(): ?string
|
||||
{
|
||||
$possibleConfigs = [
|
||||
'/etc/nginx/sites-enabled/cms.conf',
|
||||
'/etc/nginx/sites-enabled/atom.conf',
|
||||
'/etc/nginx/sites-enabled/default',
|
||||
'/etc/nginx/conf.d/atom.conf',
|
||||
'/etc/nginx/conf.d/cms.conf',
|
||||
'/etc/nginx/conf.d/default.conf',
|
||||
];
|
||||
|
||||
foreach ($possibleConfigs as $config) {
|
||||
if (file_exists($config)) {
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function detectNginxWebroot(): ?string
|
||||
{
|
||||
$configPath = $this->detectNginxConfigPath();
|
||||
if (! $configPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$content = @file_get_contents($configPath);
|
||||
if (! $content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Look for root directives that point to Client/nitro directories
|
||||
if (preg_match_all('/^\s*root\s+(.+);/m', $content, $matches)) {
|
||||
foreach ($matches[1] as $root) {
|
||||
$root = trim($root);
|
||||
if ((str_contains(strtolower($root), 'client') || str_contains(strtolower($root), 'nitro')) && $this->exists($root)) {
|
||||
return $root;
|
||||
}
|
||||
}
|
||||
// Return first valid root
|
||||
foreach ($matches[1] as $root) {
|
||||
$root = trim($root);
|
||||
if ($this->exists($root)) {
|
||||
return $root;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function detectGamedataUrlPath(): ?string
|
||||
{
|
||||
$configPath = $this->detectNginxConfigPath();
|
||||
if (! $configPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$content = @file_get_contents($configPath);
|
||||
if (! $content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Look for location /gamedata { alias /var/www/Gamedata; }
|
||||
// Return the location path (the URL path), not the alias path
|
||||
if (preg_match('/location\s+(\/gamedata)\s*\{/i', $content, $matches)) {
|
||||
return $matches[1]; // Returns /gamedata
|
||||
}
|
||||
|
||||
// Fallback: check if there's a root that includes gamedata
|
||||
if (preg_match_all('/^\s*root\s+(.+);/m', $content, $matches)) {
|
||||
foreach ($matches[1] as $root) {
|
||||
$root = trim($root);
|
||||
if (str_contains(strtolower($root), 'gamedata')) {
|
||||
$segments = explode('/', trim($root, '/'));
|
||||
|
||||
return '/' . end($segments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── PHP Configuration ─────────────────────────────────────
|
||||
|
||||
public function detectPhpVersion(): string
|
||||
{
|
||||
$result = Process::timeout(5)->run('php -r "echo PHP_MAJOR_VERSION.\'.\'.PHP_MINOR_VERSION;"');
|
||||
if ($result->successful()) {
|
||||
return trim($result->output());
|
||||
}
|
||||
|
||||
return '8.2';
|
||||
}
|
||||
|
||||
public function detectPhpIniPath(): ?string
|
||||
{
|
||||
$result = Process::timeout(5)->run('php -i 2>/dev/null | grep "Loaded Configuration File" | cut -d">" -f2 | xargs');
|
||||
if ($result->successful()) {
|
||||
$path = trim($result->output());
|
||||
if (file_exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function detectPhpFpmService(): ?string
|
||||
{
|
||||
$version = $this->detectPhpVersion();
|
||||
|
||||
$candidates = [
|
||||
"php{$version}-fpm",
|
||||
'php-fpm',
|
||||
'php8.3-fpm', 'php8.2-fpm', 'php8.1-fpm', 'php8.0-fpm', 'php7.4-fpm',
|
||||
];
|
||||
|
||||
foreach ($candidates as $service) {
|
||||
$check = Process::timeout(5)->run(
|
||||
'systemctl list-unit-files ' . escapeshellarg($service) . '.service 2>/dev/null | grep -q ' . escapeshellarg($service) . ' && echo found',
|
||||
);
|
||||
if ($check->successful() && trim($check->output()) === 'found') {
|
||||
return $service;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Java ──────────────────────────────────────────────────
|
||||
|
||||
public function detectJavaVersion(): ?string
|
||||
{
|
||||
$result = Process::timeout(5)->run('java -version 2>&1 | head -1');
|
||||
if ($result->successful()) {
|
||||
return trim($result->output());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function detectJavaPath(): ?string
|
||||
{
|
||||
$result = Process::timeout(5)->run('which java 2>/dev/null');
|
||||
if ($result->successful()) {
|
||||
$path = trim($result->output());
|
||||
if ($path !== '') {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
// Check common locations
|
||||
$commonPaths = [
|
||||
'/usr/bin/java', '/usr/local/bin/java',
|
||||
'/usr/lib/jvm/default/bin/java',
|
||||
];
|
||||
|
||||
foreach ($commonPaths as $path) {
|
||||
if (file_exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── System Tools ──────────────────────────────────────────
|
||||
|
||||
public function detectGitPath(): ?string
|
||||
{
|
||||
$result = Process::timeout(5)->run('which git 2>/dev/null');
|
||||
|
||||
return $result->successful() ? trim($result->output()) : null;
|
||||
}
|
||||
|
||||
public function detectNodePath(): ?string
|
||||
{
|
||||
$result = Process::timeout(5)->run('which node 2>/dev/null');
|
||||
|
||||
return $result->successful() ? trim($result->output()) : null;
|
||||
}
|
||||
|
||||
public function detectYarnPath(): ?string
|
||||
{
|
||||
$result = Process::timeout(5)->run('which yarn 2>/dev/null');
|
||||
|
||||
return $result->successful() ? trim($result->output()) : null;
|
||||
}
|
||||
|
||||
public function detectNpmPath(): ?string
|
||||
{
|
||||
$result = Process::timeout(5)->run('which npm 2>/dev/null');
|
||||
|
||||
return $result->successful() ? trim($result->output()) : null;
|
||||
}
|
||||
|
||||
public function detectMavenPath(): ?string
|
||||
{
|
||||
$result = Process::timeout(5)->run('which mvn 2>/dev/null || which mvnw 2>/dev/null');
|
||||
if ($result->successful()) {
|
||||
$path = trim($result->output());
|
||||
if ($path !== '') {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
// Check source path for mvnw
|
||||
$sourcePath = $this->detectEmulatorSourcePath();
|
||||
$mvnw = $sourcePath . '/mvnw';
|
||||
if (file_exists($mvnw)) {
|
||||
return $mvnw;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function detectGradlePath(): ?string
|
||||
{
|
||||
$result = Process::timeout(5)->run('which gradle 2>/dev/null');
|
||||
if ($result->successful()) {
|
||||
$path = trim($result->output());
|
||||
if ($path !== '') {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
$sourcePath = $this->detectEmulatorSourcePath();
|
||||
$gradlew = $sourcePath . '/gradlew';
|
||||
if (file_exists($gradlew)) {
|
||||
return $gradlew;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Full Detection ────────────────────────────────────────
|
||||
|
||||
public function detectAll(): array
|
||||
{
|
||||
return [
|
||||
// Emulator
|
||||
'emulator_jar_path' => $this->detectEmulatorJarPath(),
|
||||
'emulator_source_path' => $this->detectEmulatorSourcePath(),
|
||||
'emulator_service_name' => $this->detectEmulatorServiceName(),
|
||||
'emulator_java_process' => $this->detectEmulatorJavaProcess(),
|
||||
|
||||
// Nitro
|
||||
'nitro_client_path' => $this->detectNitroClientPath(),
|
||||
'nitro_renderer_path' => $this->detectNitroRendererPath(),
|
||||
'nitro_webroot' => $this->detectNitroWebroot(),
|
||||
'nitro_build_path' => $this->detectNitroBuildPath(),
|
||||
|
||||
// Gamedata
|
||||
'gamedata_path' => $this->detectGamedataPath(),
|
||||
'furniture_icons_path' => $this->detectFurnitureIconsPath(),
|
||||
'catalog_icons_path' => $this->detectCatalogIconsPath(),
|
||||
|
||||
// System
|
||||
'nginx_config_path' => $this->detectNginxConfigPath(),
|
||||
'php_version' => $this->detectPhpVersion(),
|
||||
'php_ini_path' => $this->detectPhpIniPath(),
|
||||
'php_fpm_service' => $this->detectPhpFpmService(),
|
||||
'java_version' => $this->detectJavaVersion(),
|
||||
'java_path' => $this->detectJavaPath(),
|
||||
|
||||
// Tools
|
||||
'git_path' => $this->detectGitPath(),
|
||||
'node_path' => $this->detectNodePath(),
|
||||
'yarn_path' => $this->detectYarnPath(),
|
||||
'npm_path' => $this->detectNpmPath(),
|
||||
'maven_path' => $this->detectMavenPath(),
|
||||
'gradle_path' => $this->detectGradlePath(),
|
||||
];
|
||||
}
|
||||
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->cache = [];
|
||||
}
|
||||
|
||||
// ─── Private Helpers ───────────────────────────────────────
|
||||
|
||||
private function smartSearch(array $dirNames, array $validators, array $searchDirs): ?string
|
||||
{
|
||||
// Phase 1: exact directory name match with validator check
|
||||
foreach ($searchDirs as $searchDir) {
|
||||
if (! $this->exists($searchDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($dirNames as $dirName) {
|
||||
$fullPath = rtrim((string) $searchDir, '/') . '/' . $dirName;
|
||||
|
||||
if (! $this->exists($fullPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($validators as $validator) {
|
||||
if (str_contains((string) $validator, '*')) {
|
||||
$result = Process::timeout(5)->run(
|
||||
'find ' . escapeshellarg($fullPath) . ' -maxdepth 2 -name ' . escapeshellarg((string) $validator) . ' 2>/dev/null | head -1',
|
||||
);
|
||||
if ($result->successful() && trim($result->output()) !== '') {
|
||||
return $fullPath;
|
||||
}
|
||||
} elseif ($this->exists($fullPath . '/' . $validator)) {
|
||||
return $fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: deep search via find
|
||||
foreach ($searchDirs as $searchDir) {
|
||||
if (! $this->exists($searchDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$namePattern = implode('|', $dirNames);
|
||||
$result = Process::timeout(15)->run(
|
||||
'find ' . escapeshellarg((string) $searchDir) . ' -maxdepth 4 -type d 2>/dev/null | grep -iE "(' . implode('|', array_map(preg_quote(...), $dirNames)) . ')" | head -5',
|
||||
);
|
||||
|
||||
if ($result->successful()) {
|
||||
$lines = explode("\n", trim($result->output()));
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
foreach ($validators as $validator) {
|
||||
if (str_contains((string) $validator, '*')) {
|
||||
$check = Process::timeout(5)->run(
|
||||
'find ' . escapeshellarg($line) . ' -maxdepth 2 -name ' . escapeshellarg((string) $validator) . ' 2>/dev/null | head -1',
|
||||
);
|
||||
if ($check->successful() && trim($check->output()) !== '') {
|
||||
return $line;
|
||||
}
|
||||
} elseif ($this->exists($line . '/' . $validator)) {
|
||||
return $line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function exists(string $path): bool
|
||||
{
|
||||
$result = Process::timeout(5)->run(
|
||||
'test -e ' . escapeshellarg($path) . ' && echo yes || echo no',
|
||||
);
|
||||
|
||||
return trim($result->output()) === 'yes';
|
||||
}
|
||||
|
||||
private function getSetting(string $key): ?string
|
||||
{
|
||||
try {
|
||||
$settings = resolve(SettingsService::class);
|
||||
$value = $settings->getOrDefault($key, '');
|
||||
|
||||
return ($value !== '' && $value !== '0') ? $value : null;
|
||||
} catch (\Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function saveSetting(string $key, string $value): void
|
||||
{
|
||||
try {
|
||||
$settings = resolve(SettingsService::class);
|
||||
$settings->set($key, $value);
|
||||
} catch (\Exception) {
|
||||
// Silently fail - detection still works without saving
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
readonly class CacheService
|
||||
{
|
||||
private const array CACHE_KEYS = [
|
||||
'website_settings' => 'website_settings',
|
||||
'website_permissions' => 'website_permissions',
|
||||
'points_service_settings' => 'points_service_settings',
|
||||
'radio_leaderboard' => 'radio_leaderboard*',
|
||||
'radio_leaderboard_weekly' => 'radio_leaderboard_weekly',
|
||||
'radio_leaderboard_monthly' => 'radio_leaderboard_monthly',
|
||||
'blocked_words' => 'blocked_words',
|
||||
'content_moderation_settings' => 'content_moderation_settings',
|
||||
'radio_settings_general' => 'radio_settings_general',
|
||||
'radio_settings_dj' => 'radio_settings_dj',
|
||||
'radio_ranks_active' => 'radio_ranks_active',
|
||||
'radio_banners_active' => 'radio_banners_active',
|
||||
'radio_shouts_recent' => 'radio_shouts_recent',
|
||||
'rare_value_categories' => 'rare_value_categories',
|
||||
'rare_value_categories_active' => 'rare_value_categories_active',
|
||||
'staff_positions' => 'staff_positions',
|
||||
'staff_ids' => 'staff_ids',
|
||||
'hotel_teams' => 'hotel_teams',
|
||||
];
|
||||
|
||||
private static function useRedis(): bool
|
||||
{
|
||||
return config('cache.default') === 'redis';
|
||||
}
|
||||
|
||||
public static function clearWebsiteSettings(): void
|
||||
{
|
||||
self::forget('website_settings');
|
||||
}
|
||||
|
||||
public static function clearWebsitePermissions(): void
|
||||
{
|
||||
self::forget('website_permissions');
|
||||
}
|
||||
|
||||
public static function clearPointsSettings(): void
|
||||
{
|
||||
self::forget('points_service_settings');
|
||||
}
|
||||
|
||||
public static function clearPointsUser(int $userId): void
|
||||
{
|
||||
self::forget('user_points_' . $userId);
|
||||
}
|
||||
|
||||
public static function clearLeaderboards(): void
|
||||
{
|
||||
self::forgetPattern('radio_leaderboard*');
|
||||
}
|
||||
|
||||
public static function clearContentModeration(): void
|
||||
{
|
||||
self::forget('blocked_words');
|
||||
self::forget('content_moderation_settings');
|
||||
}
|
||||
|
||||
public static function clearRadioSettings(): void
|
||||
{
|
||||
self::forget('radio_settings_general');
|
||||
self::forget('radio_settings_dj');
|
||||
self::forget('radio_ranks_active');
|
||||
self::forget('radio_banners_active');
|
||||
}
|
||||
|
||||
public static function clearRadioShouts(): void
|
||||
{
|
||||
self::forget('radio_shouts_recent');
|
||||
}
|
||||
|
||||
public static function clearRareValueCategories(): void
|
||||
{
|
||||
self::forget('rare_value_categories');
|
||||
self::forget('rare_value_categories_active');
|
||||
}
|
||||
|
||||
public static function clearStaff(): void
|
||||
{
|
||||
self::forget('staff_positions');
|
||||
self::forget('staff_ids');
|
||||
self::forget('hotel_teams');
|
||||
}
|
||||
|
||||
public static function clearApiCache(): void
|
||||
{
|
||||
self::forgetPattern('api_cache_*');
|
||||
}
|
||||
|
||||
public static function clearLeaderboard(): void
|
||||
{
|
||||
self::forget('leaderboard_credits');
|
||||
self::forget('leaderboard_duckets');
|
||||
self::forget('leaderboard_diamonds');
|
||||
self::forget('leaderboard_online');
|
||||
self::forget('leaderboard_respects');
|
||||
self::forget('leaderboard_achievements');
|
||||
}
|
||||
|
||||
public static function clearHelpCenter(): void
|
||||
{
|
||||
self::forget('help_center_categories');
|
||||
self::forget('website_rules_categories');
|
||||
}
|
||||
|
||||
public static function clearAllAppCache(): void
|
||||
{
|
||||
foreach (self::CACHE_KEYS as $key) {
|
||||
if (str_contains($key, '*')) {
|
||||
self::forgetPattern($key);
|
||||
} else {
|
||||
self::forget($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function forget(string $key): void
|
||||
{
|
||||
try {
|
||||
$prefix = config('cache.prefix', 'atomcms_cache_');
|
||||
Cache::forget($prefix . $key);
|
||||
} catch (\Exception) {
|
||||
Cache::forget($key);
|
||||
}
|
||||
}
|
||||
|
||||
private static function forgetPattern(string $pattern): void
|
||||
{
|
||||
if (self::useRedis()) {
|
||||
try {
|
||||
$connection = config('cache.stores.redis.connection', 'cache');
|
||||
$prefix = config('cache.prefix', 'atomcms_cache_');
|
||||
$fullPattern = $prefix . $pattern;
|
||||
$redis = Redis::connection($connection)->client();
|
||||
$keys = $redis->keys($fullPattern);
|
||||
if (! empty($keys)) {
|
||||
$redis->del($keys);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (\Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
$keys = self::CACHE_KEYS;
|
||||
foreach ($keys as $key) {
|
||||
if (fnmatch($pattern, $key, FNM_NOESCAPE)) {
|
||||
self::forget($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+1103
File diff suppressed because it is too large
Load Diff
Executable
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Community;
|
||||
|
||||
use App\Models\Miscellaneous\CameraWeb;
|
||||
|
||||
readonly class CameraService
|
||||
{
|
||||
public function fetchPhotos(bool $paginate = false, int $perPage = 8): mixed
|
||||
{
|
||||
$photos = CameraWeb::query()->where('visible', true)
|
||||
->latest('id')
|
||||
->with('user:id,username,look');
|
||||
|
||||
return $paginate ? $photos->paginate($perPage) : $photos->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Community\RareValues;
|
||||
|
||||
use App\Models\Community\RareValue\WebsiteRareValueCategory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
readonly class RareValueCategoriesService
|
||||
{
|
||||
private const int CACHE_TTL = 300; // 5 minutes
|
||||
|
||||
private const int PAGINATION_PER_PAGE = 12;
|
||||
|
||||
public function fetchAllCategories(): Collection
|
||||
{
|
||||
return Cache::remember('rare_categories_all', self::CACHE_TTL, fn () => WebsiteRareValueCategory::orderBy('priority')->get());
|
||||
}
|
||||
|
||||
public function fetchCategoriesByPriority(): Builder|Collection
|
||||
{
|
||||
return Cache::remember('rare_categories_priority', self::CACHE_TTL, fn () => WebsiteRareValueCategory::query()
|
||||
->orderBy('priority')
|
||||
->with(['furniture' => function ($query) {
|
||||
$query->orderBy('name');
|
||||
}])
|
||||
->get());
|
||||
}
|
||||
|
||||
public function fetchCategoryById(int $id): ?WebsiteRareValueCategory
|
||||
{
|
||||
return Cache::remember("rare_category_{$id}", self::CACHE_TTL, function () use ($id) {
|
||||
/** @var WebsiteRareValueCategory|null $result */
|
||||
$result = WebsiteRareValueCategory::with(['furniture' => function ($query) {
|
||||
$query->orderBy('name');
|
||||
}])
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
public function searchCategories(string $searchTerm): Collection
|
||||
{
|
||||
$cacheKey = 'rare_search_' . md5($searchTerm);
|
||||
|
||||
return Cache::remember($cacheKey, self::CACHE_TTL, fn () => WebsiteRareValueCategory::with(['furniture' => function ($query) use ($searchTerm) {
|
||||
$query->where('name', 'like', '%' . $searchTerm . '%')
|
||||
->orderBy('name');
|
||||
}])
|
||||
->whereHas('furniture', function ($query) use ($searchTerm) {
|
||||
$query->where('name', 'like', '%' . $searchTerm . '%');
|
||||
})
|
||||
->orderBy('priority')
|
||||
->get());
|
||||
}
|
||||
|
||||
public function getRaresWithPagination(int $categoryId, string $sortBy = 'name', string $sortOrder = 'asc'): LengthAwarePaginator
|
||||
{
|
||||
$cacheKey = "rare_category_{$categoryId}_page_" . request('page', 1) . "_{$sortBy}_{$sortOrder}";
|
||||
|
||||
return Cache::remember($cacheKey, self::CACHE_TTL, fn () => WebsiteRareValueCategory::find($categoryId)
|
||||
?->furniture()
|
||||
->orderBy($sortBy, $sortOrder)
|
||||
->paginate(self::PAGINATION_PER_PAGE) ?? collect()->paginate(self::PAGINATION_PER_PAGE));
|
||||
}
|
||||
|
||||
public function getRareStatistics(): array
|
||||
{
|
||||
return Cache::remember('rare_statistics', self::CACHE_TTL, function () {
|
||||
$categories = WebsiteRareValueCategory::withCount('furniture')->get();
|
||||
|
||||
return [
|
||||
'total_categories' => $categories->count(),
|
||||
'total_rares' => $categories->sum('furniture_count'),
|
||||
'most_valuable_category' => $categories->sortByDesc('furniture_count')->first()?->name ?? 'N/A',
|
||||
'average_rares_per_category' => $categories->avg('furniture_count') ?? 0,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function clearCache(): void
|
||||
{
|
||||
Cache::forget('rare_categories_all');
|
||||
Cache::forget('rare_categories_priority');
|
||||
Cache::forget('rare_statistics');
|
||||
|
||||
// Clear individual category caches
|
||||
WebsiteRareValueCategory::pluck('id')->each(function ($id) {
|
||||
Cache::forget("rare_category_{$id}");
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Community;
|
||||
|
||||
use App\Models\Game\Permission;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
readonly class StaffService
|
||||
{
|
||||
public function fetchStaffPositions(): Collection
|
||||
{
|
||||
$cacheEnabled = setting('enable_caching') === '1';
|
||||
|
||||
if ($cacheEnabled && Cache::has('staff_positions')) {
|
||||
return Cache::get('staff_positions');
|
||||
}
|
||||
|
||||
$minStaffRank = (int) setting('min_staff_rank', 3);
|
||||
$minRankToSeeHidden = (int) setting('min_rank_to_see_hidden_staff', 7);
|
||||
$userRank = Auth::check() ? Auth::user()->rank : 0;
|
||||
|
||||
$employees = Permission::query()
|
||||
->select('id', 'rank_name', 'badge', 'staff_color', 'job_description')
|
||||
->when($userRank < $minRankToSeeHidden, fn ($query) => $query->where('hidden_rank', false))
|
||||
->where('id', '>=', $minStaffRank)
|
||||
->orderByDesc('id')
|
||||
->with(['users' => function ($query) use ($minRankToSeeHidden) {
|
||||
$query->select('id', 'username', 'rank', 'motto', 'look', 'hidden_staff', 'online')
|
||||
->when(Auth::check() && Auth::user()->rank < $minRankToSeeHidden, fn ($query) => $query->where('hidden_staff', false));
|
||||
}])
|
||||
->get();
|
||||
|
||||
if ($cacheEnabled) {
|
||||
$cacheTimer = (int) setting('cache_timer');
|
||||
Cache::put('staff_positions', $employees, now()->addMinutes($cacheTimer));
|
||||
}
|
||||
|
||||
return $employees;
|
||||
}
|
||||
|
||||
public function fetchEmployeeIds(): array
|
||||
{
|
||||
$cacheEnabled = setting('enable_caching') === '1';
|
||||
|
||||
if ($cacheEnabled && Cache::has('staff_ids')) {
|
||||
return Cache::get('staff_ids');
|
||||
}
|
||||
|
||||
$minRank = (int) setting('min_staff_rank', 3);
|
||||
|
||||
$staffIds = User::query()->select('id')
|
||||
->where('rank', '>=', $minRank)
|
||||
->get()
|
||||
->pluck('id')->toArray();
|
||||
|
||||
if ($cacheEnabled) {
|
||||
$cacheTimer = (int) setting('cache_timer');
|
||||
Cache::put('staff_ids', $staffIds, now()->addMinutes($cacheTimer));
|
||||
}
|
||||
|
||||
return $staffIds;
|
||||
}
|
||||
}
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Community;
|
||||
|
||||
use App\Models\Community\Teams\WebsiteTeam;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
readonly class TeamService
|
||||
{
|
||||
public function fetchTeams(): Collection
|
||||
{
|
||||
$cacheEnabled = setting('enable_caching') === '1';
|
||||
|
||||
if ($cacheEnabled && Cache::has('hotel_teams')) {
|
||||
return Cache::get('hotel_teams');
|
||||
}
|
||||
|
||||
$employees = WebsiteTeam::query()->select([
|
||||
'id',
|
||||
'rank_name',
|
||||
'badge',
|
||||
'staff_color',
|
||||
'staff_background',
|
||||
'job_description',
|
||||
])
|
||||
->where('hidden_rank', false)
|
||||
->orderByDesc('id')
|
||||
->with(['users' => function ($query) {
|
||||
$query->select('id', 'username', 'look', 'motto', 'rank', 'team_id', 'online');
|
||||
}])
|
||||
->get();
|
||||
|
||||
if ($cacheEnabled) {
|
||||
$cacheTimer = (int) setting('cache_timer');
|
||||
Cache::put('hotel_teams', $employees, now()->addMinutes($cacheTimer));
|
||||
}
|
||||
|
||||
return $employees;
|
||||
}
|
||||
}
|
||||
Executable
+274
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
readonly class ContentModerationService
|
||||
{
|
||||
private const int MODERATION_CACHE_DURATION_MINUTES = 5;
|
||||
|
||||
private const string SETTINGS_CACHE_KEY = 'content_moderation_settings';
|
||||
|
||||
private const int SETTINGS_CACHE_DURATION = 300;
|
||||
|
||||
private const string BLOCKED_WORDS_CACHE_KEY = 'blocked_words';
|
||||
|
||||
private string $openAiApiKey;
|
||||
|
||||
private bool $useOpenAI;
|
||||
|
||||
private float $sensitivity;
|
||||
|
||||
/** @var array<mixed> */
|
||||
private array $blockedWords;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->openAiApiKey = config('services.openai.api_key', '');
|
||||
$this->useOpenAI = $this->openAiApiKey !== '';
|
||||
$this->sensitivity = (float) ($this->getSetting('ai_filter_sensitivity') ?? 0.8);
|
||||
$this->blockedWords = $this->loadBlockedWords();
|
||||
}
|
||||
|
||||
public function moderate(string $content, string $type = 'general'): array
|
||||
{
|
||||
$trimmedContent = trim($content);
|
||||
if ($trimmedContent === '') {
|
||||
return [
|
||||
'approved' => true,
|
||||
'score' => 0,
|
||||
'reason' => null,
|
||||
'detected_words' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$localResult = $this->localModerate($trimmedContent);
|
||||
|
||||
if ($localResult['blocked']) {
|
||||
return [
|
||||
'approved' => false,
|
||||
'score' => 1.0,
|
||||
'reason' => 'Blocked content detected',
|
||||
'detected_words' => $localResult['words'],
|
||||
'method' => 'local',
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->useOpenAI) {
|
||||
return $this->openAIModerate($trimmedContent, $type);
|
||||
}
|
||||
|
||||
return [
|
||||
'approved' => true,
|
||||
'score' => $localResult['score'],
|
||||
'reason' => null,
|
||||
'detected_words' => [],
|
||||
'method' => 'local',
|
||||
];
|
||||
}
|
||||
|
||||
private function localModerate(string $content): array
|
||||
{
|
||||
$text = strtolower(strip_tags($content));
|
||||
$detectedWords = array_filter(
|
||||
$this->blockedWords,
|
||||
fn (string $word): bool => stripos($text, $word) !== false,
|
||||
);
|
||||
|
||||
if ($detectedWords !== []) {
|
||||
return [
|
||||
'blocked' => true,
|
||||
'words' => array_values($detectedWords),
|
||||
'score' => 1.0,
|
||||
];
|
||||
}
|
||||
|
||||
$suspiciousPatterns = [
|
||||
'/(.)\1{4,}/i',
|
||||
'/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/',
|
||||
'/\b(\w*?[\x27\`]+\w*?)+\b/i',
|
||||
];
|
||||
|
||||
$patternScore = array_reduce(
|
||||
$suspiciousPatterns,
|
||||
fn (float $score, string $pattern): float => preg_match($pattern, $text) ? $score + 0.3 : $score,
|
||||
0.0,
|
||||
);
|
||||
|
||||
return [
|
||||
'blocked' => $patternScore >= $this->sensitivity,
|
||||
'words' => [],
|
||||
'score' => min($patternScore, 1.0),
|
||||
];
|
||||
}
|
||||
|
||||
private function openAIModerate(string $content, string $type): array
|
||||
{
|
||||
$cacheKey = 'moderation_' . md5($content . $type);
|
||||
|
||||
return Cache::remember($cacheKey, now()->addMinutes(self::MODERATION_CACHE_DURATION_MINUTES), function () use ($content): array {
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->openAiApiKey,
|
||||
'Content-Type' => 'application/json',
|
||||
])->post('https://api.openai.com/v1/moderations', [
|
||||
'input' => $content,
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$result = $response->json();
|
||||
$moderationResult = $result['results'][0];
|
||||
|
||||
$categories = $moderationResult['categories'];
|
||||
$categoryScores = $moderationResult['category_scores'];
|
||||
|
||||
$maxScore = 0.0;
|
||||
$triggeredCategories = [];
|
||||
|
||||
foreach ($categories as $category => $flagged) {
|
||||
$categoryScore = $categoryScores[$category] ?? 0;
|
||||
if ($flagged || $categoryScore > $this->sensitivity) {
|
||||
$triggeredCategories[] = $category;
|
||||
$maxScore = max($maxScore, $categoryScore);
|
||||
}
|
||||
}
|
||||
|
||||
$approved = $triggeredCategories === [] || $maxScore < $this->sensitivity;
|
||||
|
||||
return [
|
||||
'approved' => $approved,
|
||||
'score' => $maxScore,
|
||||
'reason' => $approved ? null : implode(', ', $triggeredCategories),
|
||||
'detected_words' => $moderationResult['flagged_tokens'] ?? [],
|
||||
'method' => 'openai',
|
||||
'categories' => $triggeredCategories,
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
|
||||
return [
|
||||
'approved' => true,
|
||||
'score' => 0,
|
||||
'reason' => null,
|
||||
'detected_words' => [],
|
||||
'method' => 'fallback',
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function addBlockedWord(string $word): bool
|
||||
{
|
||||
$cleanWord = strtolower(trim($word));
|
||||
$key = 'filter_word_' . $cleanWord;
|
||||
|
||||
WebsiteSetting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => '1', 'comment' => 'Blocked word: ' . $word],
|
||||
);
|
||||
|
||||
Cache::forget(self::BLOCKED_WORDS_CACHE_KEY);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function removeBlockedWord(string $word): bool
|
||||
{
|
||||
$cleanWord = strtolower(trim($word));
|
||||
$key = 'filter_word_' . $cleanWord;
|
||||
|
||||
WebsiteSetting::where('key', $key)->delete();
|
||||
|
||||
Cache::forget(self::BLOCKED_WORDS_CACHE_KEY);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getBlockedWords(): array
|
||||
{
|
||||
return $this->blockedWords;
|
||||
}
|
||||
|
||||
public function setSensitivity(float $sensitivity): void
|
||||
{
|
||||
$newSensitivity = max(0, min(1, $sensitivity));
|
||||
|
||||
WebsiteSetting::updateOrCreate(
|
||||
['key' => 'ai_filter_sensitivity'],
|
||||
['value' => (string) $newSensitivity, 'comment' => 'AI filter sensitivity (0-1)'],
|
||||
);
|
||||
|
||||
Cache::forget(self::SETTINGS_CACHE_KEY);
|
||||
CacheService::clearContentModeration();
|
||||
}
|
||||
|
||||
public function getSensitivity(): float
|
||||
{
|
||||
return $this->sensitivity;
|
||||
}
|
||||
|
||||
public function isUsingOpenAI(): bool
|
||||
{
|
||||
return $this->useOpenAI;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return (bool) $this->getSetting('ai_filter_enabled');
|
||||
}
|
||||
|
||||
public function shouldAutoReject(): bool
|
||||
{
|
||||
return (bool) $this->getSetting('ai_filter_auto_reject');
|
||||
}
|
||||
|
||||
public function getStats(): array
|
||||
{
|
||||
return [
|
||||
'enabled' => $this->isEnabled(),
|
||||
'auto_reject' => $this->shouldAutoReject(),
|
||||
'method' => $this->useOpenAI ? 'openai' : 'local',
|
||||
'sensitivity' => $this->sensitivity,
|
||||
'blocked_words_count' => count($this->blockedWords),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function loadBlockedWords(): array
|
||||
{
|
||||
return Cache::remember(self::BLOCKED_WORDS_CACHE_KEY, self::SETTINGS_CACHE_DURATION, fn (): array => WebsiteSetting::where('key', 'like', 'filter_word_%')
|
||||
->where('value', '1')
|
||||
->pluck('key')
|
||||
->map(fn (string $key): string => str_replace('filter_word_', '', $key))
|
||||
->values()
|
||||
->toArray());
|
||||
}
|
||||
|
||||
private function getSetting(string $key): ?string
|
||||
{
|
||||
return $this->getSettings()[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|null>
|
||||
*/
|
||||
private function getSettings(): array
|
||||
{
|
||||
return Cache::remember(self::SETTINGS_CACHE_KEY, self::SETTINGS_CACHE_DURATION, fn (): array => WebsiteSetting::whereIn('key', [
|
||||
'ai_filter_sensitivity',
|
||||
'ai_filter_enabled',
|
||||
'ai_filter_auto_reject',
|
||||
])->pluck('value', 'key')->toArray());
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Emulator\Concerns;
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
trait HasPermissionColumns
|
||||
{
|
||||
protected function getPermissionColumnsFromSchema(): array
|
||||
{
|
||||
if (! Schema::hasTable('permissions')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$columns = Schema::getColumns('permissions');
|
||||
|
||||
return collect($columns)->filter(function (array $column) {
|
||||
$columnName = $column['name'] ?? null;
|
||||
|
||||
if (! $columnName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_starts_with($columnName, 'cmd')
|
||||
|| str_starts_with($columnName, 'acc')
|
||||
|| str_ends_with($columnName, 'cmd');
|
||||
})->values()->toArray();
|
||||
}
|
||||
}
|
||||
Executable
+2524
File diff suppressed because it is too large
Load Diff
Executable
+74
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/* Credits to Kani for this */
|
||||
|
||||
readonly class FindRetrosService
|
||||
{
|
||||
private const string FIND_RETROS_VERIFY_URI = '%s/validate.php?user=%s&ip=%s';
|
||||
|
||||
private const string FIND_RETROS_REDIRECT_URI = '%s/servers/%s/vote?minimal=1&return=1';
|
||||
|
||||
private const string FIND_RETROS_CACHE_KEY = 'voted.%s';
|
||||
|
||||
private Client $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new Client(['verify' => false]);
|
||||
}
|
||||
|
||||
public function checkHasVoted(): bool
|
||||
{
|
||||
if (! config('habbo.findretros.enabled')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$ip = request()->ip();
|
||||
if ($ip === '127.0.0.1') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (request()->has('novote')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$cacheKey = sprintf(self::FIND_RETROS_CACHE_KEY, $ip);
|
||||
if (Cache::has($cacheKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$uri = sprintf(
|
||||
self::FIND_RETROS_VERIFY_URI,
|
||||
config('habbo.findretros.api'),
|
||||
config('habbo.findretros.name'),
|
||||
$ip,
|
||||
);
|
||||
|
||||
$request = $this->client->get($uri);
|
||||
$response = $request->getBody()->getContents();
|
||||
|
||||
if (in_array($response, ['1', '2'], true)) {
|
||||
Cache::put($cacheKey, true, now()->addMinutes(30));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getRedirectUri(): string
|
||||
{
|
||||
return sprintf(
|
||||
self::FIND_RETROS_REDIRECT_URI,
|
||||
config('habbo.findretros.api'),
|
||||
config('habbo.findretros.name'),
|
||||
);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\WebsiteHousekeepingPermission;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
readonly class HousekeepingPermissionsService
|
||||
{
|
||||
private const string CACHE_KEY = 'housekeeping_permissions';
|
||||
|
||||
private const int CACHE_DURATION_MINUTES = 30;
|
||||
|
||||
private Collection $permissions;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->permissions = Cache::remember(
|
||||
self::CACHE_KEY,
|
||||
now()->addMinutes(self::CACHE_DURATION_MINUTES),
|
||||
fn (): Collection => WebsiteHousekeepingPermission::all()->pluck('min_rank', 'permission'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getOrDefault(string $permissionName, bool $default = false): bool
|
||||
{
|
||||
if (! $this->permissions->has($permissionName)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$requiredRank = (int) $this->permissions->get($permissionName);
|
||||
|
||||
return auth()->check() && auth()->user()->rank >= $requiredRank;
|
||||
}
|
||||
|
||||
public static function clearCache(): void
|
||||
{
|
||||
Cache::forget(self::CACHE_KEY);
|
||||
}
|
||||
}
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Throwable;
|
||||
|
||||
readonly class InstallationService
|
||||
{
|
||||
private const string CACHE_KEY = 'app_installed';
|
||||
|
||||
public static function isComplete(): bool
|
||||
{
|
||||
if (Cache::has(self::CACHE_KEY)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (! Schema::hasTable('website_installation')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$installation = DB::table('website_installation')->first();
|
||||
$isComplete = $installation !== null && (bool) $installation->completed;
|
||||
|
||||
if ($isComplete) {
|
||||
Cache::rememberForever(self::CACHE_KEY, fn (): bool => true);
|
||||
}
|
||||
|
||||
return $isComplete;
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function setComplete(): void
|
||||
{
|
||||
Cache::rememberForever(self::CACHE_KEY, fn (): bool => true);
|
||||
}
|
||||
|
||||
public static function clearCache(): void
|
||||
{
|
||||
Cache::forget(self::CACHE_KEY);
|
||||
}
|
||||
}
|
||||
Executable
+184
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
readonly class IpLookupService
|
||||
{
|
||||
private const array BLOCKLISTS = [
|
||||
// FireHol - All Levels (werkt)
|
||||
'firehol_level1' => 'https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset',
|
||||
'firehol_level2' => 'https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level2.netset',
|
||||
'firehol_level3' => 'https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level3.netset',
|
||||
'firehol_level4' => 'https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level4.netset',
|
||||
|
||||
// Spamhaus (werkt)
|
||||
'spamhaus_drop' => 'https://www.spamhaus.org/drop/drop.txt',
|
||||
'spamhaus_edrop' => 'https://www.spamhaus.org/drop/edrop.txt',
|
||||
|
||||
// Threat Intelligence (werkt)
|
||||
'dshield' => 'https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/dshield.netset',
|
||||
'emerging_threats' => 'https://rules.emergingthreats.net/blockrules/compromised-ips.txt',
|
||||
|
||||
// Blocklist.de (werkt)
|
||||
'blocklist_de_all' => 'https://lists.blocklist.de/lists/all.txt',
|
||||
'blocklist_de_mail' => 'https://lists.blocklist.de/lists/mail.txt',
|
||||
'blocklist_de_ssh' => 'https://lists.blocklist.de/lists/ssh.txt',
|
||||
'blocklist_de_ftp' => 'https://lists.blocklist.de/lists/ftp.txt',
|
||||
'blocklist_de_sip' => 'https://lists.blocklist.de/lists/sip.txt',
|
||||
];
|
||||
|
||||
private const int CACHE_TIME = 43200;
|
||||
|
||||
public function getCountryInfo(string $ip): array
|
||||
{
|
||||
$cacheKey = "ip_country_{$ip}";
|
||||
|
||||
return Cache::remember($cacheKey, now()->addDays(7), function () use ($ip) {
|
||||
try {
|
||||
$response = Http::timeout(10)->get("http://ip-api.com/json/{$ip}");
|
||||
|
||||
if ($response->ok()) {
|
||||
$data = $response->json();
|
||||
|
||||
return [
|
||||
'country_code' => $data['countryCode'] ?? null,
|
||||
'country_name' => $data['country'] ?? null,
|
||||
'region' => $data['regionName'] ?? null,
|
||||
'city' => $data['city'] ?? null,
|
||||
'isp' => $data['isp'] ?? null,
|
||||
'org' => $data['org'] ?? null,
|
||||
'as' => $data['as'] ?? null,
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
|
||||
return ['error' => 'Failed to fetch'];
|
||||
});
|
||||
}
|
||||
|
||||
public function checkVpnProxyTor(string $ip): array
|
||||
{
|
||||
$cacheKey = "ip_check_{$ip}";
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(6), function () use ($ip) {
|
||||
$isBlocked = $this->isIpInAnyBlocklist($ip);
|
||||
|
||||
return [
|
||||
'is_vpn' => $isBlocked,
|
||||
'is_proxy' => $isBlocked,
|
||||
'is_tor' => $isBlocked,
|
||||
'is_malicious' => $isBlocked,
|
||||
'source' => 'multi_blocklist',
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function isIpInAnyBlocklist(string $ip): bool
|
||||
{
|
||||
foreach (self::BLOCKLISTS as $name => $url) {
|
||||
$cidrs = $this->getBlocklistCidrs($name, $url);
|
||||
|
||||
foreach ($cidrs as $cidr) {
|
||||
if ($this->ipInCidr($ip, $cidr)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getBlocklistCidrs(string $name, string $url): array
|
||||
{
|
||||
$cacheKey = "cidr_blocklist_{$name}";
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHours(self::CACHE_TIME), function () use ($url) {
|
||||
try {
|
||||
$response = Http::timeout(120)->get($url);
|
||||
|
||||
if (! $response->ok()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$lines = explode("\n", $response->body());
|
||||
$cidrs = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
if ($line === '' || $line === '0' || str_starts_with($line, '#') || str_starts_with($line, ';')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_contains($line, '/')) {
|
||||
$cidrs[] = $line;
|
||||
} elseif (filter_var($line, FILTER_VALIDATE_IP)) {
|
||||
$cidrs[] = $line . '/32';
|
||||
}
|
||||
}
|
||||
|
||||
return $cidrs;
|
||||
} catch (\Exception) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function ipInCidr(string $ip, string $cidr): bool
|
||||
{
|
||||
if (! str_contains($cidr, '/')) {
|
||||
return $ip === $cidr;
|
||||
}
|
||||
|
||||
try {
|
||||
[$subnet, $mask] = explode('/', $cidr);
|
||||
$mask = (int) $mask;
|
||||
|
||||
if ($mask < 0 || $mask > 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ipLong = ip2long($ip);
|
||||
$subnetLong = ip2long($subnet);
|
||||
|
||||
if ($ipLong === false || $subnetLong === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$maskLong = -1 << (32 - $mask);
|
||||
|
||||
return ($ipLong & $maskLong) === ($subnetLong & $maskLong);
|
||||
} catch (\Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function refreshBlocklists(): void
|
||||
{
|
||||
foreach (array_keys(self::BLOCKLISTS) as $name) {
|
||||
Cache::forget("cidr_blocklist_{$name}");
|
||||
}
|
||||
}
|
||||
|
||||
public function getBlocklistStats(): array
|
||||
{
|
||||
$totalCidrs = 0;
|
||||
|
||||
foreach (self::BLOCKLISTS as $name => $url) {
|
||||
$cidrs = $this->getBlocklistCidrs($name, $url);
|
||||
$totalCidrs += count($cidrs);
|
||||
}
|
||||
|
||||
return [
|
||||
'total' => $totalCidrs,
|
||||
'source' => 'multi_blocklist',
|
||||
];
|
||||
}
|
||||
}
|
||||
Executable
+1358
File diff suppressed because it is too large
Load Diff
Executable
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Parsers;
|
||||
|
||||
class ExternalTextsParser
|
||||
{
|
||||
public function getBadgeData(string $badgeCode): array
|
||||
{
|
||||
// Minimal stub implementation for compatibility with PHPStan during migration.
|
||||
return [
|
||||
'image' => '',
|
||||
'nitro' => [],
|
||||
'flash' => [],
|
||||
];
|
||||
}
|
||||
|
||||
public function updateNitroBadgeTexts(string $code, string $title, string $description): void
|
||||
{
|
||||
// stub
|
||||
}
|
||||
|
||||
public function updateFlashBadgeTexts(string $code, string $title, string $description): void
|
||||
{
|
||||
// stub
|
||||
}
|
||||
|
||||
public function getBadgeImageUrl(string $badgeCode): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Miscellaneous\WebsitePermission;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
readonly class PermissionsService
|
||||
{
|
||||
private const string CACHE_KEY = 'website_permissions';
|
||||
|
||||
private const int CACHE_DURATION_MINUTES = 30;
|
||||
|
||||
private Collection $permissions;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$data = Cache::remember(self::CACHE_KEY, now()->addMinutes(self::CACHE_DURATION_MINUTES), fn () => WebsitePermission::all()->pluck('min_rank', 'permission')->toArray());
|
||||
|
||||
$this->permissions = collect($data);
|
||||
}
|
||||
|
||||
public function getOrDefault(string $permissionName, bool $default = false): bool
|
||||
{
|
||||
if ($this->permissions->isEmpty()) {
|
||||
$permission = WebsitePermission::where('permission', $permissionName)->first();
|
||||
if (! $permission) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return auth()->check() && auth()->user()->rank >= $permission->min_rank;
|
||||
}
|
||||
|
||||
if (! $this->permissions->has($permissionName)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$requiredRank = (int) $this->permissions->get($permissionName);
|
||||
|
||||
return auth()->check() && auth()->user()->rank >= $requiredRank;
|
||||
}
|
||||
|
||||
public function all(): Collection
|
||||
{
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
Cache::forget(self::CACHE_KEY);
|
||||
}
|
||||
}
|
||||
Executable
+255
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Models\RadioListenerPoint;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
readonly class PointsService
|
||||
{
|
||||
private const string POINTS_CACHE_KEY = 'user_points_';
|
||||
|
||||
private const string LEADERBOARD_CACHE_KEY = 'radio_leaderboard';
|
||||
|
||||
private const int LEADERBOARD_CACHE_DURATION = 300;
|
||||
|
||||
private const string SETTINGS_CACHE_KEY = 'points_service_settings';
|
||||
|
||||
private const int SETTINGS_CACHE_DURATION = 300;
|
||||
|
||||
private int $pointsPerMinute;
|
||||
|
||||
private int $maxPointsPerDay;
|
||||
|
||||
private int $requestPoints;
|
||||
|
||||
private int $votePoints;
|
||||
|
||||
private int $giveawayWinPoints;
|
||||
|
||||
private int $contestWinPoints;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$settings = $this->getSettings();
|
||||
$this->pointsPerMinute = (int) ($settings['points_per_minute'] ?? 1);
|
||||
$this->maxPointsPerDay = (int) ($settings['max_points_per_day'] ?? 100);
|
||||
$this->requestPoints = (int) ($settings['points_for_request'] ?? 5);
|
||||
$this->votePoints = (int) ($settings['points_for_vote'] ?? 2);
|
||||
$this->giveawayWinPoints = (int) ($settings['points_for_giveaway_win'] ?? 50);
|
||||
$this->contestWinPoints = (int) ($settings['points_for_contest_win'] ?? 100);
|
||||
}
|
||||
|
||||
public function awardPoints(User $user, int $points, string $reason): RadioListenerPoint
|
||||
{
|
||||
$pointRecord = RadioListenerPoint::awardPoints($user, $points, $reason);
|
||||
|
||||
$this->clearUserCache($user->id);
|
||||
$this->clearLeaderboardCache();
|
||||
|
||||
return $pointRecord;
|
||||
}
|
||||
|
||||
public function deductPoints(User $user, int $points, string $reason): RadioListenerPoint
|
||||
{
|
||||
$pointRecord = RadioListenerPoint::deductPoints($user, $points, $reason);
|
||||
|
||||
$this->clearUserCache($user->id);
|
||||
$this->clearLeaderboardCache();
|
||||
|
||||
return $pointRecord;
|
||||
}
|
||||
|
||||
public function getUserPoints(User $user): int
|
||||
{
|
||||
return Cache::remember(self::POINTS_CACHE_KEY . $user->id, 3600, fn (): int => (int) ($user->radio_points ?? 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getUserHistory(User $user, int $limit = 50): array
|
||||
{
|
||||
return RadioListenerPoint::where('user_id', $user->id)
|
||||
->orderBy('earned_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{rank: int, user_id: int, username: string, avatar: string|null, points: int}>
|
||||
*/
|
||||
public function getLeaderboard(int $limit = 100): array
|
||||
{
|
||||
return Cache::remember(self::LEADERBOARD_CACHE_KEY, self::LEADERBOARD_CACHE_DURATION, function () use ($limit): array {
|
||||
/** @var Collection $collection */
|
||||
$collection = User::where('radio_points', '>', 0)
|
||||
->orderBy('radio_points', 'desc')
|
||||
->limit($limit)
|
||||
->get(['id', 'username', 'avatar', 'radio_points']);
|
||||
|
||||
return $collection->map(fn (User $user, int $index): array => [
|
||||
'rank' => $index + 1,
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'avatar' => $user->avatar ?? null,
|
||||
'points' => (int) $user->radio_points,
|
||||
])->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
public function getWeeklyLeaderboard(int $limit = 50): array
|
||||
{
|
||||
return Cache::remember(self::LEADERBOARD_CACHE_KEY . '_weekly', self::LEADERBOARD_CACHE_DURATION, function () use ($limit) {
|
||||
$weeklyPoints = RadioListenerPoint::where('earned_at', '>=', now()->subWeek())
|
||||
->selectRaw('user_id, SUM(points) as total_points')
|
||||
->groupBy('user_id')
|
||||
->orderBy('total_points', 'desc')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
$userIds = $weeklyPoints->pluck('user_id')->unique()->values();
|
||||
$users = User::whereIn('id', $userIds)
|
||||
->get(['id', 'username', 'avatar'])
|
||||
->keyBy('id');
|
||||
|
||||
return $weeklyPoints->map(fn (object $item, int $index): array => [
|
||||
'rank' => $index + 1,
|
||||
'user_id' => $item->user_id,
|
||||
'username' => $users[$item->user_id]?->username ?? 'Unknown',
|
||||
'avatar' => $users[$item->user_id]?->avatar ?? null,
|
||||
'points' => $item->total_points,
|
||||
])->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
public function getMonthlyLeaderboard(int $limit = 50): array
|
||||
{
|
||||
return Cache::remember(self::LEADERBOARD_CACHE_KEY . '_monthly', self::LEADERBOARD_CACHE_DURATION, function () use ($limit) {
|
||||
$monthlyPoints = RadioListenerPoint::where('earned_at', '>=', now()->subMonth())
|
||||
->selectRaw('user_id, SUM(points) as total_points')
|
||||
->groupBy('user_id')
|
||||
->orderBy('total_points', 'desc')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
$userIds = $monthlyPoints->pluck('user_id')->unique()->values();
|
||||
$users = User::whereIn('id', $userIds)
|
||||
->get(['id', 'username', 'avatar'])
|
||||
->keyBy('id');
|
||||
|
||||
return $monthlyPoints->map(fn (object $item, int $index): array => [
|
||||
'rank' => $index + 1,
|
||||
'user_id' => $item->user_id,
|
||||
'username' => $users[$item->user_id]?->username ?? 'Unknown',
|
||||
'avatar' => $users[$item->user_id]?->avatar ?? null,
|
||||
'points' => $item->total_points,
|
||||
])->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
public function getUserRank(User $user): int
|
||||
{
|
||||
/** @var int $count */
|
||||
$count = User::query()
|
||||
->where('radio_points', '>', $user->radio_points ?? 0)
|
||||
->count();
|
||||
|
||||
return $count + 1;
|
||||
}
|
||||
|
||||
public function awardListeningPoints(User $user, int $minutes): int
|
||||
{
|
||||
$todayPoints = RadioListenerPoint::where('user_id', $user->id)
|
||||
->where('earned_at', '>=', now()->startOfDay())
|
||||
->sum('points');
|
||||
|
||||
$availablePoints = max(0, $this->maxPointsPerDay - $todayPoints);
|
||||
$pointsToAward = min($minutes * $this->pointsPerMinute, $availablePoints);
|
||||
|
||||
if ($pointsToAward > 0) {
|
||||
$this->awardPoints($user, $pointsToAward, 'Luisteren');
|
||||
}
|
||||
|
||||
return $pointsToAward;
|
||||
}
|
||||
|
||||
public function awardRequestPoints(User $user): int
|
||||
{
|
||||
$this->awardPoints($user, $this->requestPoints, 'Song Request');
|
||||
|
||||
return $this->requestPoints;
|
||||
}
|
||||
|
||||
public function awardVotePoints(User $user): int
|
||||
{
|
||||
$this->awardPoints($user, $this->votePoints, 'Stemmen op Request');
|
||||
|
||||
return $this->votePoints;
|
||||
}
|
||||
|
||||
public function awardWinPoints(User $user, string $type): int
|
||||
{
|
||||
$winPoints = match ($type) {
|
||||
'giveaway' => $this->giveawayWinPoints,
|
||||
'contest' => $this->contestWinPoints,
|
||||
default => 10,
|
||||
};
|
||||
|
||||
$this->awardPoints($user, $winPoints, 'Winst: ' . $type);
|
||||
|
||||
return $winPoints;
|
||||
}
|
||||
|
||||
private function clearUserCache(int $userId): void
|
||||
{
|
||||
Cache::forget(self::POINTS_CACHE_KEY . $userId);
|
||||
}
|
||||
|
||||
public function clearLeaderboardCache(): void
|
||||
{
|
||||
Cache::forget(self::LEADERBOARD_CACHE_KEY);
|
||||
Cache::forget(self::LEADERBOARD_CACHE_KEY . '_weekly');
|
||||
Cache::forget(self::LEADERBOARD_CACHE_KEY . '_monthly');
|
||||
}
|
||||
|
||||
public function clearSettingsCache(): void
|
||||
{
|
||||
Cache::forget(self::SETTINGS_CACHE_KEY);
|
||||
CacheService::clearPointsSettings();
|
||||
}
|
||||
|
||||
public function getStats(): array
|
||||
{
|
||||
$totalPoints = RadioListenerPoint::sum('points');
|
||||
$totalUsers = User::where('radio_points', '>', 0)->count();
|
||||
|
||||
return [
|
||||
'total_points_awarded' => $totalPoints,
|
||||
'total_active_users' => $totalUsers,
|
||||
'points_per_minute' => $this->pointsPerMinute,
|
||||
'max_points_per_day' => $this->maxPointsPerDay,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|null>
|
||||
*/
|
||||
private function getSettings(): array
|
||||
{
|
||||
return Cache::remember(self::SETTINGS_CACHE_KEY, self::SETTINGS_CACHE_DURATION, fn (): array => WebsiteSetting::whereIn('key', [
|
||||
'points_per_minute',
|
||||
'max_points_per_day',
|
||||
'points_for_request',
|
||||
'points_for_vote',
|
||||
'points_for_giveaway_win',
|
||||
'points_for_contest_win',
|
||||
])->pluck('value', 'key')->toArray());
|
||||
}
|
||||
}
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Actions\SendCurrency;
|
||||
use App\Actions\SendFurniture;
|
||||
use App\Models\Shop\WebsiteShopArticle;
|
||||
use App\Models\User;
|
||||
|
||||
class PurchaseService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RconService $rconService,
|
||||
private readonly SendCurrency $sendCurrency,
|
||||
) {}
|
||||
|
||||
public function processPurchase(User $currentUser, WebsiteShopArticle $package, ?User $recipient = null): string
|
||||
{
|
||||
$user = $recipient ?? $currentUser;
|
||||
|
||||
$this->deductBalance($currentUser, $package);
|
||||
$this->deliverCurrencies($user, $package);
|
||||
$this->deliverRank($user, $package);
|
||||
$this->deliverBadges($user, $package);
|
||||
$this->deliverFurniture($currentUser, $package);
|
||||
|
||||
return $this->buildSuccessMessage($currentUser, $user, $package);
|
||||
}
|
||||
|
||||
private function deductBalance(User $user, WebsiteShopArticle $package): void
|
||||
{
|
||||
$user->decrement('website_balance', $package->price());
|
||||
}
|
||||
|
||||
private function deliverCurrencies(User $user, WebsiteShopArticle $package): void
|
||||
{
|
||||
$this->sendCurrency->execute($user, 'credits', $package->credits);
|
||||
$this->sendCurrency->execute($user, 'duckets', $package->duckets);
|
||||
$this->sendCurrency->execute($user, 'diamonds', $package->diamonds);
|
||||
}
|
||||
|
||||
private function deliverRank(User $user, WebsiteShopArticle $package): void
|
||||
{
|
||||
if (! $package->give_rank) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->rconService->isConnected) {
|
||||
$this->rconService->setRank($user, $package->give_rank);
|
||||
$this->rconService->disconnectUser($user);
|
||||
} else {
|
||||
$user->update(['rank' => $package->give_rank]);
|
||||
}
|
||||
}
|
||||
|
||||
private function deliverBadges(User $user, WebsiteShopArticle $package): void
|
||||
{
|
||||
if (! $package->badges) {
|
||||
return;
|
||||
}
|
||||
|
||||
$badgeList = explode(';', $package->badges);
|
||||
$ownedBadges = $user->badges()->pluck('badge_code')->toArray();
|
||||
|
||||
foreach ($badgeList as $badge) {
|
||||
if (in_array($badge, $ownedBadges)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->rconService->isConnected) {
|
||||
$this->rconService->giveBadge($user, $badge);
|
||||
} else {
|
||||
$user->badges()->updateOrCreate([
|
||||
'user_id' => $user->id,
|
||||
'badge_code' => $badge,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function deliverFurniture(User $currentUser, WebsiteShopArticle $package): void
|
||||
{
|
||||
if (! $package->furniture) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sendFurniture = app(SendFurniture::class);
|
||||
$sendFurniture->execute($currentUser, json_decode($package->furniture, true));
|
||||
}
|
||||
|
||||
private function buildSuccessMessage(User $currentUser, User $recipient, WebsiteShopArticle $package): string
|
||||
{
|
||||
$message = __('You have successfully purchased the package :name', ['name' => $package->name]);
|
||||
|
||||
if ($recipient->username !== $currentUser->username) {
|
||||
$message = __('You have successfully purchased the package :name for :username', [
|
||||
'name' => $package->name,
|
||||
'username' => $recipient->username,
|
||||
]);
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
Executable
+276
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\CurrencyTypes;
|
||||
use App\Exceptions\RconConnectionException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use JsonException;
|
||||
use Socket;
|
||||
|
||||
class RconService
|
||||
{
|
||||
private ?Socket $socket = null;
|
||||
|
||||
public bool $isConnected = false;
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private array $config = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = [
|
||||
'ip' => setting('rcon_ip'),
|
||||
'port' => (int) setting('rcon_port'),
|
||||
];
|
||||
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
private function initialize(): void
|
||||
{
|
||||
$this->socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
|
||||
|
||||
if ($this->socket === false) {
|
||||
$error = socket_strerror(socket_last_error());
|
||||
Log::error("RCON initialization failed: {$error}");
|
||||
$this->closeConnection();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! @socket_connect($this->socket, $this->config['ip'], $this->config['port'])) {
|
||||
$error = socket_strerror(socket_last_error());
|
||||
Log::error("RCON connection failed: {$error}");
|
||||
$this->closeConnection();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->isConnected = true;
|
||||
}
|
||||
|
||||
private function closeConnection(): void
|
||||
{
|
||||
if ($this->socket instanceof Socket) {
|
||||
socket_close($this->socket);
|
||||
}
|
||||
|
||||
$this->socket = null;
|
||||
$this->isConnected = false;
|
||||
}
|
||||
|
||||
public function isConnected(): bool
|
||||
{
|
||||
return $this->isConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function sendCommand(string $command, ?array $data = null): bool
|
||||
{
|
||||
if (! $this->isConnected) {
|
||||
Log::error('RCON command failed: Not connected');
|
||||
$this->closeConnection();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$payload = json_encode(['key' => $command, 'data' => $data], JSON_THROW_ON_ERROR);
|
||||
|
||||
if (! @socket_write($this->socket, $payload, strlen($payload))) {
|
||||
$error = socket_strerror(socket_last_error($this->socket));
|
||||
Log::error("RCON command ({$command}) failed: {$error}");
|
||||
$this->closeConnection();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException|JsonException
|
||||
*/
|
||||
public function sendGift(object $user, int $itemId, string $message = 'Here is a gift.'): void
|
||||
{
|
||||
$this->sendCommand('sendgift', [
|
||||
'user_id' => $user->id,
|
||||
'itemid' => $itemId,
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException|JsonException
|
||||
*/
|
||||
public function giveCredits(object $user, int $credits): void
|
||||
{
|
||||
$this->sendCommand('givecredits', [
|
||||
'user_id' => $user->id,
|
||||
'credits' => $credits,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException|JsonException
|
||||
*/
|
||||
public function giveBadge(object $user, string $badge): void
|
||||
{
|
||||
$this->sendCommand('givebadge', [
|
||||
'user_id' => $user->id,
|
||||
'badge' => $badge,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException|JsonException
|
||||
*/
|
||||
public function setMotto(object $user, string $motto): void
|
||||
{
|
||||
$this->sendCommand('setmotto', [
|
||||
'user_id' => $user->id,
|
||||
'motto' => $motto,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException|JsonException
|
||||
*/
|
||||
public function updateWordFilter(): void
|
||||
{
|
||||
$this->sendCommand('updatewordfilter');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException|JsonException
|
||||
*/
|
||||
public function disconnectUser(object $user): void
|
||||
{
|
||||
$this->sendCommand('disconnect', [
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException|JsonException
|
||||
*/
|
||||
public function givePoints(object $user, CurrencyTypes $type, int $amount): void
|
||||
{
|
||||
$this->sendCommand('givepoints', [
|
||||
'user_id' => $user->id,
|
||||
'points' => $amount,
|
||||
'type' => $type,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function giveGotw(object $user, int $amount): void
|
||||
{
|
||||
$this->givePoints($user, CurrencyTypes::Points, $amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function giveDiamonds(object $user, int $amount): void
|
||||
{
|
||||
$this->givePoints($user, CurrencyTypes::Diamonds, $amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function giveDuckets(object $user, int $amount): void
|
||||
{
|
||||
$this->givePoints($user, CurrencyTypes::Duckets, $amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function setRank(object $user, int $rank): void
|
||||
{
|
||||
$this->sendCommand('setrank', [
|
||||
'user_id' => $user->id,
|
||||
'rank' => $rank,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function updateCatalog(): void
|
||||
{
|
||||
$this->sendCommand('updatecatalog');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function alertUser(object $user, string $message): void
|
||||
{
|
||||
$this->sendCommand('alertuser', [
|
||||
'user_id' => $user->id,
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function forwardUser(object $user, int $roomId): void
|
||||
{
|
||||
$this->sendCommand('forwarduser', [
|
||||
'user_id' => $user->id,
|
||||
'room_id' => $roomId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RconConnectionException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function updateConfig(object $user, string $command): void
|
||||
{
|
||||
$this->sendCommand('executecommand', [
|
||||
'user_id' => $user->id,
|
||||
'command' => $command,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an RCON command safely from dashboard with error handling
|
||||
*
|
||||
* @param array<string, mixed> $params The parameters for the command
|
||||
*
|
||||
* @throws RconConnectionException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function sendSafelyFromDashboard(string $command, array $params, string $errorMessage): void
|
||||
{
|
||||
if (! $this->isConnected()) {
|
||||
Log::error($errorMessage . ' - RCON not connected');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sendCommand($command, $params);
|
||||
}
|
||||
}
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Miscellaneous\WebsiteLanguage;
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Throwable;
|
||||
|
||||
class SettingsService
|
||||
{
|
||||
private const string CACHE_KEY = 'website_settings';
|
||||
|
||||
private const string LANGUAGES_CACHE_KEY = 'website_languages';
|
||||
|
||||
private ?Collection $cachedSettings = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly InstallationService $installationService,
|
||||
) {}
|
||||
|
||||
public function getOrDefault(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$settings = $this->settings();
|
||||
|
||||
return $settings->get($key, $default);
|
||||
}
|
||||
|
||||
public function getLanguages(): Collection
|
||||
{
|
||||
return Cache::rememberForever(self::LANGUAGES_CACHE_KEY, function (): Collection {
|
||||
try {
|
||||
if (! Schema::hasTable('website_languages')) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return WebsiteLanguage::all();
|
||||
} catch (Throwable) {
|
||||
return collect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function clearCache(): void
|
||||
{
|
||||
Cache::forget(self::CACHE_KEY);
|
||||
Cache::forget(self::LANGUAGES_CACHE_KEY);
|
||||
}
|
||||
|
||||
public function clearInstanceCache(): void
|
||||
{
|
||||
$this->cachedSettings = null;
|
||||
Cache::forget(self::CACHE_KEY);
|
||||
Cache::forget(self::LANGUAGES_CACHE_KEY);
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value): void
|
||||
{
|
||||
WebsiteSetting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => is_bool($value) ? ($value ? '1' : '0') : (string) $value],
|
||||
);
|
||||
|
||||
// Clear cache to ensure fresh values are fetched
|
||||
self::clearCache();
|
||||
}
|
||||
|
||||
private function settings(): Collection
|
||||
{
|
||||
if ($this->isInstallationIncomplete()) {
|
||||
return $this->fetchSettings();
|
||||
}
|
||||
|
||||
$this->cachedSettings = collect(Cache::rememberForever(self::CACHE_KEY, fn () => $this->fetchSettings()->toArray()));
|
||||
|
||||
return $this->cachedSettings;
|
||||
}
|
||||
|
||||
private function isInstallationIncomplete(): bool
|
||||
{
|
||||
try {
|
||||
return ! $this->installationService->isComplete();
|
||||
} catch (Throwable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchSettings(): Collection
|
||||
{
|
||||
try {
|
||||
if (! Schema::hasTable('website_settings')) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return WebsiteSetting::query()->pluck('value', 'key');
|
||||
} catch (Throwable) {
|
||||
return collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+233
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
readonly class StreamMonitoringService
|
||||
{
|
||||
private const string CACHE_KEY = 'radio_stream_status';
|
||||
|
||||
private const int CACHE_DURATION_MINUTES = 1;
|
||||
|
||||
private const string SETTINGS_CACHE_KEY = 'radio_monitor_settings';
|
||||
|
||||
private const int SETTINGS_CACHE_DURATION = 60;
|
||||
|
||||
private string $streamUrl;
|
||||
|
||||
private int $timeout;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$settings = $this->getSettings();
|
||||
$this->streamUrl = $settings['radio_stream_url'] ?? '';
|
||||
$this->timeout = (int) ($settings['radio_monitoring_timeout'] ?? 5);
|
||||
}
|
||||
|
||||
public function getStatus(): array
|
||||
{
|
||||
return Cache::remember(self::CACHE_KEY, now()->addMinutes(self::CACHE_DURATION_MINUTES), fn (): array => $this->fetchStatus());
|
||||
}
|
||||
|
||||
private function fetchStatus(): array
|
||||
{
|
||||
$status = [
|
||||
'online' => false,
|
||||
'listeners' => 0,
|
||||
'bitrate' => 0,
|
||||
'format' => null,
|
||||
'server_type' => null,
|
||||
'last_check' => now()->toIso8601String(),
|
||||
'error' => null,
|
||||
];
|
||||
|
||||
if ($this->streamUrl === '') {
|
||||
$status['error'] = 'No stream URL configured';
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout($this->timeout)
|
||||
->withOptions([
|
||||
'verify' => false,
|
||||
'allow_redirects' => true,
|
||||
])
|
||||
->head($this->streamUrl);
|
||||
|
||||
if ($response->successful()) {
|
||||
$status['online'] = true;
|
||||
$status['listeners'] = $this->extractListeners($response);
|
||||
$status['bitrate'] = $this->extractBitrate($response);
|
||||
$status['format'] = $this->extractFormat($response);
|
||||
$status['server_type'] = $this->detectServerType($response);
|
||||
} else {
|
||||
$status['error'] = 'Stream returned status: ' . $response->status();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$status['error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
private function extractListeners(Response $response): int
|
||||
{
|
||||
$headers = [
|
||||
'x-listeners',
|
||||
'x-audiocast-listeners',
|
||||
'icecast-listeners',
|
||||
'listeners',
|
||||
];
|
||||
|
||||
foreach ($headers as $header) {
|
||||
$value = $response->header($header);
|
||||
if ($value !== null && is_numeric($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function extractBitrate(Response $response): int
|
||||
{
|
||||
$headers = ['x-bitrate', 'bitrate'];
|
||||
|
||||
foreach ($headers as $header) {
|
||||
$value = $response->header($header);
|
||||
if ($value !== null && is_numeric($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function extractFormat(Response $response): ?string
|
||||
{
|
||||
$contentType = $response->header('content-type');
|
||||
|
||||
if ($contentType === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match (true) {
|
||||
str_contains($contentType, 'mp3') => 'MP3',
|
||||
str_contains($contentType, 'ogg') => 'OGG',
|
||||
str_contains($contentType, 'aac') => 'AAC',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function detectServerType(Response $response): ?string
|
||||
{
|
||||
$server = $response->header('server');
|
||||
|
||||
if ($server !== null) {
|
||||
$serverLower = strtolower($server);
|
||||
|
||||
return match (true) {
|
||||
str_contains($serverLower, 'icecast') => 'Icecast',
|
||||
str_contains($serverLower, 'shoutcast') => 'Shoutcast',
|
||||
str_contains($serverLower, 'azurecast') => 'AzureCast',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
$headers = $response->headers();
|
||||
|
||||
return isset($headers['x-audiocast-server']) ? 'AzureCast' : null;
|
||||
}
|
||||
|
||||
public function getUptime(): array
|
||||
{
|
||||
$uptime = $this->getSetting('radio_last_online');
|
||||
|
||||
if ($uptime === null) {
|
||||
return [
|
||||
'was_ever_online' => false,
|
||||
'last_online' => null,
|
||||
'uptime_percentage' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$lastOnline = Carbon::parse($uptime);
|
||||
$now = now();
|
||||
$diff = $lastOnline->diff($now);
|
||||
|
||||
$totalMinutes = $diff->days * 24 * 60 + $diff->h * 60 + $diff->i;
|
||||
|
||||
return [
|
||||
'was_ever_online' => true,
|
||||
'last_online' => $lastOnline->toIso8601String(),
|
||||
'downtime_minutes' => $totalMinutes,
|
||||
'last_online_human' => $lastOnline->diffForHumans(),
|
||||
];
|
||||
}
|
||||
|
||||
public function recordOnlineStatus(bool $isOnline): void
|
||||
{
|
||||
if ($isOnline) {
|
||||
WebsiteSetting::updateOrCreate(
|
||||
['key' => 'radio_last_online'],
|
||||
['value' => now()->toIso8601String()],
|
||||
);
|
||||
|
||||
$this->incrementStat('radio_total_online_minutes');
|
||||
}
|
||||
|
||||
$this->incrementStat('radio_total_checks');
|
||||
}
|
||||
|
||||
private function incrementStat(string $key): void
|
||||
{
|
||||
$current = (int) ($this->getSetting($key) ?? 0);
|
||||
WebsiteSetting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => (string) ($current + 1)],
|
||||
);
|
||||
}
|
||||
|
||||
public function getStats(): array
|
||||
{
|
||||
$totalChecks = (int) ($this->getSetting('radio_total_checks') ?? 0);
|
||||
$totalOnlineMinutes = (int) ($this->getSetting('radio_total_online_minutes') ?? 0);
|
||||
|
||||
$uptimePercentage = $totalChecks > 0 ? round(($totalOnlineMinutes / max($totalChecks, 1)) * 100, 2) : 0;
|
||||
|
||||
return [
|
||||
'current_status' => $this->getStatus(),
|
||||
'uptime' => $this->getUptime(),
|
||||
'total_checks' => $totalChecks,
|
||||
'total_online_minutes' => $totalOnlineMinutes,
|
||||
'uptime_percentage' => $uptimePercentage,
|
||||
];
|
||||
}
|
||||
|
||||
private function getSetting(string $key): ?string
|
||||
{
|
||||
return $this->getSettings()[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|null>
|
||||
*/
|
||||
private function getSettings(): array
|
||||
{
|
||||
return Cache::remember(self::SETTINGS_CACHE_KEY, self::SETTINGS_CACHE_DURATION, fn (): array => WebsiteSetting::whereIn('key', [
|
||||
'radio_stream_url',
|
||||
'radio_monitoring_timeout',
|
||||
'radio_last_online',
|
||||
'radio_total_checks',
|
||||
'radio_total_online_minutes',
|
||||
])->pluck('value', 'key')->toArray());
|
||||
}
|
||||
}
|
||||
Executable
+596
@@ -0,0 +1,596 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class SystemFixService
|
||||
{
|
||||
private array $results = [];
|
||||
|
||||
public function checkAndFixAll(): array
|
||||
{
|
||||
$this->results = [];
|
||||
|
||||
// 1. Check PHP extensions
|
||||
$this->checkAndFixPhpExtensions();
|
||||
|
||||
// 2. Check Git
|
||||
$this->checkAndFixGit();
|
||||
|
||||
// 3. Check Maven
|
||||
$this->checkAndFixMaven();
|
||||
|
||||
// 4. Check Node.js
|
||||
$this->checkAndFixNode();
|
||||
|
||||
// 5. Check MySQL connection
|
||||
$this->checkDatabase();
|
||||
|
||||
// 6. Fix permissions
|
||||
$this->fixPermissions();
|
||||
|
||||
// 7. Fix open_basedir
|
||||
$this->fixOpenBaseDir();
|
||||
|
||||
// 8. Configure emulator database from .env
|
||||
$this->configureEmulatorDatabase();
|
||||
|
||||
// 9. Create missing directories
|
||||
$this->createMissingDirectories();
|
||||
|
||||
// 10. Fix storage permissions
|
||||
$this->fixStoragePermissions();
|
||||
|
||||
// 11. Check disk space
|
||||
$this->checkDiskSpace();
|
||||
|
||||
// 12. Clear old caches
|
||||
$this->clearOldCaches();
|
||||
|
||||
// 13. Fix git safe directories
|
||||
$this->fixGitSafeDirectories();
|
||||
|
||||
// 14. Create .m2 directory for Maven
|
||||
$this->fixMavenDirectory();
|
||||
|
||||
// 15. Check and install Java (needed for Maven)
|
||||
$this->checkAndFixJava();
|
||||
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
private function checkAndFixPhpExtensions(): void
|
||||
{
|
||||
$requiredExtensions = ['pdo_mysql', 'curl', 'json', 'mbstring', 'xml', 'zip', 'bcmath'];
|
||||
$missing = [];
|
||||
|
||||
foreach ($requiredExtensions as $ext) {
|
||||
if (! extension_loaded($ext)) {
|
||||
$missing[] = $ext;
|
||||
}
|
||||
}
|
||||
|
||||
if ($missing === []) {
|
||||
$this->results[] = ['item' => 'PHP Extensions', 'status' => 'ok', 'message' => 'Alle vereiste extensies geïnstalleerd'];
|
||||
} else {
|
||||
$this->results[] = ['item' => 'PHP Extensions', 'status' => 'warning', 'message' => 'Ontbrekend: ' . implode(', ', $missing)];
|
||||
}
|
||||
}
|
||||
|
||||
private function checkAndFixGit(): void
|
||||
{
|
||||
$result = shell_exec('which git 2>/dev/null || echo ""');
|
||||
if (! in_array(trim($result ?? ''), ['', '0'], true)) {
|
||||
$this->results[] = ['item' => 'Git', 'status' => 'ok', 'message' => trim($result)];
|
||||
} else {
|
||||
// Install Git
|
||||
shell_exec('sudo apt-get update && sudo apt-get install -y git 2>&1');
|
||||
$result = shell_exec('which git 2>/dev/null || echo ""');
|
||||
if (! in_array(trim($result ?? ''), ['', '0'], true)) {
|
||||
$this->results[] = ['item' => 'Git', 'status' => 'fixed', 'message' => 'Automatisch geïnstalleerd'];
|
||||
} else {
|
||||
$this->results[] = ['item' => 'Git', 'status' => 'error', 'message' => 'Installatie mislukt'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkAndFixMaven(): void
|
||||
{
|
||||
$result = shell_exec('which mvn 2>/dev/null || echo ""');
|
||||
if (! in_array(trim($result ?? ''), ['', '0'], true)) {
|
||||
$this->results[] = ['item' => 'Maven', 'status' => 'ok', 'message' => trim($result)];
|
||||
} else {
|
||||
// Install Maven
|
||||
shell_exec('sudo apt-get update && sudo apt-get install -y maven 2>&1');
|
||||
// Fix Maven repository permissions
|
||||
shell_exec('sudo mkdir -p /var/www/.m2/repository && sudo chown -R www-data:www-data /var/www/.m2');
|
||||
$result = shell_exec('which mvn 2>/dev/null || echo ""');
|
||||
if (! in_array(trim($result ?? ''), ['', '0'], true)) {
|
||||
$this->results[] = ['item' => 'Maven', 'status' => 'fixed', 'message' => 'Automatisch geïnstalleerd'];
|
||||
} else {
|
||||
$this->results[] = ['item' => 'Maven', 'status' => 'error', 'message' => 'Installatie mislukt'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkAndFixNode(): void
|
||||
{
|
||||
// Use shell command to check for node (bypasses open_basedir)
|
||||
$nodeResult = shell_exec('which node 2>/dev/null || echo ""');
|
||||
$nodePath = trim($nodeResult ?? '');
|
||||
|
||||
if ($nodePath !== '' && $nodePath !== '0') {
|
||||
$version = shell_exec("{$nodePath} --version 2>/dev/null || echo ''");
|
||||
$versionNum = trim($version ?? '');
|
||||
|
||||
// Check if version is 24.x
|
||||
if (str_starts_with($versionNum, 'v24')) {
|
||||
$this->results[] = ['item' => 'Node.js', 'status' => 'ok', 'message' => $versionNum];
|
||||
} else {
|
||||
// Upgrade to Node.js 24
|
||||
$this->installNode24();
|
||||
}
|
||||
} else {
|
||||
// Install Node.js 24
|
||||
$this->installNode24();
|
||||
}
|
||||
}
|
||||
|
||||
private function installNode24(): void
|
||||
{
|
||||
// Install Node.js 24 via NodeSource
|
||||
$commands = [
|
||||
'curl -fsSL https://deb.nodesource.com/setup_24.x | sudo bash - 2>&1',
|
||||
'sudo apt-get install -y nodejs 2>&1',
|
||||
];
|
||||
|
||||
$success = true;
|
||||
foreach ($commands as $cmd) {
|
||||
$result = shell_exec($cmd);
|
||||
if (str_contains($result ?? '', 'E:')) {
|
||||
$success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify installation
|
||||
$version = shell_exec('node --version 2>/dev/null || echo ""');
|
||||
$versionNum = trim($version ?? '');
|
||||
|
||||
if ($versionNum !== '' && $versionNum !== '0' && str_starts_with($versionNum, 'v24')) {
|
||||
$this->results[] = ['item' => 'Node.js', 'status' => 'fixed', 'message' => "Geïnstalleerd: {$versionNum}"];
|
||||
} else {
|
||||
$this->results[] = ['item' => 'Node.js', 'status' => 'error', 'message' => 'Installatie mislukt'];
|
||||
}
|
||||
}
|
||||
|
||||
private function checkDatabase(): void
|
||||
{
|
||||
try {
|
||||
$host = setting('emulator_database_host', '127.0.0.1');
|
||||
$port = setting('emulator_database_port', '3306');
|
||||
$name = setting('emulator_database_name', config('database.connections.mysql.database', ''));
|
||||
$username = setting('emulator_database_username', config('database.connections.mysql.username', ''));
|
||||
$password = setting('emulator_database_password', config('database.connections.mysql.password', ''));
|
||||
|
||||
if (empty($name) || empty($username)) {
|
||||
$this->results[] = ['item' => 'Emulator Database', 'status' => 'warning', 'message' => 'Niet geconfigureerd'];
|
||||
$this->autoFixEmulatorDatabase();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
$validationErrors = $this->validateDatabaseInputs($host, $port, $name, $username);
|
||||
if ($validationErrors !== []) {
|
||||
$this->results[] = ['item' => 'Emulator Database', 'status' => 'error', 'message' => implode(', ', $validationErrors)];
|
||||
$this->autoFixEmulatorDatabase();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Try connection
|
||||
try {
|
||||
$pdo = new \PDO(
|
||||
"mysql:host={$host};port={$port};charset=utf8mb4",
|
||||
$username,
|
||||
$password,
|
||||
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_TIMEOUT => 5],
|
||||
);
|
||||
|
||||
// Test with database
|
||||
$pdo->exec("USE `{$name}`");
|
||||
$pdo->query('SELECT 1');
|
||||
$this->results[] = ['item' => 'Emulator Database', 'status' => 'ok', 'message' => "Verbonden met {$name}"];
|
||||
} catch (\PDOException $e) {
|
||||
$errorMsg = $e->getMessage();
|
||||
if (str_contains($errorMsg, 'Unknown database')) {
|
||||
$this->results[] = ['item' => 'Emulator Database', 'status' => 'error', 'message' => "Database '{$name}' bestaat niet"];
|
||||
$this->autoFixEmulatorDatabase();
|
||||
} elseif (str_contains($errorMsg, 'Access denied')) {
|
||||
$this->results[] = ['item' => 'Emulator Database', 'status' => 'error', 'message' => 'Verkeerde gebruikersnaam/wachtwoord'];
|
||||
$this->autoFixEmulatorDatabase();
|
||||
} elseif (str_contains($errorMsg, 'Connection refused')) {
|
||||
$this->results[] = ['item' => 'Emulator Database', 'status' => 'error', 'message' => "Kan niet verbinden met {$host}:{$port}"];
|
||||
} else {
|
||||
$this->results[] = ['item' => 'Emulator Database', 'status' => 'error', 'message' => $errorMsg];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->results[] = ['item' => 'Emulator Database', 'status' => 'error', 'message' => $e->getMessage()];
|
||||
$this->autoFixEmulatorDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
private function validateDatabaseInputs(string $host, string $port, string $name, string $username): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Validate host
|
||||
if ($host === '' || $host === '0') {
|
||||
$errors[] = 'Host is leeg';
|
||||
} elseif (! filter_var($host, FILTER_VALIDATE_IP) && ! preg_match('/^[a-zA-Z0-9.-]+$/', $host)) {
|
||||
$errors[] = 'Ongeldige host: ' . $host;
|
||||
}
|
||||
|
||||
// Validate port
|
||||
if ($port === '' || $port === '0') {
|
||||
$errors[] = 'Port is leeg';
|
||||
} elseif (! is_numeric($port) || $port < 1 || $port > 65535) {
|
||||
$errors[] = 'Ongeldige port: ' . $port;
|
||||
}
|
||||
|
||||
// Validate database name
|
||||
if ($name === '' || $name === '0') {
|
||||
$errors[] = 'Database naam is leeg';
|
||||
} elseif (! preg_match('/^\w+$/', $name)) {
|
||||
$errors[] = 'Ongeldige database naam: ' . $name;
|
||||
}
|
||||
|
||||
// Validate username
|
||||
if ($username === '' || $username === '0') {
|
||||
$errors[] = 'Gebruikersnaam is leeg';
|
||||
} elseif (! preg_match('/^\w+$/', $username)) {
|
||||
$errors[] = 'Ongeldige gebruikersnaam: ' . $username;
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
private function autoFixEmulatorDatabase(): void
|
||||
{
|
||||
// Try to get correct values from .env / config
|
||||
$host = config('database.connections.mysql.host', '127.0.0.1');
|
||||
$port = config('database.connections.mysql.port', '3306');
|
||||
$name = config('database.connections.mysql.database', '');
|
||||
$username = config('database.connections.mysql.username', '');
|
||||
$password = config('database.connections.mysql.password', '');
|
||||
|
||||
if (empty($name) || empty($username)) {
|
||||
// Try to detect from MySQL process list or common names
|
||||
$detected = $this->detectDatabaseFromMysql();
|
||||
if ($detected) {
|
||||
$name = $detected['name'];
|
||||
$host = $detected['host'] ?? $host;
|
||||
$username = $detected['username'] ?? $username;
|
||||
$password = $detected['password'] ?? $password;
|
||||
} else {
|
||||
$this->results[] = ['item' => 'Emulator Database Fix', 'status' => 'warning', 'message' => 'Kon geen geldige database vinden'];
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$settings = app(SettingsService::class);
|
||||
$settings->set('emulator_database_host', $host);
|
||||
$settings->set('emulator_database_port', (string) $port);
|
||||
$settings->set('emulator_database_name', $name);
|
||||
$settings->set('emulator_database_username', $username);
|
||||
$settings->set('emulator_database_password', $password);
|
||||
|
||||
$this->results[] = ['item' => 'Emulator Database Fix', 'status' => 'fixed', 'message' => "Automatisch gefixt: {$name}@{$host}"];
|
||||
}
|
||||
|
||||
private function detectDatabaseFromMysql(): ?array
|
||||
{
|
||||
// Common database names for hotel emulators
|
||||
$commonNames = ['habbo', 'hotel', 'retro', 'emulator', 'game', 'server', 'arcturus', 'fusion'];
|
||||
|
||||
// Get CMS database credentials that work
|
||||
$cmsHost = config('database.connections.mysql.host', '127.0.0.1');
|
||||
$cmsPort = config('database.connections.mysql.port', '3306');
|
||||
$cmsUser = config('database.connections.mysql.username', '');
|
||||
$cmsPass = config('database.connections.mysql.password', '');
|
||||
$cmsDb = config('database.connections.mysql.database', '');
|
||||
|
||||
if (empty($cmsUser)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = new \PDO(
|
||||
"mysql:host={$cmsHost};port={$cmsPort};charset=utf8mb4",
|
||||
$cmsUser,
|
||||
$cmsPass,
|
||||
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_TIMEOUT => 5],
|
||||
);
|
||||
|
||||
// Get all databases the user can access
|
||||
$stmt = $pdo->query('SHOW DATABASES');
|
||||
$databases = $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||
|
||||
// Check for common names
|
||||
foreach ($commonNames as $name) {
|
||||
if (in_array($name, $databases)) {
|
||||
return [
|
||||
'name' => $name,
|
||||
'host' => $cmsHost,
|
||||
'username' => $cmsUser,
|
||||
'password' => $cmsPass,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// If CMS database exists and works, use that
|
||||
if (! empty($cmsDb) && in_array($cmsDb, $databases)) {
|
||||
return [
|
||||
'name' => $cmsDb,
|
||||
'host' => $cmsHost,
|
||||
'username' => $cmsUser,
|
||||
'password' => $cmsPass,
|
||||
];
|
||||
}
|
||||
|
||||
// Return first database that's not system db
|
||||
foreach ($databases as $db) {
|
||||
if (! in_array($db, ['information_schema', 'performance_schema', 'mysql', 'sys'])) {
|
||||
return [
|
||||
'name' => $db,
|
||||
'host' => $cmsHost,
|
||||
'username' => $cmsUser,
|
||||
'password' => $cmsPass,
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Exception) {
|
||||
// Could not detect
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function fixPermissions(): void
|
||||
{
|
||||
$directories = [
|
||||
'/var/www/emulator-source',
|
||||
'/var/www/nitro-client',
|
||||
'/var/www/nitro-renderer',
|
||||
'/var/www/Client',
|
||||
'/var/www/atomcms/storage',
|
||||
'/var/www/atomcms/bootstrap/cache',
|
||||
'/var/www/atomcms/app',
|
||||
];
|
||||
|
||||
$fixed = false;
|
||||
foreach ($directories as $dir) {
|
||||
if (is_dir($dir)) {
|
||||
// Check if already owned by www-data
|
||||
$owner = shell_exec("stat -c '%U' {$dir} 2>/dev/null || echo ''");
|
||||
if (trim($owner ?? '') !== 'www-data') {
|
||||
shell_exec("sudo chown -R www-data:www-data {$dir} 2>/dev/null || true");
|
||||
$fixed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always fix app directory permissions (new files might have wrong owner)
|
||||
shell_exec("sudo find /var/www/atomcms/app -type f -name '*.php' ! -user www-data -exec chown www-data:www-data {} \\; 2>/dev/null || true");
|
||||
|
||||
$this->results[] = ['item' => 'Permissions', 'status' => $fixed ? 'fixed' : 'ok', 'message' => $fixed ? 'Directories eigendom gefixt' : 'Al correct geconfigureerd'];
|
||||
}
|
||||
|
||||
private function fixOpenBaseDir(): void
|
||||
{
|
||||
$openBaseDir = ini_get('open_basedir');
|
||||
|
||||
if (in_array($openBaseDir, ['', '0', false], true)) {
|
||||
$this->results[] = ['item' => 'open_basedir', 'status' => 'ok', 'message' => 'Geen restrictie'];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if all required paths are in open_basedir
|
||||
$requiredPaths = ['/var/www', '/tmp', '/root'];
|
||||
$missing = [];
|
||||
|
||||
foreach ($requiredPaths as $path) {
|
||||
if (! str_contains($openBaseDir, $path)) {
|
||||
$missing[] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
if ($missing === []) {
|
||||
$this->results[] = ['item' => 'open_basedir', 'status' => 'ok', 'message' => 'Alle paden toegestaan'];
|
||||
} else {
|
||||
$this->results[] = ['item' => 'open_basedir', 'status' => 'warning', 'message' => 'Ontbrekende paden: ' . implode(', ', $missing)];
|
||||
}
|
||||
}
|
||||
|
||||
private function configureEmulatorDatabase(): void
|
||||
{
|
||||
// Only configure if not already set
|
||||
$currentHost = setting('emulator_database_host', '');
|
||||
if (! empty($currentHost)) {
|
||||
$this->results[] = ['item' => 'Emulator Database Config', 'status' => 'ok', 'message' => 'Al geconfigureerd'];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get from .env
|
||||
$host = config('database.connections.mysql.host', '127.0.0.1');
|
||||
$port = config('database.connections.mysql.port', '3306');
|
||||
$name = config('database.connections.mysql.database', '');
|
||||
$username = config('database.connections.mysql.username', '');
|
||||
$password = config('database.connections.mysql.password', '');
|
||||
|
||||
if (empty($name) || empty($username)) {
|
||||
$this->results[] = ['item' => 'Emulator Database Config', 'status' => 'warning', 'message' => 'Geen database gevonden in .env'];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = app(SettingsService::class);
|
||||
$settings->set('emulator_database_host', $host);
|
||||
$settings->set('emulator_database_port', $port);
|
||||
$settings->set('emulator_database_name', $name);
|
||||
$settings->set('emulator_database_username', $username);
|
||||
$settings->set('emulator_database_password', $password);
|
||||
|
||||
$this->results[] = ['item' => 'Emulator Database Config', 'status' => 'fixed', 'message' => "Automatisch geconfigureerd: {$name}@{$host}"];
|
||||
}
|
||||
|
||||
private function createMissingDirectories(): void
|
||||
{
|
||||
$directories = [
|
||||
'/var/www/emulator-source',
|
||||
'/var/www/nitro-client',
|
||||
'/var/www/nitro-client/dist',
|
||||
'/var/www/nitro-renderer',
|
||||
'/var/www/Client',
|
||||
'/var/www/Client/gamedata',
|
||||
'/var/www/Gamedata',
|
||||
'/root/emulator',
|
||||
];
|
||||
|
||||
$created = [];
|
||||
foreach ($directories as $dir) {
|
||||
if (! is_dir($dir)) {
|
||||
shell_exec("sudo mkdir -p {$dir} 2>/dev/null && sudo chown www-data:www-data {$dir} 2>/dev/null");
|
||||
$created[] = basename($dir);
|
||||
}
|
||||
}
|
||||
|
||||
if ($created !== []) {
|
||||
$this->results[] = ['item' => 'Directories', 'status' => 'fixed', 'message' => 'Aangemaakt: ' . implode(', ', $created)];
|
||||
} else {
|
||||
$this->results[] = ['item' => 'Directories', 'status' => 'ok', 'message' => 'Alle directories bestaan'];
|
||||
}
|
||||
}
|
||||
|
||||
private function fixStoragePermissions(): void
|
||||
{
|
||||
$storageDirs = [
|
||||
'/var/www/atomcms/storage',
|
||||
'/var/www/atomcms/storage/logs',
|
||||
'/var/www/atomcms/storage/framework',
|
||||
'/var/www/atomcms/storage/framework/cache',
|
||||
'/var/www/atomcms/storage/framework/sessions',
|
||||
'/var/www/atomcms/storage/framework/views',
|
||||
'/var/www/atomcms/bootstrap/cache',
|
||||
];
|
||||
|
||||
$fixed = false;
|
||||
foreach ($storageDirs as $dir) {
|
||||
if (is_dir($dir)) {
|
||||
$owner = shell_exec("stat -c '%U' {$dir} 2>/dev/null || echo ''");
|
||||
if (trim($owner ?? '') !== 'www-data') {
|
||||
shell_exec("sudo chown -R www-data:www-data {$dir} 2>/dev/null");
|
||||
$fixed = true;
|
||||
}
|
||||
} else {
|
||||
shell_exec("sudo mkdir -p {$dir} && sudo chown www-data:www-data {$dir} 2>/dev/null");
|
||||
$fixed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Make storage writable
|
||||
shell_exec('sudo chmod -R 775 /var/www/atomcms/storage 2>/dev/null');
|
||||
shell_exec('sudo chmod -R 775 /var/www/atomcms/bootstrap/cache 2>/dev/null');
|
||||
|
||||
$this->results[] = ['item' => 'Storage Permissions', 'status' => $fixed ? 'fixed' : 'ok', 'message' => $fixed ? 'Gefixt' : 'Al correct'];
|
||||
}
|
||||
|
||||
private function checkDiskSpace(): void
|
||||
{
|
||||
$result = shell_exec("df -h /var/www 2>/dev/null | tail -1 | awk '{print $5}'");
|
||||
$usage = trim($result ?? '0%');
|
||||
$usageNum = (int) str_replace('%', '', $usage);
|
||||
|
||||
if ($usageNum >= 90) {
|
||||
$this->results[] = ['item' => 'Disk Space', 'status' => 'error', 'message' => "Disk {$usage} vol!"];
|
||||
} elseif ($usageNum >= 75) {
|
||||
$this->results[] = ['item' => 'Disk Space', 'status' => 'warning', 'message' => "Disk {$usage} gebruikt"];
|
||||
} else {
|
||||
$this->results[] = ['item' => 'Disk Space', 'status' => 'ok', 'message' => "Disk {$usage} gebruikt"];
|
||||
}
|
||||
}
|
||||
|
||||
private function clearOldCaches(): void
|
||||
{
|
||||
// Clear old temp files
|
||||
shell_exec("find /tmp -name 'emulator-update-*' -mtime +1 -exec rm -rf {} \\; 2>/dev/null");
|
||||
shell_exec("find /tmp -name 'nitro_*' -mtime +1 -exec rm -rf {} \\; 2>/dev/null");
|
||||
shell_exec("find /tmp -name '*.lock' -mtime +1 -delete 2>/dev/null");
|
||||
|
||||
// Clear Laravel caches
|
||||
shell_exec('cd /var/www/atomcms && php artisan cache:clear 2>/dev/null');
|
||||
shell_exec('cd /var/www/atomcms && php artisan view:clear 2>/dev/null');
|
||||
|
||||
$this->results[] = ['item' => 'Cache Cleanup', 'status' => 'fixed', 'message' => 'Oude caches opgeruimd'];
|
||||
}
|
||||
|
||||
private function fixGitSafeDirectories(): void
|
||||
{
|
||||
$directories = [
|
||||
'/var/www/atomcms',
|
||||
'/var/www/emulator-source',
|
||||
'/var/www/emulator-source/Emulator',
|
||||
'/var/www/nitro-client',
|
||||
'/var/www/nitro-renderer',
|
||||
'/var/www/Client',
|
||||
'/root',
|
||||
'/root/emulator',
|
||||
'/var/www',
|
||||
];
|
||||
|
||||
foreach ($directories as $dir) {
|
||||
shell_exec("git config --global --add safe.directory {$dir} 2>/dev/null || true");
|
||||
}
|
||||
|
||||
$this->results[] = ['item' => 'Git Safe Directories', 'status' => 'fixed', 'message' => 'Alle directories toegevoegd'];
|
||||
}
|
||||
|
||||
private function fixMavenDirectory(): void
|
||||
{
|
||||
$mavenDir = '/var/www/.m2';
|
||||
if (! is_dir($mavenDir)) {
|
||||
shell_exec("sudo mkdir -p {$mavenDir}/repository && sudo chown -R www-data:www-data {$mavenDir}");
|
||||
$this->results[] = ['item' => 'Maven Directory', 'status' => 'fixed', 'message' => 'Aangemaakt'];
|
||||
} else {
|
||||
$owner = shell_exec("stat -c '%U' {$mavenDir} 2>/dev/null || echo ''");
|
||||
if (trim($owner ?? '') !== 'www-data') {
|
||||
shell_exec("sudo chown -R www-data:www-data {$mavenDir}");
|
||||
$this->results[] = ['item' => 'Maven Directory', 'status' => 'fixed', 'message' => 'Permissions gefixt'];
|
||||
} else {
|
||||
$this->results[] = ['item' => 'Maven Directory', 'status' => 'ok', 'message' => 'Al correct'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkAndFixJava(): void
|
||||
{
|
||||
$result = shell_exec('which java 2>/dev/null || echo ""');
|
||||
if (! in_array(trim($result ?? ''), ['', '0'], true)) {
|
||||
$version = shell_exec('java -version 2>&1 | head -1');
|
||||
$this->results[] = ['item' => 'Java', 'status' => 'ok', 'message' => trim($version ?? '')];
|
||||
} else {
|
||||
// Install Java
|
||||
shell_exec('sudo apt-get update && sudo apt-get install -y default-jdk 2>&1');
|
||||
$result = shell_exec('which java 2>/dev/null || echo ""');
|
||||
if (! in_array(trim($result ?? ''), ['', '0'], true)) {
|
||||
$this->results[] = ['item' => 'Java', 'status' => 'fixed', 'message' => 'Automatisch geïnstalleerd'];
|
||||
} else {
|
||||
$this->results[] = ['item' => 'Java', 'status' => 'error', 'message' => 'Installatie mislukt'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+210
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TraxService
|
||||
{
|
||||
public function syncSoundSets(): array
|
||||
{
|
||||
Log::info('[TraxService] Starting sound sets sync');
|
||||
|
||||
$furnitureData = json_decode(
|
||||
file_get_contents('/var/www/Gamedata/config/FurnitureData.json'),
|
||||
true,
|
||||
);
|
||||
|
||||
$inserted = 0;
|
||||
$soundSets = [];
|
||||
|
||||
foreach ($furnitureData['roomitemtypes']['furnitype'] ?? [] as $item) {
|
||||
$classname = $item['classname'] ?? '';
|
||||
|
||||
// Collect sound_set items
|
||||
if (str_starts_with((string) $classname, 'sound_set')) {
|
||||
$setId = (int) str_replace('sound_set_', '', $classname);
|
||||
$soundSets[] = [
|
||||
'item_id' => $item['id'],
|
||||
'set_id' => $setId,
|
||||
'name' => $item['name'] ?? $classname,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Insert sound sets into trax_songs (if table exists)
|
||||
if (DB::getSchemaBuilder()->hasTable('soundsets')) {
|
||||
DB::table('soundsets')->truncate();
|
||||
|
||||
foreach ($soundSets as $set) {
|
||||
DB::table('soundsets')->insert([
|
||||
'id' => $set['set_id'],
|
||||
'name' => $set['name'],
|
||||
'trackid' => $set['item_id'],
|
||||
]);
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('[TraxService] Sound sets sync complete', [
|
||||
'inserted' => $inserted,
|
||||
]);
|
||||
|
||||
return [
|
||||
'inserted' => $inserted,
|
||||
'total_sets' => count($soundSets),
|
||||
];
|
||||
}
|
||||
|
||||
public function syncSoundtracks(): array
|
||||
{
|
||||
Log::info('[TraxService] Starting soundtracks sync');
|
||||
|
||||
$soundFiles = glob('/var/www/Gamedata/sounds/sound_machine_sample_*.mp3');
|
||||
$existing = DB::table('soundtracks')->pluck('id')->toArray();
|
||||
|
||||
$inserted = 0;
|
||||
|
||||
foreach ($soundFiles as $file) {
|
||||
$basename = basename($file, '.mp3');
|
||||
preg_match('/sound_machine_sample_(\d+)/', $basename, $matches);
|
||||
|
||||
if (isset($matches[1]) && ($matches[1] !== '' && $matches[1] !== '0')) {
|
||||
$sampleId = (int) $matches[1];
|
||||
|
||||
if (! in_array($sampleId, $existing)) {
|
||||
DB::table('soundtracks')->insert([
|
||||
'id' => $sampleId,
|
||||
'code' => $basename,
|
||||
'name' => 'Sample ' . $sampleId,
|
||||
'author' => 'System',
|
||||
'track' => '',
|
||||
'length' => 0,
|
||||
]);
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('[TraxService] Soundtracks sync complete', [
|
||||
'inserted' => $inserted,
|
||||
]);
|
||||
|
||||
return [
|
||||
'inserted' => $inserted,
|
||||
'total' => DB::table('soundtracks')->count(),
|
||||
'samples' => count($soundFiles),
|
||||
];
|
||||
}
|
||||
|
||||
public function getStats(): array
|
||||
{
|
||||
return [
|
||||
'soundtracks' => DB::table('soundtracks')->count(),
|
||||
'sound_samples' => count(glob('/var/www/Gamedata/sounds/sound_machine_sample_*.mp3') ?: []),
|
||||
'room_trax' => DB::table('room_trax')->count(),
|
||||
'trax_playlist' => DB::table('trax_playlist')->count(),
|
||||
'soundsets' => $this->countSoundSets(),
|
||||
];
|
||||
}
|
||||
|
||||
private function countSoundSets(): int
|
||||
{
|
||||
$furnitureData = json_decode(
|
||||
file_get_contents('/var/www/Gamedata/config/FurnitureData.json'),
|
||||
true,
|
||||
);
|
||||
|
||||
$count = 0;
|
||||
foreach ($furnitureData['roomitemtypes']['furnitype'] ?? [] as $item) {
|
||||
if (str_starts_with($item['classname'] ?? '', 'sound_set')) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function importFromUrl(string $baseUrl): array
|
||||
{
|
||||
Log::info('[TraxService] Importing sounds from: ' . $baseUrl);
|
||||
|
||||
$results = [
|
||||
'downloaded' => 0,
|
||||
'failed' => 0,
|
||||
];
|
||||
|
||||
$soundPath = '/var/www/Gamedata/sounds';
|
||||
$soundUrl = rtrim($baseUrl, '/') . '/gamedata/sounds';
|
||||
|
||||
// Get list of sound samples from FurnitureData
|
||||
$furnitureData = json_decode(
|
||||
file_get_contents('/var/www/Gamedata/config/FurnitureData.json'),
|
||||
true,
|
||||
);
|
||||
|
||||
$sampleIds = [];
|
||||
foreach ($furnitureData['roomitemtypes']['furnitype'] ?? [] as $item) {
|
||||
if (str_starts_with($item['classname'] ?? '', 'sound_set')) {
|
||||
// Each sound set can have multiple samples
|
||||
for ($i = 0; $i < 200; $i++) {
|
||||
$sampleIds[] = $i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sampleIds = array_unique($sampleIds);
|
||||
|
||||
foreach ($sampleIds as $id) {
|
||||
$filename = 'sound_machine_sample_' . $id . '.mp3';
|
||||
$localFile = $soundPath . '/' . $filename;
|
||||
|
||||
if (! file_exists($localFile)) {
|
||||
$url = $soundUrl . '/' . $filename;
|
||||
$content = @file_get_contents($url);
|
||||
|
||||
if ($content && strlen($content) > 1000) {
|
||||
file_put_contents($localFile, $content);
|
||||
$results['downloaded']++;
|
||||
} else {
|
||||
$results['failed']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('[TraxService] Import complete', $results);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getTraxItems(): array
|
||||
{
|
||||
return DB::table('items_base')
|
||||
->where('item_name', 'like', '%sound%')
|
||||
->orWhere('item_name', 'like', '%trax%')
|
||||
->orWhere('interaction_type', '=', 'sound')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function linkSoundMachine(): bool
|
||||
{
|
||||
// Make sure sound_machine has correct interaction type
|
||||
$machine = DB::table('items_base')
|
||||
->where('item_name', 'sound_machine')
|
||||
->first();
|
||||
|
||||
if ($machine) {
|
||||
DB::table('items_base')
|
||||
->where('id', $machine->id)
|
||||
->update(['interaction_type' => 'sound_machine']);
|
||||
|
||||
Log::info('[TraxService] Sound machine linked');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Executable
+186
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class UpdateHistoryService
|
||||
{
|
||||
private const string TABLE = 'update_history';
|
||||
|
||||
public function ensureTableExists(): void
|
||||
{
|
||||
if (! Schema::hasTable(self::TABLE)) {
|
||||
Schema::create(self::TABLE, function ($table) {
|
||||
$table->id();
|
||||
$table->string('type'); // nitro, emulator, sql, config
|
||||
$table->string('action'); // update, build, deploy, fix
|
||||
$table->string('item')->nullable(); // filename, version, etc
|
||||
$table->string('status'); // success, failed, pending
|
||||
$table->text('message')->nullable();
|
||||
$table->string('user')->nullable();
|
||||
$table->string('ip')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->index(['type', 'created_at']);
|
||||
$table->index('status');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function log(string $type, string $action, ?string $item = null, string $status = 'success', ?string $message = null): void
|
||||
{
|
||||
$this->ensureTableExists();
|
||||
|
||||
DB::table(self::TABLE)->insert([
|
||||
'type' => $type,
|
||||
'action' => $action,
|
||||
'item' => $item,
|
||||
'status' => $status,
|
||||
'message' => $message,
|
||||
'user' => auth()->user()?->name ?? 'System',
|
||||
'ip' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getRecent(int $limit = 50): array
|
||||
{
|
||||
$this->ensureTableExists();
|
||||
|
||||
return DB::table(self::TABLE)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getByType(string $type, int $limit = 50): array
|
||||
{
|
||||
$this->ensureTableExists();
|
||||
|
||||
return DB::table(self::TABLE)
|
||||
->where('type', $type)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getByStatus(string $status, int $limit = 50): array
|
||||
{
|
||||
$this->ensureTableExists();
|
||||
|
||||
return DB::table(self::TABLE)
|
||||
->where('status', $status)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getStats(): array
|
||||
{
|
||||
$this->ensureTableExists();
|
||||
|
||||
$total = DB::table(self::TABLE)->count();
|
||||
$success = DB::table(self::TABLE)->where('status', 'success')->count();
|
||||
$failed = DB::table(self::TABLE)->where('status', 'failed')->count();
|
||||
|
||||
$lastUpdate = DB::table(self::TABLE)
|
||||
->orderBy('created_at', 'desc')
|
||||
->first();
|
||||
|
||||
$byType = DB::table(self::TABLE)
|
||||
->select('type', DB::raw('count(*) as count'))
|
||||
->groupBy('type')
|
||||
->pluck('count', 'type')
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'success' => $success,
|
||||
'failed' => $failed,
|
||||
'success_rate' => $total > 0 ? round(($success / $total) * 100) : 100,
|
||||
'last_update' => $lastUpdate,
|
||||
'by_type' => $byType,
|
||||
];
|
||||
}
|
||||
|
||||
public function getHtml(): string
|
||||
{
|
||||
$this->ensureTableExists();
|
||||
|
||||
$updates = $this->getRecent(20);
|
||||
$stats = $this->getStats();
|
||||
|
||||
$html = '<div style="font-family: -apple-system, BlinkMacSystemFont, sans-serif;">';
|
||||
|
||||
// Stats
|
||||
$html .= '<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px;">';
|
||||
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
|
||||
$html .= '<div style="font-size: 24px; font-weight: 700; color: #4ade80;">' . $stats['total'] . '</div>';
|
||||
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Totaal</div></div>';
|
||||
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
|
||||
$html .= '<div style="font-size: 24px; font-weight: 700; color: #4ade80;">' . $stats['success'] . '</div>';
|
||||
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Geslaagd</div></div>';
|
||||
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
|
||||
$html .= '<div style="font-size: 24px; font-weight: 700; color: #f87171;">' . $stats['failed'] . '</div>';
|
||||
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Mislukt</div></div>';
|
||||
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
|
||||
$html .= '<div style="font-size: 24px; font-weight: 700; color: #60a5fa;">' . $stats['success_rate'] . '%</div>';
|
||||
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Succes</div></div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// History list
|
||||
if ($updates === []) {
|
||||
$html .= '<div style="text-align: center; padding: 24px; color: #64748b;">';
|
||||
$html .= '<div style="font-size: 32px; margin-bottom: 8px;">📋</div>';
|
||||
$html .= 'Nog geen update geschiedenis</div>';
|
||||
} else {
|
||||
$html .= '<div style="max-height: 400px; overflow-y: auto;">';
|
||||
foreach ($updates as $update) {
|
||||
$icon = match ($update->status) {
|
||||
'success' => '✅',
|
||||
'failed' => '❌',
|
||||
'pending' => '⏳',
|
||||
default => '⚪'
|
||||
};
|
||||
|
||||
$typeColor = match ($update->type) {
|
||||
'nitro' => '#4ade80',
|
||||
'emulator' => '#60a5fa',
|
||||
'sql' => '#fbbf24',
|
||||
'config' => '#a78bfa',
|
||||
default => '#94a3b8'
|
||||
};
|
||||
|
||||
$time = Carbon::parse($update->created_at)->diffForHumans();
|
||||
|
||||
$html .= '<div style="display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: rgba(255,255,255,0.03); border-radius: 8px; margin-bottom: 6px;">';
|
||||
$html .= '<div style="font-size: 16px;">' . $icon . '</div>';
|
||||
$html .= '<div style="flex: 1;">';
|
||||
$html .= '<div style="display: flex; align-items: center; gap: 8px;">';
|
||||
$html .= '<span style="background: ' . $typeColor . '20; color: ' . $typeColor . '; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 600; text-transform: uppercase;">' . e($update->type) . '</span>';
|
||||
$html .= '<span style="color: #e2e8f0; font-size: 13px;">' . e($update->action) . '</span>';
|
||||
if ($update->item) {
|
||||
$html .= '<span style="color: #94a3b8; font-size: 12px;">' . e($update->item) . '</span>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
if ($update->message) {
|
||||
$html .= '<div style="color: #64748b; font-size: 11px; margin-top: 2px;">' . e($update->message) . '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
$html .= '<div style="text-align: right;">';
|
||||
$html .= '<div style="color: #64748b; font-size: 10px;">' . e($update->user) . '</div>';
|
||||
$html .= '<div style="color: #475569; font-size: 9px;">' . e($time) . '</div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
return $html . '</div>';
|
||||
}
|
||||
}
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\User;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
readonly class SessionService
|
||||
{
|
||||
public function fetchSessionLogs(Request $request): Collection
|
||||
{
|
||||
return collect(
|
||||
Auth::user()->sessions,
|
||||
)->map(function ($session) use ($request) {
|
||||
$info = $this->parseUserAgent($session->user_agent);
|
||||
|
||||
return (object) [
|
||||
'agent' => [
|
||||
'is_desktop' => $info['is_desktop'],
|
||||
'platform' => $info['platform'],
|
||||
'browser' => $info['browser'],
|
||||
],
|
||||
'ip_address' => $session->ip_address,
|
||||
'is_current_device' => $session->id === $request->session()->getId(),
|
||||
'last_active' => Carbon::createFromTimestamp($session->last_activity)->diffForHumans(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function parseUserAgent(string $userAgent): array
|
||||
{
|
||||
$platform = 'Unknown';
|
||||
$browser = 'Unknown';
|
||||
$isDesktop = true;
|
||||
|
||||
if (preg_match('/Windows NT 10\.0|Windows NT 11\.0|Macintosh|Linux|FreeBSD/', $userAgent)) {
|
||||
$platform = match (true) {
|
||||
preg_match('/Windows NT 10\.0/', $userAgent) => 'Windows 10',
|
||||
preg_match('/Windows NT 11\.0/', $userAgent) => 'Windows 11',
|
||||
preg_match('/Macintosh/', $userAgent) => 'macOS',
|
||||
preg_match('/Linux/', $userAgent) => 'Linux',
|
||||
preg_match('/FreeBSD/', $userAgent) => 'FreeBSD',
|
||||
default => 'Desktop',
|
||||
};
|
||||
} elseif (preg_match('/Android/i', $userAgent)) {
|
||||
$platform = 'Android';
|
||||
$isDesktop = false;
|
||||
} elseif (preg_match('/iPhone|iPad/i', $userAgent)) {
|
||||
$platform = preg_match('/iPad/', $userAgent) ? 'iPad' : 'iOS';
|
||||
$isDesktop = false;
|
||||
}
|
||||
|
||||
$browser = match (true) {
|
||||
preg_match('/Edg\/\d+/', $userAgent) => 'Edge',
|
||||
preg_match('/Chrome\/\d+/', $userAgent) && ! preg_match('/OPR|Opera/', $userAgent) => 'Chrome',
|
||||
preg_match('/Firefox\/\d+/', $userAgent) => 'Firefox',
|
||||
preg_match('/Safari\/\d+/', $userAgent) && ! preg_match('/Chrome/', $userAgent) => 'Safari',
|
||||
preg_match('/OPR\/\d+/', $userAgent) => 'Opera',
|
||||
preg_match('/MSIE|Trident\//', $userAgent) => 'Internet Explorer',
|
||||
default => 'Unknown',
|
||||
};
|
||||
|
||||
return [
|
||||
'platform' => $platform,
|
||||
'browser' => $browser,
|
||||
'is_desktop' => $isDesktop,
|
||||
];
|
||||
}
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\User;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
readonly class UserApiService
|
||||
{
|
||||
public function fetchUser(string $username, array $columns): ?User
|
||||
{
|
||||
/** @var User|null $user */
|
||||
$user = User::select($columns)->where('username', '=', $username)->first();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function onlineUsers($columns = ['username', 'motto', 'look'], bool $randomOrder = true): Builder|\Illuminate\Database\Query\Builder
|
||||
{
|
||||
/** @var Builder $query */
|
||||
$query = User::select($columns)->where('online', '=', '1');
|
||||
|
||||
if ($randomOrder) {
|
||||
$query = $query->inRandomOrder();
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function onlineUserCount(): int
|
||||
{
|
||||
return User::query()->where('online', '=', '1')->count();
|
||||
}
|
||||
}
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\User;
|
||||
|
||||
use App\Actions\UserActions;
|
||||
use App\Models\User;
|
||||
|
||||
class UserService extends UserActions
|
||||
{
|
||||
public function getUser(string $username): ?User
|
||||
{
|
||||
/** @var User|null $user */
|
||||
$user = User::where('username', $username)->first();
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Foundation\Vite;
|
||||
|
||||
class ViteService extends Vite
|
||||
{
|
||||
/**
|
||||
* Generate a script tag for the given URL.
|
||||
*/
|
||||
#[\Override]
|
||||
protected function makeScriptTag($url): string
|
||||
{
|
||||
return sprintf('<script type="module" src="%s" data-turbolinks-eval="false"></script>', $url);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user