api = new BotApi(config('bot.token')); if ($request) { $this->checkSecret($request); $this->request = $request; } } /** * Запускает обработку команды * * @return bool * @throws InvalidArgumentException * @throws \TelegramBot\Api\Exception * @throws Exception */ public function process(): bool { $this->parseRequestBody(); $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, }; } /** * Подготавливает объект события бота * * @return void * @throws InvalidArgumentException * @throws JsonException */ protected function parseRequestBody(): void { $body = json_decode((string)$this->request->getBody(), true); if (json_last_error() !== JSON_ERROR_NONE) { throw new JsonException(json_last_error_msg()); } $this->update = Update::fromResponse($body); } /** * Обрабатывает команду /list * * @return bool * @throws InvalidArgumentException * @throws \TelegramBot\Api\Exception * @throws Exception */ protected function processListCommand(): bool { $this->api->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->api->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->api->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing'); $replyText[] = 'Бот предоставляет короткую сводку о плейлистах, которые видны на сайте ' . $this->escape(base_url()) . '\.'; $replyText[] = 'Плейлисты проверяются сервером автоматически\.'; $replyText[] = ''; $replyText[] = 'Команды бота:'; $replyText[] = '`/list` \- список кодов всех плейлистов;'; $replyText[] = '`/info <код>` \- информация о плейлисте с указанным кодом;'; $replyText[] = '`/help` \- данная справка\;'; $replyText[] = '`/links` \- ссылки на все страницы проекта\;'; $replyText[] = '`/stats` \- статистика по плейлистам и каналам\.'; $replyText[] = ''; $replyText[] = 'Статусы плейлистов:'; $replyText[] = '🟢 \- онлайн'; $replyText[] = '🔴 \- оффлайн'; $replyText[] = '⚪ \- не проверялось'; $replyText[] = '🔞 \- есть каналы для взрослых'; return $this->reply(implode("\n", $replyText)); } /** * Обрабатывает команду /links * * @return bool * @throws \TelegramBot\Api\Exception */ protected function processLinksCommand(): bool { $this->api->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing'); $replyText[] = '*Ресурсы и страницы*'; $replyText[] = ''; $replyText[] = '🌏 Сайт: ' . $this->escape(base_url()); $replyText[] = '👩‍💻 Исходный код: ' . $this->escape(config('app.repo_url')); $replyText[] = '✈️ Telegram\-канал: @iptv\_aggregator'; $replyText[] = '✈️ Обсуждение: @iptv\_aggregator\_chat'; $replyText[] = '📚 Доп\. сведения:'; $replyText[] = '\- ' . $this->escape(config('app.repo_url') . '/.profile'); $replyText[] = '\- ' . $this->escape(base_url('faq')); return $this->reply(implode("\n", $replyText)); } /** * Обрабатывает команду /stats * * @return bool * @throws Exception */ protected function processStatsCommand(): bool { $this->api->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing'); $stats = new StatisticsService()->get(); $replyText[] = '📊 *Статистика*'; $replyText[] = ''; $replyText[] = '*Список изменён:* ' . $this->escape(ini()->updatedAt()); $replyText[] = ''; $replyText[] = '*Плейлистов:* ' . $stats['playlists']['all']; $replyText[] = '🟢 Онлайн \- ' . $stats['playlists']['online']; $replyText[] = '🔴 Оффлайн \- ' . $stats['playlists']['offline']; $replyText[] = '⚪ В очереди \- ' . $stats['playlists']['unknown']; $replyText[] = '🔞 Для взрослых \- ' . $stats['playlists']['adult']; $replyText[] = '⏪ С перемоткой \- ' . $stats['playlists']['hasCatchup']; $replyText[] = '🗞️ С телепрограммой \- ' . $stats['playlists']['hasTvg']; $replyText[] = '🗂️ С группировкой каналов \- ' . $stats['playlists']['groupped']; $replyText[] = ''; $replyText[] = '*Каналов:* ' . $stats['channels']['all']; $replyText[] = '🟢 Онлайн \- ' . $stats['channels']['online']; $replyText[] = '🔴 Оффлайн \- ' . $stats['channels']['offline']; $replyText[] = '🔞 Для взрослых \- ' . $stats['channels']['adult']; $replyText[] = ''; $replyText[] = '*Самая свежая проверка* '; $replyText[] = '🕔 ' . $this->escape($stats['playlists']['latest']['timeFmt']); $replyText[] = $this->escape(base_url($stats['playlists']['latest']['code'] . '/details')); $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->api->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, ); } }