Merge branch 'tg_webhook'

This commit is contained in:
2025-08-16 21:54:34 +03:00
27 changed files with 548 additions and 125 deletions

View File

@@ -1,5 +1,20 @@
<?php <?php
use Bastion\ApplicationFactory;
use Openguru\OpenCartFramework\Logger\OpenCartLogAdapter;
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
use Openguru\OpenCartFramework\OpenCart\OcConfigDecorator;
$sysLibPath = rtrim(DIR_SYSTEM, '/') . '/library/oc_telegram_shop';
$basePath = rtrim(DIR_APPLICATION, '/') . '/..';
if (is_readable($sysLibPath . '/oc_telegram_shop.phar')) {
require_once "phar://{$sysLibPath}/oc_telegram_shop.phar/vendor/autoload.php";
} elseif (is_dir("$basePath/oc_telegram_shop")) {
require_once "$basePath/oc_telegram_shop/vendor/autoload.php";
} else {
throw new RuntimeException('Unable to locate application directory.');
}
/** /**
* @property Document $document * @property Document $document
* @property Loader $load * @property Loader $load
@@ -163,6 +178,30 @@ class ControllerExtensionModuleTgshop extends Controller
$this->response->setOutput($this->load->view('extension/module/tgshop_init', $data)); $this->response->setOutput($this->load->view('extension/module/tgshop_init', $data));
} }
public function handle(): void
{
$app = ApplicationFactory::create([
'base_url' => HTTPS_SERVER,
'public_url' => HTTPS_CATALOG,
'telegram' => [
'bot_token' => $this->config->get('module_tgshop_bot_token'),
'chat_id' => $this->config->get('module_tgshop_chat_id'),
'owner_notification_template' => $this->config->get('module_tgshop_owner_notification_template'),
'customer_notification_template' => $this->config->get('module_tgshop_customer_notification_template'),
],
'logs' => [
'path' => DIR_LOGS,
],
]);
$app->bind(OcRegistryDecorator::class, fn () => new OcRegistryDecorator($this->registry));
$app
->withLogger(fn () => new OpenCartLogAdapter($this->log, 'TeleCartAdmin'))
->bootAndHandleRequest();
}
protected function validate(): bool protected function validate(): bool
{ {
if (! $this->user->hasPermission('modify', 'extension/module/tgshop')) { if (! $this->user->hasPermission('modify', 'extension/module/tgshop')) {

View File

@@ -146,73 +146,71 @@
{# ChatID #} {# ChatID #}
{% elseif item['type'] == 'chatid' %} {% elseif item['type'] == 'chatid' %}
<div class="input-group"> {% if module_tgshop_bot_token %}
<span class="input-group-btn"> <div class="input-group">
<button id="{{ settingKey }}-btn" class="btn btn-primary" type="button"> <span class="input-group-btn">
<i class="fa fa-refresh"></i> Получить Chat ID <button id="{{ settingKey }}-btn" class="btn btn-primary" type="button">
</button> <i class="fa fa-refresh"></i> Получить Chat ID
</span> </button>
<input type="text" </span>
name="{{ settingKey }}" <input type="text"
value="{{ attribute(_context, settingKey) }}" name="{{ settingKey }}"
placeholder="{{ item['placeholder'] }}" value="{{ attribute(_context, settingKey) }}"
id="{{ settingKey }}" placeholder="{{ item['placeholder'] }}"
class="form-control" id="{{ settingKey }}"
/> class="form-control"
<script> />
$('#{{ settingKey }}-btn').click(function () { <script>
const telegramToken = $('#module_tgshop_bot_token').val().trim(); // fetch from input $('#{{ settingKey }}-btn').click(function () {
if (! telegramToken) { const $resultLabel = $('#{{ settingKey }}-result-label');
alert('Сначала введите Telegram Bot Token!'); const telegramToken = $('#module_tgshop_bot_token').val().trim(); // fetch from input
return; if (! telegramToken) {
} alert('Сначала введите Telegram Bot Token!');
return;
}
fetch(`https://api.telegram.org/bot${telegramToken}/getUpdates`) fetch('/admin/index.php?route=extension/module/tgshop/handle&api_action=getChatId&user_token={{ user_token }}')
.then(res => res.json()) .then(async (res) => {
.then(data => { const data = await res.json().catch(() => null);
if (!data.ok || !data.result.length) {
alert('Не удалось получить обновления от бота. Убедитесь, что вы написали боту нужное кодовое слово.');
return;
}
// Ищем последнее сообщение с chat_id if (!res.ok) {
const lastMessage = data.result.reverse().find(update => update.message && update.message.chat); throw new Error(`Ошибка ${res.status}: ${data.message || res.statusText}`);
if (!lastMessage) { }
alert('Не найдено сообщений с chat_id.');
return;
}
if (lastMessage.message.text !== 'opencart_get_chatid') { $('#{{ settingKey }}').val(data.data.chat_id);
alert('Ошибка. Последнее сообщение у бота не содержит правильного кодового слова.'); $resultLabel
return; .text('✅ ChatID успешно получен и подставлен в поле. Не забудьте сохранить настройки!')
} .css('color', 'green');
})
.catch(err => {
console.error(err);
alert(err);
});
});
const chatId = lastMessage.message.chat.id; </script>
$('#{{ settingKey }}').val(chatId); // подставляем в поле
alert('ChatID успешно получен и подставлен в поле.')
})
.catch(err => {
console.error(err);
alert('Ошибка при получении chat_id. Проверьте токен и соединение.');
});
});
</script>
</div>
<button class="btn btn-link btn-xs" type="button" data-toggle="collapse" data-target="#{{ settingKey }}-collapse" aria-expanded="false" aria-controls="collapseExample">
Инструкция как получить ChatID.
</button>
<div class="collapse" id="{{ settingKey }}-collapse">
<div class="well">
<p class="text-primary">Как получить Chat ID</p>
<ol>
<li>Убедитесь, что Telegram Bot Token введён выше.</li>
<li>Откройте вашего бота в Telegram и отправьте ему кодовое слово: `opencart_get_chatid`. Важно отправить именно такое сообщение, иначе не сработает.</li>
<li>Вернитесь сюда и нажмите кнопку «Получить Chat ID» — скрипт автоматически подставит его в поле ниже.</li>
</ol>
</div> </div>
</div>
<div id="{{ settingKey }}-result-label"></div>
<button class="btn btn-link btn-xs" type="button" data-toggle="collapse" data-target="#{{ settingKey }}-collapse" aria-expanded="false" aria-controls="collapseExample">
Инструкция как получить ChatID.
</button>
<div class="collapse" id="{{ settingKey }}-collapse">
<div class="well">
<p class="text-primary">Как получить Chat ID</p>
<ol>
<li>Убедитесь, что Telegram Bot Token введён выше.</li>
<li>Откройте вашего бота в Telegram и отправьте ему кодовое слово: `opencart_get_chatid`. Важно отправить именно такое сообщение, иначе не сработает.</li>
<li>Вернитесь сюда и нажмите кнопку «Получить Chat ID» — скрипт автоматически подставит его в поле ниже.</li>
</ol>
</div>
</div>
{% else %}
<div class="alert alert-warning">
<strong>BotToken</strong> не указан. Пожалуйста, введите корректный BotToken и сохраните настройки. После этого здесь станет доступна настройка ChatID.
</div>
{% endif %}
{% elseif item['type'] == 'tg_message_template' %} {% elseif item['type'] == 'tg_message_template' %}
<div style="margin-bottom: 10px;"> <div style="margin-bottom: 10px;">
<textarea name="{{ settingKey }}" <textarea name="{{ settingKey }}"
@@ -340,6 +338,7 @@
const $input = $('#{{ settingKey }}'); const $input = $('#{{ settingKey }}');
const $resultLabel = $('#{{ settingKey }}-result-label'); const $resultLabel = $('#{{ settingKey }}-result-label');
const botToken = $input.val(); const botToken = $input.val();
const url = '/admin/index.php?route=extension/module/tgshop/handle&api_action=configureBotToken&user_token={{ user_token }}';
if (botToken.trim().length === 0) { if (botToken.trim().length === 0) {
$resultLabel $resultLabel
@@ -348,34 +347,46 @@
return; return;
} }
$input.attr('disabled', true); $input.attr('readonly', true);
$resultLabel.text('Проверяю...'); $resultLabel.text('Проверяю...');
$.ajax({ fetch(url, {
url: `https://api.telegram.org/bot${botToken}/getMe`, method: 'POST',
method: 'GET', headers: {
dataType: 'json', 'Content-Type': 'application/json',
success: function (resp) { },
if (resp.ok) { body: JSON.stringify({ botToken }),
const user = resp.result; })
.then(async (res) => {
const response = await res.json().catch(() => null);
if (res.status === 422) {
console.error(res, response);
$resultLabel $resultLabel
.text(`✅ Бот: @${user.username} (id: ${user.id})`) .text(`❌ Ошибка: ${response.error}`)
.css('color', 'green');
} else {
$resultLabel
.text(`❌ Ошибка: ${resp.description || 'неверный токен'}`)
.css('color', 'red'); .css('color', 'red');
return;
} }
},
error: function (xhr) { if (!res.ok) {
throw new Error(`Ошибка ${response.error || res.statusText}`);
}
if (! response.id) {
throw new Error(`bot token is not found in server response.`);
}
$resultLabel $resultLabel
.text(`❌ Ошибка соединения (${xhr.status})`) .text(`✅ Бот: @${response.username} (id: ${response.id}) webhook: ${response.webhook_url}`)
.css('color', 'green');
})
.catch(err => {
console.error(err);
$resultLabel
.text(`❌ Ошибка проверки BotToken.`)
.css('color', 'red'); .css('color', 'red');
}, })
complete: function () { .finally(() => $input.attr('readonly', false))
$input.attr('disabled', false);
}
});
} }
</script> </script>

View File

@@ -2,13 +2,13 @@
use App\Adapters\OcModelCatalogProductAdapter; use App\Adapters\OcModelCatalogProductAdapter;
use App\ApplicationFactory; use App\ApplicationFactory;
use App\Decorators\OcRegistryDecorator;
use Cart\Cart; use Cart\Cart;
use Cart\Currency; use Cart\Currency;
use Cart\Tax; use Cart\Tax;
use Openguru\OpenCartFramework\ImageTool\ImageTool; use Openguru\OpenCartFramework\ImageTool\ImageTool;
use Openguru\OpenCartFramework\ImageTool\ImageToolInterface; use Openguru\OpenCartFramework\ImageTool\ImageToolInterface;
use Openguru\OpenCartFramework\Logger\OpenCartLogAdapter; use Openguru\OpenCartFramework\Logger\OpenCartLogAdapter;
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
$sysLibPath = rtrim(DIR_SYSTEM, '/') . '/library/oc_telegram_shop'; $sysLibPath = rtrim(DIR_SYSTEM, '/') . '/library/oc_telegram_shop';
$basePath = rtrim(DIR_APPLICATION, '/') . '/..'; $basePath = rtrim(DIR_APPLICATION, '/') . '/..';
@@ -17,7 +17,7 @@ if (is_readable($sysLibPath . '/oc_telegram_shop.phar')) {
} elseif (is_dir("$basePath/oc_telegram_shop")) { } elseif (is_dir("$basePath/oc_telegram_shop")) {
require_once "$basePath/oc_telegram_shop/vendor/autoload.php"; require_once "$basePath/oc_telegram_shop/vendor/autoload.php";
} else { } else {
throw new RuntimeException('Unable to locate bulk products directory.'); throw new RuntimeException('Unable to locate application directory.');
} }
/** /**
@@ -52,6 +52,7 @@ class Controllerextensiontgshophandle extends Controller
'theme_dark' => $this->config->get('module_tgshop_theme_dark'), 'theme_dark' => $this->config->get('module_tgshop_theme_dark'),
'mainpage_products' => $this->config->get('module_tgshop_mainpage_products'), 'mainpage_products' => $this->config->get('module_tgshop_mainpage_products'),
'featured_products' => $this->config->get('module_tgshop_featured_products'), 'featured_products' => $this->config->get('module_tgshop_featured_products'),
'base_url' => HTTPS_SERVER,
'ya_metrika_enabled' => ! empty(trim($this->config->get('module_tgshop_yandex_metrika'))), 'ya_metrika_enabled' => ! empty(trim($this->config->get('module_tgshop_yandex_metrika'))),
'telegram' => [ 'telegram' => [
'bot_token' => $this->config->get('module_tgshop_bot_token'), 'bot_token' => $this->config->get('module_tgshop_bot_token'),

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Bastion;
use App\ServiceProviders\AppServiceProvider;
use Openguru\OpenCartFramework\Application;
use Openguru\OpenCartFramework\Cache\CacheServiceProvider;
use Openguru\OpenCartFramework\QueryBuilder\QueryBuilderServiceProvider;
use Openguru\OpenCartFramework\Router\RouteServiceProvider;
use Openguru\OpenCartFramework\Support\Arr;
use Openguru\OpenCartFramework\Telegram\TelegramServiceProvider;
class ApplicationFactory
{
public static function create(array $config): Application
{
$defaultConfig = require __DIR__ . '/config.php';
$routes = require __DIR__ . '/routes.php';
return (new Application(Arr::mergeArraysRecursively($defaultConfig, $config)))
->withRoutes(fn () => $routes)
->withServiceProviders([
QueryBuilderServiceProvider::class,
RouteServiceProvider::class,
AppServiceProvider::class,
CacheServiceProvider::class,
TelegramServiceProvider::class,
]);
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Bastion\Exceptions;
use Exception;
class BotTokenConfiguratorException extends Exception
{
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Bastion\Handlers;
use Bastion\Exceptions\BotTokenConfiguratorException;
use Bastion\Services\BotTokenConfigurator;
use Exception;
use Openguru\OpenCartFramework\Http\JsonResponse;
use Openguru\OpenCartFramework\Http\Request;
use Openguru\OpenCartFramework\Http\Response;
class SettingsHandler
{
private BotTokenConfigurator $botTokenConfigurator;
public function __construct(BotTokenConfigurator $botTokenConfigurator)
{
$this->botTokenConfigurator = $botTokenConfigurator;
}
public function configureBotToken(Request $request): JsonResponse
{
try {
$data = $this->botTokenConfigurator->configure(trim($request->json('botToken', '')));
return new JsonResponse($data);
} catch (BotTokenConfiguratorException $e) {
return new JsonResponse(['error' => $e->getMessage()], Response::HTTP_UNPROCESSABLE_ENTITY);
} catch (Exception $e) {
return new JsonResponse(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Bastion\Handlers;
use Exception;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Http\JsonResponse;
use Openguru\OpenCartFramework\Http\Response;
use Openguru\OpenCartFramework\Router\Router;
use Openguru\OpenCartFramework\Support\Arr;
use Openguru\OpenCartFramework\Telegram\TelegramService;
use RuntimeException;
class TelegramHandler
{
private CacheInterface $cache;
public function __construct(CacheInterface $cache)
{
$this->cache = $cache;
}
public function getChatId(): JsonResponse
{
$message = $this->cache->get('tg_latest_msg');
if (! $message) {
return new JsonResponse([
'message' => 'Сообщение не найдено. Убедитесь что отправили кодовое слово в чат с ботом и повторите через 10 секунд. У Вас есть 60 секунд после отправки сообщения в чат, чтобы нажать на кнопку! Это сделано в целях безопасности.'
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$chatId = Arr::get($message, 'chat.id');
if (! $chatId) {
return new JsonResponse([
'message' => 'ChatID не найден. Убедитесь что отправили кодовое слово в чат с ботом и повторите через 10 секунд.'
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
return new JsonResponse([
'data' => [
'chat_id' => $chatId,
],
]);
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Bastion\Services;
use Bastion\Exceptions\BotTokenConfiguratorException;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Logger\LoggerInterface;
use Openguru\OpenCartFramework\Router\Router;
use Openguru\OpenCartFramework\Support\Arr;
use Openguru\OpenCartFramework\Telegram\TelegramClientException;
use Openguru\OpenCartFramework\Telegram\TelegramService;
class BotTokenConfigurator
{
private TelegramService $telegramService;
private Settings $settings;
private Router $router;
private LoggerInterface $logger;
public function __construct(
TelegramService $telegramService,
Settings $settings,
Router $router,
LoggerInterface $logger
) {
$this->telegramService = $telegramService;
$this->settings = $settings;
$this->router = $router;
$this->logger = $logger;
}
/**
* @throws BotTokenConfiguratorException
*/
public function configure(string $botToken): array
{
$this->telegramService->setBotToken($botToken);
try {
$me = $this->telegramService->exec('getMe');
$webhookUrl = $this->telegramService->getWebhookUrl();
if (! $webhookUrl) {
$this->telegramService->exec('setWebhook', [
'url' => $this->getWebhookUrl(),
]);
}
$webhookUrl = $this->telegramService->getWebhookUrl();
return [
'first_name' => Arr::get($me, 'result.first_name'),
'username' => Arr::get($me, 'result.username'),
'id' => Arr::get($me, 'result.id'),
'webhook_url' => $webhookUrl,
];
} catch (TelegramClientException $exception) {
$this->logger->logException($exception);
if ($exception->getCode() === 404 || $exception->getCode() === 401) {
throw new BotTokenConfiguratorException(
'Telegram сообщает, что BotToken не верный. Проверьте корректность.'
);
}
throw new BotTokenConfiguratorException($exception->getMessage());
} catch (Exception|GuzzleException $exception) {
$this->logger->logException($exception);
throw new BotTokenConfiguratorException($exception->getMessage());
}
}
private function getWebhookUrl(): string
{
$publicUrl = rtrim($this->settings->get('public_url'), '/');
if (! $publicUrl) {
throw new BotTokenConfiguratorException('Public URL is not set in configuration.');
}
$webhook = $this->router->url($this->settings->get('tg_webhook_handler', 'webhook'));
return $publicUrl . $webhook;
}
}

View File

@@ -0,0 +1,20 @@
<?php
return [
'config_timezone' => 'UTC',
'lang' => 'en-gb',
'language_id' => 1,
'auth_user_id' => 0,
'base_url' => '/',
'db' => [
'host' => 'localhost',
'database' => 'not_set',
'username' => 'not_set',
'password' => 'not_set',
],
'logs' => [
'path' => 'not_set',
],
];

View File

@@ -0,0 +1,9 @@
<?php
use Bastion\Handlers\SettingsHandler;
use Bastion\Handlers\TelegramHandler;
return [
'configureBotToken' => [SettingsHandler::class, 'configureBotToken'],
'getChatId' => [TelegramHandler::class, 'getChatId'],
];

View File

@@ -4,6 +4,7 @@
"psr-4": { "psr-4": {
"Openguru\\OpenCartFramework\\": "framework/", "Openguru\\OpenCartFramework\\": "framework/",
"App\\": "src/", "App\\": "src/",
"Bastion\\": "bastion/",
"Tests\\": "tests/" "Tests\\": "tests/"
}, },
"files": [ "files": [

View File

@@ -20,6 +20,8 @@ class Application extends Container
private static array $events = []; private static array $events = [];
private array $serviceProviders = []; private array $serviceProviders = [];
private array $middlewareStack = []; private array $middlewareStack = [];
private array $routes = [];
private LoggerInterface $logger; private LoggerInterface $logger;
public function __construct(array $config) public function __construct(array $config)
@@ -146,4 +148,16 @@ class Application extends Container
return $this; return $this;
} }
public function withRoutes(callable $callback): Application
{
$this->routes = $callback();
return $this;
}
public function getRoutes(): array
{
return $this->routes;
}
} }

View File

@@ -2,20 +2,14 @@
namespace Openguru\OpenCartFramework\Cache; namespace Openguru\OpenCartFramework\Cache;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Container\Container;
use Openguru\OpenCartFramework\Container\ServiceProvider; use Openguru\OpenCartFramework\Container\ServiceProvider;
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
class CacheServiceProvider extends ServiceProvider class CacheServiceProvider extends ServiceProvider
{ {
public function register(): void public function register(): void
{ {
$this->container->singleton(CacheInterface::class, function (Container $container) { $this->container->singleton(CacheInterface::class, function () {
return new DatabaseCache( return new SymfonyFilesystemCache('app.cache', 0, DIR_CACHE);
$container->get(ConnectionInterface::class),
$container->get(Settings::class)->get('tables.cache'),
);
}); });
} }
} }

View File

@@ -0,0 +1,44 @@
<?php
namespace Openguru\OpenCartFramework\Cache;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
class SymfonyFilesystemCache implements CacheInterface
{
private FilesystemAdapter $cache;
public function __construct(string $namespace = 'app.cache', int $defaultLifetime = 0, ?string $cacheDir = null)
{
$this->cache = new FilesystemAdapter($namespace, $defaultLifetime, $cacheDir);
}
public function get(string $key)
{
$item = $this->cache->getItem($key);
return $item->isHit() ? $item->get() : null;
}
public function set(string $key, $value, ?int $ttlSeconds = null): void
{
$item = $this->cache->getItem($key);
$item->set($value);
if ($ttlSeconds) {
$item->expiresAfter($ttlSeconds);
}
$this->cache->save($item);
}
public function delete(string $key): void
{
$this->cache->deleteItem($key);
}
public function clear(): void
{
$this->cache->clear();
}
}

View File

@@ -1,11 +1,13 @@
<?php <?php
namespace App\Decorators; namespace Openguru\OpenCartFramework\OpenCart\Decorators;
use Cart\Cart; use Cart\Cart;
use Cart\Currency; use Cart\Currency;
use Config; use Config;
use Loader; use Loader;
use ModelCatalogProduct;
use ModelSettingSetting;
use Registry; use Registry;
use Session; use Session;
@@ -15,7 +17,8 @@ use Session;
* @property Session $session * @property Session $session
* @property Currency $currency * @property Currency $currency
* @property Config $config * @property Config $config
* @property \ModelCatalogProduct $model_catalog_product * @property ModelCatalogProduct $model_catalog_product
* @property ModelSettingSetting $model_setting_setting
*/ */
class OcRegistryDecorator class OcRegistryDecorator
{ {

View File

@@ -2,6 +2,7 @@
namespace Openguru\OpenCartFramework\Router; namespace Openguru\OpenCartFramework\Router;
use Openguru\OpenCartFramework\Application;
use Openguru\OpenCartFramework\Container\Container; use Openguru\OpenCartFramework\Container\Container;
use Openguru\OpenCartFramework\Container\ServiceProvider; use Openguru\OpenCartFramework\Container\ServiceProvider;
@@ -10,7 +11,9 @@ class RouteServiceProvider extends ServiceProvider
public function register(): void public function register(): void
{ {
$this->container->singleton(Router::class, function (Container $container) { $this->container->singleton(Router::class, function (Container $container) {
return new Router(); $routes = Application::getInstance()->getRoutes();
return new Router($routes);
}); });
} }
} }

View File

@@ -0,0 +1,14 @@
<?php
namespace Openguru\OpenCartFramework\Telegram;
use Exception;
use Throwable;
class TelegramClientException extends Exception
{
public function __construct($message, $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -3,16 +3,17 @@
namespace Openguru\OpenCartFramework\Telegram; namespace Openguru\OpenCartFramework\Telegram;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use Openguru\OpenCartFramework\Support\Arr;
class TelegramService class TelegramService
{ {
private Client $client;
private ?string $botToken; private ?string $botToken;
public function __construct(?string $botToken = null) public function __construct(?string $botToken = null)
{ {
$this->botToken = $botToken; $this->botToken = $botToken;
$this->client = $this->createGuzzleClient("https://api.telegram.org/bot{$botToken}/");
} }
public function escapeTelegramMarkdownV2(string $text): string public function escapeTelegramMarkdownV2(string $text): string
@@ -37,13 +38,15 @@ class TelegramService
return; return;
} }
$client = $this->createGuzzleClient("https://api.telegram.org/bot{$this->botToken}/");
$query = [ $query = [
'chat_id' => $chatId, 'chat_id' => $chatId,
'text' => $text, 'text' => $text,
'parse_mode' => 'MarkdownV2', 'parse_mode' => 'MarkdownV2',
]; ];
$this->client->get('sendMessage', [ $client->get('sendMessage', [
'query' => $query, 'query' => $query,
]); ]);
} }
@@ -62,4 +65,52 @@ class TelegramService
return $this; return $this;
} }
/**
* Command used to exec telegram api methods
* @see https://core.telegram.org/bots/api
*
* @throws GuzzleException
* @throws TelegramClientException
* @throws \JsonException
*/
public function exec(string $methodName, array $params = []): array
{
if (! $this->botToken) {
throw new TelegramClientException('BotToken is empty');
}
$client = $this->createGuzzleClient("https://api.telegram.org/bot{$this->botToken}/");
try {
$response = $client->post($methodName, [
'json' => $params,
]);
$json = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
if (! $json['ok']) {
throw new TelegramClientException(
$json['code'] ?? 'Unknown Code' . ': ' . $json['description'] ?? 'Unknown Error'
);
}
return $json;
} catch (ClientException $exception) {
$response = $exception->getResponse()->getBody()->getContents();
$decoded = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
throw new TelegramClientException(
$decoded['description'] ?? $exception->getMessage(),
$decoded['code'] ?? $exception->getCode(),
);
}
}
public function getWebhookUrl(): string
{
$webhookInfo = $this->exec('getWebhookInfo');
return Arr::get($webhookInfo, 'result.url');
}
} }

View File

@@ -11,6 +11,7 @@ class TelegramValidateInitDataMiddleware
private array $excluded = [ private array $excluded = [
'testTgMessage', 'testTgMessage',
'manifest', 'manifest',
'webhook',
]; ];
public function __construct(SignatureValidator $signatureValidator) public function __construct(SignatureValidator $signatureValidator)

View File

@@ -17,8 +17,10 @@ class ApplicationFactory
public static function create(array $config): Application public static function create(array $config): Application
{ {
$defaultConfig = require __DIR__ . '/config.php'; $defaultConfig = require __DIR__ . '/config.php';
$routes = require __DIR__ . '/routes.php';
return (new Application(Arr::mergeArraysRecursively($defaultConfig, $config))) return (new Application(Arr::mergeArraysRecursively($defaultConfig, $config)))
->withRoutes(fn () => $routes)
->withServiceProviders([ ->withServiceProviders([
QueryBuilderServiceProvider::class, QueryBuilderServiceProvider::class,
CacheServiceProvider::class, CacheServiceProvider::class,

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Handlers;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Http\JsonResponse;
use Openguru\OpenCartFramework\Http\Request;
use Openguru\OpenCartFramework\Support\Arr;
class TelegramHandler
{
private CacheInterface $cache;
public function __construct(CacheInterface $cache)
{
$this->cache = $cache;
}
public function webhook(Request $request): JsonResponse
{
$message = Arr::get($request->json(), 'message', []);
$this->cache->set('tg_latest_msg', $message, 60);
return new JsonResponse([]);
}
}

View File

@@ -5,13 +5,11 @@ namespace App\ServiceProviders;
use App\Exceptions\CustomExceptionHandler; use App\Exceptions\CustomExceptionHandler;
use Openguru\OpenCartFramework\Container\ServiceProvider; use Openguru\OpenCartFramework\Container\ServiceProvider;
use Openguru\OpenCartFramework\Contracts\ExceptionHandlerInterface; use Openguru\OpenCartFramework\Contracts\ExceptionHandlerInterface;
use Openguru\OpenCartFramework\Router\Router;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
public function register(): void public function register(): void
{ {
$this->container->get(Router::class)->loadRoutesFromFile(__DIR__ . '/../routes.php');
$this->container->singleton(ExceptionHandlerInterface::class, function () { $this->container->singleton(ExceptionHandlerInterface::class, function () {
return new CustomExceptionHandler(); return new CustomExceptionHandler();
}); });

View File

@@ -2,8 +2,8 @@
namespace App\Services; namespace App\Services;
use App\Decorators\OcRegistryDecorator;
use Cart\Cart; use Cart\Cart;
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
class CartService class CartService
{ {

View File

@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace App\Services; namespace App\Services;
use App\Decorators\OcRegistryDecorator;
use App\Exceptions\OrderValidationFailedException; use App\Exceptions\OrderValidationFailedException;
use Cassandra\Date; use Cassandra\Date;
use DateTime; use DateTime;
use Exception; use Exception;
use Openguru\OpenCartFramework\Config\Settings; use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Logger\LoggerInterface; use Openguru\OpenCartFramework\Logger\LoggerInterface;
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface; use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
use Openguru\OpenCartFramework\Support\Arr; use Openguru\OpenCartFramework\Support\Arr;
use Openguru\OpenCartFramework\Telegram\TelegramService; use Openguru\OpenCartFramework\Telegram\TelegramService;

View File

@@ -3,12 +3,12 @@
namespace App\Services; namespace App\Services;
use App\Adapters\OcModelCatalogProductAdapter; use App\Adapters\OcModelCatalogProductAdapter;
use App\Decorators\OcRegistryDecorator;
use Cart\Currency; use Cart\Currency;
use Cart\Tax; use Cart\Tax;
use Exception; use Exception;
use Openguru\OpenCartFramework\Config\Settings; use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\ImageTool\ImageToolInterface; use Openguru\OpenCartFramework\ImageTool\ImageToolInterface;
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
use Openguru\OpenCartFramework\QueryBuilder\Builder; use Openguru\OpenCartFramework\QueryBuilder\Builder;
use Openguru\OpenCartFramework\QueryBuilder\JoinClause; use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
use Openguru\OpenCartFramework\Support\Arr; use Openguru\OpenCartFramework\Support\Arr;

View File

@@ -2,32 +2,11 @@
return [ return [
'config_timezone' => 'UTC', 'config_timezone' => 'UTC',
'lang' => 'en-gb', 'lang' => 'en-gb',
'language_id' => 1, 'language_id' => 1,
'auth_user_id' => 0, 'auth_user_id' => 0,
'base_url' => 'http://localhost', 'base_url' => 'http://localhost',
'search_cache_seconds' => 60,
'chunk_size' => 100,
'retry_count' => 3,
'activity_log_retention_limit' => 5000,
'tables' => [
'selected_products' => 'bp_selected_products',
'simulation_results' => 'bp_simulation_results',
'activity_logs' => 'bp_activity_logs',
'cache' => 'bp_cache',
'task_progress' => 'bp_task_progress',
'task_steps' => 'bp_task_steps',
'search_results' => 'bp_search_results',
'settings' => 'bp_settings',
],
'db' => [ 'db' => [
'host' => 'localhost', 'host' => 'localhost',
'database' => 'not_set', 'database' => 'not_set',

View File

@@ -5,6 +5,7 @@ use App\Handlers\CartHandler;
use App\Handlers\OrderHandler; use App\Handlers\OrderHandler;
use App\Handlers\ProductsHandler; use App\Handlers\ProductsHandler;
use App\Handlers\SettingsHandler; use App\Handlers\SettingsHandler;
use App\Handlers\TelegramHandler;
return [ return [
'products' => [ProductsHandler::class, 'handle'], 'products' => [ProductsHandler::class, 'handle'],
@@ -19,4 +20,6 @@ return [
'settings' => [SettingsHandler::class, 'index'], 'settings' => [SettingsHandler::class, 'index'],
'manifest' => [SettingsHandler::class, 'manifest'], 'manifest' => [SettingsHandler::class, 'manifest'],
'testTgMessage' => [SettingsHandler::class, 'testTgMessage'], 'testTgMessage' => [SettingsHandler::class, 'testTgMessage'],
'webhook' => [TelegramHandler::class, 'webhook'],
]; ];