src/Controller/ChatController.php line 296

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Controller;
  4. use App\Controller\BaseController;
  5. use Doctrine\DBAL\Connection;
  6. use Predis\Client as RedisClient;
  7. use Psr\Log\LoggerInterface;
  8. use Symfony\Component\HttpFoundation\JsonResponse;
  9. use Symfony\Component\HttpFoundation\Request;
  10. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  11. use Symfony\Component\Routing\Annotation\Route;
  12. use Symfony\Component\Security\Csrf\CsrfToken;
  13. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  14. use Symfony\Component\Validator\Constraints as Assert;
  15. use Symfony\Component\Validator\Validator\ValidatorInterface;
  16. final class ChatController extends BaseController
  17. {
  18.     /* настройки */
  19.     private const DEPOSIT_THRESHOLD        0;      // ₽
  20.     private const CHAT_HISTORY_LIMIT       20;     // сообщений, хранимых в Redis
  21.     private const MIN_MESSAGE_INTERVAL     0.7;    // cек между сообщениями одного пользователя
  22.     private const DUPLICATE_WINDOW_SECONDS 5;      // таймаут, в течение которого одно и то же сообщение считается дубликатом
  23.     private const ROLE_MAP = [
  24.         => 'user',
  25.         => 'admin',
  26.         => 'moderator',
  27.         => 'yt',
  28.         => 'system',
  29.     ];
  30.     /**
  31.      * @Route("/api/chat/message", name="chat_message", methods={"POST"})
  32.      */
  33.     public function send(
  34.         Request                   $request,
  35.         SessionInterface          $session,
  36.         Connection                $db,
  37.         RedisClient               $redis,
  38.         ValidatorInterface        $validator,
  39.         LoggerInterface           $logger,
  40.         CsrfTokenManagerInterface $csrf
  41.     ): JsonResponse {
  42.         $rawToken $request->headers->get('X-CSRF-TOKEN')
  43.             ?: $request->request->get('_csrf_token');
  44.         if (!$rawToken || !$csrf->isTokenValid(new CsrfToken('general'$rawToken))) {
  45.             return $this->json(['success' => false'mess' => 'CSRF‑токен недействителен'], 419);
  46.         }
  47.         $now     microtime(true);
  48.         $lastMsg = (float) $session->get('last_chat_time'0.0);
  49.         if ($now $lastMsg self::MIN_MESSAGE_INTERVAL) {
  50.             return $this->json(['success' => false'mess' => 'Сообщения слишком частые. Подождите чуть‑чуть.']);
  51.         }
  52.         $session->set('last_chat_time'$now);
  53.         $sid $session->get('hash');
  54.         if (!$sid) {
  55.             return $this->json(['success' => false'mess' => 'Авторизуйтесь!'], 401);
  56.         }
  57.         $textRaw $request->request->get('text');
  58.         if ($textRaw === null) {
  59.             $payload json_decode($request->getContent(), true);
  60.             if (is_array($payload)) {
  61.                 $textRaw $payload['text'] ?? null;
  62.             }
  63.         }
  64.         $errors $validator->validate(
  65.             ['text' => $textRaw],
  66.             new Assert\Collection([
  67.                 'text' => [
  68.                     new Assert\NotBlank(['message' => 'Сообщение не может быть пустым']),
  69.                     new Assert\Length(['max' => 60'maxMessage' => 'Слишком длинное сообщение']),
  70.                 ],
  71.             ])
  72.         );
  73.         if (count($errors) > 0) {
  74.             return $this->json(['success' => false'mess' => $errors[0]->getMessage()]);
  75.         }
  76.         try {
  77.             $db->beginTransaction();
  78.             $user $db->fetchAssociative(
  79.                 'SELECT id, login, img, admin, chatban FROM users WHERE hash = :hash FOR UPDATE',
  80.                 ['hash' => $sid]
  81.             );
  82.             if (!$user) {
  83.                 $db->rollBack();
  84.                 return $this->json(['success' => false'mess' => 'Пользователь не найден']);
  85.             }
  86.             if ((int) $user['chatban'] === 1) {
  87.                 $db->rollBack();
  88.                 return $this->json(['success' => false'mess' => 'Вы заблокированы в чате.']);
  89.             }
  90.             $sumDeposits = (float) $db->fetchOne(
  91.                 'SELECT COALESCE(SUM(suma),0) FROM deposits WHERE user_id = :uid AND status = 1',
  92.                 ['uid' => $user['id']]
  93.             );
  94.             if ($sumDeposits self::DEPOSIT_THRESHOLD) {
  95.                 $db->rollBack();
  96.                 return $this->json([
  97.                     'success' => false,
  98.                     'mess'    => sprintf('Для чата пополните баланс минимум на %.0f ₽'self::DEPOSIT_THRESHOLD),
  99.                 ]);
  100.             }
  101.             $db->commit();
  102.         } catch (\Throwable $e) {
  103.             if ($db->isTransactionActive()) {
  104.                 $db->rollBack();
  105.             }
  106.             $logger->error('Chat auth/deposit check failed', ['e' => $e]);
  107.             return $this->json(['success' => false'mess' => 'Ошибка сервера'], 500);
  108.         }
  109.         $dupKey   "chat:last_message:" $user['id'];
  110.         $lastText $redis->get($dupKey);
  111.         if ($lastText !== null && $lastText === $textRaw) {
  112.             return $this->json(['success' => false'mess' => 'Повторять одно и то же не нужно']);
  113.         }
  114.         $redis->setex($dupKeyself::DUPLICATE_WINDOW_SECONDS$textRaw);
  115.         $cleanText $this->sanitizeMessage((string) $textRaw);
  116.         if ($cleanText === '') {
  117.             return $this->json(['success' => false'mess' => 'Сообщение не может быть пустым!']);
  118.         }
  119.         $role    self::ROLE_MAP[$user['admin']] ?? 'user';
  120.         $timeStr = (new \DateTimeImmutable())->format('H:i');
  121.         $payload = [
  122.             'user_id'    => (int) $user['id'],
  123.             'avatar'     => (string) ($user['img'] ?? ''),
  124.             'username'   => $this->truncateUsername((string) ($user['login'] ?? '')),
  125.             'role'       => $role,
  126.             'text'       => $cleanText,
  127.             'time'       => $timeStr,
  128.             'message_id' => bin2hex(random_bytes(8))
  129.         ];
  130.         try {
  131.             $redis->publish('chat_message'json_encode($payloadJSON_UNESCAPED_UNICODE JSON_UNESCAPED_SLASHES));
  132.             $redis->lpush('chat_messages'json_encode($payloadJSON_UNESCAPED_UNICODE JSON_UNESCAPED_SLASHES));
  133.             $redis->ltrim('chat_messages'0self::CHAT_HISTORY_LIMIT 1);
  134.         } catch (\Throwable $e) {
  135.             $logger->error('Failed to write to Redis', ['e' => $e]);
  136.             return $this->json(['success' => false'mess' => 'Ошибка сервера'], 500);
  137.         }
  138.         return $this->json([
  139.             'success' => true,
  140.             'mess'    => null,
  141.             'data'    => $payload,
  142.         ]);
  143.     }
  144.     
  145.     /**
  146.      * @Route("/api/chat/delete", name="chat_delete", methods={"POST"})
  147.      */
  148.     public function deleteMessage(
  149.         Request $request,
  150.         SessionInterface $session,
  151.         Connection $db,
  152.         RedisClient $redis,
  153.         LoggerInterface $logger,
  154.         CsrfTokenManagerInterface $csrf
  155.     ): JsonResponse {
  156.         $rawToken $request->request->get('_csrf_token');
  157.         if (!$rawToken || !$csrf->isTokenValid(new CsrfToken('general'$rawToken))) {
  158.             return $this->json(['success' => false'mess' => 'CSRF‑токен недействителен'], 419);
  159.         }
  160.         $sid $session->get('hash');
  161.         if (!$sid) {
  162.             return $this->json(['success' => false'mess' => 'Авторизуйтесь!'], 401);
  163.         }
  164.         $user $db->fetchAssociative('SELECT id, login, admin FROM users WHERE hash = :hash', ['hash' => $sid]);
  165.         if (!$user || !in_array($user['admin'], [12])) {
  166.             return $this->json(['success' => false'mess' => 'Недостаточно прав'], 403);
  167.         }
  168.         $messageId $request->request->get('id');
  169.         if (!$messageId) {
  170.             return $this->json(['success' => false'mess' => 'ID не указан']);
  171.         }
  172.         try {
  173.             $messages $redis->lrange('chat_messages'0, -1);
  174.             foreach ($messages as $msg) {
  175.                 $data json_decode($msgtrue);
  176.                 if ($data['message_id'] == $messageId) {
  177.                     $redis->lrem('chat_messages'1$msg);
  178.                     break;
  179.                 }
  180.             }
  181.             $redis->publish('chat_delete'json_encode(['id' => $messageId], JSON_UNESCAPED_UNICODE));
  182.             return $this->json(['success' => true]);
  183.         } catch (\Throwable $e) {
  184.             $logger->error('Ошибка удаления сообщения', ['e' => $e]);
  185.             return $this->json(['success' => false'mess' => 'Ошибка сервера'], 500);
  186.         }
  187.     }
  188.     
  189.     /**
  190.      * @Route("/api/chat/ban", name="chat_ban", methods={"POST"})
  191.      */
  192.     public function banUser(
  193.         Request $request,
  194.         SessionInterface $session,
  195.         Connection $db,
  196.         CsrfTokenManagerInterface $csrf
  197.     ): JsonResponse {
  198.         $rawToken $request->request->get('_csrf_token');
  199.         if (!$rawToken || !$csrf->isTokenValid(new CsrfToken('general'$rawToken))) {
  200.             return $this->json(['success' => false'mess' => 'CSRF‑токен недействителен'], 419);
  201.         }
  202.         $sid $session->get('hash');
  203.         if (!$sid) {
  204.             return $this->json(['success' => false'mess' => 'Авторизуйтесь!'], 401);
  205.         }
  206.         $current $db->fetchAssociative('SELECT id, admin FROM users WHERE hash = :hash', ['hash' => $sid]);
  207.         if (!$current || !in_array($current['admin'], [12])) {
  208.             return $this->json(['success' => false'mess' => 'Недостаточно прав'], 403);
  209.         }
  210.         $userId = (int) $request->request->get('id');
  211.         if ($userId <= 0) {
  212.             return $this->json(['success' => false'mess' => 'Некорректный ID']);
  213.         }
  214.         $db->update('users', ['chatban' => 1], ['id' => $userId]);
  215.         return $this->json(['success' => true'mess' => 'Пользователь заблокирован']);
  216.     }
  217.     
  218.     /**
  219.      * @Route("/api/chat/clear", name="chat_clear", methods={"POST"})
  220.      */
  221.     public function clearChat(
  222.         Request $request,
  223.         SessionInterface $session,
  224.         Connection $db,
  225.         RedisClient $redis,
  226.         CsrfTokenManagerInterface $csrf
  227.     ): JsonResponse {
  228.         $rawToken $request->request->get('_csrf_token');
  229.         if (!$rawToken || !$csrf->isTokenValid(new CsrfToken('general'$rawToken))) {
  230.             return $this->json(['success' => false'mess' => 'CSRF‑токен недействителен'], 419);
  231.         }
  232.         $sid $session->get('hash');
  233.         if (!$sid) {
  234.             return $this->json(['success' => false'mess' => 'Авторизуйтесь!'], 401);
  235.         }
  236.         $current $db->fetchAssociative('SELECT id, admin FROM users WHERE hash = :hash', ['hash' => $sid]);
  237.         if (!$current || !in_array($current['admin'], [12])) {
  238.             return $this->json(['success' => false'mess' => 'Недостаточно прав'], 403);
  239.         }
  240.         $redis->del('chat_messages');
  241.         $redis->publish('chat_clear'json_encode(['clear' => true]));
  242.         
  243.         $payload = [
  244.             'user_id'  => 0,
  245.             'avatar'   => '/assets/icons/little-logo.svg',
  246.             'username' => 'Система',
  247.             'role'     => 'system',
  248.             'text'     => 'Чат был очищен модератором.',
  249.             'time'     => (new \DateTimeImmutable())->format('H:i'),
  250.         ];
  251.         $redis->publish('chat_message'json_encode($payloadJSON_UNESCAPED_UNICODE));
  252.         $redis->lpush('chat_messages'json_encode($payloadJSON_UNESCAPED_UNICODE));
  253.         return $this->json(['success' => true'mess' => 'Чат очищен']);
  254.     }
  255.     
  256.     /**
  257.      * @Route("/api/chat/send-promo-auto", name="chat_send_promo_auto", methods={"POST"})
  258.      */
  259.     public function sendPromoCodeAuto(Request $requestConnection $dbRedisClient $redis): JsonResponse {
  260.         $secret $request->get('secret') ?? json_decode($request->getContent(), true)['secret'] ?? null;
  261.         if ($secret !== 'fasdfniODSX1b1xz') {
  262.             return $this->json(['success' => false'mess' => 'Доступ запрещён'], 403);
  263.         }
  264.         $code strtoupper(bin2hex(random_bytes(3))); 
  265.         $db->insert('promo', [
  266.             'name'        => $code,
  267.             'price'       => 50,
  268.             'count'       => 25,
  269.             'auto'        => 1,
  270.             'daydep'      => 0,
  271.             'deposit'      => 0,
  272.             'created_by'  => "system"
  273.         ]);
  274.         $message "🎁 Новый промокод: <b>$code</b><br>💸 50₽ на баланс<br>🔁 Доступно 25 активаций!";
  275.         $payload = [
  276.             'user_id'    => 0,
  277.             'avatar'     => '/assets/icons/little-logo.svg',
  278.             'username'   => 'Система',
  279.             'role'       => 'system',
  280.             'text'       => $message,
  281.             'time'       => (new \DateTimeImmutable())->format('H:i'),
  282.             'message_id' => bin2hex(random_bytes(8))
  283.         ];
  284.         $json json_encode($payloadJSON_UNESCAPED_UNICODE JSON_UNESCAPED_SLASHES);
  285.         $redis->publish('chat_message'$json);
  286.         $redis->lpush('chat_messages'$json);
  287.         $redis->ltrim('chat_messages'0self::CHAT_HISTORY_LIMIT 1);
  288.         return $this->json(['success' => true'code' => $code]);
  289.     }
  290.     private function sanitizeMessage(string $raw): string
  291.     {
  292.         $msg trim($raw);
  293.         $msg preg_replace('/[\p{C}]/u'''$msg);
  294.         $msg mb_substr($msg0500'UTF-8');
  295.         $msg htmlspecialchars($msgENT_QUOTES ENT_SUBSTITUTE ENT_HTML5'UTF-8');
  296.         $msg preg_replace('~https?://[^\s<]+~iu''[ссылка]'$msg);
  297.         $msg str_ireplace(['х.у.й.н.я''Скам''СКАМ''СкАм','СКам''сKам''vk.com''t.me''@''тгк''канал''подписывайтесь''подписывайся''скам','6ля''6лядь''6лять''b3ъeб''cock''cunt''e6aль''ebal''eblan''eбaл''eбaть''eбyч''eбать''eбёт''eблантий''fuck''fucker''fucking''xyёв''xyй''xyя''xуе','xуй''xую''zaeb''zaebal''zaebali''zaebat''архипиздрит''ахуел''ахуеть''бздение''бздеть''бздех''бздецы''бздит''бздицы''бздло''бзднуть''бздун''бздунья''бздюха''бздюшка''бздюшко''бля''блябу''блябуду''бляд''бляди''блядина''блядище''блядки''блядовать''блядство''блядун''блядуны''блядунья''блядь''блядюга''блять''вафел''вафлёр''взъебка''взьебка''взьебывать''въеб''въебался''въебенн''въебусь''въебывать''выблядок''выблядыш''выеб''выебать''выебен''выебнулся''выебон''выебываться''выпердеть''высраться''выссаться''вьебен''гавно''гавнюк''гавнючка''гамно''гандон''гнид''гнида''гниды''говенка''говенный''говешка''говназия''говнецо''говнище''говно''говноед''говнолинк''говночист''говнюк''говнюха''говнядина''говняк''говняный''говнять''гондон''доебываться''долбоеб''долбоёб''долбоящер''дрисня''дрист''дристануть''дристать''дристун''дристуха''дрочелло''дрочена''дрочила''дрочилка''дрочистый''дрочить''дрочка''дрочун''е6ал''е6ут''еб твою мать''ёб твою мать''ёбaн''ебaть''ебyч''ебал''ебало''ебальник''ебан''ебанамать''ебанат''ебаная''ёбаная''ебанический''ебанный''ебанныйврот''ебаное''ебануть''ебануться''ёбаную''ебаный''ебанько''ебарь''ебат''ёбат''ебатория''ебать''ебать-копать''ебаться''ебашить''ебёна''ебет''ебёт''ебец''ебик''ебин''ебись''ебическая''ебки''ебла''еблан''ебливый''еблище''ебло''еблыст''ебля''ёбн''ебнуть''ебнуться''ебня''ебошить''ебская''ебский''ебтвоюмать''ебун''ебут''ебуч''ебуче''ебучее''ебучий''ебучим''ебущ''ебырь''елда''елдак''елдачить''жопа''жопу''заговнять''задрачивать''задристать''задрота''зае6''заё6''заеб''заёб''заеба''заебал''заебанец''заебастая''заебастый''заебать''заебаться''заебашить''заебистое''заёбистое''заебистые''заёбистые''заебистый''заёбистый''заебись''заебошить''заебываться''залуп''залупа''залупаться''залупить''залупиться''замудохаться''запиздячить''засерать''засерун''засеря''засирать''засрун''захуячить''заябестая''злоеб''злоебучая''злоебучее''злоебучий''ибанамат''ибонех''изговнять''изговняться''изъебнуться''ипать''ипаться''ипаццо''Какдвапальцаобоссать''конча''курва''курвятник''лох''лошарa''лошара''лошары''лошок''лярва''малафья''манда''мандавошек''мандавошка''мандавошки''мандей''мандень''мандеть''мандища''мандой''манду''мандюк''минет''минетчик''минетчица''млять''мокрощелка''мокрощёлка''мразь''мудak''мудaк''мудаг''мудак''муде''мудель''мудеть''муди''мудил''мудила''мудистый''мудня''мудоеб''мудозвон''мудоклюй''на хер''на хуй''набздел''набздеть''наговнять''надристать''надрочить''наебать''наебет''наебнуть''наебнуться''наебывать''напиздел''напиздели''напиздело''напиздили''насрать''настопиздить''нахер''нахрен''нахуй''нахуйник''не ебет''не ебёт''невротебучий''невъебенно''нехира''нехрен''Нехуй''нехуйственно''ниибацо''ниипацца''ниипаццо''ниипет''никуя''нихера''нихуя''обдристаться''обосранец''обосрать''обосцать''обосцаться''обсирать''объебос''обьебать обьебос''однохуйственно''опездал''опизде''опизденивающе''остоебенить''остопиздеть''отмудохать''отпиздить''отпиздячить''отпороть''отъебись''охуевательский''охуевать''охуевающий''охуел''охуенно''охуеньчик''охуеть''охуительно''охуительный''охуяньчик''охуячивать''охуячить''очкун''падла''падонки''падонок''паскуда''педерас''педик''педрик''педрила''педрилло''педрило''педрилы''пездень''пездит''пездишь''пездо''пездят''пердануть''пердеж''пердение''пердеть''пердильник''перднуть''пёрднуть''пердун''пердунец''пердунина''пердунья''пердуха''пердь''переёбок''пернуть''пёрнуть''пи3д''пи3де''пи3ду''пиzдец''пидар''пидарaс''пидарас''пидарасы''пидары''пидор''пидорасы''пидорка''пидорок''пидоры''пидрас''пизда''пиздануть''пиздануться''пиздарваньчик''пиздато''пиздатое''пиздатый''пизденка''пизденыш''пиздёныш''пиздеть''пиздец''пиздит''пиздить''пиздиться''пиздишь''пиздища''пиздище''пиздобол''пиздоболы''пиздобратия''пиздоватая''пиздоватый''пиздолиз''пиздонутые''пиздорванец''пиздорванка''пиздострадатель''пизду''пиздуй''пиздун''пиздунья''пизды''пиздюга''пиздюк''пиздюлина''пиздюля''пиздят''пиздячить''писбшки''писька''писькострадатель''писюн''писюшка''по хуй''по хую''подговнять''подонки''подонок''подъебнуть''подъебнуться''поебать''поебень''поёбываает''поскуда''посрать''потаскуха''потаскушка''похер''похерил''похерила''похерили''похеру''похрен''похрену''похуй''похуист''похуистка''похую''придурок''приебаться''припиздень''припизднутый''припиздюлина''пробзделся''проблядь''проеб''проебанка''проебать''промандеть''промудеть''пропизделся''пропиздеть''пропиздячить''раздолбай''разхуячить''разъеб''разъеба''разъебай''разъебать''распиздай''распиздеться''распиздяй''распиздяйство''распроеть''сволота''сволочь''сговнять''секель''серун''серька''сестроеб''сикель''сила''сирать''сирывать''соси''спиздел''спиздеть''спиздил''спиздила''спиздили''спиздит''спиздить''срака''сраку''сраный''сранье''срать''срун''ссака''ссышь''стерва''страхопиздище''сука''суки''суходрочка''сучара''сучий''сучка''сучко''сучонок''сучье''сцание''сцать''сцука''сцуки''сцуконах''сцуль''сцыха''сцышь''съебаться''сыкун''трахае6''трахаеб''трахаёб''трахатель''ублюдок''уебать''уёбища''уебище''уёбище''уебищное''уёбищное''уебк''уебки''уёбки''уебок''уёбок''урюк''усраться''ушлепок''х_у_я_р_а''хyё''хyй''хyйня''хамло''хер''херня''херовато''херовина''херовый''хитровыебанный''хитрожопый''хуeм''хуе''хуё''хуевато''хуёвенький''хуевина''хуево''хуевый''хуёвый''хуек''хуёк''хуел''хуем''хуенч''хуеныш''хуенький''хуеплет''хуеплёт''хуепромышленник''хуерик''хуерыло''хуесос''хуесоска''хуета''хуетень''хуею''хуи''хуй''хуйком''хуйло''хуйня''хуйрик''хуище''хуля''хую''хуюл''хуя''хуяк''хуякать''хуякнуть''хуяра''хуясе''хуячить''целка''чмо''чмошник''чмырь''шалава''шалавой''шараёбиться''шлюха''шлюхой''шлюшка''ябывает'], '****'$msg);
  298.         return trim($msg);
  299.     }
  300.     
  301.     private function truncateUsername(string $name): string
  302.     {
  303.         $maxLength 6;
  304.         $safe htmlspecialchars($nameENT_QUOTES ENT_SUBSTITUTE ENT_HTML5'UTF-8');
  305.         if (mb_strlen($safe'UTF-8') <= $maxLength) {
  306.             return $safe;
  307.         }
  308.         return mb_substr($safe0$maxLength'UTF-8') . '****';
  309.     }
  310. }