Files
Escapepage/src/Game/Service/GameResponseService.php
2026-01-08 15:49:47 +01:00

796 lines
28 KiB
PHP

<?php
namespace App\Game\Service;
use App\Game\Enum\DecodeMessage;
use App\Game\Enum\SessionSettingType;
use App\Game\Entity\Player;
use App\Game\Entity\SessionSetting;
use App\Game\Repository\SessionSettingRepository;
use App\Tech\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
class GameResponseService
{
public function __construct(
private Security $security,
private PlayerService $playerService,
private SessionSettingRepository $sessionSettingRepository,
private HubInterface $hub,
private EntityManagerInterface $entityManager,
) {
}
public function getGameResponse(string $raw) : array
{
$info = json_decode($raw, true);
$message = $info['message'] ?? '';
$ts = $info['ts'] ?? '';
if(!is_string($message))
return ['error' => 'Invalid message.'];
$user = $this->security->getUser();
if(!$user instanceof User)
return ['error' => 'You are not logged in.'];
$player = $this->playerService->GetCurrentlyActiveAsPlayer($user);
if(!$player)
return ['error' => 'You are not in a game.'];
// TODO: Here i need to add a message handler to save the message in a big log.
$data = [];
if(str_starts_with($message, '/')) {
$data = $this->checkGameCommando($message, $player);
} else {
$data = $this->checkConsoleCommando($message, $player);
}
return $data;
}
private function getRechten(Player $player): array
{
$settingName = SessionSettingType::tryFrom('RightsForPlayer' . $player->getScreen());
if (!$settingName) {
return [];
}
$setting = $this->sessionSettingRepository->getSetting($player->getSession(), $settingName, $player);
if (!$setting || !$setting->getValue()) {
return [];
}
return json_decode($setting->getValue(), true) ?? [];
}
private function checkGameCommando(string $message, Player $player) : array
{
$messagePart = explode(' ', $message);
$rechten = $this->getRechten($player);
switch($messagePart[0]) {
case '/chat':
if(!in_array('chat', $rechten))
return ['result' => ['Unknown command']];
if($this->handleChatMessage($message, $player))
return ['result' => ['succesfully send']];
else
return ['result' => ['Error sending']];
case '/help':
return ['result' => $this->getHelpCommand($rechten)];
case '/decode':
if(!in_array('decode', $rechten))
return ['result' => ['Unknown command']];
return ['result' => [$this->handleDecodeMessage($messagePart[1], $player)]];
case '/verify':
if(!in_array('verify', $rechten))
return ['result' => ['Unknown command']];
$result = $this->handleVerifyMessage($message, $player);
return ['result' => [$result]];
default:
return ['result' => ['Unknown command']];
}
}
private function checkConsoleCommando(string $message, Player $player, bool $sudo = false) : array
{
$messagePart = explode(' ', $message);
$rechten = $this->getRechten($player);
switch($messagePart[0]) {
case 'help':
return ['result' => $this->getHelpCommand($rechten)];
case 'ls':
if(!in_array('ls', $rechten))
return ['result' => ['Unknown command']];
$files = $this->getAllCurrentFilesInDirectory($player);
return ['result' => $files];
case 'cd':
if(!in_array('cd', $rechten))
return ['result' => ['Unknown command']];
$pwd = $this->playerService->getCurrentPwdOfPlayer($player);
if(!$pwd)
return ['result' => ['Unknown command']];
$newLocation = $this->goToNewDir($pwd, $messagePart[1], $player);
if($newLocation === false)
return ['result' => ['Unknown path']];
$this->playerService->saveCurrentPwdOfPlayer($player, $newLocation);
return ['result' => ['Path: ' . $newLocation]];
case 'pwd':
if(!in_array('pwd', $rechten))
return ['result' => ['Unknown command']];
$pwd = $this->playerService->getCurrentPwdOfPlayer($player);
return ['result' => ['Path: ' . $pwd]];
case 'rm':
if(!in_array('rm', $rechten))
return ['result' => ['Unknown command']];
$pwd = $this->playerService->getCurrentPwdOfPlayer($player);
if(!$pwd)
return ['result' => ['Unknown command']];
if (!isset($messagePart[1])) {
return ['result' => ['Usage: rm {filename}']];
}
$filename = $messagePart[1];
$fullPath = ($pwd === '/' ? '' : $pwd) . '/' . $filename;
if(!$this->isAllowedToRemove($fullPath, $player, $sudo))
return ['result' => ['You are not allowed to remove this file.']];
$this->playerService->addDeletedFileToSession($player, $fullPath);
return ['result' => ['File removed: ' . $filename]];
case 'sudo':
if(!in_array('sudo', $rechten))
return ['result' => ['Unknown command']];
$sudo = array_shift($messagePart);
$message = implode(' ', $messagePart);
return $this->checkConsoleCommando($message, $player, true);
default:
return ['result' => ['Unknown command']];
}
}
private function getHelpCommand(mixed $rechten) : array
{
$messages = [];
foreach($rechten as $recht) {
switch($recht) {
case 'chat':
$messages[] = '/chat';
$messages[] = ' Use /chat {message} to send the message to the other agents.';
$messages[] = ' If you want to send a message specifically to one other agent, use the id of the agent after /chat, like /chat 6 {message}';
$messages[] = ' This will send the message only to agent with id 6.';
$messages[] = ' USAGE: /chat {message}';
$messages[] = ' USAGE: /chat 6 {message}';
$messages[] = '';
break;
case 'help':
$messages[] = '/help';
$messages[] = ' This shows this help.';
$messages[] = ' USAGE: /help';
$messages[] = '';
break;
case 'decode':
$messages[] = '/decode';
$messages[] = ' This message will decode the message followed by it.';
$messages[] = ' Every agent has a different way to decode messages. This is a security measure. The AI Virus has no access to all decoders.';
$messages[] = ' USAGE: /decode {message}';
$messages[] = '';
break;
case 'pwd':
$messages[] = 'pwd';
$messages[] = ' This message will let you know what your current location is.';
$messages[] = ' It will show you the folder you are in so you can continue navigating the server.';
$messages[] = ' USAGE: pwd';
$messages[] = '';
break;
case 'cat':
$messages[] = 'cat';
$messages[] = ' To read a file, use cat {filename}.';
$messages[] = ' This will print the full content of the file on the screen.';
$messages[] = ' USAGE: cat {filename}';
$messages[] = '';
break;
case 'ls':
$messages[] = 'ls';
$messages[] = ' To show all the files in the current directory, use ls.';
$messages[] = ' This will print the full list of directories and files of the current location on your screen.';
$messages[] = ' USAGE: ls';
$messages[] = '';
break;
case 'rm':
$messages[] = 'rm';
$messages[] = ' Use rm to delete a file.';
$messages[] = ' Be careful with this command. It can not be undone and we do not want to lose any valuable data.';
$messages[] = ' USAGE: rm {filename}';
$messages[] = '';
break;
case 'cd':
$messages[] = 'cd';
$messages[] = ' Use cd to move to a different directory.';
$messages[] = ' You can go into a folder by using cd {foldername}, or a folder up by using "cd ..".';
$messages[] = ' Using cd / moves you to the root directory.';
$messages[] = ' USAGE: cd {directory}';
$messages[] = '';
break;
case 'sudo':
$messages[] = 'sudo';
$messages[] = ' If you do not have enough rights to execute a command, you can use sudo to execute it as root.';
$messages[] = ' This is only possible for verified users. To verify yourself, use the /verify command.';
$messages[] = ' USAGE: sudo {command}';
$messages[] = '';
break;
case 'verify':
$messages[] = '/verify';
$messages[] = ' You can verify yourself by using this command.';
$messages[] = ' Use this command and follow instructions to verify yourself.';
$messages[] = ' USAGE: /verify';
$messages[] = '';
break;
}
}
return $messages;
}
private function handleChatMessage(string $message, Player $player) : bool
{
$messageParts = explode(' ', $message);
$toSingle = false;
if (isset($messageParts[1]) &&
is_numeric($messageParts[1]) &&
$messageParts[1] >= 1 &&
$messageParts[1] <= $player->getSession()->getGame()->getNumberOfPlayers()) {
$toSingle = true;
}
$chatMessage = array_shift($messageParts);
$sendTo = 0;
if ($toSingle) {
$sendTo = array_shift($messageParts);
$chatMessage = array_shift($messageParts);
}
$message = $player->getUser()->getUsername() . ': ' . $chatMessage . ' ';
foreach($messageParts as $messagePart) {
$message .= $messagePart . ' ';
}
$message = trim($message);
$activeGame = $player->getSession()?->getId();
if(is_null($activeGame))
return false;
$topic = $_ENV['MERCURE_TOPIC_BASE'] . '/game/hub-' . $activeGame;
$this->hub->publish(new Update($topic, json_encode([$sendTo, $message])));
$this->updateChatTracking($player, (int)$sendTo);
$this->checkAndRegenerateVerifyCodes($player, $chatMessage . ' ' . implode(' ', $messageParts));
return true;
}
private function checkAndRegenerateVerifyCodes(Player $player, string $messageContent): void
{
$screen = $player->getScreen();
$session = $player->getSession();
$verifyCodesSettingName = SessionSettingType::tryFrom('VerifyCodesForPlayer' . $screen);
if (!$verifyCodesSettingName) {
return;
}
$setting = $this->sessionSettingRepository->getSetting($session, $verifyCodesSettingName, $player);
if (!$setting) {
return;
}
$codes = json_decode($setting->getValue() ?? '[]', true) ?? [];
$regenerated = false;
foreach ($codes as $targetPlayerScreen => $code) {
if (str_contains($messageContent, (string)$code)) {
$codes[$targetPlayerScreen] = bin2hex(random_bytes(3));
$regenerated = true;
}
}
if ($regenerated) {
$setting->setValue(json_encode($codes));
$this->entityManager->persist($setting);
$this->entityManager->flush();
// Notify the player that their codes have changed
$topic = $_ENV['MERCURE_TOPIC_BASE'] . '/game/hub-' . $session->getId();
$notification = "Security Alert: One of your verify codes was shared and has been regenerated.";
// We send it only to this player (screen)
$this->hub->publish(new Update($topic, json_encode([$screen, $notification])));
}
}
private function updateChatTracking(Player $player, int $sendTo): void
{
$rights = $this->getRechten($player);
if(in_array('verify', $rights))
return;
$trackingSettingName = SessionSettingType::tryFrom('ChatTrackingForPlayer' . $player->getScreen());
if (!$trackingSettingName) {
return;
}
$setting = $this->sessionSettingRepository->getSetting($player->getSession(), $trackingSettingName, $player);
if (!$setting) {
$setting = new SessionSetting();
$setting->setSession($player->getSession());
$setting->setPlayer($player);
$setting->setName($trackingSettingName);
$setting->setValue(json_encode([]));
}
$tracking = json_decode($setting->getValue() ?? '[]', true) ?? [];
if (!in_array($sendTo, $tracking)) {
$tracking[] = $sendTo;
$setting->setValue(json_encode($tracking));
$this->entityManager->persist($setting);
$this->entityManager->flush();
$this->checkAndGrantVerifyRight($player, $tracking);
}
}
private function checkAndGrantVerifyRight(Player $player, array $tracking): void
{
$screen = $player->getScreen();
$requiredTargets = [0]; // Everyone
$numPlayers = $player->getSession()->getGame()->getNumberOfPlayers();
for ($i = 1; $i <= $numPlayers; $i++) {
if ($i !== $screen) {
$requiredTargets[] = $i;
}
}
// Check if all required targets are in tracking
foreach ($requiredTargets as $target) {
if (!in_array($target, $tracking)) {
return;
}
}
// Grant verify right
$rightsSettingName = SessionSettingType::tryFrom('RightsForPlayer' . $screen);
if (!$rightsSettingName) {
return;
}
$setting = $this->sessionSettingRepository->getSetting($player->getSession(), $rightsSettingName, $player);
if (!$setting) {
return; // Should have been initialized
}
$rights = json_decode($setting->getValue() ?? '[]', true) ?? [];
$newRights = ['verify', 'cat'];
$updated = false;
foreach ($newRights as $newRight) {
if (!in_array($newRight, $rights)) {
$rights[] = $newRight;
$updated = true;
}
}
if ($updated) {
$setting->setValue(json_encode($rights));
$this->entityManager->persist($setting);
$this->entityManager->flush();
}
}
private function handleDecodeMessage(string $message, Player $player)
{
$userNumber = $player->getScreen();
preg_match('/\d+/', $message, $matches);
$num = $matches[0] ?? null;
$randomString = $this->generateRandomString(250, 500);
if(is_null($num) || $num != $userNumber)
return $randomString;
foreach (DecodeMessage::cases() as $decodeMessage) {
if ($decodeMessage->name === $message) {
return $decodeMessage->value;
}
}
return $randomString;
}
private function generateRandomString(int $min, int $max): string
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ';
$charactersLength = strlen($characters);
$randomString = '';
$length = random_int($min, $max);
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[random_int(0, $charactersLength - 1)];
}
return $randomString;
}
private function handleVerifyMessage(string $message, Player $player) : string
{
$messageParts = explode(' ', $message);
if (count($messageParts) < 2) {
return 'Usage: /verify {code}';
}
$code = $messageParts[1];
$screen = $player->getScreen();
$session = $player->getSession();
$progressSettingName = SessionSettingType::tryFrom('VerificationProgressForPlayer' . $screen);
if (!$progressSettingName) {
return 'Error: Invalid player screen.';
}
$progressSetting = $this->sessionSettingRepository->getSetting($session, $progressSettingName, $player);
if (!$progressSetting) {
return 'Error: Verification progress setting not found.';
}
$progress = json_decode($progressSetting->getValue() ?? '[]', true) ?? [];
$verifiedBy = null;
foreach ($session->getPlayers() as $otherPlayer) {
if ($otherPlayer->getId() === $player->getId()) {
continue;
}
$otherScreen = $otherPlayer->getScreen();
$codesSettingName = SessionSettingType::tryFrom('VerifyCodesForPlayer' . $otherScreen);
if (!$codesSettingName) {
continue;
}
$codesSetting = $this->sessionSettingRepository->getSetting($session, $codesSettingName, $otherPlayer);
if (!$codesSetting) {
continue;
}
$codes = json_decode($codesSetting->getValue() ?? '[]', true) ?? [];
if (isset($codes[$screen]) && $codes[$screen] === $code) {
$verifiedBy = $otherScreen;
break;
}
}
if ($verifiedBy !== null) {
if (!in_array($verifiedBy, $progress)) {
$progress[] = $verifiedBy;
$progressSetting->setValue(json_encode($progress));
$this->entityManager->persist($progressSetting);
$this->entityManager->flush();
$response = 'You have been successfully verified by Agent ' . $verifiedBy . '.';
if (count($progress) >= 2) {
$this->grantVerificationRights($player);
$response .= ' You have received additional rights!';
}
return $response;
} else {
return 'You were already verified by Agent ' . $verifiedBy . '.';
}
}
return 'Invalid verification code.';
}
private function grantVerificationRights(Player $player): void
{
$screen = $player->getScreen();
$rightsSettingName = SessionSettingType::tryFrom('RightsForPlayer' . $screen);
if (!$rightsSettingName) {
return;
}
$setting = $this->sessionSettingRepository->getSetting($player->getSession(), $rightsSettingName, $player);
if (!$setting) {
return;
}
$rights = json_decode($setting->getValue() ?? '[]', true) ?? [];
$newRights = ['cd', 'decode'];
$updated = false;
foreach ($newRights as $newRight) {
if (!in_array($newRight, $rights)) {
$rights[] = $newRight;
$updated = true;
}
}
if ($updated) {
$setting->setValue(json_encode($rights));
$this->entityManager->persist($setting);
$this->entityManager->flush();
$this->checkIfAllPlayersVerified($player);
}
}
private function checkIfAllPlayersVerified(Player $player): void
{
$session = $player->getSession();
$everyoneVerifiedSetting = $this->sessionSettingRepository->getSetting($session, SessionSettingType::EVERYONE_VERIFIED, $player);
if ($everyoneVerifiedSetting && $everyoneVerifiedSetting->getValue() === 'true') {
return;
}
$allVerified = true;
foreach ($session->getPlayers() as $otherPlayer) {
$otherScreen = $otherPlayer->getScreen();
$progressSettingName = SessionSettingType::tryFrom('VerificationProgressForPlayer' . $otherScreen);
if (!$progressSettingName) {
continue;
}
$progressSetting = $this->sessionSettingRepository->getSetting($session, $progressSettingName, $otherPlayer);
$progress = json_decode($progressSetting?->getValue() ?? '[]', true) ?? [];
if (count($progress) < $session->getGame()->getNumberOfPlayers() - 1) {
$allVerified = false;
break;
}
}
if ($allVerified) {
if (!$everyoneVerifiedSetting) {
$everyoneVerifiedSetting = new SessionSetting();
$everyoneVerifiedSetting->setSession($session);
$everyoneVerifiedSetting->setPlayer($player);
$everyoneVerifiedSetting->setName(SessionSettingType::EVERYONE_VERIFIED);
}
$everyoneVerifiedSetting->setValue('true');
$this->entityManager->persist($everyoneVerifiedSetting);
$this->entityManager->flush();
$topic = $_ENV['MERCURE_TOPIC_BASE'] . '/game/hub-' . $session->getId();
$message = "Mainframe Help Modus: Agents Doyle, Vega and Lennox rapports have been updated with coded messages.";
$this->hub->publish(new Update($topic, json_encode([0, $message])));
}
}
private function goToNewDir(string $pwd, string $newPwd, Player $player) : string|bool
{
$allPossiblePaths = $this->getAllPossiblePaths($player);
$dirParts = explode('/', $newPwd);
$int = count($dirParts);
if($dirParts[0] == '') {
$newDir = '';
$startPart = 1;
} else {
$newDir = $pwd;
$startPart = 0;
}
for($i = $startPart; $i < $int; $i++) {
if($dirParts[$i] == '..')
$newDir = $this->getPrevPath($newDir);
else
$newDir .= '/' . $dirParts[$i];
if(!in_array($newDir, $allPossiblePaths))
return false;
}
return $newDir;
}
private function getPrevPath(string $pwd) : string
{
$pwdParts = explode('/', $pwd);
array_pop($pwdParts);
$pwd = implode('/', $pwdParts);
return $pwd;
}
private function getAllPossiblePaths(Player $player) : array
{
$paths = [];
$paths[] = '/';
$paths[] = '/var';
$paths[] = '/var/arrest';
$paths[] = '/var/www';
$paths[] = '/var/marriage';
$paths[] = '/var/rapports';
$paths[] = '/var/linking';
$paths[] = '/etc';
$paths[] = '/etc/short';
$paths[] = '/etc/long';
$paths[] = '/etc/arrest';
$paths[] = '/etc/power';
$paths[] = '/etc/break';
$paths[] = '/etc/handle';
$paths[] = '/etc/freak';
$paths[] = '/etc/host';
$paths[] = '/home';
$playerNames = ['root', 'Luke', 'Charles', 'William', 'Peter'];
$players = $player->getSession()->getPlayers();
foreach($players as $player) {
$playerNames[] = $player->getUser()->getUsername();
}
$playerNames = array_unique($playerNames);
foreach($playerNames as $name) {
$paths[] = '/home/' . $name;
}
return $paths;
}
private function isAllowedToRemove(string $file, Player $player, bool $sudo) : bool
{
if(!$this->fileExists($file, $player))
return false;
if(str_starts_with($file, '/var/rapports/'))
return false;
$rights = $this->getRechten($player);
if(in_array('sudo', $rights) || $sudo)
return true;
$sudoFiles = [
'/var/arrest/handle.sh',
'/var/arrest/cell.sh',
'/var/marriage/divorce.sh',
];
return !in_array($file, $sudoFiles);
}
private function fileExists(string $file, Player $player) : bool
{
$files = $this->getAllPossibleFiles($player);
if(in_array($file, $files))
return true;
return false;
}
private function getAllPossibleFiles(Player $player = null) : array
{
$files = [];
$files[] = '/var/arrest/handle.sh';
$files[] = '/var/arrest/bars.sh';
$files[] = '/var/arrest/cell.sh';
$files[] = '/var/marriage/share.sh';
$files[] = '/var/marriage/divorce.sh';
$files[] = '/var/rapports/095_07-14.txt';
$files[] = '/var/rapports/007_19-52.txt';
$files[] = '/var/rapports/083_25-39.txt';
$files[] = '/var/rapports/019_31-11.txt';
$files[] = '/var/rapports/075_46-77.txt';
$files[] = '/var/rapports/031_53-28.txt';
$files[] = '/var/rapports/072_61-05.txt';
$files[] = '/var/rapports/064_72-90.txt';
$files[] = '/var/rapports/091_81-33.txt';
$files[] = '/var/rapports/079_89-47.txt';
$files[] = '/var/rapports/098_92-14.txt';
$files[] = '/var/rapports/012_94-31.txt';
$files[] = '/var/rapports/016_98-07.txt';
$files[] = '/var/rapports/087_102-45.txt';
$files[] = '/var/rapports/094_110-19.txt';
$files[] = '/var/rapports/063_117-56.txt';
$files[] = '/var/rapports/017_123-88.txt';
$files[] = '/var/rapports/093_138-24.txt';
$files[] = '/var/rapports/001_145-93.txt';
$files[] = '/var/rapports/index.txt';
$players = $player->getSession()->getPlayers();
foreach($players as $player) {
$files[] = '/var/home/' . $player->getUser()->getUsername() . '/verifyCodes.txt';
}
return $files;
}
private function getAllCurrentFilesInDirectory(Player $player) : array
{
$pwd = $this->playerService->getCurrentPwdOfPlayer($player);
if (!$pwd) {
return [];
}
$allPaths = $this->getAllPossiblePaths($player);
$allFiles = $this->getAllPossibleFiles();
$deletedFiles = $this->playerService->getDeletedFilesOfSession($player);
$entries = [];
// Find directories in current pwd
foreach ($allPaths as $path) {
if ($path === $pwd) {
continue;
}
// Check if $path is a direct child of $pwd
$parent = $this->getPrevPath($path);
if ($parent === $pwd) {
$parts = explode('/', $path);
$entries[] = [end($parts) . '/', 'dir'];
}
}
// Find files in current pwd
foreach ($allFiles as $file) {
if (in_array($file, $deletedFiles)) {
continue;
}
$parent = $this->getPrevPath($file);
if ($parent === $pwd) {
$parts = explode('/', $file);
$entries[] = [end($parts), 'file'];
}
}
sort($entries);
return $entries;
}
}