704 lines
20 KiB
PHP
Executable File
704 lines
20 KiB
PHP
Executable File
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace mirzaev\arming_bot\models;
|
||
|
||
// Files of the project
|
||
use mirzaev\arming_bot\models\core,
|
||
mirzaev\arming_bot\controllers\core as controller,
|
||
mirzaev\arming_bot\models\catalog,
|
||
mirzaev\arming_bot\models\suspension,
|
||
mirzaev\arming_bot\models\account,
|
||
mirzaev\arming_bot\models\enumerations\language,
|
||
mirzaev\arming_bot\models\enumerations\currency;
|
||
|
||
// Framework for Telegram
|
||
use Zanzara\Zanzara,
|
||
Zanzara\Context as context,
|
||
Zanzara\Telegram\Type\Input\InputFile,
|
||
Zanzara\Telegram\Type\File\Document as telegram_document,
|
||
Zanzara\Middleware\MiddlewareNode as Node,
|
||
Zanzara\Telegram\Type\User as user;
|
||
|
||
/**
|
||
* Model of chat (telegram)
|
||
*
|
||
* @package mirzaev\arming_bot\models
|
||
*
|
||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||
*/
|
||
final class telegram extends core
|
||
{
|
||
/**
|
||
* Экранирование символов для Markdown
|
||
*
|
||
* @param string $text Текст для экранирования
|
||
* @param array $exception Символы которые будут исключены из списка для экранирования
|
||
*
|
||
* @return string Экранированный текст
|
||
*/
|
||
public static function unmarkdown(string $text, array $exceptions = []): string
|
||
{
|
||
// Инициализация реестра символом для конвертации
|
||
$from = array_diff(
|
||
[
|
||
'#',
|
||
'*',
|
||
'_',
|
||
'=',
|
||
'.',
|
||
'[',
|
||
']',
|
||
'(',
|
||
')',
|
||
'-',
|
||
'>',
|
||
'<',
|
||
'!',
|
||
'`'
|
||
],
|
||
$exceptions
|
||
);
|
||
|
||
// Инициализация реестра целей для конвертации
|
||
$to = [];
|
||
foreach ($from as $symbol) $to[] = "\\$symbol";
|
||
|
||
// Конвертация и выход (успех)
|
||
return str_replace($from, $to, $text);
|
||
}
|
||
|
||
/**
|
||
* Инициализация запчасти
|
||
*
|
||
* Проверяет существование запчасти
|
||
*
|
||
* @param string $spare Запчасть
|
||
*
|
||
* @return string|bool Запчасть, если найдена, иначе false
|
||
*/
|
||
public static function spares(string $spare): string|bool
|
||
{
|
||
// Поиск запчастей и выход (успех)
|
||
return match (mb_strtolower($spare)) {
|
||
'цевьё' => 'Цевьё',
|
||
default => false
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Главное меню
|
||
*
|
||
* Команда: /start
|
||
*
|
||
* @param context $ctx
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function menu(context $ctx): void
|
||
{
|
||
// Инициализация клавиатуры
|
||
$keyboard = [
|
||
[
|
||
['text' => '🛒 Каталог', 'web_app' => ['url' => 'https://arming.dev.mirzaev.sexy']]
|
||
],
|
||
[
|
||
['text' => '🏛️ О компании'],
|
||
['text' => '💬 Контакты']
|
||
],
|
||
[
|
||
['text' => '🎯 Сообщество']
|
||
]
|
||
];
|
||
|
||
if ($ctx->get('account')?->access['settings']) $keyboard[] = [['text' => '⚙️ Настройки']];
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage(
|
||
static::unmarkdown(<<<TXT
|
||
Это сообщение будет отображаться (оно должно быть обязательно) при вызове главного меню командой /start (создаёт кнопки меню снизу)
|
||
TXT),
|
||
[
|
||
'reply_markup' => [
|
||
'keyboard' => $keyboard,
|
||
'resize_keyboard' => true
|
||
],
|
||
'disable_notification' => true
|
||
]
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Начало работы с чат-роботом
|
||
*
|
||
* Команда: /start
|
||
*
|
||
* @param context $ctx
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function start(context $ctx): void
|
||
{
|
||
// Главное меню
|
||
static::menu($ctx);
|
||
}
|
||
|
||
/**
|
||
* Контакты
|
||
*
|
||
* Команда: /contacts
|
||
*
|
||
* @param context $ctx
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function contacts(context $ctx): void
|
||
{
|
||
// Отправка сообщения
|
||
$ctx->sendMessage(static::unmarkdown(<<<TXT
|
||
Здесь придумать текст для раздела "Контакты"
|
||
TXT), [
|
||
'reply_markup' => [
|
||
'inline_keyboard' => [
|
||
[
|
||
['text' => '⚡ Связь с менеджером', 'url' => 'https://t.me/iarming'],
|
||
],
|
||
[
|
||
['text' => '📨 Почта', 'callback_data' => 'mail']
|
||
],
|
||
[
|
||
['text' => '🪖 Сайт', 'url' => 'https://arming.ru'],
|
||
['text' => '🛒 Wildberries', 'url' => 'https://www.wildberries.ru/seller/137386'],
|
||
['text' => '🛒 Ozon', 'url' => 'https://www.ozon.ru/seller/arming-1086587/products/?miniapp=seller_1086587'],
|
||
]
|
||
]
|
||
],
|
||
'link_preview_options' => [
|
||
'is_disabled' => true
|
||
],
|
||
'disable_notification' => true
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Почта
|
||
*
|
||
* @param context $ctx
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function _mail(context $ctx): void
|
||
{
|
||
// Отправка сообщения
|
||
$ctx->sendMessage(static::unmarkdown(<<<TXT
|
||
[info@arming.ru](mailto::info@arming.ru)
|
||
TXT, ['[', ']', '(', ')']), [
|
||
'link_preview_options' => [
|
||
'is_disabled' => true
|
||
],
|
||
'disable_notification' => true
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Компания
|
||
*
|
||
* Команда: /company
|
||
*
|
||
* @param context $ctx
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function company(context $ctx): void
|
||
{
|
||
// Отправка сообщения
|
||
$ctx->sendMessage(
|
||
static::unmarkdown(<<<TXT
|
||
Здесь придумать текст для раздела "Компания"
|
||
TXT),
|
||
[
|
||
'reply_markup' => [
|
||
'inline_keyboard' => [
|
||
[
|
||
['text' => '📄 Публичная оферта', 'web_app' => ['url' => 'https://arming.dev.mirzaev.sexy/offer']],
|
||
]
|
||
]
|
||
],
|
||
'link_preview_options' => [
|
||
'is_disabled' => true
|
||
]
|
||
]
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Сообщество
|
||
*
|
||
* Команда: /community
|
||
*
|
||
* @param context $ctx
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function community(context $ctx): void
|
||
{
|
||
// Отправка сообщения
|
||
$ctx->sendMessage(static::unmarkdown(<<<TXT
|
||
Здесь придумать текст для раздела "Сообщество"
|
||
TXT), [
|
||
'reply_markup' => [
|
||
'inline_keyboard' => [
|
||
[
|
||
['text' => '💬 Основной чат', 'url' => 'https://t.me/arming_zone'],
|
||
]
|
||
]
|
||
],
|
||
'link_preview_options' => [
|
||
'is_disabled' => true
|
||
],
|
||
'disable_notification' => true
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Настройки (доступ только авторизованным)
|
||
*
|
||
* Команда: /settings
|
||
*
|
||
* @param context $ctx
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function settings(context $ctx): void
|
||
{
|
||
if ($ctx->get('account')?->access['settings']) {
|
||
// Авторизован доступ к настройкам
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage(
|
||
static::unmarkdown(<<<TXT
|
||
Панель управления чат-роботом ARMING
|
||
TXT),
|
||
[
|
||
'reply_markup' => [
|
||
'inline_keyboard' => [
|
||
[
|
||
['text' => '📦 Импорт товаров', 'callback_data' => 'import_request'],
|
||
]
|
||
]
|
||
],
|
||
'link_preview_options' => [
|
||
'is_disabled' => true
|
||
],
|
||
'disable_notification' => true
|
||
]
|
||
);
|
||
} else {
|
||
// Не авторизован доступ к настройкам
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage('⛔ *Нет доступа*');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Запросить файл для импорта товаров (доступ только авторизованным)
|
||
*
|
||
* @param context $ctx
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function import_request(context $ctx): void
|
||
{
|
||
if ($ctx->get('account')?->access['settings']) {
|
||
// Авторизован доступ к настройкам
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage(static::unmarkdown('Отправьте документ в формате xlsx со списком товаров'))
|
||
->then(function ($message) use ($ctx) {
|
||
// Отправка файла
|
||
$ctx->sendDocument(new InputFile(CATALOG_EXAMPLE), ['disable_notification' => true]);
|
||
|
||
// Импорт файла
|
||
$ctx->nextStep([static::class, 'import'], true);
|
||
});
|
||
} else {
|
||
// Не авторизован доступ к настройкам
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage('⛔ *Нет доступа*');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Импорт товаров (доступ только авторизованным)
|
||
*
|
||
* @param context $ctx
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function import(context $ctx): void
|
||
{
|
||
if (($account = $ctx->get('account'))?->access['settings']) {
|
||
// Авторизован доступ к настройкам
|
||
|
||
// Инициализация документа
|
||
$document = $ctx->getMessage()?->getDocument();
|
||
|
||
if ($document instanceof telegram_document) {
|
||
// Инициализирован документ
|
||
|
||
// Инициализация файла
|
||
$ctx->getFile($document->getFileId())->then(function ($file) use ($ctx, $document, $account) {
|
||
|
||
if ($file->getFileSize() <= 50000000) {
|
||
// Не превышает 50 мегабайт (50 000 000 байт) размер файла
|
||
|
||
if (pathinfo(parse_url($file->getFilePath())['path'], PATHINFO_EXTENSION) === 'xlsx') {
|
||
// Имеет расширение xlsx файл
|
||
|
||
// Initializing the directory in the storage
|
||
if (!file_exists($storage = STORAGE . DIRECTORY_SEPARATOR . 'import' . DIRECTORY_SEPARATOR . $account->getKey() . DIRECTORY_SEPARATOR . time()))
|
||
mkdir($storage, 0775, true);
|
||
|
||
// Сохранение файла
|
||
file_put_contents(
|
||
$import = $storage . DIRECTORY_SEPARATOR . 'import.xlsx',
|
||
file_get_contents('https://api.telegram.org/file/bot' . KEY . '/' . parse_url($file->getFilePath())['path'])
|
||
);
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage(sprintf(
|
||
<<<'TXT'
|
||
🔬 *Выполняется анализ:* %s \(%s байт\)
|
||
TXT,
|
||
static::unmarkdown($document->getFileName()),
|
||
static::unmarkdown((string) $file->getFileSize())
|
||
))
|
||
->then(function ($message) use ($ctx, $import) {
|
||
// Инициализация счётчика загруженных товаров
|
||
$categories_loaded
|
||
= $products_loaded
|
||
= $categories_created
|
||
= $products_created
|
||
= $categories_updated
|
||
= $products_updated
|
||
= $categories_deleted
|
||
= $products_deleted
|
||
= $categories_old
|
||
= $products_old
|
||
= $categories_new
|
||
= $products_new
|
||
= 0;
|
||
|
||
// Import
|
||
catalog::import(
|
||
$import,
|
||
$categories_loaded,
|
||
$categories_created,
|
||
$categories_updated,
|
||
$categories_deleted,
|
||
$categories_old,
|
||
$categories_new,
|
||
$products_loaded,
|
||
$products_created,
|
||
$products_updated,
|
||
$products_deleted,
|
||
$products_old,
|
||
$products_new,
|
||
language: $account->language ?? settings::active()?->language ?? language::en, // @todo add languages
|
||
currency: $account->currency ?? settings::active()?->currency ?? currency::usd // @todo add currencies
|
||
);
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage(<<<TXT
|
||
🏷 *Категории*
|
||
|
||
*Загружено:* $categories_loaded
|
||
|
||
*Добавлено:* $categories_created
|
||
*Обновлено:* $categories_updated
|
||
*Удалено:* $categories_deleted
|
||
|
||
*Было:* $categories_old
|
||
*Стало:* $categories_new
|
||
TXT)
|
||
->then(function ($message) use ($ctx, $products_loaded, $products_created, $products_updated, $products_deleted, $products_old, $products_new) {
|
||
$ctx->sendMessage(<<<TXT
|
||
📦 *Товары*
|
||
|
||
*Загружено:* $products_loaded
|
||
|
||
*Добавлено:* $products_created
|
||
*Обновлено:* $products_updated
|
||
*Удалено:* $products_deleted
|
||
|
||
*Было:* $products_old
|
||
*Стало:* $products_new
|
||
TXT)
|
||
->then(function ($message) use ($ctx) {
|
||
// Завершение диалога
|
||
$ctx->endConversation();
|
||
});
|
||
});
|
||
});
|
||
} else {
|
||
// Не имеет расширение xlsx файл
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage(static::unmarkdown('Файл должен иметь расширение xlsx'));
|
||
}
|
||
} else {
|
||
// Превышает 50 мегабайт (50000000 байт) размер файла
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage(static::unmarkdown('Размер файла не должен превышать 50 мегабайт'));
|
||
}
|
||
});
|
||
} else {
|
||
// Не инициализирован документ
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage(static::unmarkdown('Отправьте документ в формате xlsx со списком товаров'));
|
||
}
|
||
} else {
|
||
// Не авторизован доступ к настройкам
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage('⛔ *Нет доступа*');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Инициализация аккаунта (middleware)
|
||
*
|
||
* @param context $ctx
|
||
* @param Node $next
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function account(context $ctx, Node $next): void
|
||
{
|
||
// Выполнение заблокировано?
|
||
if ($ctx->get('stop')) return;
|
||
|
||
// Инициализация аккаунта Telegram
|
||
$telegram = $ctx->getEffectiveUser();
|
||
|
||
// Инициализация аккаунта
|
||
$account = account::initialize($telegram->getId(), $telegram);
|
||
|
||
if ($account) {
|
||
// Инициализирован аккаунт
|
||
|
||
if ($account->banned) {
|
||
// Заблокирован аккаунт
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage('⛔ *Ты заблокирован*')
|
||
->then(function ($message) use ($ctx) {
|
||
// Завершение диалога
|
||
$ctx->endConversation();
|
||
});
|
||
|
||
// Блокировка дальнейшего выполнения
|
||
$ctx->set('stop', true);
|
||
} else {
|
||
// Не заблокирован аккаунт
|
||
|
||
// Запись в буфер
|
||
$ctx->set('account', $account);
|
||
|
||
// Продолжение выполнения
|
||
$next($ctx);
|
||
}
|
||
} else {
|
||
// Не инициализирован аккаунт
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Инициализация статуса технических работ (middleware)
|
||
*
|
||
* @param context $ctx
|
||
* @param Node $next
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function suspension(context $ctx, Node $next): void
|
||
{
|
||
// Выполнение заблокировано?
|
||
if ($ctx->get('stop')) return;
|
||
|
||
// Поиск технических работ
|
||
$suspension = suspension::search();
|
||
|
||
if ($suspension && $suspension->targets['telegram-robot']) {
|
||
// Найдена активная приостановка
|
||
|
||
// Инициализация аккаунта
|
||
$account = $ctx->get('account');
|
||
|
||
if ($account) {
|
||
// Инициализирован аккаунт
|
||
|
||
foreach ($suspension->access as $type => $status) {
|
||
// Перебор статусов доступа
|
||
|
||
if ($status && $account->{$type}) {
|
||
// Авторизован аккаунт
|
||
|
||
// Продолжение выполнения
|
||
$next($ctx);
|
||
|
||
// Выход (успех)
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Инициализация сообщения
|
||
$message = "⚠️ *Работа приостановлена*\n*Оставшееся время\:* " . $suspension->message($account->language ?? settings::active()?->language ?? 'en');
|
||
|
||
// Добавление описания причины приостановки, если найдена
|
||
if (!empty($suspension->description))
|
||
$message .= "\n\n" . $suspension->description[$account->language ?? settings::active()?->language ?? 'en'] ?? array_values($suspension->description)[0];
|
||
|
||
// Отправка сообщения
|
||
$ctx->sendMessage($message)
|
||
->then(function ($message) use ($ctx) {
|
||
// Завершение диалога
|
||
$ctx->endConversation();
|
||
});
|
||
|
||
// Блокировка дальнейшего выполнения
|
||
$ctx->set('stop', true);
|
||
} else {
|
||
// Не найдена активная приостановка
|
||
|
||
// Продолжение выполнения
|
||
$next($ctx);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Cart attach
|
||
*
|
||
* @param context $ctx
|
||
* @param string $share Sharing hash
|
||
*
|
||
* @return void
|
||
*/
|
||
public static function cart_attach(context $ctx, string $share): void
|
||
{
|
||
// Initializing account
|
||
$account = $ctx->get('account');
|
||
|
||
if ($account) {
|
||
// Initialized the account
|
||
|
||
// Initializing cart
|
||
$cart = cart::_read(
|
||
filter: 'd.share == @share',
|
||
sort: 'd.updated DESC, d.created DESC, d._key DESC',
|
||
amount: 1,
|
||
page: 1,
|
||
parameters: ['share' => $share]
|
||
);
|
||
|
||
// Deinitializing unnecessary variables
|
||
unset($share);
|
||
|
||
// Unsharing the cart
|
||
$cart->unshare();
|
||
|
||
if ($cart instanceof cart) {
|
||
// Initialized the cart
|
||
|
||
// Connecting the cart to the account
|
||
$edge = $account->connect($cart);
|
||
|
||
if (!empty($edge)) {
|
||
// Connected the cart to the account
|
||
|
||
// Initializing products in the cart
|
||
$products = $cart->products(language: $account->language ?? language::ru, currency: $account->currency ?? currency::rub);
|
||
|
||
if (!empty($products)) {
|
||
// Initialized products in the cart
|
||
|
||
// Declaring total cost of products
|
||
$cost = 0;
|
||
|
||
// Declaring formatted list of products for message
|
||
$list = '';
|
||
|
||
// Initializing iterator of rows
|
||
$row = 0;
|
||
|
||
foreach ($products as $product) {
|
||
// Iterating over products
|
||
|
||
// Generating formatted list of products for message
|
||
$list .= static::unmarkdown(++$row . '. ' . $product['document']['name'] . ' (' . $product['amount'] . 'шт)') . "\n";
|
||
|
||
// Generating total cost of products
|
||
$cost += $product['document']['cost'] * $product['amount'];
|
||
}
|
||
|
||
// Deinitializing unnecessary variables
|
||
unset($products, $product, $row);
|
||
|
||
// Initializing currency symbol
|
||
$symbol = ($account->currency ?? currency::rub)->symbol();
|
||
|
||
// Initializing delivery cost for message
|
||
$delivery_cost = $cart->buffer['delivery']['cost'];
|
||
|
||
// Initializing delivery days for message
|
||
$delivery_days = $cart->buffer['delivery']['days'];
|
||
|
||
// Initializing delivery address for message
|
||
$delivery_address = $cart->buffer['delivery']['location']['name'] . ', ' . $cart->buffer['delivery']['street'];
|
||
|
||
// Deinitializing unnecessary variables
|
||
unset($cart);
|
||
|
||
$ctx->sendMessage(
|
||
<<<TXT
|
||
🛒 *Добавлена корзина*
|
||
|
||
$list
|
||
*Стоимость:* $cost$symbol \+ $delivery_cost$symbol \($delivery_days дней\)
|
||
*Адрес доставки:* $delivery_address
|
||
TXT,
|
||
[
|
||
'reply_markup' => [
|
||
'inline_keyboard' => [
|
||
[
|
||
/* ['text' => '🧾 Оплатить', 'web_app' => ['url' => 'https://arming.dev.mirzaev.sexy']] */
|
||
['text' => '📦 Оформить заказ', 'url' => 'https://auth.robokassa.ru/Merchant/Index.aspx?MerchantLogin=demo&OutSum=11&Description=Покупка в демо магазине&SignatureValue=2c113e992e2c985e43e348ff3c12f32b'],
|
||
]
|
||
],
|
||
'disable_notification' => true
|
||
]
|
||
]
|
||
);
|
||
}
|
||
|
||
// Deinitializing unnecessary variables
|
||
unset($cart, $list);
|
||
}
|
||
}
|
||
|
||
// Deinitializing unnecessary variables
|
||
unset($cart);
|
||
}
|
||
|
||
// Deinitializing unnecessary variables
|
||
unset($account);
|
||
}
|
||
}
|