первая инициализация и перенос на сервер

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2024-09-05 22:00:04 +07:00
parent c41a723af7
commit c3dec4417d
10 changed files with 2024 additions and 680 deletions

View File

@ -1,16 +1,23 @@
{
"name": "mirzaev/spetsresurs-telegram-registry-requests",
"name": "mirzaev/arming_bot",
"type": "robot",
"tags": [
"telegram",
"chat-robot",
"military",
"shop"
],
"require": {
"triagens/arangodb": "^3.8",
"mirzaev/arangodb": "^1.0",
"badfarm/zanzara": "^0.9.1",
"nyholm/psr7": "^1.8"
"nyholm/psr7": "^1.8",
"react/filesystem": "^0.1.2"
},
"license": "WTFPL",
"autoload": {
"psr-4": {
"mirzaev\\spetsresurs\\telegram\\registry\\requests\\": "mirzaev/spetsresurs/telegram/registry/requests/system/"
"mirzaev\\arming_bot\\": "mirzaev/arming_bot/system/"
}
},
"authors": [
@ -22,7 +29,8 @@
"minimum-stability": "stable",
"config": {
"allow-plugins": {
"php-http/discovery": true
"php-http/discovery": true,
"wyrihaximus/composer-update-bin-autoload-path": true
}
}
}

1398
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,615 @@
<?php
// Фреймворк ArangoDB
use mirzaev\arangodb\connection,
mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document,
ArangoDBClient\Cursor,
ArangoDBClient\Statement as _statement;
// Фреймворк Telegram
use Zanzara\Zanzara,
Zanzara\Context,
Zanzara\Config,
Zanzara\Telegram\Type\Input\InputFile,
Zanzara\Telegram\Type\File\Document as telegram_document,
Zanzara\Telegram\Type\File\File,
Zanzara\Middleware\MiddlewareNode as Node;
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
// Версия робота
define('VERSION', '1.0.0');
// Путь до настроек
define('SETTINGS', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'settings');
// Путь до хранилища
define('STORAGE', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'storage');
// Файл в формате xlsx с примером excel-документа для импорта каталога
define('CATALOG_EXAMPLE', STORAGE . DIRECTORY_SEPARATOR . 'example.xlsx');
// Файл в формате xlsx для импорта каталога
define('CATALOG_IMPORT', STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx');
// Ключ чат-робота Telegram
define('KEY', require(SETTINGS . DIRECTORY_SEPARATOR . 'key.php'));
// Инициализация библиотек
require __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
// Инициализация инстанции соединения с ArangoDB
$arangodb = new connection(require SETTINGS . DIRECTORY_SEPARATOR . 'arangodb.php');
/**
* Экранирование символов для Markdown
*
* @param string $text Текст для экранирования
* @param array $exception Символы которые будут исключены из списка для экранирования
*
* @return string Экранированный текст
*/
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
*/
function spares(string $spare): string|bool
{
// Поиск запчастей и выход (успех)
return match (mb_strtolower($spare)) {
'цевьё' => 'Цевьё',
default => false
};
}
/**
* Авторизация
*
* @param string $id Идентификатор Telegram
* @param bool $registration Регистрация, если аккаунт не найден
*
* @return _document|null Инстанция аккаунта, если найден
*/
function authorization(string $id, bool $registration = true): _document|null
{
global $arangodb;
if (collection::init($arangodb->session, 'account')) {
if ($account = collection::search($arangodb->session, sprintf("FOR d IN account FILTER d.id == '%s' RETURN d", $id))) {
// Найден аккаунт
// Возврат (успех)
return $account;
} else if ($registration) {
// Не найден аккаунт и запрошена его регистрация
// Создание аккаунта
document::write($arangodb->session, 'account', ['id' => $id, 'banned' => false, 'settings' => false, 'version' => VERSION]);
// Авторизация (без регистрации)
return authorization($id, false);
} else {
// Не найден аккаунт и не запрошена его регистрация
// Выход (провал)
return null;
}
} else throw new exception('Не удалось инициализировать коллекцию: account');
// Выход (провал)
return null;
}
/**
* Главное меню
*
* Команда: /start
*
* @param Context $ctx
*
* @return void
*/
function menu(Context $ctx): void
{
// Инициализация клавиатуры
$keyboard = [
[
['text' => '🛒 Каталог']
],
[
['text' => '💬 Контакты'],
['text' => '🏛️ О компании']
],
[
['text' => '🎯 Сообщество']
]
];
if ($ctx->get('account')?->settings) $keyboard[] = [['text' => '⚙️ Настройки']];
// Отправка сообщения
$ctx->sendMessage(
unmarkdown(<<<TXT
Это сообщение будет отображаться (оно должно быть обязательно) при вызове главного меню командой /start (создаёт кнопки меню снизу)
TXT),
[
'reply_markup' => [
'keyboard' => $keyboard,
'resize_keyboard' => true
],
'disable_notification' => true
]
);
}
/**
* Категории
*
* Команда: /catalog
*
* @param Context $ctx
*
* @return void
*/
function categories(Context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(unmarkdown(<<<TXT
Выберите категорию
TXT), [
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '🗜️ Тюнинг', 'callback_data' => 'tuning']
]
]
],
'link_preview_options' => [
'is_disabled' => true
],
'disable_notification' => true
]);
}
/**
* Тюнинг
*
* @param Context $ctx
*
* @return void
*/
function tuning(Context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(<<<TXT
Выберите запчасть
TXT, [
'reply_markup' => [
'inline_keyboard' => [
[
['text' => 'Цевьё', 'callback_data' => 'brands']
]
]
],
'link_preview_options' => [
'is_disabled' => true
],
'disable_notification' => true
]);
}
/**
* Бренды
*
* @param Context $ctx
*
* @return void
*/
function brands(Context $ctx): void
{
if ($spare = spares($ctx->getMessage()->getText())) {
// Инициализирована запчасть
// Отправка сообщения
$ctx->sendMessage(<<<TXT
Выберите бренд
TXT, [
'link_preview_options' => [
'is_disabled' => true
],
'disable_notification' => true
])->then(function ($message) use ($ctx) {
$ctx;
});
} else {
// Не инициализирована запчасть
// Отправка сообщения
$ctx->sendMessage('⚠️ *Не найдена запчасть*');
}
}
/**
* Контакты
*
* Команда: /contacts
*
* @param Context $ctx
*
* @return void
*/
function contacts(Context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(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://arming.ru']
]
]
],
'link_preview_options' => [
'is_disabled' => true
],
'disable_notification' => true
]);
}
/**
* Почта
*
* @param Context $ctx
*
* @return void
*/
function _mail(Context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(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
*/
function company(Context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(
unmarkdown(<<<TXT
Здесь придумать текст для раздела "Компания"
TXT),
/* [
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '⚡ Связь с менеджером', 'url' => 'https://git.mirzaev.sexy/mirzaev/mashtrash'],
['text' => '📨 Почта', 'text' => ''],
],
[
['text' => '🪖 Сайт', 'url' => '']
['text' => '🛒 Wildberries', 'url' => '']
]
]
],
'link_preview_options' => [
'is_disabled' => true
]
] */
);
}
/**
* Сообщество
*
* Команда: /community
*
* @param Context $ctx
*
* @return void
*/
function community(Context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(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
*/
function settings(Context $ctx): void
{
if ($ctx->get('account')?->settings) {
// Авторизован доступ к настройкам
// Отправка сообщения
$ctx->sendMessage(
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
*/
function import_request(Context $ctx): void
{
if ($ctx->get('account')?->settings) {
// Авторизован доступ к настройкам
// Отправка сообщения
$ctx->sendMessage(unmarkdown('Отправьте документ в формате xlsx со списком товаров'))
->then(function ($message) use ($ctx) {
// Отправка файла
$ctx->sendDocument(new InputFile(CATALOG_EXAMPLE), ['disable_notification' => true]);
// Импорт файла
$ctx->nextStep("import");
});
} else {
// Не авторизован доступ к настройкам
// Отправка сообщения
$ctx->sendMessage('⛔ *Нет доступа*');
}
}
/**
* Импорт товаров (доступ только авторизованным)
*
* @param Context $ctx
*
* @return void
*/
function import(Context $ctx): void
{
if ($ctx->get('account')?->settings) {
// Авторизован доступ к настройкам
// Инициализация документа
$document = $ctx->getMessage()?->getDocument();
if ($document instanceof telegram_document) {
// Инициализирован документ
// Инициализация файла
$ctx->getFile($document->getFileId())->then(function ($file) use ($ctx) {
if ($file->getFileSize() <= 50000000) {
// Не превышает 50 мегабайт (50000000 байт) размер файла
if ($file->getFilePath()['extension'] === 'xlsx') {
// Имеет расширение xlsx файл
// Сохранение файла
file_put_contents(STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx', file_get_contents('https://api.telegram.org/file/bot' . KEY . '/' . $file->getFilePath()));
// Инициализация счётчика загруженных товаров
$loaded = $created = $updated = $deleted = $old = $new = 0;
// Отправка сообщения
$ctx->sendMessage(<<<TXT
*Загружено для обработки:* $loaded
*Добавлено:* $created
*Обновлено:* $updated
*Удалено:* $deleted
*Опубликовано в магазине:* $old \-\> *$new*
TXT)
->then(function ($message) use ($ctx) {
// Завершение диалога
$ctx->endConversation();
});
} else {
// Не имеет расширение xlsx файл
// Отправка сообщения
$ctx->sendMessage(unmarkdown('Файл должен иметь расширение xlsx'));
}
} else {
// Превышает 50 мегабайт (50000000 байт) размер файла
// Отправка сообщения
$ctx->sendMessage(unmarkdown('Размер файла не должен превышать 50 мегабайт'));
}
});
} else {
// Не инициализирован документ
// Отправка сообщения
$ctx->sendMessage(unmarkdown('Отправьте документ в формате xlsx со списком товаров'));
}
} else {
// Не авторизован доступ к настройкам
// Отправка сообщения
$ctx->sendMessage('⛔ *Нет доступа*');
}
}
$config = new Config();
$config->setParseMode(Config::PARSE_MODE_MARKDOWN);
$config->useReactFileSystem(true);
$bot = new Zanzara(KEY, $config);
$bot->onCommand('start', function (Context $ctx): void {
// Главное меню
menu($ctx);
});
/* $bot->onUpdate(function (Context $ctx): void {}); */
/**
* Инициализация аккаунта (middleware)
*
* @param Context $ctx
* @param Node $next
*
* @return void
*/
$account = function (Context $ctx, Node $next): void {
// Выполнение заблокировано?
if ($ctx->get('stop')) return;
// Авторизация аккаунта
$account = authorization($ctx->getEffectiveUser()->getId());
if ($account instanceof _document) {
// Инициализирован аккаунт (подразумевается)
if ($account->banned) {
// Заблокирован аккаунт
// Отправка сообщения
$ctx->sendMessage('⛔ *Ты заблокирован*');
// Завершение диалога
$ctx->endConversation();
// Блокировка дальнейшего выполнения
$ctx->set('stop', true);
} else {
// Не заблокирован аккаунт
// Запись в буфер
$ctx->set('account', $account);
// Продолжение выполнения
$next($ctx);
}
} else {
// Не инициализирован аккаунт
}
};
$bot->onCommand('catalog', fn($ctx) => categories($ctx));
$bot->onCommand('contacts', fn($ctx) => contacts($ctx));
$bot->onCommand('company', fn($ctx) => company($ctx));
$bot->onCommand('community', fn($ctx) => community($ctx));
$bot->onCommand('settings', fn($ctx) => settings($ctx));
$bot->onText('🛒 Каталог', fn($ctx) => categories($ctx));
$bot->onText('💬 Контакты', fn($ctx) => contacts($ctx));
$bot->onText('🏛️ О компании', fn($ctx) => company($ctx));
$bot->onText('🎯 Сообщество', fn($ctx) => community($ctx));
$bot->onText('⚙️ Настройки', fn($ctx) => settings($ctx));
$bot->onCbQueryData(['mail'], fn($ctx) => _mail($ctx));
$bot->onCbQueryData(['import_request'], fn($ctx) => import_request($ctx));
$bot->onCbQueryData(['tuning'], fn($ctx) => tuning($ctx));
$bot->onCbQueryData(['brands'], fn($ctx) => brands($ctx));
$bot->middleware($account)->run();

Binary file not shown.

Binary file not shown.

View File

@ -1,675 +0,0 @@
<?php
// Фреймворк ArangoDB
use mirzaev\arangodb\connection,
mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document,
ArangoDBClient\Cursor,
ArangoDBClient\Statement as _statement;
// Фреймворк Telegram
use Zanzara\Zanzara;
use Zanzara\Context;
use Zanzara\Config;
require __DIR__ . '/../../../../../../../vendor/autoload.php';
$arangodb = new connection(require __DIR__ . '/../settings/arangodb.php');
/* ini_set('error_reporting', E_ALL);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1); */
function escape(string $text)
{
return str_replace(
[
'#',
'*',
'_',
'=',
'.',
'[',
']',
'(',
')',
'-',
'>',
'<'
],
[
'\#',
'\*',
'\_',
'\\=',
'\.',
'\[',
'\]',
'\(',
'\)',
'\-',
'\>',
'\<'
],
$text
);
}
/**
* Авторизация
*
* @param string $id Идентификатор Telegram
*
* @return _document|null|false (инстанция аккаунта, если подключен и авторизован; null, если не подключен; false, если подключен но неавторизован)
*/
function authorization(string $id): _document|null|false
{
global $arangodb;
if (collection::init($arangodb->session, 'telegram')) {
if ($telegram = collection::search($arangodb->session, sprintf("FOR d IN telegram FILTER d.id == '%s' RETURN d", $id))) {
if ($telegram->number === null) return null;
else if (
$telegram->active
&& collection::init($arangodb->session, 'account')
&& $account = collection::search(
$arangodb->session,
sprintf(
"FOR d IN account FILTER d.number == '%s' RETURN d",
$telegram->number,
$telegram->getId()
)
)
) return $account;
else return false;
}
} else throw new exception('Не удалось инициализировать коллекцию');
return false;
}
/**
* Сотрудник
*
* @param string $id Идентификатор аккаунта
*
* @return _document|null|false (инстанция аккаунта, если подключен и авторизован; null, если не подключен; false, если подключен но неавторизован)
*/
function worker(string $id): _document|null|false
{
global $arangodb;
return collection::search(
$arangodb->session,
sprintf(
<<<'AQL'
FOR d IN worker
LET e = (
FOR e IN account_edge_worker
FILTER e._from == '%s'
SORT e.created DESC, e._key DESC
LIMIT 1
RETURN e
)
FILTER d._id == e[0]._to
SORT d.created DESC, d._key DESC
LIMIT 1
RETURN d
AQL,
$id
)
);
}
function registration(string $id, string $number): bool
{
global $arangodb;
if (collection::init($arangodb->session, 'telegram')) {
if ($telegram = collection::search($arangodb->session, sprintf("FOR d IN telegram FILTER d.id == '%s' RETURN d", $id))) {
// Найден аккаунт
// Запись номера
$telegram->number = $number;
if (!document::update($arangodb->session, $telegram)) return false;
} else if (
$number === null
|| !$telegram = collection::search(
$arangodb->session,
sprintf(
"FOR d IN telegram FILTER d._id == '%s' RETURN d",
document::write($arangodb->session, 'telegram', ['id' => $id, 'active' => false, 'number' => $number])
)
)
) return false;
// Инициализация ребра: account -> telegram
if (
collection::init($arangodb->session, 'account')
&& ($account = collection::search(
$arangodb->session,
sprintf(
"FOR d IN account FILTER d.number == '%d' RETURN d",
$telegram->number
)
))
&& collection::init($arangodb->session, 'connection', true)
&& (collection::search(
$arangodb->session,
sprintf(
"FOR d IN connection FILTER d._from == '%s' && d._to == '%s' RETURN d",
$account->getId(),
$telegram->getId()
)
)
?? collection::search(
$arangodb->session,
sprintf(
"FOR d IN connection FILTER d._id == '%s' RETURN d",
document::write(
$arangodb->session,
'connection',
['_from' => $account->getId(), '_to' => $telegram->getId()]
)
)
))
) {
// Инициализировано ребро: account -> telegram
// Активация
$telegram->active = true;
return document::update($arangodb->session, $telegram);
}
} else throw new exception('Не удалось инициализировать коллекцию');
return false;
}
function generateAuthenticationKeyboard(): array
{
return [
'reply_markup' => [
'keyboard' => [
[
['text' => '🔐 Аутентификация', 'request_contact' => true]
]
],
'resize_keyboard' => true
]
];
}
function generateMenu(Context $ctx): void
{
if ($account = authorization($ctx->getMessage()?->getFrom()?->getId() ?? $ctx->getCallbackQuery()->getFrom()->getId())) {
// Успешная авторизация
if (!$account->active) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else if ($account->banned) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else if (!($worker = worker($account->getId()))->active) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else if ($worker->fired) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else {
// Активен аккаунт
$ctx->sendMessage('👋 Здравствуйте, ' . preg_replace('/([._\-()!#])/', '\\\$1', $account->name['first']), [
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '🔍 Активные заявки', 'callback_data' => 'day']
]
],
'remove_keyboard' => true
]
])->then(function ($message) use ($ctx) {
$ctx->setChatDataItem("menu", $message);
});
}
}
}
/**
* Прочитать заявки из ArangoDB
*
* @param int $amount Количество
* @param ?string $date За какую дату (unixtime)
* @param int $page Страница
* @param _document $worker Сотрудник
*
* @return Cursor
*/
function requests(int $amount = 5, ?string $date = null, int $page = 1, _document $worker): Cursor
{
global $arangodb;
// Инициализация значения даты по умолчанию
$date ??= time();
// Фильтрация номера страницы
if ($page < 1) $page = 1;
// Инициализация номера страницы для вычислний
--$page;
// Инициализация сдвига
$offset = $page === 0 ? 0 : $page * $amount;
return (new _statement(
$arangodb->session,
[
'query' => sprintf(
// d.date < %s там специально, не менять на <=
"FOR d IN task FILTER ((d.date >= %s && d.date < %s && d.start >= '05:00') || (d.date >= %s && d.date < %s && d.start < '05:00')) && d.worker == null && d.market != null && d.confirmed != true && d.published == true && d.completed != true && (FOR m IN market FILTER m.id == d.market && IS_ARRAY(m.bans) SORT m.created DESC, m._key DESC LIMIT 1 RETURN !POSITION(m.bans, \"%s\"))[0] SORT d.created DESC, d._key DESC LIMIT %d, %d RETURN d",
$from = (new DateTime("@$date"))->setTime(0, 0)->format('U'),
$to = (new DateTime("@$date"))->modify('+1 day')->setTime(0, 0)->format('U'),
$to,
(new DateTime("@$date"))->modify('+2 day')->setTime(0, 0)->format('U'),
$worker->id,
$offset,
$amount + $offset - ($page > 0)
),
"batchSize" => 1000,
"sanitize" => true
]
))->execute();
}
function generateEmojis(): string
{
return '&#' . hexdec(trim(array_rand(file(__DIR__ . '/../emojis.txt')))) . ';';
}
function requests_next(Context $ctx): void
{
$ctx->getChatDataItem('requests_page')->then(function ($page) use ($ctx) {
$ctx->setChatDataItem('requests_page', ($page ?? 1) + 1)->then(function () use ($ctx, $page) {
search($ctx);
});
});
}
function requests_previous(Context $ctx): void
{
$ctx->getChatDataItem('requests_page')->then(function ($page) use ($ctx) {
$ctx->setChatDataItem('requests_page', ($page ?? 2) - 1)->then(function () use ($ctx) {
search($ctx);
});
});
}
function request_choose(Context $ctx): void
{
if (($account = authorization($ctx->getCallbackQuery()->getFrom()->getId())) instanceof _document) {
// Авторизован
if (!$account->active) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else if ($account->banned) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else if (!($worker = worker($account->getId()))->active) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else if ($worker->fired) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else {
// Активен аккаунт
// Инициализация ключа инстанции task в базе данных
preg_match('/\->\s#(\d+)\n/', $ctx->getCallbackQuery()->getMessage()->getText(), $matches);
// Запись ключа инстанции task (заявка на которую регистрируется сотрудник)
$ctx->setChatDataItem("request_confirmation_target", $matches[1]);
// Запрос подтверждения
$ctx->sendMessage("⚡ *Подтверждение записи*\n\n" . preg_replace('/(^[^:\s\n\r]+:)/m', '*$1*', preg_replace('/(\\\#\d+)/', '*$1*', escape($ctx->getCallbackQuery()->getMessage()->getText()))) . "\n\n*⚠️ Вы подтверждаете отправку запроса?*", [
'reply_markup' => [
'inline_keyboard' => [
[
['text' => 'Подтвердить', 'callback_data' => 'request_confirmed'],
['text' => 'Отменить', 'callback_data' => 'request_rejected']
]
]
]
])->then(function ($message) use ($ctx) {
// Запись сообщения в кеш (на случай необходимости его удаления)
$ctx->setChatDataItem("request_confirmation", $message);
});
}
}
}
function request_confirmed(Context $ctx): void
{
global $arangodb;
if (($account = authorization($ctx->getCallbackQuery()->getFrom()->getId())) instanceof _document) {
// Авторизован
$ctx->getChatDataItem("request_confirmation_target")->then(function ($_key) use ($ctx, $arangodb, $account) {
// Прочитана запрашиваемая заявка
// Инициализация инстанции task в базе данных (выбранного задания)
$task = collection::search($arangodb->session, sprintf("FOR d IN task FILTER d._key == '%s' && d.published == true && d.completed != true && worker == null RETURN d", $_key));
if ($task instanceof _document) {
// Найдена заявка (подразумевается, что не занята)
if ($worker ??= worker($account->getId())) {
// Найден сотрудник
// Запись идентификатора нового сотрудника
$task->worker = $worker->id;
// Снятие с публикации
$task->published = false;
if (document::update($arangodb->session, $task)) {
// Записано обновление в базу данных
$ctx->getChatDataItem("request_all")->then(function ($requests = []) use ($ctx) {
// Удаление сообщений связанных с запросом
foreach ($requests ?? [] as $_message) $ctx->deleteMessage($_message->getChat()->getId(), $_message->getMessageId());
});
$ctx->setChatDataItem("request_all", []);
$ctx->getChatDataItem("request_confirmation")->then(function ($message) use ($ctx) {
$ctx->deleteMessage($message->getChat()->getId(), $message->getMessageId());
});
$ctx->setChatDataItem("request_confirmation_target", null);
$ctx->sendMessage("✅ *Вы зарегистрировались на заявку:* \#$_key", ['reply_markup' => ['remove_keyboard' => true]])->then(function () use ($ctx) {
generateMenu($ctx);
});
// End of the process
$ctx->endConversation();
} else $ctx->sendMessage("❎ *Не удалось принять заявку:* \#$_key", ['reply_markup' => ['remove_keyboard' => true]])->then(function () use ($ctx) {
generateMenu($ctx);
});
} else $ctx->sendMessage("❎ *Не удалось принять заявку:* \#$_key", ['reply_markup' => ['remove_keyboard' => true]])->then(function () use ($ctx) {
generateMenu($ctx);
});
} else $ctx->sendMessage("❎ *Не удалось принять заявку:* \#$_key", ['reply_markup' => ['remove_keyboard' => true]])->then(function () use ($ctx) {
generateMenu($ctx);
});
});
}
}
function request_rejected(Context $ctx): void
{
$ctx->getChatDataItem("request_confirmation_target")->then(function ($_key) use ($ctx) {
// Прочитана запрашиваемая заявка
$ctx->getChatDataItem("request_confirmation")->then(function ($message) use ($ctx) {
$ctx->deleteMessage($message->getChat()->getId(), $message->getMessageId());
});
$ctx->setChatDataItem("request_confirmation_target", null);
$ctx->sendMessage("✅ *Вы отменили регистрацию на заявку:* \#$_key", ['reply_markup' => ['remove_keyboard' => true]])->then(function () use ($ctx) {
generateMenu($ctx);
});
// End of the process
$ctx->endConversation();
});
}
function day(Context $ctx): void
{
if (($account = authorization($ctx->getMessage()?->getFrom()?->getId() ?? $ctx->getCallbackQuery()->getFrom()->getId())) instanceof _document) {
// Авторизован
if (!$account->active) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else if ($account->banned) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else if (!($worker = worker($account->getId()))->active) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else if ($worker->fired) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else {
// Активен аккаунт
// Инициализация буфера клавиатуры
$keyboard = [];
// Генерация кнопок с выбором даты
for ($i = 1, $r = 0; $i < 15; ++$i) $keyboard[$i > 4 * ($r + 1) ? ++$r : $r][] = ['text' => ($date = (new DateTime)->modify("+$i day"))->format('d.m.Y'), 'callback_data' => $date->format('U')];
$ctx->setChatDataItem('requests_page', 1)->then(function () use ($ctx, $keyboard) {
// Отправка меню
$ctx->sendMessage('📅 Выберите дату', [
'reply_markup' => [
'inline_keyboard' => $keyboard
]
])->then(function ($message) use ($ctx) {
$ctx->getChatDataItem("menu")->then(function ($message) use ($ctx) {
// Удаление главного меню
if ($message) $ctx->deleteMessage($message->getChat()->getId(), $message->getMessageId());
$ctx->setChatDataItem("menu", null);
});
// Запись сообщения в кеш (на случай необходимости его удаления при смене страницы)
$ctx->setChatDataItem("request_day", $message);
});
});
$ctx->nextStep("search");
}
}
}
function search(Context $ctx): void
{
global $arangodb;
if (($account = authorization($ctx->getMessage()?->getFrom()?->getId() ?? $ctx->getCallbackQuery()->getFrom()->getId())) instanceof _document) {
// Авторизован
if (!$account->active) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else if ($account->banned) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else if (!($worker = worker($account->getId()))->active) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else if ($worker->fired) $ctx->sendMessage('⚠️ Свяжитесь с оператором');
else {
// Активен аккаунт
$ctx->getChatDataItem('requests_page')->then(function ($page) use ($ctx, $arangodb, $worker) {
// Найдена текущая страница
// Значение страницы по умолчанию
if (empty($page)) {
$page = 1;
$ctx->setChatDataItem('requests_page', 1);
}
$generate = function ($date) use ($ctx, $page, $arangodb, $worker) {
// Поиск заявок в ArangoDB
$tasks = requests(4, (string) $date, $page, $worker);
// Подсчёт количества прочитанных заявок из базы данных
$count = $tasks->getCount();
// Проверка существования избытка
$excess = $count > 3;
// Обрезка заявок до размера страницы (3 заявки на 1 странице)
$tasks = array_slice($tasks->getAll(), 0, 3);
if ($count === 0) {
$ctx->sendMessage('📦 *Заявок нет*')->then(function ($message) use ($ctx) {
$ctx->getChatDataItem("request_all")->then(function ($requests = []) use ($ctx, $message) {
// Удаление сообщений связанных с запросом
foreach ($requests ?? [] as $_message) $ctx->deleteMessage($_message->getChat()->getId(), $_message->getMessageId());
$ctx->setChatDataItem("request_all", $requests = [$message]);
});
});
} else {
// Найдены заявки
$ctx->getChatDataItem("request_day")->then(function ($message) use ($ctx, $arangodb, $tasks, $page, $excess) {
// Удаление предыдущего меню с выбором даты
if ($message) $ctx->deleteMessage($message->getChat()->getId(), $message->getMessageId());
$ctx->setChatDataItem("request_day", null)->then(function () use ($ctx, $arangodb, $tasks, $page, $excess) {
$ctx->getChatDataItem("request_all")->then(function ($requests = []) use ($ctx, $arangodb, $tasks, $excess, $page) {
// Удаление сообщений связанных с запросом
foreach ($requests ?? [] as $_message) $ctx->deleteMessage($_message->getChat()->getId(), $_message->getMessageId());
$ctx->setChatDataItem("request_all", [])->then(function () use ($ctx, $arangodb, $tasks, $excess, $page) {
foreach ($tasks as $i => $task) {
// Перебор найденных заявок
if (($market = collection::search(
$arangodb->session,
sprintf(
"FOR d IN market FILTER d.id == '%s' RETURN d",
$task->market
)
)) instanceof _document) {
// Найден магазин
$ctx->getChatDataItem("request_$i")->then(function ($message) use ($ctx, $task, $market, $tasks, $i, $page, $excess) {
// Удаление предыдущего сообщения на этой позиции
if ($message) $ctx->deleteMessage($message->getChat()->getId(), $message->getMessageId());
$ctx->setChatDataItem("request_$i", null)->then(function () use ($ctx, $task, $market, $tasks, $i, $page, $excess) {
// Генерация эмодзи
/* $emoji = generateEmojis(); */
// Отправка сообщения
$ctx->sendMessage(
preg_replace(
'/([._\-()!#])/',
'\\\$1',
"*#$task->market* -\> *#{$task->getKey()}*\n" . (new DateTime('@' . $task->date))->format('d.m.Y') . " (" . $task->start . " - " . $task->end . ")\n\n*Город:* $market->city\n*Адрес:* $market->address\n*Работа:* $task->work" . (mb_strlen($task->description) > 0 ? "\n\n$task->description" : '')
),
[
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '✅ Отправить запрос', 'callback_data' => 'request_choose']
]
]
]
]
)->then(function ($message) use ($ctx, $tasks, $i, $page, $excess) {
// Запись сообщения в кеш (на случай необходимости его удаления при смене страницы)
$ctx->setChatDataItem("request_$i", $message)->then(function () use ($ctx, $message, $tasks, $i, $page, $excess) {
$ctx->getChatDataItem("request_all")->then(function ($requests = []) use ($ctx, $message, $tasks, $i, $page, $excess) {
$ctx->setChatDataItem("request_all", $requests = ($requests ?? []) + [count($requests) => $message])->then(function () use ($ctx, $tasks, $i, $page, $excess) {
if ($i === array_key_last($tasks)) {
// End of the process
$ctx->endConversation();
// Удаление предыдущего меню
$ctx->getChatDataItem("request_menu")->then(function ($message) use ($ctx, $page, $excess) {
if ($message) $ctx->deleteMessage($message->getChat()->getId(), $message->getMessageId());
$ctx->setChatDataItem("request_menu", null)->then(function () use ($ctx, $page, $excess) {
// Инициализация буфера для меню поиска
$keyboard = [];
// Генерация кнопки: "Предыдущая страница"
if ($page > 1) $keyboard[] = ['text' => 'Назад', 'callback_data' => 'requests_previous'];
// Генерация кнопки: "Отображённая страница"
$keyboard[] = ['text' => $page, 'callback_data' => 'requests_current'];
// Генерация кнопки: "Следующая страница"
if ($excess) $keyboard[] = ['text' => 'Вперёд', 'callback_data' => 'requests_next'];
// Отправка меню
$ctx->sendMessage('🔍 Выберите заявку', [
'reply_markup' => [
'inline_keyboard' => [
$keyboard
]
]
])->then(function ($message) use ($ctx) {
// Запись сообщения в кеш (на случай необходимости его удаления при смене страницы)
$ctx->setChatDataItem("request_menu", $message);
});
});
});
}
});
});
});
});
});
});
}
}
});
});
});
});
}
};
// Инициализация даты и генерация
$ctx->getChatDataItem('requests_date')->then(function ($old) use ($ctx, $generate) {
$new = $ctx->getCallbackQuery()->getData();
if ($new === (string) (int) $new && $new <= PHP_INT_MAX && $new >= ~PHP_INT_MAX) $ctx->setChatDataItem('requests_date', $new)->then(fn () => $generate($new));
else $generate($old);
});
});
}
}
}
$config = new Config();
$config->setParseMode(Config::PARSE_MODE_MARKDOWN);
$bot = new Zanzara(require(__DIR__ . '/../settings/key.php'), $config);
$stop = false;
$bot->onUpdate(function (Context $ctx) use (&$stop): void {
$message = $ctx->getMessage();
if (
isset($message)
&& ($contact = $message->getContact())
&& $contact->getUserId() === $message->getFrom()->getId()
) {
// Передан контакт со своими данными (подразумевается второй шаг аутентификации и запуск регистрации)
// Запуск регистрации
if (registration($contact->getUserId(), $contact->getPhoneNumber())) {
// Успешная регистрация
$ctx->sendMessage('✅ *Аккаунт подключен*', ['reply_markup' => ['remove_keyboard' => true]])->then(function () use ($ctx) {
generateMenu($ctx);
});
$stop = true;
} else $ctx->sendMessage('⛔ *Вы не авторизованы*', generateAuthenticationKeyboard());
} else if ($message?->getText() !== '🔐 Аутентификация' && !authorization($message?->getFrom()?->getId() ?? $ctx->getCallbackQuery()->getFrom()->getId())) {
$ctx->sendMessage('⛔ *Вы не авторизованы*', generateAuthenticationKeyboard());
$stop = true;
}
});
$bot->onCommand('start', function (Context $ctx) use ($stop): void {
if ($stop) return;
$ctx->getChatDataItem("request_all")->then(function ($requests = []) use ($ctx) {
// Удаление сообщений связанных с запросом
foreach ($requests ?? [] as $_message) $ctx->deleteMessage($_message->getChat()->getId(), $_message->getMessageId());
$ctx->setChatDataItem("request_all", []);
});
$ctx->getChatDataItem("menu")->then(function ($message) use ($ctx) {
// Удаление главного меню
if ($message) $ctx->deleteMessage($message->getChat()->getId(), $message->getMessageId());
$ctx->setChatDataItem("menu", null);
});
$ctx->getChatDataItem("request_day")->then(function ($message) use ($ctx) {
// Удаление меню выбора даты
if ($message) $ctx->deleteMessage($message->getChat()->getId(), $message->getMessageId());
$ctx->setChatDataItem("request_day", null);
});
generateMenu($ctx);
});
$bot->onCbQueryData(['requests_next'], fn ($ctx) => requests_next($ctx));
$bot->onCbQueryData(['requests_previous'], fn ($ctx) => requests_previous($ctx));
$bot->onCbQueryData(['request_choose'], fn ($ctx) => request_choose($ctx));
$bot->onCbQueryData(['request_confirmed'], fn ($ctx) => request_confirmed($ctx));
$bot->onCbQueryData(['request_rejected'], fn ($ctx) => request_rejected($ctx));
$bot->onCommand('day', fn ($ctx) => day($ctx));
$bot->onCbQueryData(['day'], fn ($ctx) => day($ctx));
$bot->run();