feat(bot): add bot commands
This commit is contained in:
@@ -11,7 +11,7 @@ use Openguru\OpenCartFramework\Config\Settings;
|
|||||||
use Openguru\OpenCartFramework\Logger\LoggerInterface;
|
use Openguru\OpenCartFramework\Logger\LoggerInterface;
|
||||||
use Openguru\OpenCartFramework\Router\Router;
|
use Openguru\OpenCartFramework\Router\Router;
|
||||||
use Openguru\OpenCartFramework\Support\Arr;
|
use Openguru\OpenCartFramework\Support\Arr;
|
||||||
use Openguru\OpenCartFramework\Telegram\TelegramClientException;
|
use Openguru\OpenCartFramework\Telegram\Exceptions\TelegramClientException;
|
||||||
use Openguru\OpenCartFramework\Telegram\TelegramService;
|
use Openguru\OpenCartFramework\Telegram\TelegramService;
|
||||||
|
|
||||||
class BotTokenConfigurator
|
class BotTokenConfigurator
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace Openguru\OpenCartFramework\Cache;
|
namespace Openguru\OpenCartFramework\Cache;
|
||||||
|
|
||||||
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
|
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
|
||||||
use Openguru\BulkProducts\Modules\Shared\Cache\CacheInterface;
|
|
||||||
|
|
||||||
class DatabaseCache implements CacheInterface
|
class DatabaseCache implements CacheInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Openguru\OpenCartFramework\Http;
|
|||||||
use Openguru\OpenCartFramework\Support\Arr;
|
use Openguru\OpenCartFramework\Support\Arr;
|
||||||
use Openguru\OpenCartFramework\Support\Utils;
|
use Openguru\OpenCartFramework\Support\Utils;
|
||||||
|
|
||||||
class Request
|
final class Request
|
||||||
{
|
{
|
||||||
private $query;
|
private $query;
|
||||||
private $request;
|
private $request;
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Openguru\OpenCartFramework\Telegram\Commands;
|
||||||
|
|
||||||
|
class ChatIdCommand extends TelegramCommand
|
||||||
|
{
|
||||||
|
public function handle(array $update): void
|
||||||
|
{
|
||||||
|
$chatId = $update['message']['chat']['id'];
|
||||||
|
|
||||||
|
$message = sprintf('Chat ID: %s', $chatId);
|
||||||
|
|
||||||
|
$this->telegram->sendMessage($chatId, $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Openguru\OpenCartFramework\Telegram\Commands;
|
||||||
|
|
||||||
|
use Openguru\OpenCartFramework\Telegram\Contracts\TelegramCommandInterface;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\TelegramBotStateManager;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\TelegramService;
|
||||||
|
|
||||||
|
abstract class TelegramCommand implements TelegramCommandInterface
|
||||||
|
{
|
||||||
|
protected TelegramService $telegram;
|
||||||
|
protected TelegramBotStateManager $state;
|
||||||
|
|
||||||
|
public function __construct(TelegramService $telegram, TelegramBotStateManager $stateManager)
|
||||||
|
{
|
||||||
|
$this->telegram = $telegram;
|
||||||
|
$this->state = $stateManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Openguru\OpenCartFramework\Telegram\Contracts;
|
||||||
|
|
||||||
|
interface TelegramCommandInterface
|
||||||
|
{
|
||||||
|
public function handle(array $update): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Openguru\OpenCartFramework\Telegram\Enums;
|
||||||
|
|
||||||
|
class ChatAction
|
||||||
|
{
|
||||||
|
public const TYPING = 'typing';
|
||||||
|
public const UPLOAD_PHOTO = 'upload_photo';
|
||||||
|
public const RECORD_VIDEO = 'record_video';
|
||||||
|
public const UPLOAD_VIDEO = 'upload_video';
|
||||||
|
public const RECORD_VOICE = 'record_voice';
|
||||||
|
public const UPLOAD_VOICE = 'upload_voice';
|
||||||
|
public const UPLOAD_DOCUMENT = 'upload_document';
|
||||||
|
public const CHOOSE_STICKER = 'choose_sticker';
|
||||||
|
public const FIND_LOCATION = 'find_location';
|
||||||
|
public const RECORD_VIDEO_NOTE = 'record_video_note';
|
||||||
|
public const UPLOAD_VIDEO_NOTE = 'upload_video_note';
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Openguru\OpenCartFramework\Telegram;
|
namespace Openguru\OpenCartFramework\Telegram\Exceptions;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Openguru\OpenCartFramework\Telegram\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class TelegramCommandNotFoundException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct($command = "", $code = 0, Throwable $previous = null)
|
||||||
|
{
|
||||||
|
$message = "Telegram command `$command` not found";
|
||||||
|
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Openguru\OpenCartFramework\Telegram;
|
namespace Openguru\OpenCartFramework\Telegram\Exceptions;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Openguru\OpenCartFramework\Telegram;
|
namespace Openguru\OpenCartFramework\Telegram;
|
||||||
|
|
||||||
use Openguru\OpenCartFramework\Http\Request;
|
use Openguru\OpenCartFramework\Http\Request;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\Exceptions\TelegramInvalidSignatureException;
|
||||||
|
|
||||||
class SignatureValidator
|
class SignatureValidator
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Openguru\OpenCartFramework\Telegram;
|
||||||
|
|
||||||
|
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
||||||
|
|
||||||
|
class TelegramBotStateManager
|
||||||
|
{
|
||||||
|
private CacheInterface $cache;
|
||||||
|
|
||||||
|
public function __construct(CacheInterface $cache)
|
||||||
|
{
|
||||||
|
$this->cache = $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStateKey(string $userId, string $chatId): string
|
||||||
|
{
|
||||||
|
return md5('tg-state-' . $userId . '-' . $chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setState(string $handler, string $userId, string $chatId, array $data = []): void
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'handler' => $handler,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'chat_id' => $chatId,
|
||||||
|
'data' => $data,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->cache->set($this->getStateKey($userId, $chatId), $payload, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getState(string $userId, string $chatId): ?array
|
||||||
|
{
|
||||||
|
return $this->cache->get($this->getStateKey($userId, $chatId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasState(string $userId, string $chatId): bool
|
||||||
|
{
|
||||||
|
return ! empty($this->cache->get($this->getStateKey($userId, $chatId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearState(string $userId, string $chatId): void
|
||||||
|
{
|
||||||
|
$this->cache->delete($this->getStateKey($userId, $chatId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentStateCommandHandler(string $userId, string $chatId): string
|
||||||
|
{
|
||||||
|
$state = $this->getState($userId, $chatId);
|
||||||
|
|
||||||
|
return $state['handler'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Openguru\OpenCartFramework\Telegram;
|
||||||
|
|
||||||
|
use Openguru\OpenCartFramework\Telegram\Exceptions\TelegramCommandNotFoundException;
|
||||||
|
|
||||||
|
class TelegramCommandsRegistry
|
||||||
|
{
|
||||||
|
private array $commands = [];
|
||||||
|
|
||||||
|
public function addCommand(string $command, string $handler, ?string $description = null): void
|
||||||
|
{
|
||||||
|
$this->commands[$command] = [
|
||||||
|
'handler' => $handler,
|
||||||
|
'description' => $description,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $command): string
|
||||||
|
{
|
||||||
|
if (! array_key_exists($command, $this->commands)) {
|
||||||
|
throw new TelegramCommandNotFoundException($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->commands[$command]['handler'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ use GuzzleHttp\Client;
|
|||||||
use GuzzleHttp\Exception\ClientException;
|
use GuzzleHttp\Exception\ClientException;
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
use Openguru\OpenCartFramework\Support\Arr;
|
use Openguru\OpenCartFramework\Support\Arr;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\Enums\ChatAction;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\Exceptions\TelegramClientException;
|
||||||
|
|
||||||
class TelegramService
|
class TelegramService
|
||||||
{
|
{
|
||||||
@@ -22,6 +24,7 @@ class TelegramService
|
|||||||
foreach ($specials as $char) {
|
foreach ($specials as $char) {
|
||||||
$text = str_replace($char, '\\' . $char, $text);
|
$text = str_replace($char, '\\' . $char, $text);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,23 +35,35 @@ class TelegramService
|
|||||||
return str_replace(array_keys($variables), $values, $template);
|
return str_replace(array_keys($variables), $values, $template);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sendMessage(int $chatId, string $text): void
|
/**
|
||||||
{
|
* @throws TelegramClientException
|
||||||
|
* @throws GuzzleException
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
|
public function sendMessage(
|
||||||
|
int $chatId,
|
||||||
|
string $text,
|
||||||
|
array $replyMarkup = [],
|
||||||
|
string $chatAction = ChatAction::TYPING,
|
||||||
|
string $parseMode = 'MarkdownV2'
|
||||||
|
): void {
|
||||||
if (! $this->botToken) {
|
if (! $this->botToken) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$client = $this->createGuzzleClient("https://api.telegram.org/bot{$this->botToken}/");
|
$this->sendChatAction($chatId, $chatAction);
|
||||||
|
|
||||||
$query = [
|
$params = [
|
||||||
'chat_id' => $chatId,
|
'chat_id' => $chatId,
|
||||||
'text' => $text,
|
'text' => $text,
|
||||||
'parse_mode' => 'MarkdownV2',
|
'parse_mode' => $parseMode,
|
||||||
];
|
];
|
||||||
|
|
||||||
$client->get('sendMessage', [
|
if ($replyMarkup) {
|
||||||
'query' => $query,
|
$params['reply_markup'] = json_encode($replyMarkup, JSON_THROW_ON_ERROR);
|
||||||
]);
|
}
|
||||||
|
|
||||||
|
$this->exec('sendMessage', $params);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createGuzzleClient(string $uri): Client
|
private function createGuzzleClient(string $uri): Client
|
||||||
@@ -113,4 +128,54 @@ class TelegramService
|
|||||||
|
|
||||||
return Arr::get($webhookInfo, 'result.url');
|
return Arr::get($webhookInfo, 'result.url');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sendChatAction(int $chatId, string $action): void
|
||||||
|
{
|
||||||
|
$this->exec('sendChatAction', [
|
||||||
|
'chat_id' => $chatId,
|
||||||
|
'action' => $action,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function escapeTgSpecialCharacters(string $text): string
|
||||||
|
{
|
||||||
|
// Набор спецсимволов для MarkdownV2
|
||||||
|
$specials = '_*[]()~`>#+-=|{}.!';
|
||||||
|
|
||||||
|
$out = '';
|
||||||
|
$len = strlen($text); // работаем побайтно: спецсимволы — ASCII
|
||||||
|
$prevWasBackslash = false;
|
||||||
|
|
||||||
|
for ($i = 0; $i < $len; $i++) {
|
||||||
|
$ch = $text[$i];
|
||||||
|
|
||||||
|
if ($prevWasBackslash) {
|
||||||
|
// Предыдущий был "\", этот символ считаем уже экранированным — просто добавим как есть
|
||||||
|
$out .= $ch;
|
||||||
|
$prevWasBackslash = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ch === '\\') {
|
||||||
|
// Запоминаем, что встретили слеш; пока не знаем, что дальше
|
||||||
|
$out .= '\\';
|
||||||
|
$prevWasBackslash = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если текущий символ — спецсимвол и он НЕ экранирован (мы уже знаем, что предыдущий не "\")
|
||||||
|
if (strpos($specials, $ch) !== false) {
|
||||||
|
$out .= '\\' . $ch;
|
||||||
|
} else {
|
||||||
|
$out .= $ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если строка закончилась на одиночный "\" — довэкранируем его
|
||||||
|
if ($prevWasBackslash) {
|
||||||
|
$out .= '\\';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ class ErrorBag
|
|||||||
$this->errors[$field][] = $message;
|
$this->errors[$field][] = $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function first(): array
|
public function first(): ?array
|
||||||
{
|
{
|
||||||
foreach ($this->errors as $error) {
|
foreach ($this->errors as $error) {
|
||||||
return $error;
|
return $error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function firstOfAll(): array
|
public function firstOfAll(): array
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class Validator implements ValidatorInterface
|
|||||||
private ErrorBag $errors;
|
private ErrorBag $errors;
|
||||||
private array $customMessages;
|
private array $customMessages;
|
||||||
private array $fieldNames;
|
private array $fieldNames;
|
||||||
|
private array $validationRules;
|
||||||
|
|
||||||
public function __construct(array $validationRules = [], array $customMessages = [])
|
public function __construct(array $validationRules = [], array $customMessages = [])
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace App\Exceptions;
|
|||||||
use Openguru\OpenCartFramework\Contracts\ExceptionHandlerInterface;
|
use Openguru\OpenCartFramework\Contracts\ExceptionHandlerInterface;
|
||||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||||
use Openguru\OpenCartFramework\Http\Response;
|
use Openguru\OpenCartFramework\Http\Response;
|
||||||
use Openguru\OpenCartFramework\Telegram\TelegramInvalidSignatureException;
|
use Openguru\OpenCartFramework\Telegram\Exceptions\TelegramInvalidSignatureException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class CustomExceptionHandler implements ExceptionHandlerInterface
|
class CustomExceptionHandler implements ExceptionHandlerInterface
|
||||||
|
|||||||
@@ -2,25 +2,90 @@
|
|||||||
|
|
||||||
namespace App\Handlers;
|
namespace App\Handlers;
|
||||||
|
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use Mockery\Exception;
|
||||||
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
||||||
|
use Openguru\OpenCartFramework\Container\Container;
|
||||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||||
use Openguru\OpenCartFramework\Http\Request;
|
use Openguru\OpenCartFramework\Http\Request;
|
||||||
|
use Openguru\OpenCartFramework\Logger\LoggerInterface;
|
||||||
use Openguru\OpenCartFramework\Support\Arr;
|
use Openguru\OpenCartFramework\Support\Arr;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\Contracts\TelegramCommandInterface;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\Exceptions\TelegramCommandNotFoundException;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\TelegramBotStateManager;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\TelegramCommandsRegistry;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\TelegramService;
|
||||||
|
|
||||||
class TelegramHandler
|
class TelegramHandler
|
||||||
{
|
{
|
||||||
private CacheInterface $cache;
|
private CacheInterface $cache;
|
||||||
|
private TelegramCommandsRegistry $telegramCommandsRegistry;
|
||||||
|
private Container $container;
|
||||||
|
private TelegramBotStateManager $telegramBotStateManager;
|
||||||
|
private LoggerInterface $logger;
|
||||||
|
private TelegramService $telegramService;
|
||||||
|
|
||||||
public function __construct(CacheInterface $cache)
|
public function __construct(
|
||||||
{
|
CacheInterface $cache,
|
||||||
|
TelegramCommandsRegistry $telegramCommandsRegistry,
|
||||||
|
Container $container,
|
||||||
|
TelegramBotStateManager $telegramBotStateManager,
|
||||||
|
LoggerInterface $logger,
|
||||||
|
TelegramService $telegramService
|
||||||
|
) {
|
||||||
$this->cache = $cache;
|
$this->cache = $cache;
|
||||||
|
$this->telegramCommandsRegistry = $telegramCommandsRegistry;
|
||||||
|
$this->container = $container;
|
||||||
|
$this->telegramBotStateManager = $telegramBotStateManager;
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->telegramService = $telegramService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws GuzzleException
|
||||||
|
* @throws \JsonException
|
||||||
|
*/
|
||||||
public function webhook(Request $request): JsonResponse
|
public function webhook(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$message = Arr::get($request->json(), 'message', []);
|
$update = $request->json();
|
||||||
|
$userId = $update['message']['from']['id'];
|
||||||
|
$chatId = $update['message']['chat']['id'];
|
||||||
|
|
||||||
$this->cache->set('tg_latest_msg', $message, 60);
|
try {
|
||||||
|
$message = Arr::get($update, 'message', []);
|
||||||
|
|
||||||
|
$this->cache->set('tg_latest_msg', $message, 60);
|
||||||
|
|
||||||
|
$text = Arr::get($message, 'text', '');
|
||||||
|
|
||||||
|
// command starts from "/"
|
||||||
|
if (strpos($text, '/') === 0) {
|
||||||
|
$this->telegramBotStateManager->clearState($userId, $chatId);
|
||||||
|
$command = substr($text, 1);
|
||||||
|
$handler = $this->telegramCommandsRegistry->resolve($command);
|
||||||
|
|
||||||
|
/** @var TelegramCommandInterface $concrete */
|
||||||
|
$concrete = $this->container->get($handler);
|
||||||
|
$concrete->handle($update);
|
||||||
|
|
||||||
|
return new JsonResponse([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue state
|
||||||
|
$hasState = $this->telegramBotStateManager->hasState($userId, $chatId);
|
||||||
|
if ($hasState) {
|
||||||
|
$handler = $this->telegramBotStateManager->getCurrentStateCommandHandler($userId, $chatId);
|
||||||
|
/** @var TelegramCommandInterface $concrete */
|
||||||
|
$concrete = $this->container->get($handler);
|
||||||
|
$concrete->handle($update);
|
||||||
|
|
||||||
|
return new JsonResponse([]);
|
||||||
|
}
|
||||||
|
} catch (TelegramCommandNotFoundException $exception) {
|
||||||
|
$this->telegramService->sendMessage($chatId, 'Неверная команда');
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
$this->logger->logException($exception);
|
||||||
|
}
|
||||||
|
|
||||||
return new JsonResponse([]);
|
return new JsonResponse([]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
namespace App\ServiceProviders;
|
namespace App\ServiceProviders;
|
||||||
|
|
||||||
use App\Exceptions\CustomExceptionHandler;
|
use App\Exceptions\CustomExceptionHandler;
|
||||||
|
use App\Telegram\LinkCommand;
|
||||||
use Openguru\OpenCartFramework\Container\ServiceProvider;
|
use Openguru\OpenCartFramework\Container\ServiceProvider;
|
||||||
use Openguru\OpenCartFramework\Contracts\ExceptionHandlerInterface;
|
use Openguru\OpenCartFramework\Contracts\ExceptionHandlerInterface;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\Commands\ChatIdCommand;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\TelegramCommandsRegistry;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -13,5 +16,19 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$this->container->singleton(ExceptionHandlerInterface::class, function () {
|
$this->container->singleton(ExceptionHandlerInterface::class, function () {
|
||||||
return new CustomExceptionHandler();
|
return new CustomExceptionHandler();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->registerTelegramCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function registerTelegramCommands(): void
|
||||||
|
{
|
||||||
|
$this->container->singleton(TelegramCommandsRegistry::class, function () {
|
||||||
|
return new TelegramCommandsRegistry();
|
||||||
|
});
|
||||||
|
|
||||||
|
$registry = $this->container->get(TelegramCommandsRegistry::class);
|
||||||
|
|
||||||
|
$registry->addCommand('id', ChatIdCommand::class, 'Возвращает ChatID текущего чата.');
|
||||||
|
$registry->addCommand('link', LinkCommand::class, 'Генератор Telegram сообщений с кнопкой');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Telegram;
|
||||||
|
|
||||||
|
use GuzzleHttp\Exception\ClientException;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use JsonException;
|
||||||
|
use Openguru\OpenCartFramework\Logger\LoggerInterface;
|
||||||
|
use Openguru\OpenCartFramework\Support\Arr;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\Commands\TelegramCommand;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\Enums\ChatAction;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\TelegramBotStateManager;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\TelegramService;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class LinkCommand extends TelegramCommand
|
||||||
|
{
|
||||||
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
TelegramService $telegram,
|
||||||
|
TelegramBotStateManager $stateManager,
|
||||||
|
LoggerInterface $logger
|
||||||
|
) {
|
||||||
|
parent::__construct($telegram, $stateManager);
|
||||||
|
$this->logger = $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws GuzzleException
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
public function handle(array $update): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$userId = $update['message']['from']['id'];
|
||||||
|
$chatId = $update['message']['chat']['id'];
|
||||||
|
|
||||||
|
$state = $this->state->getState($userId, $chatId);
|
||||||
|
|
||||||
|
if (! $state) {
|
||||||
|
$greeting = $this->telegram->escapeTgSpecialCharacters(
|
||||||
|
<<<MARKDOWN
|
||||||
|
Это удобный инструмент, который поможет вам 📎 создать красивое сообщение с кнопкой для открытия вашего 🛒 Telecart магазина.
|
||||||
|
|
||||||
|
📌 Такое сообщение можно закрепить в канале или группе.
|
||||||
|
📤 Переслать клиентам в личные сообщения.
|
||||||
|
🚀 Или использовать повторно, когда нужно поделиться магазином.
|
||||||
|
|
||||||
|
Давайте начнём — отправьте текст, который вы хотите разместить в сообщении 👇
|
||||||
|
MARKDOWN
|
||||||
|
);
|
||||||
|
$this->telegram->sendMessage($chatId, $greeting);
|
||||||
|
|
||||||
|
$this->state->setState(self::class, $userId, $chatId, [
|
||||||
|
'step' => 'message_text',
|
||||||
|
'data' => [
|
||||||
|
'message_text' => '',
|
||||||
|
'btn_text' => '',
|
||||||
|
'btn_link' => '',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$step = $state['data']['step'];
|
||||||
|
|
||||||
|
if ($step === 'message_text') {
|
||||||
|
$message = $update['message']['text'];
|
||||||
|
$state['data']['data']['message_text'] = $message;
|
||||||
|
$state['data']['step'] = 'btn_text';
|
||||||
|
$this->state->setState(self::class, $userId, $chatId, $state['data']);
|
||||||
|
|
||||||
|
$text = <<<MARKDOWN
|
||||||
|
🔸 Отлично\!
|
||||||
|
Теперь укажите, какой текст будет на кнопке 👇
|
||||||
|
|
||||||
|
✍️ Напишите короткую, понятную фразу, например:
|
||||||
|
• `Открыть магазин`
|
||||||
|
• `Каталог товаров`
|
||||||
|
• `Начать покупки`
|
||||||
|
MARKDOWN;
|
||||||
|
$this->telegram->sendMessage($chatId, $text);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($step === 'btn_text') {
|
||||||
|
$message = $update['message']['text'];
|
||||||
|
$state['data']['data']['btn_text'] = $message;
|
||||||
|
$state['data']['step'] = 'btn_link';
|
||||||
|
$this->state->setState(self::class, $userId, $chatId, $state['data']);
|
||||||
|
|
||||||
|
$template = <<<MARKDOWN
|
||||||
|
🌐 Теперь отправьте *ссылку на Telegram Mini App*\.
|
||||||
|
Ссылка должна начинаться с `https://`
|
||||||
|
|
||||||
|
📎 Инструкция, где взять ссылку:
|
||||||
|
👉 {LINK}
|
||||||
|
MARKDOWN;
|
||||||
|
|
||||||
|
$text = $this->telegram->prepareMessage($template, [
|
||||||
|
'{LINK}' => 'https://telecart-labs.github.io/docs/telegram/telegram/#direct-link',
|
||||||
|
]);
|
||||||
|
$this->telegram->sendMessage($chatId, $text);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($step === 'btn_link') {
|
||||||
|
$message = $update['message']['text'];
|
||||||
|
$state['data']['data']['btn_link'] = $message;
|
||||||
|
$this->state->setState(self::class, $userId, $chatId, $state['data']);
|
||||||
|
|
||||||
|
$messageText = Arr::get($state, 'data.data.message_text', 'Текст сообщения');
|
||||||
|
$btnText = $this->telegram->escapeTgSpecialCharacters(
|
||||||
|
Arr::get($state, 'data.data.btn_text', 'Открыть магазин')
|
||||||
|
);
|
||||||
|
$btnLink = $message;
|
||||||
|
|
||||||
|
$replyMarkup = [
|
||||||
|
'inline_keyboard' => [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'text' => $btnText,
|
||||||
|
'url' => $btnLink,
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->telegram->sendMessage(
|
||||||
|
$chatId,
|
||||||
|
$this->telegram->escapeTgSpecialCharacters($messageText),
|
||||||
|
$replyMarkup,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state->clearState($userId, $chatId);
|
||||||
|
} catch (ClientException $exception) {
|
||||||
|
$this->telegram->sendMessage($chatId, 'Ошибка: ' . $exception->getResponse()->getBody()->getContents());
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$this->logger->logException($exception);
|
||||||
|
$this->telegram->sendMessage($chatId, 'Произошла ошибка');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Telegram;
|
||||||
|
|
||||||
|
use Openguru\OpenCartFramework\Telegram\TelegramService;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class TelegramServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private TelegramService $service;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->service = new TelegramService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDoesNotEscapeNormalCharacters(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('hello world', $this->service->escapeTgSpecialCharacters('hello world'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEscapesSingleSpecialCharacters(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('\_', $this->service->escapeTgSpecialCharacters('_'));
|
||||||
|
$this->assertEquals('\#', $this->service->escapeTgSpecialCharacters('#'));
|
||||||
|
$this->assertEquals('\+', $this->service->escapeTgSpecialCharacters('+'));
|
||||||
|
$this->assertEquals('\(', $this->service->escapeTgSpecialCharacters('('));
|
||||||
|
$this->assertEquals('\)', $this->service->escapeTgSpecialCharacters(')'));
|
||||||
|
$this->assertEquals('\!', $this->service->escapeTgSpecialCharacters('!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDoesNotDoubleEscapeAlreadyEscaped(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('\#', $this->service->escapeTgSpecialCharacters('\#'));
|
||||||
|
$this->assertEquals('\\\\', $this->service->escapeTgSpecialCharacters('\\\\')); // двойной бэкслеш остаётся двойным
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEscapesInsideText(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('price is 100\#', $this->service->escapeTgSpecialCharacters('price is 100#'));
|
||||||
|
$this->assertEquals('a\(b\)c', $this->service->escapeTgSpecialCharacters('a(b)c'));
|
||||||
|
$this->assertEquals('a\+b', $this->service->escapeTgSpecialCharacters('a+b'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEscapesBackslashAtEnd(): void
|
||||||
|
{
|
||||||
|
// висячий бэкслеш должен быть продублирован
|
||||||
|
$this->assertEquals('backslash\\\\', $this->service->escapeTgSpecialCharacters('backslash\\'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDoesNotEscapeEscapedSpecialCharacter(): void
|
||||||
|
{
|
||||||
|
// \# должен остаться \#
|
||||||
|
$this->assertEquals('\#tag', $this->service->escapeTgSpecialCharacters('\#tag'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMultipleSpecialCharactersInRow(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('\#\+\=', $this->service->escapeTgSpecialCharacters('#+='));
|
||||||
|
$this->assertEquals('\#\+\=text', $this->service->escapeTgSpecialCharacters('#+=text'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmojiAndMultibyteCharactersAreUntouched(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('Привет 👋', $this->service->escapeTgSpecialCharacters('Привет 👋'));
|
||||||
|
$this->assertEquals('emoji\#', $this->service->escapeTgSpecialCharacters('emoji#'));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user