🆙 Add fixed cms 🆙

This commit is contained in:
Remco
2026-02-02 19:30:21 +01:00
parent b1a2cab62d
commit b67e0ec2b9
3982 changed files with 193682 additions and 0 deletions
@@ -0,0 +1,24 @@
<?php
namespace App\Services\Articles;
use App\Models\Articles\WebsiteArticle;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
class ArticleService
{
public function getArticles(bool $paginate = false, int $perPage = 8): array|Collection|LengthAwarePaginator
{
$query = WebsiteArticle::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::where('slug', '=', $slug)->firstOrFail();
}
}
@@ -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;
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();
}
}
@@ -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;
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) {
$existingReaction->update(['active' => ! $existingReaction->active]);
} else {
$article->reactions()->create([
'reaction' => $reaction,
]);
}
return [
'success' => true,
'added' => $existingReaction->active ?? true,
'username' => $user->username,
];
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Services\Community;
use App\Models\Miscellaneous\CameraWeb;
class CameraService
{
public function fetchPhotos(bool $paginate = false, int $perPage = 8): mixed
{
$photos = CameraWeb::where('visible', true)
->latest('id')
->with('user:id,username,look');
return $paginate ? $photos->paginate($perPage) : $photos->get();
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Services\Community\RareValues;
use App\Models\Community\RareValue\WebsiteRareValueCategory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
class RareValueCategoriesService
{
public function fetchAllCategories(): Collection
{
return WebsiteRareValueCategory::all();
}
public function fetchCategoriesByPriority(): Builder|Collection
{
return WebsiteRareValueCategory::orderBy('priority')->with('furniture')->get();
}
public function fetchCategoryById(int $id): ?WebsiteRareValueCategory
{
return WebsiteRareValueCategory::orderBy('priority')->whereId($id)->with('furniture')->first();
}
public function searchCategories(string $searchTerm): Collection
{
return WebsiteRareValueCategory::orderBy('priority')->whereHas('furniture', function ($query) use ($searchTerm) {
$query->where('name', 'like', '%' . $searchTerm . '%');
})
->with(['furniture' => function ($query) use ($searchTerm) {
$query->where('name', 'like', '%' . $searchTerm . '%');
}])
->get();
}
}
@@ -0,0 +1,5 @@
<?php
namespace App\Services\Community\RareValues;
class RareValuesService {}
@@ -0,0 +1,35 @@
<?php
namespace App\Services\Community;
use App\Models\Community\Staff\WebsiteOpenPosition;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
class StaffApplicationService
{
public function storeApplication(User $user, int $positionId, string $content): void
{
$user->applications()->create([
'rank_id' => $positionId,
'content' => $content,
]);
}
public function fetchOpenPositions(): Collection
{
return WebsiteOpenPosition::canApply()->with('permission')->get();
}
public function hasUserAppliedForPosition($user, $positionId): bool
{
return $user->applications()->where('rank_id', $positionId)->exists();
}
public function isPositionOpenForApplication($position): bool
{
$currentTime = now();
return $position->apply_from <= $currentTime && $position->apply_to >= $currentTime;
}
}
@@ -0,0 +1,60 @@
<?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;
class StaffService
{
public function fetchStaffPositions(): Collection
{
$cacheEnabled = setting('enable_caching') === '1';
if ($cacheEnabled && Cache::has('staff_positions')) {
return Cache::get('staff_positions');
}
$employees = Permission::query()
->select('id', 'rank_name', 'badge', 'staff_color', 'job_description')
->when(Auth::user()->rank < (int) setting('min_rank_to_see_hidden_staff'), fn ($query) => $query->where('hidden_rank', false))
->where('id', '>=', setting('min_staff_rank'))
->orderByDesc('id')
->with(['users' => function ($query) {
$query->select('id', 'username', 'rank', 'motto', 'look', 'hidden_staff', 'online')
->when(Auth::user()->rank < (int) setting('min_rank_to_see_hidden_staff'), 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');
}
$staffIds = User::select('id')
->where('rank', '>=', setting('min_staff_rank'))
->get()
->pluck('id')->toArray();
if ($cacheEnabled) {
$cacheTimer = (int) setting('cache_timer');
Cache::put('staff_ids', $staffIds, now()->addMinutes($cacheTimer));
}
return $staffIds;
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Services\Community;
use App\Models\Community\Teams\WebsiteTeam;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Cache;
class TeamService
{
public function fetchTeams(): Collection
{
$cacheEnabled = setting('enable_caching') === '1';
if ($cacheEnabled && Cache::has('hotel_teams')) {
return Cache::get('hotel_teams');
}
$employees = WebsiteTeam::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;
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Services\Emulator\Drivers;
use App\Services\Emulator\EmulatorInterface;
use Illuminate\Support\Facades\Schema;
class ArcturusDriver implements EmulatorInterface
{
public function getCurrencyBalance(\App\Models\User $user, string $type): int
{
if (! $user->relationLoaded('currencies')) {
$user->load('currencies');
}
$currencyType = match ($type) {
'duckets' => 0,
'diamonds' => 5,
'points' => 101,
default => 0,
};
return $user->currencies->where('type', $currencyType)->first()->amount ?? 0;
}
public function getPermissionColumns(): array
{
// Ensure the permissions table exists to avoid errors during migrations or setup
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();
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Services\Emulator\Drivers;
use App\Services\Emulator\EmulatorInterface;
use Illuminate\Support\Facades\Schema;
class SadieDriver implements EmulatorInterface
{
public function getCurrencyBalance(\App\Models\User $user, string $type): int
{
// Sadie Emulator might store currencies in the 'users' table columns
// or a different table entirely.
// Implement the specific data retrieval logic here.
// Example: if Sadie uses columns on the users table:
// return match ($type) {
// 'duckets' => $user->duckets ?? 0,
// 'diamonds' => $user->diamonds ?? 0,
// default => 0,
// };
return 0;
}
public function getPermissionColumns(): 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;
}
// Adjust this filter logic to match Sadie's permission column naming convention
return str_starts_with($columnName, 'cmd')
|| str_starts_with($columnName, 'acc')
|| str_ends_with($columnName, 'cmd');
})->values()->toArray();
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Services\Emulator;
use App\Models\User;
interface EmulatorInterface
{
public function getCurrencyBalance(User $user, string $type): int;
/**
* Get the list of columns that represent permissions in the database.
*/
public function getPermissionColumns(): array;
}
@@ -0,0 +1,79 @@
<?php
namespace App\Services;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Cache;
/* Credits to Kani for this */
class FindRetrosService
{
/**
* The findretros verification uri.
*/
public const FIND_RETROS_VERIFY_URI = '%s/validate.php?user=%s&ip=%s';
/**
* The findretros redirect uri.
*/
public const FIND_RETROS_REDIRECT_URI = '%s/servers/%s/vote?minimal=1&return=1';
public const FIND_RETROS_CACHE_KEY = 'voted.%s';
/**
* The guzzle client instance
*/
protected Client $client;
/**
* Initialise Find Retros Service
*/
public function __construct()
{
$this->client = new Client(['verify' => false]);
}
/**
* Check the user has voted.
*/
public function checkHasVoted(): bool
{
if (! config('habbo.findretros.enabled')) {
return true;
}
$cacheKey = sprintf(self::FIND_RETROS_CACHE_KEY, request()->ip());
if (request()->ip() === '127.0.0.1') {
return true;
}
if (request()->has('novote')) {
return true;
}
if (Cache::has($cacheKey)) {
return true;
}
$uri = sprintf(self::FIND_RETROS_VERIFY_URI, config('habbo.findretros.api'), config('habbo.findretros.name'), request()->ip());
$request = $this->client->get($uri);
$response = $request->getBody()->getContents();
if (in_array($response, ['1', '2'])) {
Cache::put($cacheKey, true, now()->addMinutes(30));
return true;
}
return false;
}
/**
* Retrieve the find retros redirect url.
*/
public function getRedirectUri(): string
{
return sprintf(self::FIND_RETROS_REDIRECT_URI, config('habbo.findretros.api'), config('habbo.findretros.name'));
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Services;
use App\Models\WebsiteHousekeepingPermission;
use Illuminate\Support\Collection;
class HousekeepingPermissionsService
{
public ?Collection $permissions;
public function __construct()
{
$this->permissions = WebsiteHousekeepingPermission::all()->pluck('min_rank', 'permission');
}
public function getOrDefault(string $permissionName, bool $default = false): bool
{
if (! array_key_exists($permissionName, $this->permissions->toArray())) {
return $default;
}
return auth()->check() && auth()->user()->rank >= (int) $this->permissions->get($permissionName);
}
}
@@ -0,0 +1,60 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Throwable;
class InstallationService
{
private ?bool $installationComplete = null;
public function isComplete(): bool
{
if ($this->installationComplete !== null) {
return $this->installationComplete;
}
if (Cache::has('app_installed')) {
$this->installationComplete = true;
return true;
}
try {
if (! Schema::hasTable('website_installation')) {
$this->installationComplete = false;
return false;
}
$installation = DB::table('website_installation')->first();
$isComplete = $installation && $installation->completed;
if ($isComplete) {
Cache::rememberForever('app_installed', fn () => true);
}
$this->installationComplete = $isComplete;
return $isComplete;
} catch (Throwable) {
$this->installationComplete = false;
return false;
}
}
public static function setComplete(): void
{
Cache::rememberForever('app_installed', fn () => true);
}
public static function clearCache(): void
{
Cache::forget('app_installed');
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class IpLookupService
{
private string $baseUrl = 'https://api.ipdata.co';
public function __construct(private readonly string $apiKey) {}
public function ipLookup(string $ip)
{
$response = Http::acceptJson()->get(sprintf('%s/%s?api-key=%s', $this->baseUrl, $ip, $this->apiKey));
if (! $response->ok()) {
$message = array_key_exists('message', $response->json()) ? $response->json()['message'] : 'Unknown error';
return [
'message' => $message,
'status' => $response->status(),
];
}
return $response->json();
}
}
@@ -0,0 +1,28 @@
<?php
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 '';
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Services;
use App\Models\Miscellaneous\WebsitePermission;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
class PermissionsService
{
public ?Collection $permissions;
public function __construct()
{
Cache::remember('website_permissions', now()->addMinutes(30), fn () => WebsitePermission::all()->pluck('min_rank', 'permission'));
$this->permissions = Cache::get('website_permissions');
}
public function getOrDefault(string $permissionName, bool $default = false): bool
{
if (! array_key_exists($permissionName, $this->permissions->toArray())) {
return $default;
}
return auth()->check() && auth()->user()->rank >= (int) $this->permissions->get($permissionName);
}
}
+277
View File
@@ -0,0 +1,277 @@
<?php
namespace App\Services;
use App\Enums\CurrencyTypes;
use App\Exceptions\RconConnectionException;
use Illuminate\Support\Facades\Log;
use JsonException;
use Socket;
class RconService
{
protected ?Socket $socket = null;
public bool $isConnected = false;
protected 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) {
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)
{
if (! $this->isConnected) {
$error = 'RCON command failed: Not connected';
Log::error($error);
$this->closeConnection();
return $this->isConnected;
}
$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 $this->isConnected;
}
return $this->isConnected;
}
/**
* @throws RconConnectionException|JsonException
*/
public function sendGift($user, int $item_id, string $message = 'Here is a gift.'): void
{
$this->sendCommand('sendgift', [
'user_id' => $user->id,
'itemid' => $item_id,
'message' => $message,
]);
}
/**
* @throws RconConnectionException|JsonException
*/
public function giveCredits($user, int $credits): void
{
$this->sendCommand('givecredits', [
'user_id' => $user->id,
'credits' => $credits,
]);
}
/**
* @throws RconConnectionException|JsonException
*/
public function giveBadge($user, string $badge): void
{
$this->sendCommand('givebadge', [
'user_id' => $user->id,
'badge' => $badge,
]);
}
/**
* @throws RconConnectionException|JsonException
*/
public function setMotto($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($user): void
{
$this->sendCommand('disconnect', [
'user_id' => $user->id,
'username' => $user->username,
]);
}
/**
* @throws RconConnectionException|JsonException
*/
public function givePoints($user, CurrencyTypes $type, int $amount): void
{
$this->sendCommand('givepoints', [
'user_id' => $user->id,
'points' => $amount,
'type' => $type,
]);
}
/**
* @throws RconConnectionException
* @throws JsonException
*/
public function giveGotw($user, int $amount): void
{
$this->givePoints($user, CurrencyTypes::Points, $amount);
}
/**
* @throws RconConnectionException
* @throws JsonException
*/
public function giveDiamonds($user, int $amount): void
{
$this->givePoints($user, CurrencyTypes::Diamonds, $amount);
}
/**
* @throws RconConnectionException
* @throws JsonException
*/
public function giveDuckets($user, int $amount): void
{
$this->givePoints($user, CurrencyTypes::Duckets, $amount);
}
/**
* @throws RconConnectionException
* @throws JsonException
*/
public function setRank($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($user, string $message): void
{
$this->sendCommand('alertuser', [
'user_id' => $user->id,
'message' => $message,
]);
}
/**
* @throws RconConnectionException
* @throws JsonException
*/
public function forwardUser($user, int $roomId): void
{
$this->sendCommand('forwarduser', [
'user_id' => $user->id,
'room_id' => $roomId,
]);
}
/**
* @throws RconConnectionException
* @throws JsonException
*/
public function updateConfig($user, string $command): void
{
$this->sendCommand('executecommand', [
'user_id' => $user->id,
'command' => $command,
]);
}
/**
* Send an RCON command safely from dashboard with error handling
*
* @param string $command The command to send
* @param array $params The parameters for the command
* @param string $errorMessage The error message to log if command fails
* @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);
}
}
@@ -0,0 +1,62 @@
<?php
namespace App\Services;
use App\Models\Miscellaneous\WebsiteSetting;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Throwable;
class SettingsService
{
private ?\Illuminate\Support\Collection $cachedSettings = null;
protected function settings()
{
if ($this->cachedSettings !== null) {
return $this->cachedSettings;
}
if ($this->isInstallationIncomplete()) {
$this->cachedSettings = $this->fetchSettings();
return $this->cachedSettings;
}
$this->cachedSettings = Cache::rememberForever('website_settings', fn () => $this->fetchSettings());
return $this->cachedSettings;
}
public function getOrDefault(string $key, mixed $default = null): mixed
{
return $this->settings()->get($key, $default);
}
public static function clearCache(): void
{
Cache::forget('website_settings');
}
private function isInstallationIncomplete(): bool
{
try {
return ! app(InstallationService::class)->isComplete();
} catch (Throwable) {
return true;
}
}
private function fetchSettings()
{
try {
if (! Schema::hasTable('website_settings')) {
return collect();
}
return WebsiteSetting::query()->pluck('value', 'key');
} catch (Throwable) {
return collect();
}
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Services\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Jenssegers\Agent\Agent;
class SessionService
{
public function fetchSessionLogs(Request $request): Collection
{
return collect(
Auth::user()->sessions,
)->map(function ($session) use ($request) {
$agent = $this->createAgent($session);
return (object) [
'agent' => [
'is_desktop' => $agent->isDesktop(),
'platform' => $agent->platform(),
'browser' => $agent->browser(),
],
'ip_address' => $session->ip_address,
'is_current_device' => $session->id === $request->session()->getId(),
'last_active' => Carbon::createFromTimestamp($session->last_activity)->diffForHumans(),
];
});
}
protected function createAgent($session): Agent
{
return tap(new Agent, function ($agent) use ($session) {
$agent->setUserAgent($session->user_agent);
});
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Services\User;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
class UserApiService
{
public function fetchUser(string $username, array $columns): User
{
return User::select($columns)->where('username', '=', $username)->first();
}
public function onlineUsers($columns = ['username', 'motto', 'look'], bool $randomOrder = true): Builder
{
$query = User::select($columns)->where('online', '=', '1');
if ($randomOrder) {
$query = $query->inRandomOrder();
}
return $query;
}
public function onlineUserCount(): int
{
return User::where('online', '=', '1')->count();
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Services\User;
use App\Actions\UserActions;
use App\Models\User;
class UserService extends UserActions
{
public function getUser(string $username): ?User
{
return User::where('username', $username)->first();
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Services;
use Illuminate\Foundation\Vite;
class ViteService extends Vite
{
/**
* Generate a script tag for the given URL.
*/
protected function makeScriptTag($url): string
{
return sprintf('<script type="module" src="%s" data-turbolinks-eval="false"></script>', $url);
}
}