diff --git a/composer.json b/composer.json index 4b1ff7f..4f2e73b 100755 --- a/composer.json +++ b/composer.json @@ -27,7 +27,8 @@ "twig/twig": "^3.10", "twig/extra-bundle": "^3.7", "twig/intl-extra": "^3.10", - "avadim/fast-excel-reader": "^2.19" + "avadim/fast-excel-reader": "^2.19", + "openswoole/core": "22.1.5" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 78efa9e..61dbfab 100755 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e580e133abf1c4a60898f255e006a3e9", + "content-hash": "2a3687f9c81d7a26a57b654136653c18", "packages": [ { "name": "avadim/fast-excel-helper", @@ -918,6 +918,77 @@ ], "time": "2023-11-13T09:31:12+00:00" }, + { + "name": "openswoole/core", + "version": "22.1.5", + "source": { + "type": "git", + "url": "https://github.com/openswoole/core.git", + "reference": "06dae68fdac73341ccf565ecef388434bd893141" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/openswoole/core/zipball/06dae68fdac73341ccf565ecef388434bd893141", + "reference": "06dae68fdac73341ccf565ecef388434bd893141", + "shasum": "" + }, + "require": { + "ext-openswoole": ">=22.0", + "php": ">=7.4", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-middleware": "^1.0.0" + }, + "require-dev": { + "ext-curl": "*", + "ext-sockets": "*", + "friendsofphp/php-cs-fixer": "^3.6", + "openswoole/ide-helper": "^22.0", + "php-http/psr7-integration-tests": "^1.1", + "phpunit/phpunit": "^9.5" + }, + "suggest": { + "ext-mysqli": "*", + "ext-pdo": "*", + "ext-redis": "Required to use redis database, and the required version is greater than or equal to 3.1.3" + }, + "type": "library", + "autoload": { + "files": [ + "src/Coroutine/functions.php" + ], + "psr-4": { + "OpenSwoole\\Core\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "OpenSwoole Group", + "email": "hello@openswoole.com" + } + ], + "description": "Openswoole core library", + "homepage": "https://openswoole.com", + "keywords": [ + "http", + "http2", + "mqtt", + "openswoole", + "php", + "tcp", + "websocket" + ], + "support": { + "docs": "https://openswoole.com/docs", + "issues": "https://github.com/openswoole/openswoole/issues", + "pull-request": "https://github.com/openswoole/openswoole/pulls", + "source": "https://github.com/openswoole/openswoole" + }, + "time": "2023-12-10T19:02:13+00:00" + }, { "name": "opis/closure", "version": "3.6.3", @@ -1596,6 +1667,119 @@ }, "time": "2023-04-04T09:50:52+00:00" }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, { "name": "psr/log", "version": "1.1.4", diff --git a/mirzaev/arming_bot/system/controllers/catalog.php b/mirzaev/arming_bot/system/controllers/catalog.php index c2e03bd..77a27f3 100755 --- a/mirzaev/arming_bot/system/controllers/catalog.php +++ b/mirzaev/arming_bot/system/controllers/catalog.php @@ -6,7 +6,6 @@ namespace mirzaev\arming_bot\controllers; // Files of the project use mirzaev\arming_bot\controllers\core, - mirzaev\arming_bot\models\catalog as model, mirzaev\arming_bot\models\entry, mirzaev\arming_bot\models\category, mirzaev\arming_bot\models\product; @@ -39,6 +38,9 @@ final class catalog extends core preg_match('/[\d]+/', $parameters['identifier'] ?? '', $matches); $identifier = $matches[0] ?? null; + // Initializint the buffer of respnse + $html = []; + if (!empty($parameters['identifier'])) { // Передана категория (идентификатор) @@ -70,30 +72,28 @@ final class catalog extends core // Запись товаров из буфера в глобальную переменную шаблонизатора $this->view->products = $product; + + // Generating filters + $this->view->filters = [ + 'brands' => product::collect( + 'd.brand.' . $this->language->name, + errors: $this->errors['catalog'] + ) + ]; + + // Generating HTML and writing to the buffer of response + $html = [ + 'filters' => count($this->view->products) > 0 ? $this->view->render('catalog/elements/filters.html') : null, + 'products' => $this->view->render('catalog/elements/products/2columns.html') + ]; } } else { // Не передана категория // Поиск категорий: "categories" (самый верхний уровень) $this->view->categories = entry::ascendants(descendant: new category, errors: $this->errors['catalog']); - - // Search for products - /* $this->view->products = product::read( - filter: 'd.deleted != true && d.hidden != true', - sort: 'd.promoting ASC, d.position ASC, d.created DESC', - amount: 6, - errors: $this->errors['catalog'] - ); */ } - // Generating filters - $this->view->filters = [ - 'brands' => product::collect( - 'd.brand.' . $this->language->name, - errors: $this->errors['catalog'] - ) - ]; - if ($_SERVER['REQUEST_METHOD'] === 'GET') { // GET request @@ -114,10 +114,8 @@ final class catalog extends core echo json_encode( [ 'title' => $title ?? '', - 'html' => [ + 'html' => $html + [ 'categories' => $this->view->render('catalog/elements/categories.html'), - 'products' => $this->view->render('catalog/elements/products/2columns.html'), - 'filters' => $this->view->render('catalog/elements/filters.html') ], 'errors' => $this->errors ] diff --git a/mirzaev/arming_bot/system/controllers/core.php b/mirzaev/arming_bot/system/controllers/core.php index 88b3ce0..5a10749 100755 --- a/mirzaev/arming_bot/system/controllers/core.php +++ b/mirzaev/arming_bot/system/controllers/core.php @@ -117,8 +117,8 @@ class core extends controller $this->settings = settings::active(); // Initializing of language - if ($this->account?->language) $this->language = $this->account->language ?? language::en; - else if ($this->settings?->language) $this->language = $this->settings->language ?? language::en; + if ($this->account?->language) $this->language = $this->account->language ?? language::en->name; + else if ($this->settings?->language) $this->language = $this->settings->language ?? language::en->name; // Initializing of preprocessor of views $this->view = new templater( diff --git a/mirzaev/arming_bot/system/controllers/session.php b/mirzaev/arming_bot/system/controllers/session.php index fcc5643..1ff7ecb 100755 --- a/mirzaev/arming_bot/system/controllers/session.php +++ b/mirzaev/arming_bot/system/controllers/session.php @@ -6,8 +6,12 @@ namespace mirzaev\arming_bot\controllers; // Files of the project use mirzaev\arming_bot\controllers\core, + mirzaev\arming_bot\models\session as model, mirzaev\arming_bot\models\account; +// Framework for ArangoDB +use mirzaev\arangodb\document; + /** * Controller of session * @@ -34,11 +38,20 @@ final class session extends core if ($_SERVER['REQUEST_METHOD'] === 'POST') { // POST request + // Declaring variables in the correct scope + $identifier = $domain = $language = null; + if ($connected = isset($this->account)) { // Found the account + // Initializing identifier of the account + $identifier = $this->account->identifier; + // Initializing language of the account $language = $this->account->language; + + // Initializing domain of the account + $domain = $this->account->domain; } else { // Not found the account @@ -46,7 +59,7 @@ final class session extends core $buffer = $parameters; - unset($buffer['authentication'], $buffer['hash']); + unset($buffer['hash']); ksort($buffer); $prepared = []; @@ -71,10 +84,10 @@ final class session extends core $data = json_decode($parameters['user']); // Initializing of the account - $account = account::initialization( + $account = account::initialize( $data->id, [ - 'id' => $data->id, + 'identifier' => $data->id, 'name' => [ 'first' => $data->first_name, 'last' => $data->last_name @@ -92,8 +105,14 @@ final class session extends core // Connecting the account to the session $connected = $this->session->connect($account, $this->errors['session']); + // Initializing identifier of the account + $identifier = $account->identifier; + // Initializing language of the account $language = $account->language; + + // Initializing domain of the account + $domain = $account->domain; } } } @@ -112,7 +131,9 @@ final class session extends core echo json_encode( [ 'connected' => (bool) $connected, - 'language' => $language ?? null, + 'identifier' => $identifier ?? null, + 'domain' => $domain ?? null, + 'language' => $language?->name ?? null, 'errors' => $this->errors ] ); @@ -131,4 +152,37 @@ final class session extends core // Exit (fail) return null; } + + + + /** + * Connect session to the telegram account + * + * @param array $parameters Parameters of the request (POST + GET) + */ + public function write(array $parameters = []): ?string + { + if (!empty($parameters) && $this->session instanceof model) { + // Found data of the program and active session + + foreach ($parameters as $name => $value) { + // Iterate over parameters + + // Validation of the parameter + if (mb_strlen($value) > 4096) continue; + + // Write data of the program to the implement object of session document from ArangoDB + $this->session->{$name} = json_decode($value, true, 10); + } + + // Write from implement object to session document from ArangoDB + document::update($this->session->__document(), $this->errors['session']); + + // Exit (success) + return null; + } + + // Exit (fail) + return null; + } } diff --git a/mirzaev/arming_bot/system/models/account.php b/mirzaev/arming_bot/system/models/account.php index 6eea86c..8c897c7 100755 --- a/mirzaev/arming_bot/system/models/account.php +++ b/mirzaev/arming_bot/system/models/account.php @@ -104,12 +104,6 @@ final class account extends core implements arangodb_document_interface ], 'domain' => $registration->getUsername(), 'robot' => $registration->isBot(), - 'banned' => false, - 'tester' => false, - 'developer' => false, - 'access' => [ - 'settings' => false - ], 'menus' => [ 'attachments' => $registration->getAddedToAttachmentMenu() ], @@ -124,6 +118,12 @@ final class account extends core implements arangodb_document_interface 'inline' => $registration->getSupportsInlineQueries() ] ]) + [ + 'banned' => false, + 'tester' => false, + 'developer' => false, + 'access' => [ + 'settings' => false + ], 'version' => ROBOT_VERSION, 'active' => true ], diff --git a/mirzaev/arming_bot/system/models/product.php b/mirzaev/arming_bot/system/models/product.php index d3c943e..50ab827 100755 --- a/mirzaev/arming_bot/system/models/product.php +++ b/mirzaev/arming_bot/system/models/product.php @@ -213,13 +213,13 @@ final class product extends core /** * Collect parameter from all products * - * @param string $name Name of the parameter (AQL path) + * @param string $return Return (AQL path) * @param array &$errors Registry of errors * * @return array Array with found unique parameter values from all products (can be empty) */ public static function collect( - string $name = 'd._key', + string $return = 'd._key', array &$errors = [] ): array { try { @@ -227,13 +227,15 @@ final class product extends core // Initialized the collection if ($result = collection::execute( - <<<'AQL' - FOR d IN @@collection - RETURN DISTINCT @parameter - AQL, + sprintf( + <<<'AQL' + FOR d IN @@collection + RETURN DISTINCT %s + AQL, + empty($return) ? 'd._key' : $return + ), [ '@collection' => static::COLLECTION, - 'parameter' => $name ], errors: $errors )) { diff --git a/mirzaev/arming_bot/system/models/session.php b/mirzaev/arming_bot/system/models/session.php index d6b7196..35e6fb2 100755 --- a/mirzaev/arming_bot/system/models/session.php +++ b/mirzaev/arming_bot/system/models/session.php @@ -10,7 +10,8 @@ use mirzaev\arming_bot\models\account, mirzaev\arming_bot\models\enumerations\session as verification, mirzaev\arming_bot\models\traits\status, mirzaev\arming_bot\models\traits\document as arangodb_document_trait, - mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface; + mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface, + mirzaev\arming_bot\models\enumerations\language; // Framework for ArangoDB use mirzaev\arangodb\collection, @@ -158,11 +159,14 @@ final class session extends core implements arangodb_document_interface // Found active settings // Initializing the object - $account = new static; + $account = new account; if (method_exists($account, '__document')) { // Object can implement a document from ArangoDB + // Abstractioning of parameters + $result->language = language::{$result->language} ?? 'en'; + // Writing the instance of account document from ArangoDB to the implement object $account->__document($result); diff --git a/mirzaev/arming_bot/system/public/index.php b/mirzaev/arming_bot/system/public/index.php index d4f7db0..64ed9c1 100755 --- a/mirzaev/arming_bot/system/public/index.php +++ b/mirzaev/arming_bot/system/public/index.php @@ -24,7 +24,7 @@ define('SETTINGS', realpath('..' . DIRECTORY_SEPARATOR . 'settings')); define('INDEX', __DIR__); define('THEME', 'default'); -// Инициализация библиотек +// Initialize dependencies require __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR @@ -33,13 +33,14 @@ require __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; -// Инициализация маршрутизатора +// Initialize the router $router = new router; -// Initializing of routes +// Initialize routes $router ->write('/', 'catalog', 'index', 'GET') ->write('/search', 'catalog', 'search', 'POST') + ->write('/session/write', 'session', 'write', 'POST') ->write('/session/connect/telegram', 'session', 'telegram', 'POST') ->write('/category/$identifier', 'catalog', 'index', 'POST') ->write('/category', 'catalog', 'index', 'POST') @@ -65,8 +66,8 @@ $router ->sort(); var_dump($router->routes); */ -// Инициализация ядра +// Initialize the core $core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false)); -// Обработка запроса +// Handle the request echo $core->start(); diff --git a/mirzaev/arming_bot/system/public/js/account.js b/mirzaev/arming_bot/system/public/js/account.js new file mode 100755 index 0000000..583d21e --- /dev/null +++ b/mirzaev/arming_bot/system/public/js/account.js @@ -0,0 +1,117 @@ +"use strict"; + +// Import dependencies +import("/js/core.js").then(() => + import("/js/damper.js").then(() => + import("/js/telegram.js").then(() => { + const dependencies = setInterval(() => { + if ( + typeof core === "function" && + typeof core.damper === "function" && + typeof core.telegram === "function" + ) { + clearInterval(dependencies); + clearTimeout(timeout); + initialization(); + } + }, 10); + const timeout = setTimeout(() => { + clearInterval(dependencies); + initialization(); + }, 5000); + + function initialization() { + if (typeof core.account === "undefined") { + // Not initialized + + // Write to the core + core.account = class account { + // Wrap of indicator of the account + static wrap = document.getElementById("account"); + + // Indicator of the account + static indicator = this.wrap.getElementsByTagName("i")[0]; + + // Description of the account + static description = this.wrap.getElementsByTagName("small")[0]; + + // Statuc of the account + static connected = false; + + // Duration of the disconnected status + static timeout = 0; + + // Instance of the time counter in disconnected status + static counter; + + // Socket address (xn--e1ajlli это сокет) + static socket = "wss://arming.dev.mirzaev.sexy:9502"; + + // Instance of account to the socket + static session; + + // Iterval for reconnect + static interval; + + // Attempts to connect (when core.account.readyState === 0) + static attempts = 0; + + // Interval for block + static block; + + /** + * Authentication + * + * @return {void} + */ + static authentication() { + core.status_loading.removeAttribute("disabled"); + + const timer_for_response = setTimeout(() => { + core.status_loading.setAttribute("disabled", true); + }, 3000); + + if (core.telegram.api.initData.length > 0) { + core.request( + "/session/connect/telegram", + core.telegram.api.initData, + ) + .then((json) => { + if ( + json.errors !== null && + typeof json.errors === "object" && + json.errors.length > 0 + ) { + // Errors received + } else { + // Errors not received + + if (json.connected === true) { + core.status_loading.setAttribute("disabled", true); + clearTimeout(timer_for_response); + + const a = + core.status_account.getElementsByTagName("a")[0]; + a.setAttribute("onclick", "core.account.profile()"); + a.innerText = json.domain.length > 0 + ? "@" + json.domain + : "ERROR"; + } + + if ( + json.language !== null && + typeof json.language === "string" && + json.langiage.length === 2 + ) { + core.language = json.language; + } + } + }); + } + } + }; + } + } + }) + ) +); diff --git a/mirzaev/arming_bot/system/public/js/authentication.js b/mirzaev/arming_bot/system/public/js/authentication.js index 9a7b1cf..8ad006f 100755 --- a/mirzaev/arming_bot/system/public/js/authentication.js +++ b/mirzaev/arming_bot/system/public/js/authentication.js @@ -2,64 +2,34 @@ // Import dependencies import("/js/core.js").then(() => - import("/js/damper.js").then(() => { - import("/js/telegram.js").then(() => { - const dependencies = setInterval(() => { - if ( - typeof core === "function" && - typeof core.damper === "function" && - typeof core.telegram === "function" - ) { + import("/js/damper.js").then(() => + import("/js/telegram.js").then(() => + import("/js/account.js").then(() => { + const dependencies = setInterval(() => { + if ( + typeof core === "function" && + typeof core.damper === "function" && + typeof core.telegram === "function" && + typeof core.account === "function" + ) { + clearInterval(dependencies); + clearTimeout(timeout); + initialization(); + } + }, 10); + const timeout = setTimeout(() => { clearInterval(dependencies); - clearTimeout(timeout); initialization(); + }, 5000); + + function initialization() { + core.session.telegram(); + if (core.telegram.api.initData.length > 0) { + core.account.authentication(); + } + core.telegram.api.ready(); } - }, 10); - const timeout = setTimeout(() => { - clearInterval(dependencies); - initialization(); - }, 5000); - - function initialization() { - const timer_for_response = setTimeout(() => { - core.loading.setAttribute("disabled", true); - - const p = document.createElement("p"); - p.innerText = "Not authenticated"; - - core.footer.appendChild(p); - }, 3000); - - core.request( - "/session/connect/telegram", - "authentication=telegram&" + core.telegram.api.initData, - ) - .then((json) => { - if ( - json.errors !== null && - typeof json.errors === "object" && - json.errors.length > 0 - ) { - // Errors received - } else { - // Errors not received - - if (json.connected === true) { - core.loading.setAttribute("disabled", true); - - clearTimeout(timer_for_response); - } - - if ( - json.language !== null && - typeof json.language === "string" && - json.langiage.length === 2 - ) { - core.language = json.language; - } - } - }); - } - }); - }) + }) + ) + ) ); diff --git a/mirzaev/arming_bot/system/public/js/catalog.js b/mirzaev/arming_bot/system/public/js/catalog.js index bc67a1c..4277f4f 100755 --- a/mirzaev/arming_bot/system/public/js/catalog.js +++ b/mirzaev/arming_bot/system/public/js/catalog.js @@ -38,7 +38,7 @@ import("/js/core.js").then(() => /** * Select a category (interface) * - * @param {HTMLElement} button Button of category + * @param {HTMLElement} button Button of a category * @param {bool} clean Clear search bar? * @param {bool} force Ignore the damper? * @@ -76,13 +76,13 @@ import("/js/core.js").then(() => * * @return {Promise} Request to the server */ - static __category(category, clean = true) { - if (typeof category === "string") { + static __category(identifier, clean = true) { + if (typeof identifier === "string") { // Received required parameters return core.request( "/category" + - ("/" + category).replace(/^\/*/, "/").trim().replace( + ("/" + identifier).replace(/^\/*/, "/").trim().replace( /\/*$/, "", ), @@ -146,9 +146,11 @@ import("/js/core.js").then(() => element, search.nextSibling, ); - - element.outerHTML = json.html.categories; + } else { + core.main.append(element); } + + element.outerHTML = json.html.categories; } } else { // Not received categories (deinitialization of the categories) @@ -156,11 +158,58 @@ import("/js/core.js").then(() => const categories = core.main.querySelector( 'section[data-catalog-type="categories"', ); + if (categories instanceof HTMLElement) { categories.remove(); } } + if ( + typeof json.html.filters === "string" && + json.html.filters.length > 0 + ) { + // Received filters (reinitialization of the filters) + + const filters = core.main.querySelector( + 'section[data-section="filters"]', + ); + + if (filters instanceof HTMLElement) { + // Found list of filters + + filters.outerHTML = json.html.filters; + } else { + // Not found list of categories + + const element = document.createElement("section"); + + const categories = core.main.querySelector( + 'section[data-catalog-type="categories"]', + ); + + if (categories instanceof HTMLElement) { + core.main.insertBefore( + element, + categories.nextSibling, + ); + } else { + core.main.append(element); + } + + element.outerHTML = json.html.filters; + } + } else { + // Not received categories (deinitialization of the categories) + + const filters = core.main.querySelector( + 'section[data-section="filters"', + ); + + if (filters instanceof HTMLElement) { + filters.remove(); + } + } + if ( typeof json.html.products === "string" && json.html.products.length > 0 @@ -179,32 +228,8 @@ import("/js/core.js").then(() => // Not found list of products const element = document.createElement("section"); - - const categories = core.main.querySelector( - 'section[data-catalog-type="categories"]', - ); - - if (categories instanceof HTMLElement) { - core.main.insertBefore( - element, - categories.nextSibling, - ); - - element.outerHTML = json.html.products; - } else { - const search = core.main.querySelector( - 'search[data-section="search"]', - ); - - if (search instanceof HTMLElement) { - core.main.insertBefore( - element, - search.nextSibling, - ); - - element.outerHTML = json.html.products; - } - } + core.main.append(element); + element.outerHTML = json.html.products; } } else { // Not received products (deinitialization of the products) @@ -234,14 +259,14 @@ import("/js/core.js").then(() => static search(event, element, force = false) { element.classList.remove("error"); - if (element.innerText.length === 1) { + if (element.value.length === 1) { return; } else if (event.keyCode === 13) { // Button: "enter" element.setAttribute("disabled", true); - this.__search(element); + this._search(element, force); } else { // Button: any @@ -375,12 +400,17 @@ import("/js/core.js").then(() => /** * Open product card (interface) * - * @param {string} identifier Identifier of a product + * @param {HTMLElement} button Button of a product * @param {bool} force Ignore the damper? * * @return {void} */ - static product(identifier, force = false) { + static product(button, force = false) { + // Initializing identifier of the category + const identifier = button.getAttribute( + "data-product-identifier", + ); + this._product(identifier, force); } @@ -406,7 +436,7 @@ import("/js/core.js").then(() => * @return {Promise} Request to the server */ static __product(identifier) { - if (typeof identifier === "number") { + if (typeof identifier === "string") { // return core.request(`/product/${identifier}`) diff --git a/mirzaev/arming_bot/system/public/js/connection.js b/mirzaev/arming_bot/system/public/js/connection.js new file mode 100755 index 0000000..8596603 --- /dev/null +++ b/mirzaev/arming_bot/system/public/js/connection.js @@ -0,0 +1,315 @@ +"use strict"; + +// Import dependencies +import("/js/core.js").then(() => + import("/js/damper.js").then(() => { + const dependencies = setInterval(() => { + if ( + typeof core === "function" && + typeof core.damper === "function" + ) { + clearInterval(dependencies); + clearTimeout(timeout); + initialization(); + } + }, 10); + const timeout = setTimeout(() => { + clearInterval(dependencies); + initialization(); + }, 5000); + + function initialization() { + if (typeof core.connection === "undefined") { + // Not initialized + + // Write to the core + core.connection = class connection { + // Wrap of indicator of the connection + static wrap = document.getElementById("connection"); + + // Indicator of the connection + static indicator = this.wrap.getElementsByTagName("i")[0]; + + // Description of the connection + static description = this.wrap.getElementsByTagName("small")[0]; + + // Statuc of the connection + static connected = false; + + // Duration of the disconnected status + static timeout = 0; + + // Instance of the time counter in disconnected status + static counter; + + // Socket address (xn--e1ajlli это сокет) + static socket = "wss://arming.dev.mirzaev.sexy:9502"; + + // Instance of connection to the socket + static session; + + // Iterval for reconnect + static interval; + + // Attempts to connect (when core.connection.readyState === 0) + static attempts = 0; + + // Interval for block + static block; + + /** + * Initialize status of the connection to socket + * + * @param {bool} connected Connected? + * + * @return {void} + */ + static status(connected = false) { + if (this.indicator instanceof HTMLElement) { + // Initialized the indicator + + if (this.connected = connected) { + // Connected + + this.wrap.setAttribute("title", "Connected"); + + this.indicator.classList.remove("disconnected"); + this.indicator.classList.add("connected"); + + clearInterval(this.counter); + this.description.innerText = ""; + this.counter = undefined; + this.timeout = 0; + } else { + // Disconnected + + this.wrap.setAttribute("title", "Disconnected"); + + this.indicator.classList.remove("connected"); + this.indicator.classList.add("disconnected"); + + if (typeof this.counter === "undefined") { + this.counter = setInterval(() => { + this.timeout += 0.01; + this.description.innerText = this.timeout.toFixed(2); + }, 10); + } + } + } + } + + /** + * Connect to the socket + * + * @param {bool|number} interval Connection check interval (ms) + * @param {function} preprocessing Will be executed every cycle + * @param {function} onmessage New message + * @param {function} onopen Connection opened + * @param {function} onclose Connection closed + * @param {function} onerror An error has occurred + * + * @return {Promise} + */ + static connect( + interval = false, + preprocessing, + onmessage, + onopen, + onclose, + onerror, + ) { + return new Promise((resolve, reject) => { + try { + if (typeof interval === "number" && interval > 0) { + // Connect with automatic reconnect + + if (typeof this.interval === "undefined") { + this.interval = setInterval(() => { + preprocessing(); + + if ( + !(this.session instanceof WebSocket) || + (this.session.readyState === 3 || + this.session.readyState === 4) || + (this.session.readyState === 0 && + ++this.attempts > 10) + ) { + this.attempts = 0; + + if (this.session instanceof WebSocket) { + this.session.close(); + } + + this.session = new WebSocket(this.socket); + this.session.addEventListener("message", (e) => { + try { + const json = JSON.parse(e.data); + + if (json.type === "registration") { + // Подключение сокета к сессии + + fetch("/socket/registration", { + method: "POST", + headers: { + "Content-Type": + "application/x-www-form-urlencoded", + }, + body: `key=${json.key}`, + }); + } + } catch (_e) {} + }); + this.session.addEventListener("message", onmessage); + this.session.addEventListener("open", onopen); + this.session.addEventListener("close", onclose); + this.session.addEventListener("error", onerror); + + resolve(this.session); + } else resolve(this.session); + }, interval); + } + } else { + // Connect without reconnecting + + if ( + !(this.session instanceof WebSocket) || + (this.session.readyState === 3 || + this.session.readyState === 4) + ) { + if (this.session instanceof WebSocket) { + this.session.close(); + } + + this.session = new WebSocket(this.socket); + this.session.addEventListener("message", onmessage); + this.session.addEventListener("open", onopen); + this.session.addEventListener("close", onclose); + this.session.addEventListener("error", onerror); + + resolve(this.session); + } else resolve(this.session); + } + } catch (_e) {} + }); + } + + /** + * Core is connected to the socket? + * + * @return {bool} + */ + static connected() { + return this.session instanceof WebSocket && + this.session.readyState === 1; + } + }; + } + + + core.connection.connect( + 3000, + () => + core.connection.status( + core.connection.session instanceof WebSocket && + core.connection.session.readyState === 1, + ), + (e) => { + try { + const json = JSON.parse(e.data); + + if (json.target === "task") { + // Заявка + + // Инициализация строки + const row = document.getElementById(json._key); + + if (row instanceof HTMLElement) { + // Инициализирована строка + + if (json.type === "blocked") { + // Заблокирована заявка + + // Запись статуса: "заблокирована" + row.setAttribute("data-blocked", json.account._key); + row.setAttribute( + "title", + "Редактирует: " + json.account.name, + ); + + // Удалить блокировку (60000 === 1 минута) (в базе данных стоит expires 1 минута тоже) + setTimeout(() => { + // Удаление статуса: "заблокирована" + row.removeAttribute("data-blocked"); + row.removeAttribute("title"); + + // Обновление строки + tasks.row(row); + }, 60000); + } else if (json.type === "unblocked") { + // Разблокирована заявка + + // Удаление статуса: "заблокирована" + row.removeAttribute("data-blocked"); + row.removeAttribute("title"); + + // Обновление строки + tasks.row(row); + } else if (json.type === "updated") { + // Обновлена заявка + + // Обновление строки + tasks.row(row); + } else if (json.type === "deleted") { + // Удалена заявка + + // Удаление строки + row.remove(); + } + } + } + } catch (_e) {} + // Инициализация идентифиатора + //const id = row.getAttribute("id"); + + // Инициализация количества непрочитанных сообщений + //const messages = row.lastElementChild.innerText; + + // Инициализация статуса активной строки + //const selected = row.getAttribute("data-selected"); + + // Реинициализация строки + //row.outerHTML = data.rows; + + // Реинициализация перезаписанной строки + //row = document.getElementById(id); + + // Копирование статуса активной строки + //if ( + // typeof selected === "string" && + // selected === "true" && + // document.body.contains(document.getElementById("popup")) + //) { + // row.setAttribute("data-selected", "true"); + //} + }, + (e) => { + //connection.status( + // core.connection instanceof WebSocket && + // core.connection.readyState === 1, + //) + //console.log("Connected to WebSocket!"); + }, + (e) => { + //connection.status( + // core.connection instanceof WebSocket && + // core.connection.readyState === 1, + //) + //console.log("Connection closed"); + }, + (e) => { + //console.log("Error happens"); + }, + ); + } + }) +); diff --git a/mirzaev/arming_bot/system/public/js/core.js b/mirzaev/arming_bot/system/public/js/core.js index b35dd0d..dd0468f 100755 --- a/mirzaev/arming_bot/system/public/js/core.js +++ b/mirzaev/arming_bot/system/public/js/core.js @@ -8,8 +8,11 @@ const core = class core { // Language static language = "ru"; - // Label for the "loding" element - static loading = document.getElementById("loading"); + // Label for the "loading" element + static status_loading = document.getElementById("loading"); + + // Label for the "account" element + static status_account = document.getElementById("account"); // Label for the
element static header = document.body.getElementsByTagName("header")[0]; @@ -49,4 +52,185 @@ const core = class core { return await fetch(encodeURI(address), { method, headers, body }) .then((response) => response[type]()); } + + /** + * Сгенерировать окно выбора действия + * + * @param {string} title Верхний колонтинул + * @param {string} text Основное содержимое окна + * @param {string} left Содержимое левой кнопки + * @param {object} left_css Перечисление CSS-классов левой кнопки (массив) + * @param {function} left_click Действие после нажатия на левую кнопку + * @param {string} right Содержимое правой кнопки + * @param {object} right_css Перечисление CSS-классов правой кнопки (массив) + * @param {function} right_click Действие после нажатия на правую кнопку + * + * @return {void} + */ + /* static choose = core.damper( + ( + title = "Выбор действия", + text = "", + left = "Да", + left_css = ["grass"], + left_click = () => {}, + right = "Нет", + right_css = ["clay"], + right_click = () => {}, + ) => { + // Инициализация оболочки всплывающего окна + this.popup_body.wrap = document.createElement("div"); + this.popup_body.wrap.setAttribute("id", "popup"); + + // Инициализация всплывающего окна + const popup = document.createElement("section"); + popup.classList.add("list", "small"); + + // Инициализация заголовка всплывающего окна + const title_h3 = document.createElement("h3"); + title_h3.classList.add("unselectable"); + title_h3.innerText = title; + + // Инициализация оболочки с основной информацией + const main = document.createElement("section"); + main.classList.add("main"); + + // Инициализация колонки + const column = document.createElement("div"); + column.classList.add("column"); + + // Инициализация текста + const text_p = document.createElement("p"); + text_p.innerText = text; + + // Инициализация строки + const row = document.createElement("div"); + row.classList.add("row", "buttons"); + + // Инициализация левой кнопки + const left_button = document.createElement("button"); + left_button.classList.add(...left_css); + left_button.innerText = left; + left_button.addEventListener("click", left_click); + + // Инициализация правой кнопки + const right_button = document.createElement("button"); + right_button.classList.add(...right_css); + right_button.innerText = right; + right_button.addEventListener("click", right_click); + + // Инициализация окна с ошибками + this.popup_body.errors = document.createElement("section"); + this.popup_body.errors.classList.add( + "errors", + "window", + "list", + "calculated", + "hidden", + ); + this.popup_body.errors.setAttribute("data-errors", true); + + // Инициализация элемента-тела (оболочки) окна с ошибками + const errors = document.createElement("section"); + errors.classList.add("body"); + + // Инициализация элемента-списка ошибок + const dl = document.createElement("dl"); + + // Инициализация активного всплывающего окна + const old = document.getElementById("popup"); + + if (old instanceof HTMLElement) { + // Найдено активное окно + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + + // Удаление активного окна + old.remove(); + } + + // Запись в документ + popup.appendChild(title_h3); + + column.appendChild(text_p); + + row.appendChild(left_button); + row.appendChild(right_button); + column.appendChild(row); + + main.appendChild(column); + popup.appendChild(main); + + this.popup_body.wrap.appendChild(popup); + document.body.appendChild(this.popup_body.wrap); + + errors.appendChild(dl); + this.popup_body.errors.appendChild(errors); + this.popup_body.wrap.appendChild(this.popup_body.errors); + + // Инициализация ширины окна с ошибками + this.popup_body.errors.style.setProperty( + "--calculated-width", + popup.offsetWidth + "px", + ); + + // Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup) + function top(errors) { + errors.style.setProperty("transition", "0s"); + errors.style.setProperty( + "--top", + popup.offsetTop + popup.offsetHeight + 12 + "px", + ); + setTimeout(() => errors.style.removeProperty("transition"), 100); + } + top(this.popup_body.errors); + const resize = new ResizeObserver(() => top(this.popup_body.errors)); + resize.observe(this.popup_body.wrap); + + // Инициализация функции закрытия всплывающего окна + const click = () => { + // Блокировка + if (this.freeze) return; + + // Удаление всплывающего окна + this.popup_body.wrap.remove(); + + // Удаление статуса активной строки + row.removeAttribute("data-selected"); + + // Деинициализация быстрых действий по кнопкам + document.removeEventListener("keydown", this.buttons); + + // Сброс блокировки + this.freeze = false; + }; + + // Инициализация функции добавления функции закрытия всплывающего окна + const enable = () => + this.popup_body.wrap.addEventListener("click", click); + + // Инициализация функции удаления функции закрытия всплывающего окна + const disable = () => + this.popup_body.wrap.removeEventListener("click", click); + + // Первичная активация функции удаления всплывающего окна + enable(); + + // Добавление функции удаления всплывающего окна по событиям + popup.addEventListener("mouseenter", disable); + popup.addEventListener("mouseleave", enable); + + // Добавление функции удаления всплывающего окна по кнопкам + left_button.addEventListener("click", click); + right_button.addEventListener("click", click); + + // Фокусировка + right_button.focus(); + }, + 300, + ); */ }; diff --git a/mirzaev/arming_bot/system/public/js/session.js b/mirzaev/arming_bot/system/public/js/session.js index c659ee5..8774c5f 100755 --- a/mirzaev/arming_bot/system/public/js/session.js +++ b/mirzaev/arming_bot/system/public/js/session.js @@ -2,44 +2,47 @@ // Import dependencies import("/js/core.js").then(() => - import("/js/damper.js").then(() => { - const dependencies = setInterval(() => { - if ( - typeof core === "function" && - typeof core.damper === "function" - ) { + import("/js/damper.js").then(() => + import("/js/telegram.js").then(() => { + const dependencies = setInterval(() => { + if ( + typeof core === "function" && + typeof core.damper === "function" && + typeof core.telegram === "function" + ) { + clearInterval(dependencies); + clearTimeout(timeout); + initialization(); + } + }, 10); + const timeout = setTimeout(() => { clearInterval(dependencies); - clearTimeout(timeout); initialization(); + }, 5000); + + function initialization() { + if (typeof core.session === "undefined") { + // Not initialized + + // Write to the core + core.session = class session { + /** + * Send data of Telegram program settings to the session + * + * @return {void} + */ + static telegram() { + // + const { initData, initDataUnsafe, ...data } = core.telegram.api; + + core.request( + "/session/write", + "telegram=" + JSON.stringify(data), + ); + } + }; + } } - }, 10); - const timeout = setTimeout(() => { - clearInterval(dependencies); - initialization(); - }, 5000); - - function initialization() { - if (typeof core.session === "undefined") { - // Not initialized - - // Write to the core - core.session = class session { - /** - * Current position in hierarchy of the categories - */ - static categories = []; - - /** - * @return {void} - */ - static connect() { - core.request( - "/session/connect/telegram", - window.Telegram.WebApp.initData, - ); - } - }; - } - } - }) + }) + ) ); diff --git a/mirzaev/arming_bot/system/public/robot.php b/mirzaev/arming_bot/system/public/robot.php index 2150b84..8bb905c 100755 --- a/mirzaev/arming_bot/system/public/robot.php +++ b/mirzaev/arming_bot/system/public/robot.php @@ -36,7 +36,7 @@ define('CATALOG_IMPORT', STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx'); // Ключ чат-робота Telegram define('KEY', require(SETTINGS . DIRECTORY_SEPARATOR . 'key.php')); -// Инициализация библиотек +// Initialize dependencies require __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR diff --git a/mirzaev/arming_bot/system/public/socket.php b/mirzaev/arming_bot/system/public/socket.php new file mode 100644 index 0000000..d65b575 --- /dev/null +++ b/mirzaev/arming_bot/system/public/socket.php @@ -0,0 +1,384 @@ +column('id', Table::TYPE_INT, 4); +$accounts->column('type', Table::TYPE_STRING, 16); +$accounts->create(); + +$server->set([ + // Process + 'daemonize' => 1, + 'user' => 'www-data', + 'group' => 'www-data', + /* 'chroot' => '/data/server/', */ + 'open_cpu_affinity' => true, + /* 'cpu_affinity_ignore' => [0, 1], */ + 'pid_file' => '/var/run/php/ebala-socket.pid', + + // Server + /* 'reactor_num' => 8, + 'worker_num' => 2, */ + 'message_queue_key' => 'mq1', + 'dispatch_mode' => 4, + 'discard_timeout_request' => true, + /* 'dispatch_func' => 'my_dispatch_function', */ + + // Worker + /* 'max_request' => 0, + 'max_request_grace' => $max_request / 2, */ + + // HTTP Server max execution time, since v4.8.0 + 'max_request_execution_time' => 5, // 30s + + // Task worker + /* 'task_ipc_mode' => 2, + 'task_max_request' => 100, + 'task_tmpdir' => '/tmp', + 'task_worker_num' => 8, + 'task_enable_coroutine' => true, + 'task_use_object' => true, */ + + // Logging + 'log_level' => 1, + 'log_file' => __DIR__ . '/../logs/openswoole.log', + 'log_rotation' => Constant::LOG_ROTATION_DAILY, + 'log_date_format' => '%Y-%m-%d %H:%M:%S', + 'log_date_with_microseconds' => false, + /* 'request_slowlog_file' => false, */ + + // Enable trace logs + 'trace_flags' => Constant::TRACE_ALL, + + // TCP + 'input_buffer_size' => 2097152, + 'buffer_output_size' => 32 * 1024 * 1024, // byte in unit + 'tcp_fastopen' => true, + 'max_conn' => 10000, + 'tcp_defer_accept' => false, + 'open_tcp_keepalive' => true, + 'tcp_keepidle' => 30, // Check if there is no data for 4s. + /* 'tcp_keepinterval' => 1, // Check if there is data every 1s */ + 'tcp_keepcount' => 10, + 'open_tcp_nodelay' => true, + /* 'pipe_buffer_size' => 32 * 1024 * 1024, */ + 'socket_buffer_size' => 128 * 1024 * 1024, + + // Kernel + 'backlog' => 512, + 'kernel_socket_send_buffer_size' => 65535, + 'kernel_socket_recv_buffer_size' => 65535, + + // TCP Parser + 'open_eof_check' => true, + 'open_eof_split' => true, + 'package_eof' => '\r\n', + 'open_length_check' => true, + 'package_length_type' => 'N', + 'package_body_offset' => 8, + 'package_length_offset' => 8, + 'package_max_length' => 2 * 1024 * 1024, // 2MB + /* 'package_length_func' => 'my_package_length_func', */ + + // Coroutine + 'enable_coroutine' => true, + 'max_coroutine' => 3000, + 'send_yield' => true, + + // tcp server + 'heartbeat_idle_time' => 600, + 'heartbeat_check_interval' => 30, + 'enable_delay_receive' => false, + 'enable_reuse_port' => false, + 'enable_unsafe_event' => false, + + // Protocol + 'open_http_protocol' => true, + 'open_http2_protocol' => true, + 'open_websocket_protocol' => true, + 'open_mqtt_protocol' => false, + + // HTTP2 + 'http2_header_table_size' => 4095, + 'http2_initial_window_size' => 65534, + 'http2_max_concurrent_streams' => 1281, + 'http2_max_frame_size' => 16383, + 'http2_max_header_list_size' => 4095, + + // SSL + 'ssl_cert_file' => '/etc/letsencrypt/live/xn--80aksgi6f.xn--24-mlca2chbdebu6a.xn--p1ai/fullchain.pem', + 'ssl_key_file' => '/etc/letsencrypt/live/xn--80aksgi6f.xn--24-mlca2chbdebu6a.xn--p1ai/privkey.pem', + 'ssl_ciphers' => 'ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP', + 'ssl_protocols' => Constant::SSL_TLSv1_3, // added from v4.5.4 + 'ssl_verify_peer' => false, + /* 'ssl_sni_certs' => [ + "cs.php.net" => [ + 'ssl_cert_file' => __DIR__ . "/config/sni_server_cs_cert.pem", + 'ssl_key_file' => __DIR__ . "/config/sni_server_cs_key.pem" + ], + "uk.php.net" => [ + 'ssl_cert_file' => __DIR__ . "/config/sni_server_uk_cert.pem", + 'ssl_key_file' => __DIR__ . "/config/sni_server_uk_key.pem" + ], + "us.php.net" => [ + 'ssl_cert_file' => __DIR__ . "/config/sni_server_us_cert.pem", + 'ssl_key_file' => __DIR__ . "/config/sni_server_us_key.pem", + ], + ], */ + + // Static Files + 'document_root' => __DIR__, + 'enable_static_handler' => true, + /* 'static_handler_locations' => ['/static', '/app/images'], */ + 'http_index_files' => ['index.html', 'index.txt'], + + // Source File Reloading + 'reload_async' => true, + 'max_wait_time' => 30, + + // HTTP Server + 'http_parse_post' => true, + 'http_parse_cookie' => true, + 'upload_tmp_dir' => '/tmp', + + // Compression + 'http_compression' => true, + 'http_compression_level' => 3, // 1 - 9 + 'compression_min_length' => 20, + + // Websocket + 'websocket_compression' => true, + 'open_websocket_close_frame' => true, + 'open_websocket_ping_frame' => true, // added from v4.5.4 + 'open_websocket_pong_frame' => true, // added from v4.5.4 + + // TCP User Timeout + 'tcp_user_timeout' => 120, + + // DNS Server + 'dns_server' => '1.1.1.1', + /* 'dns_cache_refresh_time' => 60, */ + /* 'enable_preemptive_scheduler' => 0, */ + + /* 'open_fastcgi_protocol' => 0, */ + 'open_redis_protocol' => 0, + + 'event_object' => false, +]); + +/* $server->set([ + 'ssl_cert_file' => __DIR__ + . DIRECTORY_SEPARATOR . '..' + . DIRECTORY_SEPARATOR . 'settings' + . DIRECTORY_SEPARATOR . 'certificates' + . DIRECTORY_SEPARATOR . '84.22.137.106.pem', + 'ssl_key_file' => __DIR__ + . DIRECTORY_SEPARATOR . '..' + . DIRECTORY_SEPARATOR . 'settings' + . DIRECTORY_SEPARATOR . 'certificates' + . DIRECTORY_SEPARATOR . '84.22.137.106-key.pem' +]); */ + +$server->on("Start", function (Server $server) use ($core, $model) { + // Очистка баз данных для сокетов + collection::truncate($model->arangodb->session, 'socket'); + collection::truncate($model->arangodb->session, 'session_edge_socket'); + + // Запись в буфер вывода (журнал) + echo "Swoole WebSocket Server is started at " . $server->host . ":" . $server->port . "\n"; +}); + +$server->on('Open', function (Server $server, Request $request) use ($accounts, $core, $model) { + // Инициализация идентификатора + $id = $request->fd; + + // Запись в базу данных + $accounts->set((string) $id, [ + 'id' => $id + ]); + + // Запись в буфер вывода (журнал) + echo '[' . date('Y.m.d h:i:s', time()) . "][$id] Connected ({$accounts->count()})" . PHP_EOL; + + // Регистрация + if (document::write($model->arangodb->session, 'socket', [ + 'id' => (int) $id, + 'key' => $key = sodium_bin2hex(sodium_crypto_generichash((string) $id)), + 'expires' => (int) strtotime('+1 hour') + ])) { + // Создана инстанция сокета (регистрация) + + // Отправка данных для подключения + $server->push($id, sprintf( + <<on('Message', function (Server $server, Frame $frame) use ($accounts, $core, $model) { + // Запись в буфер вывода (журнал) + echo '[' . date('Y.m.d h:i:s', time()) . "][$frame->fd] Message: $frame->data" . PHP_EOL; + + if ($account = socket::account($frame->fd)) { + // Авторизован аккаунт + + // Инициализация полученных данных + $message = json_decode($frame->data, false, 2); + + // Инициализация имени + $name = trim((!empty($account->name['first']) ? mb_strtoupper(mb_substr($account->name['first'], 0, 1)) . '.' : '') . (!empty($account->name['last']) ? ' ' . mb_strtoupper(mb_substr($account->name['last'], 0, 1)) . '.' : '') . (!empty($account->name['second']) ? ' ' . $account->name['second'] : ''), ' '); + + try { + if ($message->target === 'task') { + // Заявка + + if ($message->type === 'block') { + // Блокировка редактирования + + if ($account->status() && ($account->type === 'administrator' || $account->type === 'operator')) + if (task::block((int) $message->_key, (int) $account->getKey())) { + // Заблокирована заявка + + // Отправка сообщения всем сокетам + foreach ($accounts as $key => $value) + if ((int) $key !== $frame->fd) + $server->push((int) $key, <<_key, + "account": { + "_key": {$account->getKey()}, + "name": "$name" + } + } + JSON); + } + } else if ($message->type === 'unblock') { + // Разблокировка редактирования + + if ($account->status() && ($account->type === 'administrator' || $account->type === 'operator')) + if (task::unblock((int) $message->_key, (int) $account->getKey())) { + // Заблокирована заявка + + + // Отправка сообщения всем сокетам + foreach ($accounts as $key => $value) + if ((int) $key !== $frame->fd) + $server->push((int) $key, <<_key, + "account": { + "_key": {$account->getKey()}, + "name": "$name" + } + } + JSON); + } + } else if ($message->type === 'update') { + // Обновление + + // Отправка сообщения всем сокетам + foreach ($accounts as $key => $value) + if ((int) $key !== $frame->fd) + $server->push((int) $key, <<_key + } + JSON); + } else if ($message->type === 'delete') { + // Обновление + + // Отправка сообщения всем сокетам + foreach ($accounts as $key => $value) + if ((int) $key !== $frame->fd) + $server->push((int) $key, <<_key + } + JSON); + } + } + } catch (exception $e) { + var_dump($e); + } + } +}); + +$server->on('Close', function (Server $server, int $id) use ($accounts, $core, $model) { + // Удаление из базы данных + $accounts->del((string) $id); + + // Запись в буфер вывода (журнал) + echo '[' . date('Y.m.d h:i:s', time()) . "][$id] Disconnect (server) ({$accounts->count()})" . PHP_EOL; +}); + +$server->on('Disconnect', function (Server $server, int $id) use ($accounts, $core, $model) { + // Удаление из базы данных + $accounts->del((string) $id); + + // Запись в буфер вывода (журнал) + echo '[' . date('Y.m.d h:i:s', time()) . "][$id] Disconnect (client) ({$accounts->count()})" . PHP_EOL; +}); + +// Запуск (точка входа) +$server->start(); + diff --git a/mirzaev/arming_bot/system/public/themes/default/css/account.css b/mirzaev/arming_bot/system/public/themes/default/css/account.css new file mode 100644 index 0000000..25571a9 --- /dev/null +++ b/mirzaev/arming_bot/system/public/themes/default/css/account.css @@ -0,0 +1,11 @@ +@charset "UTF-8"; + +section#account { + z-index: 999; + position: fixed; + bottom: 20px; + left: 20px; + height: 16px; + display: flex; +} + diff --git a/mirzaev/arming_bot/system/public/themes/default/css/catalog/1row_with_description.css b/mirzaev/arming_bot/system/public/themes/default/css/catalog/1row_with_description.css index e560926..0bd65a0 100644 --- a/mirzaev/arming_bot/system/public/themes/default/css/catalog/1row_with_description.css +++ b/mirzaev/arming_bot/system/public/themes/default/css/catalog/1row_with_description.css @@ -79,7 +79,7 @@ main>section[data-section="catalog"]>article.product>a { padding: 4px 8px 4px 8px; overflow: hidden; text-overflow: ellipsis; - word-break: break-all; + white-space: nowrap; font-weight: bold; backdrop-filter: brightness(0.4) contrast(1.2); color: var(--text-light, var(--tg-theme-text-color)); diff --git a/mirzaev/arming_bot/system/public/themes/default/css/catalog/2columns.css b/mirzaev/arming_bot/system/public/themes/default/css/catalog/2columns.css index 4cd747d..a11ce9e 100644 --- a/mirzaev/arming_bot/system/public/themes/default/css/catalog/2columns.css +++ b/mirzaev/arming_bot/system/public/themes/default/css/catalog/2columns.css @@ -44,7 +44,7 @@ main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[ filter: brightness(60%); } -main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:hover>img { +main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:is(:hover, :focus)>img { filter: unset; } @@ -92,16 +92,16 @@ main>section[data-section="catalog"][data-catalog-type="products"]>div.column>ar cursor: pointer; } -main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover { +main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:is(:hover, :focus) { /* flex-grow: 0.1; */ /* background-color: var(--tg-theme-secondary-bg-color); */ } -main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover>* { +main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:is(:hover, :focus)>* { transition: 0s; } -main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:not(:hover)>* { +main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:not(:is(:hover, :focus))>* { transition: 0.2s ease-out; } @@ -163,19 +163,7 @@ main>section[data-section="cart"]>i.icon.shopping.cart { } main>section[data-section="filters"] { - --diameter: 4rem; - z-index: 999; - right: 5vw; - bottom: 5vw; - position: fixed; - width: var(--diameter); - height: var(--diameter); + max-height: 2.5rem; display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - border-radius: 100%; - background-color: var(--tg-theme-button-color); + align-items: start; } - -main>section[data-section="filters"][data-filter="brand"] {} diff --git a/mirzaev/arming_bot/system/public/themes/default/css/connection.css b/mirzaev/arming_bot/system/public/themes/default/css/connection.css new file mode 100644 index 0000000..7a0a6a1 --- /dev/null +++ b/mirzaev/arming_bot/system/public/themes/default/css/connection.css @@ -0,0 +1,35 @@ +@charset "UTF-8"; + +section#connection { + z-index: 999; + position: fixed; + bottom: 20px; + right: 20px; + height: 16px; + display: flex; +} + +section#connection>i#indicator { + width: 16px; + height: 16px; + display: block; + cursor: help; + border-radius: 100%; +} + +section#connection>small { + margin-right: 7px; + height: 16px; + display: flex; + align-items: center; + font-weight: bold; + color: var(--socket-text); +} + +section#connection>i#indicator.disconnected:not(.connected) { + background-color: var(--socket-disconnected); +} + +section#connection>i#indicator.connected:not(.disconnected) { + background-color: var(--socket-connected); +} diff --git a/mirzaev/arming_bot/system/public/themes/default/css/interface/select.css b/mirzaev/arming_bot/system/public/themes/default/css/interface/select.css new file mode 100755 index 0000000..86f4817 --- /dev/null +++ b/mirzaev/arming_bot/system/public/themes/default/css/interface/select.css @@ -0,0 +1,115 @@ +@charset "UTF-8"; + +section[data-type="select"] { + --width: max(14rem, 20vw); + --height-element: 2rem; + --height-close: var(--height-element); + --height-open: max-content; + position: relative; + width: var(--width); + height: var(--height-close); + display: flex; + flex-direction: column; + cursor: context-menu; + border-radius: 0.75rem; + overflow-x: hidden; + background-color: var(--tg-theme-button-color); + transition: 0s; +} + +section[data-type="select"]:not(:focus):after { + z-index: 30; + content: ''; + top: calc(50% - 2.5px); + right: 1rem; + position: absolute; + width: 0; + height: 0; + pointer-events: none; + border-left: 5px solid transparent; + border-top: 5px solid var(--tg-theme-button-text-color, black); + border-right: 5px solid transparent; +} + +section[data-type="select"]>input { + left: -99999px; + position: absolute; + opacity: 0; +} + +section[data-type="select"]>label { + z-index: 10; + order: 2; + top: 0; + position: absolute; + width: 100%; + height: var(--height-element); + padding: 0 1rem; + display: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + box-sizing: border-box; + pointer-events: none; + color: var(--tg-theme-button-text-color); + transition: 0s; +} + +section[data-type="select"]:is([data-select="open"], :focus)>input:not(:checked)+label[for$='title'] { + display: none; +} + +section[data-type="select"]:is([data-select="open"], :focus)>input:not(:checked)+label[for$='all']:not(:hover, :active, :focus) { + filter: brightness(90%); +} + +section[data-type="select"]>input:not(:checked)+label { + cursor: pointer; + filter: brightness(80%); +} + +section[data-type="select"]:is([data-select="open"], :focus)>input+label:hover { + filter: brightness(110%); +} + +section[data-type="select"]:is([data-select="open"], :focus)>input:checked+label:hover { + filter: brightness(120%); +} + +section[data-type="select"]:is([data-select="open"], :focus)>input+label:is(:active, :focus) { + filter: brightness(60%); +} + +section[data-type="select"]:is([data-select="open"], :focus)>input:checked+label:is(:active, :focus) { + filter: brightness(70%); +} + +section[data-type="select"]>input:checked+label { + order: 1; + max-width: calc(var(--width) - 2rem - 10px); + display: inline; + line-height: var(--height-element); + padding-right: 0; +} + +section[data-type="select"]:is([data-select="open"], :focus)>input:checked+label { + max-width: initial; + padding-right: initial; +} + +section[data-type="select"]:is([data-select="open"], :focus) { + height: var(--height-open, max-content); +} + +section[data-type="select"]:is([data-select="open"], :focus)>label { + position: relative; + display: inline; + line-height: var(--height-element); + pointer-events: all; +} + +@media only screen and (max-width: 500px) { + section[data-type="select"]:only-child { + --width: 100% + } +} diff --git a/mirzaev/arming_bot/system/public/themes/default/css/main.css b/mirzaev/arming_bot/system/public/themes/default/css/main.css index 19ae8a9..dbbc0cc 100755 --- a/mirzaev/arming_bot/system/public/themes/default/css/main.css +++ b/mirzaev/arming_bot/system/public/themes/default/css/main.css @@ -2,8 +2,14 @@ :root { --text-light: #fafaff; + + --socket-connected: #2be851; + --socket-disconnected: #8e8181; + --socket-text: #b09999; } + + * { text-decoration: none; outline: none; @@ -45,6 +51,10 @@ main { transition: 0s; } +main>section:last-child { + margin-bottom: 5rem; +} + main>*[data-section] { --gap: 16px; --width: calc(100% - var(--gap) * 2); @@ -112,7 +122,7 @@ search:has(input:disabled) { cursor: pointer; } -:is(button, a[type="button"]) { +:is(button, :is(a, label)[type="button"]) { padding: 8px 16px; display: flex; justify-content: center; @@ -129,11 +139,11 @@ a[type="button"] { height: 23px; } -:is(button, a[type="button"]):is(:hover) { +:is(button, :is(a, label)[type="button"]):is(:hover, :focus) { filter: brightness(120%); } -:is(button, a[type="button"]):active { +:is(button, :is(a, label)[type="button"]):active { filter: brightness(80%); transition: 0s; } diff --git a/mirzaev/arming_bot/system/views/themes/default/account.html b/mirzaev/arming_bot/system/views/themes/default/account.html new file mode 100755 index 0000000..e7a6a05 --- /dev/null +++ b/mirzaev/arming_bot/system/views/themes/default/account.html @@ -0,0 +1,23 @@ +{% block css %} + +{% endblock %} + +{% block body %} +{% if account is empty %} +
+ + + +
+{% else %} +
+ + @{{ account.domain }} + +
+{% endif %} +{% endblock %} + +{% block js %} + +{% endblock %} diff --git a/mirzaev/arming_bot/system/views/themes/default/catalog/elements/categories.html b/mirzaev/arming_bot/system/views/themes/default/catalog/elements/categories.html index d286602..521e844 100755 --- a/mirzaev/arming_bot/system/views/themes/default/catalog/elements/categories.html +++ b/mirzaev/arming_bot/system/views/themes/default/catalog/elements/categories.html @@ -3,15 +3,15 @@ data-catalog-level="{{ level ?? 0 }}"> {% for category in categories %} {% if category.images %} - + {{ category.name[language] }}

{{ category.name[language] }}

{% else %} - +

{{ category.name[language] }}

{% endif %} diff --git a/mirzaev/arming_bot/system/views/themes/default/catalog/elements/filters.html b/mirzaev/arming_bot/system/views/themes/default/catalog/elements/filters.html index ca2cdfc..0ee4cf6 100755 --- a/mirzaev/arming_bot/system/views/themes/default/catalog/elements/filters.html +++ b/mirzaev/arming_bot/system/views/themes/default/catalog/elements/filters.html @@ -1,8 +1,14 @@ {% if filters is not empty %} -
- {% for brand in filters.brands %} - - - {% endfor %} +
+
+ + + + + {% for brand in filters.brands %} + + + {% endfor %} +
{% endif %} diff --git a/mirzaev/arming_bot/system/views/themes/default/catalog/elements/products/2columns.html b/mirzaev/arming_bot/system/views/themes/default/catalog/elements/products/2columns.html index 8934553..253a890 100755 --- a/mirzaev/arming_bot/system/views/themes/default/catalog/elements/products/2columns.html +++ b/mirzaev/arming_bot/system/views/themes/default/catalog/elements/products/2columns.html @@ -1,13 +1,13 @@ {% macro card(product) %} {% set title = product.name[language] ~ ' ' ~ product.brand[language] ~ format_dimensions(product.dimensions.x, product.dimensions.y, product.dimensions.z, ' ') ~ ' ' ~ product.weight ~ 'г' %} diff --git a/mirzaev/arming_bot/system/views/themes/default/catalog/elements/search.html b/mirzaev/arming_bot/system/views/themes/default/catalog/elements/search.html index 5fcf67d..526314b 100755 --- a/mirzaev/arming_bot/system/views/themes/default/catalog/elements/search.html +++ b/mirzaev/arming_bot/system/views/themes/default/catalog/elements/search.html @@ -1,4 +1,4 @@ - + diff --git a/mirzaev/arming_bot/system/views/themes/default/catalog/page.html b/mirzaev/arming_bot/system/views/themes/default/catalog/page.html index 1fb68e8..6b3017a 100755 --- a/mirzaev/arming_bot/system/views/themes/default/catalog/page.html +++ b/mirzaev/arming_bot/system/views/themes/default/catalog/page.html @@ -3,6 +3,7 @@ {% block css %} {{ parent() }} + @@ -13,9 +14,9 @@ {% include "/themes/default/catalog/elements/search.html" %} {% include "/themes/default/catalog/elements/categories.html" %} +{% include "/themes/default/catalog/elements/filters.html" %} {% include "/themes/default/catalog/elements/products/2columns.html" %} -{% include "/themes/default/catalog/elements/filters.html" %} {% endblock %} {% block js %} diff --git a/mirzaev/arming_bot/system/views/themes/default/connection.html b/mirzaev/arming_bot/system/views/themes/default/connection.html new file mode 100755 index 0000000..9b6fd21 --- /dev/null +++ b/mirzaev/arming_bot/system/views/themes/default/connection.html @@ -0,0 +1,14 @@ +{% block css %} + +{% endblock %} + +{% block body %} +
+ 0.00 + +
+{% endblock %} + +{% block js %} + +{% endblock %} diff --git a/mirzaev/arming_bot/system/views/themes/default/core.html b/mirzaev/arming_bot/system/views/themes/default/core.html index 005d5fb..65b9455 100755 --- a/mirzaev/arming_bot/system/views/themes/default/core.html +++ b/mirzaev/arming_bot/system/views/themes/default/core.html @@ -19,7 +19,7 @@ -
+
diff --git a/mirzaev/arming_bot/system/views/themes/default/index.html b/mirzaev/arming_bot/system/views/themes/default/index.html index 3a560bf..a849d91 100755 --- a/mirzaev/arming_bot/system/views/themes/default/index.html +++ b/mirzaev/arming_bot/system/views/themes/default/index.html @@ -1,16 +1,22 @@ {% extends "/themes/default/core.html" %} {% use "/themes/default/header.html" with css as header_css, body as header, js as header_js %} +{% use "/themes/default/connection.html" with css as connection_css, body as connection_body, js as connection_js %} +{% use "/themes/default/account.html" with css as account_css, body as account_body, js as account_js %} {% use "/themes/default/footer.html" with css as footer_css, body as footer, js as footer_js %} {% block css %} {{ parent() }} {{ block('header_css') }} +{{ block('connection_css') }} +{{ block('account_css') }} {{ block('footer_css') }} {% endblock %} {% block body %} {{ block('header') }} +{{ block('connection_body') }} +{{ block('account_body') }}
{% block main %} {{ main|raw }} @@ -21,6 +27,8 @@ {% block js %} {{ parent() }} -{{ block('footer_js') }} {{ block('header_js') }} +{{ block('connection_js') }} +{{ block('account_js') }} +{{ block('footer_js') }} {% endblock %} diff --git a/mirzaev/arming_bot/system/views/themes/default/js.html b/mirzaev/arming_bot/system/views/themes/default/js.html index 9edefd0..24c922f 100755 --- a/mirzaev/arming_bot/system/views/themes/default/js.html +++ b/mirzaev/arming_bot/system/views/themes/default/js.html @@ -3,5 +3,6 @@ + {% endblock %}