checkSecret($request); $body = json_decode((string)$request->getBody(), true); if (json_last_error() !== JSON_ERROR_NONE) { throw new JsonException(json_last_error_msg()); } $this->bot = new BotApi(config('bot.token')); $this->update = Update::fromResponse($body); } /** * Запсукает обработку команды * * @return bool * @throws InvalidArgumentException * @throws \TelegramBot\Api\Exception * @throws Exception */ public function process(): bool { $commandText = $this->getBotCommandText(); return match (true) { str_starts_with($commandText, '/start') => $this->processHelpCommand(), str_starts_with($commandText, '/list') => $this->processListCommand(), str_starts_with($commandText, '/info') => $this->processInfoCommand(), str_starts_with($commandText, '/help') => $this->processHelpCommand(), str_starts_with($commandText, '/links') => $this->processLinksCommand(), str_starts_with($commandText, '/stats') => $this->processStatsCommand(), default => true, }; } /** * Обрабатывает команду /list * * @return bool * @throws InvalidArgumentException * @throws \TelegramBot\Api\Exception * @throws Exception */ protected function processListCommand(): bool { $this->bot->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing'); $playlists = ini()->getPlaylists(); if (empty($playlists)) { $replyText = 'Плейлистов нет'; } else { $replyText = []; foreach ($playlists as $code => $pls) { $statusEmoji = match ($pls['isOnline']) { true => '🟢', false => '🔴', default => '⚪', }; in_array('adult', $pls['tags'] ?? []) === true && $statusEmoji .= "🔞"; $replyText[] = "$statusEmoji \[$code\]"; } $replyText = "Полный список плейлистов:\n\n**>" . implode("\n>", $replyText) . "||\n"; } return $this->reply($replyText, InlineKeyboardMarkup::fromResponse([ 'inline_keyboard' => [ [ [ 'text' => 'Список на сайте', 'url' => base_url(), ], [ 'text' => 'FAQ', 'url' => base_url('faq'), ] ] ] ])); } /** * Обрабатывает команду /info * * @return bool * @throws InvalidArgumentException * @throws \TelegramBot\Api\Exception */ protected function processInfoCommand(): bool { $this->bot->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing'); $message = $this->update->getMessage(); $text = $message->getText(); $command = $this->getBotCommand(); $code = mb_substr($text, $command->getLength() + 1 , mb_strlen($text)); if (empty($code)) { return $this->reply('Укажите код плейлиста с сайта, чтобы получить результаты его проверки, ' . 'например, `/info ru` или `/info@iptv_aggregator_bot ru`'); } try { $pls = ini()->getPlaylist($code); } catch (PlaylistNotFoundException) { return $this->reply("Плейлист `$code` не найден"); } $statusEmoji = match ($pls['isOnline']) { true => '🟢', false => '🔴', default => '⚪', }; in_array('adult', $pls['tags']) && $statusEmoji .= "🔞"; $replyText[] = $statusEmoji . ' [' . $this->escape($pls['name']) . '](' . base_url("$code/details") . ")\n"; empty($pls['description']) || $replyText[] = $this->escape($pls['description']) . "\n"; if ($pls['isOnline'] === null) { $replyText[] = "⏲️ *Проверка:* в очереди"; } else { $now = DateTimeImmutable::createFromTimestamp(time()); $checkedAt = DateTimeImmutable::createFromTimestamp($pls['checkedAt']); $minutes = $checkedAt->diff($now)->i; $replyText[] = "⏲️ *Проверка:* " . ($minutes > 1 ? "$minutes мин\. назад" : "только что"); if ($pls['isOnline'] === true) { $replyText[] = "📺 *Каналов:* " . count($pls['channels'] ?? []) . ' \(онлайн ' . $pls['onlineCount'] . ', оффлайн ' . $pls['offlineCount'] . "\)"; $replyText[] = "⏪ *Перемотка:* " . ($pls['hasCatchup'] === true ? '*есть*' : 'нет'); $replyText[] = "🗞️ *Телепрограмма:* " . ($pls['hasTvg'] === true ? '*есть*' : 'нет'); if (count($pls['groups'] ?? []) > 0) { $groups = array_map(fn (array $group) => $this->escape($group['name']), $pls['groups']); $replyText[] = "\n🗂️ *Группы* \(" . count($pls['groups'] ?? []) . "\):\n**>\- " . implode("\n>\- ", $groups) . '||'; } else { $replyText[] = "🗂️ *Группы:* нет"; } if (count($pls['tags'] ?? []) > 0) { $replyText[] = "\n🏷️ *Теги* \(" . count($pls['tags']) . "\):\n**>" . $this->escape(trim(implode(' ', $pls['tags']))) . "||\n"; } else { $replyText[] = "🏷️ *Теги:* нет"; } } else { $replyText[] = "❌ *Ошибка проверки:*\n**>" . $this->escape($pls['content']) . "||\n"; } } $replyText[] = "🔗 *Ссылка для ТВ:* \(скопируй подходящую\)"; if (config('app.mirror_url')) { $replyText[] = '\- `' . mirror_url("$code") . '`'; $replyText[] = '\- `' . mirror_url("$code.m3u") . '`'; } $replyText[] = '\- `' . base_url("$code") . '`'; $replyText[] = '\- `' . base_url("$code.m3u") . "`\n"; return $this->reply( implode("\n", $replyText), InlineKeyboardMarkup::fromResponse([ 'inline_keyboard' => [ [ [ 'text' => is_null($pls['isOnline']) ? 'Подробности' : 'Список каналов', 'url' => base_url("$code/details"), ], [ 'text' => 'FAQ', 'url' => base_url('faq'), ] ] ] ]), ); } /** * Обрабатывает команду /help * * @return bool * @throws \TelegramBot\Api\Exception */ protected function processHelpCommand(): bool { $this->bot->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing'); $replyText[] = 'Бот предоставляет короткую сводку о плейлистах, которые видны на сайте ' . $this->escape(base_url()) . '\.'; $replyText[] = 'Плейлисты проверяются сервером автоматически\.'; $replyText[] = ''; $replyText[] = 'Команды бота:'; $replyText[] = '`/list` \- список кодов всех плейлистов;'; $replyText[] = '`/info <код>` \- информация о плейлисте с указанным кодом;'; $replyText[] = '`/help` \- данная справка\.'; $replyText[] = ''; $replyText[] = 'Статусы плейлистов:'; $replyText[] = '🟢 \- онлайн'; $replyText[] = '🔴 \- оффлайн'; $replyText[] = '⚪ \- не проверялось'; $replyText[] = '🔞 \- есть каналы для взрослых'; return $this->reply(implode("\n", $replyText)); } /** * Обрабатывает команду /links * * @return bool * @throws \TelegramBot\Api\Exception */ protected function processLinksCommand(): bool { $this->bot->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing'); $replyText[] = '*Ресурсы и страницы*'; $replyText[] = ''; $replyText[] = '🌏 Сайт: ' . $this->escape(base_url()); config('app.mirror_url') && $replyText[] = '🪞 Зеркало: ' . $this->escape(mirror_url()); $replyText[] = '👩‍💻 Исходный код: ' . $this->escape('https://git.axenov.dev/IPTV'); $replyText[] = '✈️ Telegram\-канал: @iptv\_aggregator'; $replyText[] = '✈️ Обсуждение: @iptv\_aggregator\_chat'; $replyText[] = '📚 Доп\. сведения:'; $replyText[] = '\- ' . $this->escape('https://git.axenov.dev/IPTV/.profile'); $replyText[] = '\- ' . $this->escape(base_url('faq')); return $this->reply(implode("\n", $replyText)); } /** * Обрабатывает команду /stats * * @return bool * @throws Exception */ protected function processStatsCommand(): bool { $this->bot->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing'); $allChannels = []; foreach (ini()->getPlaylists() as $pls) { $allChannels = array_merge($allChannels, $pls['channels'] ?? []); } $onlinePls = array_filter( ini()->getPlaylists(), static fn (array $pls) => $pls['isOnline'] === true, ); $offlinePls = array_filter( ini()->getPlaylists(), static fn (array $pls) => $pls['isOnline'] === false, ); $unknownPls = array_filter( ini()->getPlaylists(), static fn (array $pls) => $pls['isOnline'] === null, ); $adultPls = array_filter( $onlinePls, static fn (array $pls) => in_array('adult', $pls['tags']), ); $catchupPls = array_filter( $onlinePls, static fn (array $pls) => $pls['hasCatchup'] === true, ); $tvgPls = array_filter( $onlinePls, static fn (array $pls) => $pls['hasTvg'] === true, ); $grouppedPls = array_filter( $onlinePls, static fn (array $pls) => count($pls['groups'] ?? []) > 0 ); $onlineCh = $offlineCh = $adultCh = []; foreach ($onlinePls as $pls) { $tmpOnline = array_filter( $pls['channels'] ?? [], static fn (array $ch) => $ch['isOnline'] === true, ); $tmpOffline = array_filter( $pls['channels'] ?? [], static fn (array $ch) => $ch['isOnline'] === false, ); $tmpAdult = array_filter( $pls['channels'] ?? [], static fn (array $ch) => in_array('adult', $ch['tags']), ); $onlineCh = array_merge($onlineCh, $tmpOnline); $offlineCh = array_merge($offlineCh, $tmpOffline); $adultCh = array_merge($adultCh, $tmpAdult); } $replyText[] = '📊 *Статистика*'; $replyText[] = ''; $replyText[] = '*Список изменён:* ' . $this->escape(ini()->updatedAt()); $replyText[] = ''; $replyText[] = '*Плейлистов:* ' . count(ini()->getPlaylists()); $replyText[] = '🟢 Онлайн \- ' . count($onlinePls); $replyText[] = '🔴 Оффлайн \- ' . count($offlinePls); $replyText[] = '⚪ В очереди \- ' . count($unknownPls); $replyText[] = '🔞 Для взрослых \- ' . count($adultPls); $replyText[] = '⏪ С перемоткой \- ' . count($catchupPls); $replyText[] = '🗞️ С телепрограммой \- ' . count($tvgPls); $replyText[] = '🗂️ С группировкой каналов \- ' . count($grouppedPls); $replyText[] = ''; $replyText[] = '*Каналов:* ' . count($allChannels); $replyText[] = '🟢 Онлайн \- ' . count($onlineCh); $replyText[] = '🔴 Оффлайн \- ' . count($offlineCh); $replyText[] = '🔞 Для взрослых \- ' . count($adultCh); $replyText[] = ''; $replyText[] = ''; return $this->reply(implode("\n", $replyText)); } /** * Сверяет секретный заголовок с заданным в конфиге * * @param ServerRequestInterface $request * @return void * @throws InvalidTelegramSecretException */ protected function checkSecret(ServerRequestInterface $request): void { $secret = config('bot.secret'); if (empty($secret)) { return; } $header = $request->getHeaderLine('X-Telegram-Bot-Api-Secret-Token'); if (empty($header) || $header !== $secret) { throw new InvalidTelegramSecretException(); } } /** * Возвращает объект, описывающий команду бота * * @return MessageEntity|null */ protected function getBotCommand(): ?MessageEntity { return array_filter( $this->update->getMessage()->getEntities(), static fn (MessageEntity $entity) => $entity->getType() === 'bot_command', )[0] ?? null; } /** * Возвращает текст команды бота * * @return string|null */ protected function getBotCommandText(): ?string { $text = $this->update->getMessage()->getText(); $command = $this->getBotCommand(); return $command ? mb_substr($text, 0, $command->getLength()) : null; } /** * Отправляет текстовое сообщение в ответ на команду, отправленную пользователем * * @param string $text * @param InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $keyboard * @return bool */ protected function reply( string $text, InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $keyboard = null, ): bool { try { $this->bot->sendMessage( chatId: $this->update->getMessage()->getChat()->getId(), text: $text, parseMode: 'MarkdownV2', replyToMessageId: $this->update->getMessage()->getMessageId(), replyMarkup: $keyboard, ); } catch (Throwable $e) { error_log($e->getMessage()); return false; } return true; } /** * Экранирует служебные символы в строке * * @param string $string * @return string */ protected function escape(string $string): string { return str_replace( ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'], ['\_', '\*', '\[', '\]', '\(', '\)', '\~', '\`', '\>', '\#', '\+', '\-', '\=', '\|', '\{', '\}', '\.', '\!'], $string, ); } }