Compare commits

..

No commits in common. "32cc78da1cad22e1807d66f06833e71b4db4d1b4" and "9525ce012b2226ecd69ccb684b31343e7778dbbb" have entirely different histories.

148 changed files with 0 additions and 13351 deletions

1
.gitignore vendored
View File

@ -1 +0,0 @@
vendor

0
LICENSE Executable file → Normal file
View File

107
README.md Executable file → Normal file
View File

@ -1,109 +1,2 @@
# huesos # huesos
Basis for developing chat-robots with "Web App" technology for Telegram
## Installation
### AnangoDB
1. Create a Graph with the specified values
**Name:** catalog<br>
<br>
**edgeDefinition:** entry<br>
**fromCollections:** categoy, product<br>
**toCollections:** category
2. Create a Graph with the specified values
**Name:** sessions<br>
<br>
**edgeDefinition:** connect<br>
**fromCollections:** account<br>
**toCollections:** session
3. Create indexes for the "product" collection
**Type:** "Inverted Index"<br>
**Fields:** name.ru<br>
**Analyzer:** "text_ru"<br>
**Search field:** true<br>
**Name:** name_ru<br>
<br>
*Add indexes for all search parameters and for all languages (search language is selected based on the user's language, <br>
otherwise from the default language specified in the active settings from **settings** collection document)*<br>
<br>
*See fields in the `mirzaev/arming_bot/models/product`<br>
**name.ru**, **description.ru** and **compatibility.ru***
4. Create a View with the specified values
**type:** search-alias (you can also use "arangosearch")<br>
**name:** **product**s_search<br>
**indexes:**
```json
"indexes": [
{
"collection": "product",
"index": "title_ru" # THIS IS AN EXAMPLE
}
]
```
### NGINX
1. Example of NGINX server file
```nginx
location / {
try_files $uri $uri/ /index.php;
}
location ~ /(?<type>categories|products) {
root /var/www/arming_bot/mirzaev/arming_bot/system/storage;
try_files $uri =404;
}
location ~ \.php$ {
...
}
```
### SystemD (or any alternative you like)
1. Execute: `sudo cp telegram-huesos.service /etc/systemd/system/telegram-huesos.service`
*before you execute the command think about **what it does** and whether the **paths** are specified correctly*<br>
*the configuration file is very simple and you can remake it for any alternative to SystemD that you like*
## Settings
Settings of chat-robot and Web App<br>
<br>
Make sure you have a **settings** collection (can be created automatically) and at least one document with the "status" parameter set to "active"
```json
{
"status": "active"
}
```
### language
Language for system messages if user language could not be determined<br>
<br>
**Value:** en
## Suspensions
System of suspensions of chat-robot and Web App<br>
<br>
Make sure you have a **suspension** collection (can be created automatically)
```json
{
"end": 1726068961,
"targets": {
"chat-robot": true,
"web app": true
}
"access": {
"tester": true,
"developer": true
},
"description": {
"ru": "Разрабатываю каталог, поиск и корзину",
"en": "I am developing a catalog, search and cart"
}
}
```

View File

@ -1,44 +0,0 @@
{
"name": "mirzaev/arming_bot",
"description": "Chat-robot for tuning weapons",
"homepage": "https://t.me/arming_bot",
"type": "chat-robot",
"keywords": [
"telegram",
"chat-robot",
"military",
"shop"
],
"readme": "README.md",
"license": "WTFPL",
"authors": [
{
"name": "Arsen Mirzaev Tatyano-Muradovich",
"email": "arsen@mirzaev.sexy"
}
],
"require": {
"triagens/arangodb": "^3.8",
"mirzaev/minimal": "^2.2",
"mirzaev/arangodb": "^1.3",
"badfarm/zanzara": "^0.9.1",
"nyholm/psr7": "^1.8",
"react/filesystem": "^0.1.2",
"twig/twig": "^3.10",
"twig/extra-bundle": "^3.7",
"twig/intl-extra": "^3.10",
"avadim/fast-excel-reader": "^2.19"
},
"autoload": {
"psr-4": {
"mirzaev\\arming_bot\\": "mirzaev/arming_bot/system/"
}
},
"minimum-stability": "stable",
"config": {
"allow-plugins": {
"php-http/discovery": true,
"wyrihaximus/composer-update-bin-autoload-path": true
}
}
}

5690
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,258 +0,0 @@
<?php
declare(strict_types=1);
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;
/**
* Controller of catalog
*
* @package mirzaev\arming_bot\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class catalog extends core
{
/**
* Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => [],
'catalog' => []
];
/**
* Catalog
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function index(array $parameters = []): ?string
{
// Initializing identifier of a category
preg_match('/[\d]+/', $parameters['identifier'] ?? '', $matches);
$identifier = $matches[0] ?? null;
if (!empty($parameters['identifier'])) {
// Передана категория (идентификатор)
// Инициализация актуальной категории
$category = category::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $identifier], errors: $this->errors['catalog']);
if ($category instanceof category) {
// Found the category
// Поиск категорий или товаров входящих в актуальную категорию
$entries = entry::search(
document: $category,
amount: 30,
errors: $this->errors['catalog']
);
// Объявление буферов категорий и товаров (важно - в единственном числе, по параметру из базы данных)
$category = $product = [];
foreach ($entries as $entry) {
// Перебор вхождений
// Запись массивов категорий и товаров ($category и $product) в буфер глобальной переменной шаблонизатора
${$entry->_type}[] = $entry;
}
// Запись категорий из буфера в глобальную переменную шаблонизатора
$this->view->categories = $category;
// Запись товаров из буфера в глобальную переменную шаблонизатора
$this->view->products = $product;
}
} 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
// Exit (success)
return $this->view->render('catalog/page.html');
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// Initializing a response headers
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Initializing of the output buffer
ob_start();
// Generating the reponse
echo json_encode(
[
'title' => $title ?? '',
'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
]
);
// Initializing a response headers
header('Content-Length: ' . ob_get_length());
// Sending and deinitializing of the output buffer
ob_end_flush();
flush();
// Exit (success)
return null;
}
// Exit (fail)
return null;
}
/**
* Search
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function search(array $parameters = []): ?string
{
// Initializing of text fore search
preg_match('/[\w\s]+/u', $parameters['text'] ?? '', $matches);
$text = $matches[0] ?? null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// Search for products
$this->view->products = isset($text) ? product::read(
search: $text,
filter: 'd.deleted != true && d.hidden != true',
sort: 'd.position ASC, d.name ASC, d.created DESC',
amount: 30,
language: $this->language,
errors: $this->errors['catalog']
) : [];
// Initializing a response headers
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Initializing of the output buffer
ob_start();
// Generating the reponse
echo json_encode(
[
'title' => $title ?? '',
'html' => [
'products' => $this->view->render('catalog/elements/products/2columns.html')
],
'errors' => $this->errors
]
);
// Initializing a response headers
header('Content-Length: ' . ob_get_length());
// Sending and deinitializing of the output buffer
ob_end_flush();
flush();
// Exit (success)
return null;
}
// Exit (fail)
return null;
}
/**
* Product
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function product(array $parameters = []): ?string
{
// Initializing identifier of a product
preg_match('/[\d]+/', $parameters['identifier'] ?? '', $matches);
$identifier = $matches[0] ?? null;
if (!empty($identifier)) {
// Received identifier of the product
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// Search for products
$product = product::read(
filter: "d.identifier == @identifier && d.deleted != true && d.hidden != true",
sort: 'd.created DESC',
amount: 1,
return: '{identifier: d.identifier, name: d.name.@language, description: d.description.@language, cost: d.cost, weight: d.weight, dimensions: d.dimensions, brand: d.brand.@language, compatibility: d.compatibility.@language, images: d.images[*].storage}',
parameters: ['identifier' => (int) $identifier],
language: $this->language,
errors: $this->errors['catalog']
)[0]?->getAll();
// Initializing a response headers
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Initializing of the output buffer
ob_start();
// Generating the reponse
echo json_encode(
[
'product' => $product,
'errors' => $this->errors
]
);
// Initializing a response headers
header('Content-Length: ' . ob_get_length());
// Sending and deinitializing of the output buffer
ob_end_flush();
flush();
// Exit (success)
return null;
}
}
// Exit (fail)
return null;
}
}

View File

@ -1,206 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\views\templater,
mirzaev\arming_bot\models\core as models,
mirzaev\arming_bot\models\account,
mirzaev\arming_bot\models\session,
mirzaev\arming_bot\models\settings,
mirzaev\arming_bot\models\suspension,
mirzaev\arming_bot\models\enumerations\language;
// Framework for PHP
use mirzaev\minimal\controller;
/**
* Core of controllers
*
* @package mirzaev\arming_bot\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class core extends controller
{
/**
* Postfix for name of controllers files
*/
final public const string POSTFIX = '';
/**
* Instance of the settings
*/
protected readonly settings $settings;
/**
* Instance of a session
*/
protected readonly session $session;
/**
* Instance of an account
*/
protected readonly ?account $account;
/**
* Language
*/
protected language $language = language::en;
/**
* Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => []
];
/**
* Constructor of an instance
*
* @param bool $initialize Initialize a controller?
*
* @return void
*
* @todo settings account и session не имеют проверок на возврат null
*/
public function __construct(bool $initialize = true)
{
// Blocking requests from CloudFlare (better to write this blocking into nginx config file)
if (isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] === 'nginx-ssl early hints') return;
// For the extends system
parent::__construct($initialize);
if ($initialize) {
// Initializing is requested
// Initializing of models core (connect to ArangoDB...)
new models(true);
// Initializing of the date until which the session will be active
$expires = strtotime('+1 week');
// Initializing of default value of hash of the session
$_COOKIE["session"] ??= null;
// Initializing of a session
$this->session = new session($_COOKIE["session"], $expires, $this->errors['session']);
// Handle a problems with initializing a session
if (!empty($this->errors['session'])) exit(1);
// телеграм не сохраняет куки
/* else if ($_COOKIE["session"] !== $this->session->hash) {
// Hash of the session is changed (implies that the session has expired and recreated)
// Write a new hash of the session to cookies
setcookie(
'session',
$this->session->hash,
[
'expires' => $expires,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'strict'
]
);
} */
// Initializing of the account
$this->account = $this->session->account($this->errors['account']);
// Initializing of the settings
$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;
// Initializing of preprocessor of views
$this->view = new templater(
session: $this->session,
account: $this->account,
settings: $this->settings
);
// @todo перенести в middleware
// Search for suspensions
$suspension = suspension::search();
if ($suspension && $suspension->targets['web app']) {
// Found a suspension
if ($this->account) {
// Initialized account
foreach ($suspension->access as $type => $status) {
// Перебор статусов доступа
if ($status && $this->account->{$type}) {
// Authorized account
// Exit (success)
return $this;
}
}
// Exit (success)
goto suspension;
} else {
// Not initialized account
// Send the suspension page and exit (success)
suspension:
// Write title of the page to templater global variables
$this->view->title = match ($this->language) {
language::en => 'Suspended',
language::ru => 'Приостановлено',
default => 'Suspended'
};
// Write description of the suspension to templater global variables
$this->view->description = $suspension->description[$this->language] ?? array_values($suspension->description)[0];
// Write message of remaining time of the suspension to templater global variables
$this->view->remain = [
'title' => match ($this->language) {
language::en => 'Time remaining: ',
language::ru => 'Осталось времени: ',
default => 'Time remaining: '
},
'value' => $suspension?->message()
];
// Send the suspension page
echo $this->view->render('suspension/page.html');
// Exit (success)
exit(0);
}
}
}
}
/**
* Check of initialization
*
* Checks whether a property is initialized in a document instance from ArangoDB
*
* @param string $name Name of the property from ArangoDB
*
* @return bool The property is initialized?
*/
public function __isset(string $name): bool
{
// Check of initialization of the property and exit (success)
return match ($name) {
default => isset($this->{$name})
};
}
}

View File

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\product;
/**
* Index controller
*
* @package mirzaev\arming_bot\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class index extends core
{
/**
* Render the main page
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function index(array $parameters = []): ?string
{
// Exit (success)
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render('index.html');
// Exit (fail)
return null;
}
}

View File

@ -1,134 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\account;
/**
* Controller of session
*
* @package mirzaev\arming_bot\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class session extends core
{
/**
* Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => []
];
/**
* Connect session to the telegram account
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function telegram(array $parameters = []): ?string
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
if ($connected = isset($this->account)) {
// Found the account
// Initializing language of the account
$language = $this->account->language;
} else {
// Not found the account
if (count($parameters) > 1 && isset($parameters['hash'])) {
$buffer = $parameters;
unset($buffer['authentication'], $buffer['hash']);
ksort($buffer);
$prepared = [];
foreach ($buffer as $key => $value) {
if (is_array($value)) {
$prepared[] = $key . '=' . json_encode($value, JSON_UNESCAPED_UNICODE);
} else {
$prepared[] = $key . '=' . $value;
}
}
$key = hash_hmac('sha256', require(SETTINGS . DIRECTORY_SEPARATOR . 'key.php'), 'WebAppData', true);
$hash = bin2hex(hash_hmac('sha256', implode(PHP_EOL, $prepared), $key, true));
if (hash_equals($hash, $parameters['hash'])) {
// Data confirmed (according to telegram documentation)
if (time() - $parameters['auth_date'] < 86400) {
// Authorization date less than 1 day ago
// Initializing data of the account
$data = json_decode($parameters['user']);
// Initializing of the account
$account = account::initialization(
$data->id,
[
'id' => $data->id,
'name' => [
'first' => $data->first_name,
'last' => $data->last_name
],
'domain' => $data->username,
'language' => $data->language_code,
'messages' => $data->allows_write_to_pm
],
$this->errors['account']
);
if ($account instanceof account) {
// Initialized the account
// Connecting the account to the session
$connected = $this->session->connect($account, $this->errors['session']);
// Initializing language of the account
$language = $account->language;
}
}
}
}
}
// Initializing a response headers
header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
// Initializing of the output buffer
ob_start();
// Generating the reponse
echo json_encode(
[
'connected' => (bool) $connected,
'language' => $language ?? null,
'errors' => $this->errors
]
);
// Initializing a response headers
header('Content-Length: ' . ob_get_length());
// Sending and deinitializing of the output buffer
ob_end_flush();
flush();
// Exit (success)
return null;
}
// Exit (fail)
return null;
}
}

View File

@ -1,154 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
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\enumerations\language;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Framework for Telegram
use Zanzara\Telegram\Type\User as telegram;
// Library for ArangoDB
use ArangoDBClient\Document as _document;
// Built-in libraries
use exception;
/**
* Model of account
*
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class account extends core implements arangodb_document_interface
{
use status, arangodb_document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'account';
/**
* Initialize
*
* @param int $identifier Identifier of the account
* @param telegram|array|null $registration Данные для регистрация, если аккаунт не найден
* @param array &$errors Registry of errors
*
* @return static|null Объект аккаунта, если найден
*/
public static function initialize(int $identifier, telegram|array|null $registration = null, array &$errors = []): static|null
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
// Initializing the account
$result = collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d.identifier == @identifier
RETURN d
AQL,
[
'@collection' => static::COLLECTION,
'identifier' => $identifier
],
errors: $errors
);
if ($result instanceof _document) {
// Initialized the account
// Initializing the object
$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);
// Exit (success)
return $account;
}
} else if ($registration) {
// Not found the account and registration is requested
// Creating account
$_id = document::write(
static::COLLECTION,
(is_array($registration)
? $registration :
[
'identifier' => $registration->getId(),
'name' => [
'first' => $registration->getFirstName(),
'last' => $registration->getLastName()
],
'domain' => $registration->getUsername(),
'robot' => $registration->isBot(),
'banned' => false,
'tester' => false,
'developer' => false,
'access' => [
'settings' => false
],
'menus' => [
'attachments' => $registration->getAddedToAttachmentMenu()
],
'messages' => true,
'groups' => [
'join' => $registration->getCanJoinGroups(),
'messages' => $registration->getCanReadAllGroupMessages()
],
'premium' => $registration->isPremium(),
'language' => language::{$registration->getLanguageCode()}->name ?? 'en',
'queries' => [
'inline' => $registration->getSupportsInlineQueries()
]
]) + [
'version' => ROBOT_VERSION,
'active' => true
],
errors: $errors
);
if ($_id) {
// Created account
// Initializing of the account (without registration request)
return static::initialize($identifier, errors: $errors);
} else throw new exception('Failed to register account');
} else throw new exception('Failed to find account');
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
}

View File

@ -1,510 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\product,
mirzaev\arming_bot\models\category,
mirzaev\arming_bot\models\entry,
mirzaev\arming_bot\models\traits\files,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\traits\yandex\disk as yandex;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Framework for Excel
use avadim\FastExcelReader\Excel as excel;
// Built-in libraries
use exception;
/**
* Model of the catalog
*
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class catalog extends core
{
use yandex, files {
yandex::download as yandex;
}
/**
* Collect parameter from all products
*
* @param string $documment Path to the EXCEL-document
* @param int &$categories_loaded Counter of loaded categories
* @param int &$categories_created Counter of created categories
* @param int &$categories_updated Counter of updated categories
* @param int &$categories_deleted Counter of deleted categories
* @param int &$categories_new Counter of new categories
* @param int &$categories_old Counter of old categories
* @param int &$products_loaded Counter of loaded products
* @param int &$products_created Counter of created products
* @param int &$products_updated Counter of updated products
* @param int &$products_deleted Counter of deleted products
* @param int &$products_new Counter of new products
* @param int &$products_old Counter of old products
* @param language $language Language
* @param array &$errors Registry of errors
*
* @return void
*
* @todo
* 1. Сначала создать все категории и затем снова по циклу пройтись уже создавать entry между ними
* 2. Сжимать изображения
*/
public static function import(
string $document,
int &$categories_loaded = 0,
int &$categories_created = 0,
int &$categories_updated = 0,
int &$categories_deleted = 0,
int &$categories_old = 0,
int &$categories_new = 0,
int &$products_loaded = 0,
int &$products_created = 0,
int &$products_updated = 0,
int &$products_deleted = 0,
int &$products_old = 0,
int &$products_new = 0,
language $language = language::en,
array &$errors = []
): void {
try {
// Initializing the spreadsheet
$spreadsheet = excel::open($document);
// Inititalizing worksheets
$categories = $spreadsheet->getSheet('Категории');
$products = $spreadsheet->getSheet('Товары');
// Counting old documents
$categories_old = collection::count(category::COLLECTION, errors: $errors);
$products_old = collection::count(product::COLLECTION, errors: $errors);
// Initializing the buffer of handler categories and products
$handled = [
'categories' => [],
'products' => []
];
foreach (
$categories->nextRow(
[
'A' => 'identifier',
'B' => 'name',
'C' => 'category',
'D' => 'images',
'E' => 'position'
],
excel::KEYS_FIRST_ROW
) as $number => $row
) {
// Iterate over categories
try {
if (!empty($row['identifier']) && !empty($row['name'])) {
// Required cells are filled in
// Incrementing the counter of loaded categories
++$categories_loaded;
// Declaring the variable with the status that a new category has been created
$created = false;
// Declaring the variable with the category
$category = null;
// Initializing the category
$category = category::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $row['identifier']], errors: $errors);
if ($category instanceof category) {
// Initialized the category
// Initializing name of the category
if (empty($category->name) || empty($category->name[$language->name]) || $category->name[$language->name] !== $row['name'])
$category->name = [$language->name => $row['name']] + ($category->name ?? []);
// Initializing position of the category
if (empty($category->position) || $category->position === $row['position'])
$category->position = $row['position'];
} else {
// Not initialized the category
// Creating the category
$_id = $created = category::write((int) $row['identifier'], [$language->name => $row['name']], $row['position'] ?? null, $errors);
// Initializing the category
$category = category::_read('d._id == @_id', parameters: ['_id' => $_id], errors: $errors);
// Incrementing the counter of created categories
if ($created) ++$categories_created;
};
if ($category instanceof category) {
// Initialized the category
if (!empty($row['category'])) {
// Received the ascendant category
// Initializing the ascendant category
$ascendant = category::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $row['category']], errors: $errors);
if ($ascendant instanceof category) {
// Found the ascendant category
// Deleting entries of the category in ArangoDB
entry::banish($category, $errors);
// Writing the category as an entry to the ascendant category in ArangoDB
entry::write($category, $ascendant, $errors);
}
}
if (!empty($row['images'])) {
// Received images
// Initializing new images of the category
$images = explode(' ', trim($row['images']));
// Reinitialize images? (true, if no images found or their amount does not match)
$reinitialize = !$category->images || count($category->images) !== count($images);
// Checking the identity of existing images with new images (if reinitialization of images has not yet been requested)
if (!$reinitialize) foreach ($category->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break;
if ($reinitialize) {
// Requested reinitialization of images
// Initializing the buffer of images
$buffer = [];
foreach ($images as $index => $image) {
// Iterating over new images
// Skipping empty URI`s
if (empty($image = trim($image))) continue;
// Initializing path to directory of the image in storage
$directory = DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $row['identifier'];
// Initializing URL of the image in storage
$url = STORAGE . $directory;
// Initializing URN of the image in storage
$urn = $index . '.jpg';
// Initializing URI of the image in storage
$uri = $url . DIRECTORY_SEPARATOR . $urn;
// Initializing the directory in storage
if (!file_exists($url)) mkdir($url, 0775, true);
if (static::yandex($image, $uri, errors: $errors)) {
// The image is downloaded
// Writing the image to the buffer if images
$buffer[] = [
'source' => $image,
'storage' => $directory . DIRECTORY_SEPARATOR . $urn
];
}
}
// Initializing images of the category
$category->images = $buffer;
}
}
// Writing in ArangoDB
$updated = document::update($category->__document(), errors: $errors);
// Incrementing the counter of updated categories
if ($updated && !$created) ++$categories_updated;
} else throw new exception("Failed to initialize category: {$row['name']} ($number)");
}
// Writing to the registry of handled categories and products
$handled['categories'][] = $row['identifier'];
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
}
foreach (
$products->nextRow(
[
'A' => 'identifier',
'B' => 'name',
'C' => 'category',
'D' => 'description',
'E' => 'cost',
'F' => 'weight',
'G' => 'x',
'H' => 'y',
'I' => 'z',
'J' => 'brand',
'K' => 'compatibility',
'L' => 'images',
'M' => 'position'
],
excel::KEYS_FIRST_ROW
) as $number => $row
) {
// Iterate over products
try {
if (!empty($row['identifier']) && !empty($row['name'])) {
// Required cells are filled in
// Incrementing the counter of loaded products
++$products_loaded;
// Declaring the variable with the status that a new product has been created
$created = false;
// Declaring the variable with the product
$product = null;
// Initializing the product
$product = product::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $row['identifier']], errors: $errors);
if ($product instanceof product) {
// Initialized the product
// Initializing name of the product
if (empty($product->name) || empty($product->name[$language->name]) || $product->name[$language->name] !== $row['name'])
$product->name = [$language->name => $row['name']] + ($product->name ?? []);
// Initializing description of the product
if (empty($product->description) || empty($product->description[$language->name]) || $product->description[$language->name] !== $row['description'])
$product->description = [$language->name => $row['description']] + ($product->description ?? []);
// Initializing brand of the product
if (empty($product->brand) || empty($product->brand[$language->name]) || $product->brand[$language->name] !== $row['brand'])
$product->brand = [$language->name => $row['brand']] + ($product->brand ?? []);
// Initializing compatibility of the product
if (empty($product->compatibility) || empty($product->brand[$language->name]) || $product->compatibility[$language->name] !== $row['compatibility'])
$product->compatibility = [$language->name => $row['compatibility']] + ($product->compatibility ?? []);
// Initializing position of the product
if (empty($product->position) || $product->position !== $row['position'])
$product->position = $row['position'];
} else {
// Not initialized the product
// Creating the product
$_id = product::write(
(int) $row['identifier'],
[$language->name => $row['name']],
[$language->name => $row['description']],
(float) $row['cost'],
(float) $row['weight'],
['x' => $row['x'], 'y' => $row['y'], 'z' => $row['z']],
[$language->name => $row['brand']],
[$language->name => $row['compatibility']],
$row['position'] ?? null,
errors: $errors
);
// Initializing the product
$product = $created = product::_read('d._id == @_id', parameters: ['_id' => $_id], errors: $errors);
// Incrementing the counter of created products
if ($created) ++$products_created;
}
if ($product instanceof product) {
// Initialized the product
if (!empty($row['category'])) {
// Received the category
// Initializing the category
$category = category::_read(sprintf('d.identifier == %u', (int) $row['category']), errors: $errors);
if ($category instanceof category) {
// Found the ascendant category
// Deleting entries of the product in ArangoDB
entry::banish($product, $errors);
// Writing the product as an entry to the ascendant category in ArangoDB
entry::write($product, $category, $errors);
}
}
if (!empty($row['images'])) {
// Received images
// Initializing new images of the category
$images = explode(' ', trim($row['images']));
// Reinitialize images? (true, if no images found or their amount does not match)
$reinitialize = !$product->images || count($product->images) !== count($images);
// Checking the identity of existing images with new images (if reinitialization of images has not yet been requested)
if (!$reinitialize) foreach ($product->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break;
if ($reinitialize) {
// Requested reinitialization of images
// Initializing the buffer of images
$buffer = [];
foreach ($images as $index => $image) {
// Iterating over new images
// Skipping empty URI`s
if (empty($image = trim($image))) continue;
// Initializing path to directory of the image in storage
$directory = DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $row['identifier'];
// Initializing URL of the image in storage
$url = STORAGE . $directory;
// Initializing URN of the image in storage
$urn = $index . '.jpg';
// Initializing URI of the image in storage
$uri = $url . DIRECTORY_SEPARATOR . $urn;
// Initializing the directory in storage
if (!file_exists($url)) mkdir($url, 0775, true);
if (static::yandex($image, $uri, errors: $errors)) {
// The image is downloaded
// Writing the image to the buffer if images
$buffer[] = [
'source' => $image,
'storage' => $directory . DIRECTORY_SEPARATOR . $urn
];
}
}
// Initializing images of the category
$product->images = $buffer;
}
}
// Writing in ArangoDB
$updated = document::update($product->__document(), errors: $errors);
// Incrementing the counter of updated categories
if ($updated && !$created) ++$products_updated;
} else throw new exception("Failed to initialize product: {$row['name']} ($number)");
}
// Writing to the registry of handled categories and products
$handled['products'][] = $row['identifier'];
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
}
// Deleting old categories
foreach (
category::_read(
/* filter: sprintf('%s - d.updated > 3600', time()), */
sort: 'd.updated DESC',
amount: 100000,
errors: $errors
) ?? [] as $document
) {
// Iterating over categories
// Initializing the category
$category = new category(document: $document);
if (
$category instanceof category
&& array_search($category->identifier, $handled['categories']) === false
) {
// Not found identifier of the product in the buffer of handled categories and products
// Deleting images of the category from storage
static::delete(STORAGE . DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $category->identifier, errors: $errors);
// Deleting entries of the category in ArangoDB
entry::banish($category, errors: $errors);
// Deleting the category in ArangoDB
document::delete($category->__document(), errors: $errors);
// Incrementing the counter of deleted categories
++$categories_deleted;
}
}
// Deleting old products
foreach (
product::_read(
/* filter: sprintf('%s - d.updated > 3600', time()), */
sort: 'd.updated DESC',
amount: 100000,
errors: $errors
) ?? [] as $document
) {
// Iterating over products
// Initializing the category
$product = new product(document: $document);
if (
$product instanceof product
&& array_search($product->identifier, $handled['products']) === false
) {
// Not found identifier of the product in the buffer of handled categories and products
// Deleting images of the product from storage
static::delete(STORAGE . DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $product->identifier, errors: $errors);
// Deleting entries of the product in ArangoDB
entry::banish($product, errors: $errors);
// Deleting the product in ArangoDB
document::delete($product->__document(), errors: $errors);
// Incrementing the counter of deleted products
++$products_deleted;
}
}
// Counting new documents
$categories_new = collection::count(category::COLLECTION, errors: $errors);
$products_new = collection::count(product::COLLECTION, errors: $errors);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
}
}

View File

@ -1,85 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Built-in libraries
use exception;
/**
* Model of category
*
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class category extends core implements arangodb_document_interface
{
use arangodb_document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'category';
/**
* Write the category
*
* @param int $identifier Identifier (unique)
* @param array $name Name [['en' => value], ['ru' => значение]]
* @param int|null $position Position for soring in the catalog (ASC)
* @param array &$errors Registry of errors
*
* @return string|null Identifier (_id) of the document in ArangoDB, if created
*
* @todo
* 1. Bind parameters
*/
public static function write(
int $identifier,
array $name = [['en' => 'ERROR']],
?int $position = null,
array &$errors = []
): string|null {
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
// Writing to ArangoDB and exit (success)
return document::write(
static::COLLECTION,
[
'identifier' => $identifier,
'name' => $name,
'position' => $position,
'version' => ROBOT_VERSION
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
}

View File

@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
// Framework for ArangoDB
use mirzaev\arangodb\enumerations\collection\type;
/**
* Model of connect
*
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class connect extends core implements arangodb_document_interface
{
use arangodb_document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'connect';
/**
* Type of the collection in ArangoDB
*/
public const type TYPE = type::edge;
}

View File

@ -1,234 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Framework for PHP
use mirzaev\minimal\model;
// Framework for ArangoDB
use mirzaev\arangodb\connection as arangodb,
mirzaev\arangodb\collection,
mirzaev\arangodb\enumerations\collection\type;
// Library for ArangoDB
use ArangoDBClient\Document as _document;
// Built-in libraries
use exception;
/**
* Core of models
*
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class core extends model
{
/**
* Postfix for name of models files
*/
final public const string POSTFIX = '';
/**
* Path to the file with settings of connecting to the ArangoDB
*/
final public const string ARANGODB = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'settings' . DIRECTORY_SEPARATOR . 'arangodb.php';
/**
* Instance of the session of ArangoDB
*
* @todo ПЕРЕДЕЛАТЬ В php 8.4
*/
protected static arangodb $arangodb;
/**
* Name of the collection in ArangoDB
*/
public const string COLLECTION = 'THIS_COLLECTION_SHOULD_NOT_EXIST';
/**
* Type of the collection in ArangoDB
*/
public const type TYPE = type::document;
/**
* Constructor of an instance
*
* @param bool $initialize Initialize a model?
* @param ?arangodb $arangodb Instance of a session of ArangoDB
*
* @return void
*/
public function __construct(bool $initialize = false, ?arangodb $arangodb = null)
{
// For the extends system
parent::__construct($initialize);
if ($initialize) {
// Initializing is requested
// Writing an instance of a session of ArangoDB to the property
self::$arangodb = $arangodb ?? new arangodb(require static::ARANGODB);
}
}
/**
* Read document from ArangoDB
*
* @param string $filter Expression for filtering (AQL)
* @param string $sort Expression for sorting (AQL)
* @param int $amount Amount of documents for collect
* @param int $page Page
* @param string $return Expression describing the parameters to return (AQL)
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
* @param array &$errors Registry of errors
*
* @return mixed An array of instances of documents from ArangoDB, if they are found
*/
public static function _read(
string $filter = '',
string $sort = 'd.created DESC, d._key DESC',
int $amount = 1,
int $page = 1,
string $return = 'd',
array $parameters = [],
array &$errors = []
): _document|static|array|null {
try {
if (collection::initialize(static::COLLECTION, static::TYPE)) {
// Initialized the collection
// Read from ArangoDB
$result = collection::execute(
sprintf(
<<<'AQL'
FOR d IN @@collection
%s
%s
LIMIT @offset, @amount
RETURN %s
AQL,
empty($filter) ? '' : "FILTER $filter",
empty($sort) ? '' : "SORT $sort",
empty($return) ? 'd' : $return
),
[
'@collection' => static::COLLECTION,
'offset' => --$page <= 0 ? 0 : $page * $amount,
'amount' => $amount
] + $parameters,
errors: $errors
);
if ($result instanceof _document) {
// Received only 1 document and
// Initializing the object
$object = new static;
if (method_exists($object, '__document')) {
// Object can implement a document from ArangoDB
// Writing the instance of document from ArangoDB to the implement object
$object->__document($result);
// Exit (success)
return $object;
}
}
// Exit (success)
return $result;
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Write
*
* @param string $name Name of the property
* @param mixed $value Value of the property
*
* @return void
*/
public function __set(string $name, mixed $value = null): void
{
match ($name) {
'arangodb' => (function () use ($value) {
if (isset(static::$arangodb)) throw new exception('Forbidden to reinitialize the session of ArangoDB ($this::$arangodb)', 500);
else if ($value instanceof arangodb) self::$arangodb = $value;
else throw new exception('Session of connection to ArangoDB ($this::$arangodb) is need to be mirzaev\arangodb\connection', 500);
})(),
default => parent::__set($name, $value)
};
}
/**
* Read
*
* @param string $name Name of the property
*
* @return mixed Content of the property, if they are found
*/
public function __get(string $name): mixed
{
return match ($name) {
default => parent::__get($name)
};
}
/**
* Delete
*
* @param string $name Name of the property
*
* @return void
*/
public function __unset(string $name): void
{
// Deleting a property and exit (success)
parent::__unset($name);
}
/**
* Check of initialization
*
* @param string $name Name of the property
*
* @return bool The property is initialized?
*/
public function __isset(string $name): bool
{
// Check of initialization of the property and exit (success)
return parent::__isset($name);
}
/**
* Call a static property or method
*
* @param string $name Name of the property or the method
* @param array $arguments Arguments for the method
*/
public static function __callStatic(string $name, array $arguments): mixed
{
return match ($name) {
'arangodb' => (new static)->__get('arangodb'),
default => throw new exception("Not found: $name", 500)
};
}
}

View File

@ -1,314 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
// Library for ArangoDB
use ArangoDBClient\Document as _document;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document,
mirzaev\arangodb\enumerations\collection\type;
// Built-in libraries
use exception;
/**
* Model of entry
*
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class entry extends core implements arangodb_document_interface
{
use arangodb_document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'entry';
/**
* Type of the collection in ArangoDB
*/
public const type TYPE = type::edge;
/**
* Write an entry
*
* @param category|product $from Descendant document
* @param category $to Ascendant document
* @param array &$errors Registry of errors
*
* @return string|null Identifier (_id) of instance of the entry document in ArangoDB, if created
*/
public static function write(
category|product $from,
category $to,
array &$errors = []
): string|null {
try {
if (collection::initialize($from::COLLECTION, $from::TYPE, errors: $errors)) {
if (collection::initialize($to::COLLECTION, $to::TYPE, errors: $errors)) {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized collections
// Creating the entry and exit (success)
return document::write(
static::COLLECTION,
[
'_from' => $from->getId(),
'_to' => $to->getId(),
'version' => ROBOT_VERSION ?? '0.0.0'
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $to::TYPE . ' collection: ' . $to::COLLECTION);
} else throw new exception('Failed to initialize ' . $from::TYPE . ' collection: ' . $from::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Find ascendants
*
* Find ascendants that are not descendants for anyone
*
* @param category|product $descendant Descendant document
* @param array &$errors Registry of errors
*
* @return array|null Ascendants that are not descendants for anyone, if found
*/
public static function ascendants(
category|product $descendant,
array &$errors = []
): ?array {
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
if ($ascendants = collection::execute(
<<<'AQL'
FOR d IN @@collection
FOR ascendant IN OUTBOUND d @@edge
RETURN DISTINCT ascendant
AQL,
[
'@collection' => $descendant::COLLECTION,
'@edge' => static::COLLECTION
],
errors: $errors
)) {
// Found ascendants
// Exit (success)
return is_array($ascendants) ? $ascendants : [$ascendants];
} else return [];
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Check existence of entry between documents
*
* @param category|product $from Descendant document
* @param category $to Ascendant document
* @param array &$errors Registry of errors
*
* @return ?_document The entry edge, if found
*/
public static function check(
category|product $from,
category $to,
array &$errors = []
): ?_document {
try {
if (collection::initialize($from::COLLECTION, $from::TYPE, errors: $errors)) {
if (collection::initialize($to::COLLECTION, $to::TYPE, errors: $errors)) {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized collections
if ($entry = collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d._from == @from && d._to == @to
SORT d.updated DESC, d.created DESC
LIMIT 1
RETURN d
AQL,
[
'@collection' => static::COLLECTION,
'from' => $from->getId(),
'to' => $to->getId()
],
errors: $errors
)) {
// Found the entry between $from and $to
// Exit (success)
return is_array($entry) ? $entry[0] : $entry;
} else return null;
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $to::TYPE . ' collection: ' . $to::COLLECTION);
} else throw new exception('Failed to initialize ' . $from::TYPE . ' collection: ' . $from::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Поиск вхождений (подкатегории или товары)
*
* Находит вхождения через ребро entry
* Генерирует _type со значениями "category" и "product"
* относительно того есть ли у документа ещё вложения
* (подразумевается, что у product вложений быть не может)
* Объединяет возвращаемые объекты документа с переменной _type
*
* @param category|product $document Ascendant document
* @param string|null $filter Expression for filtering (AQL)
* @param string|null $sort Expression for sorting (AQL)
* @param int $page Страница
* @param int $amount Количество товаров на странице
* @param array &$errors Registry of errors
*
* @return array Массив с найденными вхождениями (может быть пустым)
*/
public static function search(
category|product $document,
?string $filter = 'v.deleted != true && v.hidden != true',
?string $sort = 'v.position ASC, v.created DESC',
int $page = 1,
int $amount = 100,
array &$errors = []
): array {
try {
if (collection::initialize($document::COLLECTION, $document::TYPE, errors: $errors)) {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized collections
// Execute and exit (success)
return is_array($result = collection::execute(
sprintf(
<<<'AQL'
FOR v IN 1..1 INBOUND @document GRAPH @graph
%s
%s
LIMIT @offset, @amount
LET _type = (FOR v2 IN INBOUND v._id GRAPH @graph RETURN v2)[0] ? "category" : "product"
RETURN MERGE(v, {_type})
AQL,
empty($filter) ? '' : "FILTER $filter",
empty($sort) ? '' : "SORT $sort",
),
[
'graph' => 'catalog',
'document' => $document->getId(),
'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
'amount' => $amount
],
errors: $errors
)) ? $result : [$result];
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $document::TYPE . ' collection: ' . $document::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return [];
}
/**
* Banish the document from the catalog
*
* Removes all entry edges associated with the document
*
* @param categoru|product $document Document for banishing
* @param array &$errors Registry of errors
*
* @return void
*/
public static function banish(category|product $document, array &$errors = []): void
{
try {
if (collection::initialize($document::COLLECTION, $document::TYPE, errors: $errors)) {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized collections
// Execute and exit (success)
collection::execute(
<<<'AQL'
FOR d IN @@collection
// FILTER d._from == @_id || d._to == @_id
FILTER d._from == @_id
REMOVE d IN @@collection
AQL,
[
'@collection' => static::COLLECTION,
'_id' => $document->getId()
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $document::TYPE . ' collection: ' . $document::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return;
}
}

View File

@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models\enumerations;
/**
* Types of human languages
*
* @package mirzaev\arming_bot\models\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
enum language
{
case en;
case ru;
/**
* Translate label of language
*
* @param language|null $language Language into which to translate
*
* @return string Translated label of language
*
* @todo
* 1. More languages
* 2. Cases???
*/
public function translate(?language $language = language::en): string
{
// Exit (success)
return match ($this) {
language::en => match ($language) {
language::en => 'English',
language::ru => 'Английский'
},
language::ru => match ($language) {
language::en => 'Russian',
language::ru => 'Русский'
}
};
}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models\enumerations;
/**
* Types of session verification
*
* @package mirzaev\arming_bot\models\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
enum session
{
case hash_only;
case hash_else_address;
}

View File

@ -1,77 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models\interfaces;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
/**
* Interface for implementing a document instance from ArangoDB
*
* @param _document $document An instance of the ArangoDB document from ArangoDB (protected readonly)
*
* @package mirzaev\arming_bot\models\traits
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
interface document
{
/**
* Write
*
* Write a property into an instance of the ArangoDB document
*
* @param string $name Name of the property
* @param mixed $value Content of the property
*
* @return void
*/
public function __set(string $name, mixed $value = null): void;
/**
* Read
*
* Read a property from an instance of the ArangoDB docuemnt
*
* @param string $name Name of the property
*
* @return mixed Content of the property
*/
public function __get(string $name): mixed;
/**
* Delete
*
* Deinitialize the property in an instance of the ArangoDB document
*
* @param string $name Name of the property
*
* @return void
*/
public function __unset(string $name): void;
/**
* Check of initialization
*
* Check of initialization of the property into an instance of the ArangoDB document
*
* @param string $name Name of the property
*
* @return bool The property is initialized?
*/
public function __isset(string $name): bool;
/**
* Execute a method
*
* Execute a method from an instance of the ArangoDB document
*
* @param string $name Name of the method
* @param array $arguments Arguments for the method
*
* @return mixed Result of execution of the method
*/
public function __call(string $name, array $arguments = []): mixed;
}

View File

@ -1,259 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\traits\document as arangodb_document_trait;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Built-in libraries
use exception;
/**
* Model of a product
*
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class product extends core
{
use arangodb_document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'product';
/**
* Write a product
*
* @param int $identifier Identifier (unique)
* @param array $name Name [['en' => value], ['ru' => значение]]
* @param array|null $description Description [['en' => value], ['ru' => значение]]
* @param float $cost Cost
* @param float $weight Weight
* @param array $dimensions Dimensions ['x' => 0.0, 'y' => 0.0, 'z' => 0.0]
* @param array|null $brand Brand [['en' => value], ['ru' => значение]]
* @param array|null $compatibility Compatibility [['en' => value], ['ru' => значение]]
* @param array $images Images (first will be thumbnail)
* @param int|null $position Position for sorting in the catalog (ASC)
* @param array $data Data
* @param array &$errors Registry of errors
*
* @return string|null Identifier (_id) of instance of the product document in ArangoDB, if created
*
* @todo
* 1. Bind parameters
*/
public static function write(
int $identifier,
array $name = [['en' => 'ERROR']],
?array $description = [['en' => 'ERROR']],
float $cost = 0,
float $weight = 0,
array $dimensions = ['x' => 0, 'y' => 0, 'z' => 0],
?array $brand = [['en' => 'ERROR']],
?array $compatibility = [['en' => 'ERROR']],
array $images = [],
?int $position = null,
array $data = [],
array &$errors = []
): string|null {
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
// Writing in ArangoDB and exit (success)
return document::write(
static::COLLECTION,
[
'identifier' => $identifier,
'name' => $name,
'description' => $description,
'cost' => $cost ?? 0,
'weight' => $weight ?? 0,
'dimensions' => [
'x' => $dimensions['x'] ?? 0,
'y' => $dimensions['y'] ?? 0,
'z' => $dimensions['z'] ?? 0,
],
'brand' => $brand,
'compatibility' => $compatibility,
'images' => $images,
'position' => $position,
'version' => ROBOT_VERSION
] + $data,
errors: $errors
);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Read products
*
* @param string|null $search Search (text)
* @param string|null $filter Flter (AQL)
* @param string|null $sort Sort (AQL)
* @param int $page Page
* @param int $amount Amount per page
* @param string|null $return Return (AQL)
* @param language $language Language code (en, ru...)
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
* @param array &$errors Registry of errors
*
* @return array Массив с найденными товарами (может быть пустым)
*/
public static function read(
?string $search = null,
?string $filter = 'd.deleted != true && d.hidden != true',
?string $sort = 'd.position ASC, d.created DESC',
int $page = 1,
int $amount = 100,
?string $return = 'd',
language $language = language::en,
array $parameters = [],
array &$errors = []
): array {
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
// Initializing parameters for search
if ($search) $parameters += [
'search' => $search,
'analyzer' => 'text_' . $language->name
];
// Reading products
$documents = collection::execute(
sprintf(
<<<'AQL'
FOR d IN @@collection %s
%s
%s
LIMIT @offset, @amount
RETURN %s
AQL,
empty($search) ? '' : <<<'AQL'
SEARCH
LEVENSHTEIN_MATCH(
d.name.@language,
TOKENS(@search, @analyzer)[0],
1,
false
) OR
levenshtein_match(
d.description.@language,
tokens(@search, @analyzer)[0],
1,
false
) OR
levenshtein_match(
d.compatibility.@language,
tokens(@search, @analyzer)[0],
1,
false
)
AQL,
empty($filter) ? '' : "FILTER $filter",
empty($search) ? (empty($sort) ? '' : "SORT $sort") : (empty($sort) ? "SORT BM25(d) DESC" : "SORT BM25(d) DESC, $sort"),
empty($return) ? 'd' : $return
),
[
'@collection' => empty($search) ? static::COLLECTION : static::COLLECTION . 's_search',
'language' => $language->name,
'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
'amount' => $amount,
] + $parameters,
errors: $errors
);
if ($documents) {
// Found products
// Exit (success)
return is_array($documents) ? $documents : [$documents];
} else return [];
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return [];
}
/**
* Collect parameter from all products
*
* @param string $name Name of the parameter (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',
array &$errors = []
): array {
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
if ($result = collection::execute(
<<<'AQL'
FOR d IN @@collection
RETURN DISTINCT @parameter
AQL,
[
'@collection' => static::COLLECTION,
'parameter' => $name
],
errors: $errors
)) {
// Found parameters
// Exit (success)
return is_array($result) ? $result : [$result];
} else return [];
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return [];
}
}

View File

@ -1,364 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\account,
mirzaev\arming_bot\models\connect,
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;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
// Built-in libraries
use exception;
/**
* Model of a session
*
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class session extends core implements arangodb_document_interface
{
use status, arangodb_document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'session';
/**
* Type of sessions verification
*/
public const verification VERIFICATION = verification::hash_else_address;
/**
* Constructor of instance
*
* Initialize of a session and write them to the $this->document property
*
* @param ?string $hash Hash of the session in ArangoDB
* @param ?int $expires Date of expiring of the session (used for creating a new session)
* @param array &$errors Registry of errors
*
* @return static
*/
public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
if (isset($hash) && $document = $this->hash($hash, errors: $errors)) {
// Found the instance of the ArangoDB document of session and received a session hash
// Writing document instance of the session from ArangoDB to the property of the implementing object
$this->__document($document);
} else if (static::VERIFICATION === verification::hash_else_address && $document = $this->address($_SERVER['REMOTE_ADDR'], errors: $errors)) {
// Found the instance of the ArangoDB document of session and received a session hash
// Writing document instance of the session from ArangoDB to the property of the implementing object
$this->__document($document);
} else {
// Not found the instance of the ArangoDB document of session
// Initializing a new session and write they into ArangoDB
$_id = document::write(
static::COLLECTION,
[
'active' => true,
'expires' => $expires ?? time() + 604800,
'address' => $_SERVER['REMOTE_ADDR'],
'x-forwarded-for' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null,
'referer' => $_SERVER['HTTP_REFERER'] ?? null,
'useragent' => $_SERVER['HTTP_USER_AGENT'] ?? null
]
);
if ($session = collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d._id == @_id && d.expires > @time && d.active == true
RETURN d
AQL,
[
'@collection' => static::COLLECTION,
'_id' => $_id,
'time' => time()
],
errors: $errors
)) {
// Found the instance of just created new session
// Generating a hash and write into the instance of the ArangoDB document of session property
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
if (document::update($session, errors: $errors)) {
// Update is writed to ArangoDB
// Writing instance of the session document from ArangoDB to the property of the implementing object
$this->__document($session);
} else throw new exception('Failed to write the session data');
} else throw new exception('Failed to create or find just created session');
}
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
}
/**
* Search for a connected account
*
* @param array &$errors Registry of errors
*
* @return account|null An object implements the instance of the account document from ArangoDB, if found
*/
public function account(array &$errors = []): ?account
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
if (collection::initialize(connect::COLLECTION, connect::TYPE, errors: $errors)) {
if (collection::initialize(account::COLLECTION, account::TYPE, errors: $errors)) {
// Initialized collections
// Search for connected account
$result = collection::execute(
<<<AQL
FOR v IN INBOUND @session GRAPH sessions
SORT v.created DESC
LIMIT 1
RETURN v
AQL,
[
'session' => $this->getId()
],
errors: $errors
);
if ($result instanceof _document) {
// Found active settings
// Initializing the object
$account = new static;
if (method_exists($account, '__document')) {
// Object can implement a document from ArangoDB
// Writing the instance of account document from ArangoDB to the implement object
$account->__document($result);
// Exit (success)
return $account;
}
} else return null;
} else throw new exception('Failed to initialize ' . account::TYPE . ' collection: ' . account::COLLECTION);
} else throw new exception('Failed to initialize ' . connect::TYPE . ' collection: ' . connect::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Connect account to session
*
* @param account $account Account
* @param array &$errors Registry of errors
*
* @return string|null The identifier of the created edge of the "connect" collection, if created
*/
public function connect(account $account, array &$errors = []): ?string
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
if (collection::initialize(connect::COLLECTION, connect::TYPE, errors: $errors)) {
if (collection::initialize(account::COLLECTION, account::TYPE, errors: $errors)) {
// Collections initialized
// The instance of the session document from ArangoDB is initialized?
isset($this->document) || throw new exception('The instance of the sessoin document from ArangoDB is not initialized');
// Writing document and exit (success)
return document::write(
connect::COLLECTION,
[
'_from' => $account->getId(),
'_to' => $this->document->getId()
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . account::TYPE . ' collection: ' . account::COLLECTION);
} else throw new exception('Failed to initialize ' . connect::TYPE . ' collection: ' . connect::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Search by hash
*
* Search for the session in ArangoDB by hash
*
* @param string $hash Hash of the session in ArangoDB
* @param array &$errors Registry of errors
*
* @return _document|null instance of document of the session in ArangoDB
*/
public static function hash(string $hash, array &$errors = []): ?_document
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Collection initialized
// Search the session data in ArangoDB
return collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d.hash == @hash && d.expires > $time && d.active == true
RETURN d
AQL,
[
'@collection' => static::COLLECTION,
'hash' => $hash,
'time' => time()
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Search by IP-address
*
* Search for the session in ArangoDB by IP-address
*
* @param string $address IP-address writed to the session in ArangoDB
* @param array &$errors Registry of errors
*
* @return _document|null instance of document of the session in ArangoDB
*/
public static function address(string $address, array &$errors = []): ?_document
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Collection initialized
// Search the session data in ArangoDB
return collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d.address == @address && d.expires > @time && d.active == true
RETURN d
AQL,
[
'@collection' => static::COLLECTION,
'address' => $address,
'time' => time()
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Write to buffer of the session
*
* @param array $data Data for merging
* @param array &$errors Registry of errors
*
* @return bool Is data has written into the session document from ArangoDB?
*/
public function write(array $data, array &$errors = []): bool
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
// The instance of the session document from ArangoDB is initialized?
isset($this->document) || throw new exception('The instance of the sessoin document from ArangoDB is not initialized');
// Writing data into buffer of the instance of the session document from ArangoDB
$this->document->buffer = array_replace_recursive(
$this->document->buffer ?? [],
[$_SERVER['INTERFACE'] => array_replace_recursive($this->document->buffer[$_SERVER['INTERFACE']] ?? [], $data)]
);
// Writing to ArangoDB and exit (success)
return document::update($this->document, errors: $errors);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return false;
}
}

View File

@ -1,110 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface,
mirzaev\arming_bot\models\enumerations\language;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
// Built-in libraries
use exception;
/**
* Model of settings
*
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class settings extends core implements arangodb_document_interface
{
use arangodb_document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'settings';
/**
* Search for active settings
*
* @param array|null $create Данные для создания, если настройки не найдены
* @param array &$errors Registry of errors
*
* @return static|null Object implements the instance of settngs document from ArangoDB
*/
public static function active(array|null $create = null, array &$errors = []): static|null
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
// Search for active settings
$result = collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d.status == 'active'
SORT d.updated DESC
LIMIT 1
RETURN d
AQL,
[
'@collection' => static::COLLECTION
],
errors: $errors
);
if ($result instanceof _document) {
// Found active settings
// Initializing the object
$settings = new static;
if (method_exists($settings, '__document')) {
// Object can implement a document from ArangoDB
// Abstractioning of parameters
$result->language = language::{$result->language} ?? 'en';
// Writing the instance of settings document from ArangoDB to the implement object
$settings->__document($result);
// Exit (success)
return $settings;
}
} else if ($create) {
// Not found active settings and requested their creating
// Creating a settings
document::write(static::COLLECTION, ['status' => 'active'] + $create, errors: $errors);
// Re-search (without creating) and exit (success || fail)
return static::active(errors: $errors);
} else throw new exception('Active settings not found');
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
}

View File

@ -1,186 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
// Built-in libraries
use exception,
datetime;
/**
* Model of a suspension
*
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class suspension extends core implements arangodb_document_interface
{
use arangodb_document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'suspension';
/**
* Search for active suspension
*
* @param array &$errors Registry of errors
*
* @return static|null Object implements the instance of suspension from ArangoDB
*/
public static function search(array &$errors = []): static|null
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
// Search for active suspension
$result = collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d.end > @time
SORT d.end DESC
LIMIT 1
RETURN d
AQL,
[
'@collection' => static::COLLECTION,
'time' => time()
],
errors: $errors
);
if ($result instanceof _document) {
// Found active settings
// Initializing the object
$suspension = new static;
if (method_exists($suspension, '__document')) {
// Object can implement a document from ArangoDB
// Writing the instance of suspension document from ArangoDB to the implement object
$suspension->__document($result);
// Exit (success)
return $suspension;
}
} else return null;
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Generate message about remaining time
*
* @param language|null $language Language of the generated text (otherwise used from settings.language)
* @param array &$errors Registry of errors
*
* @return string|null Text: "? days, ? hours and ? minutes"
*/
public function message(?language $language = language::en, array &$errors = []): ?string
{
try {
// Initializing the time until the suspension ends
$difference = date_diff(new datetime('@' . $this->document->end), new datetime());
// Generate text about remaining time and exit (success)
return sprintf(
'%u %s, %u %s и %u %s',
$difference->d,
match ($difference->d > 20 ? $difference->d % 10 : $difference->d % 100) {
1 => match ($language) {
language::en => 'day',
language::ru => 'день',
default => 'day'
},
2, 3, 4 => match ($language) {
language::en => 'days',
language::ru => 'дня',
default => 'days'
},
default => match ($language) {
language::en => 'days',
language::ru => 'дней',
default => 'days'
}
},
$difference->h,
match ($difference->h > 20 ? $difference->h % 10 : $difference->h % 100) {
1 => match ($language) {
language::en => 'hours',
language::ru => 'час',
default => 'hour'
},
2, 3, 4 => match ($language) {
language::en => 'hours',
language::ru => 'часа',
default => 'hours'
},
default => match ($language) {
language::en => 'hours',
language::ru => 'часов',
default => 'hours'
}
},
$difference->i,
match ($difference->i > 20 ? $difference->i % 10 : $difference->i % 100) {
1 => match ($language) {
language::en => 'minute',
language::ru => 'минута',
default => 'minute'
},
2, 3, 4 => match ($language) {
language::en => 'minutes',
language::ru => 'минуты',
default => 'minutes'
},
default => match ($language) {
language::en => 'minutes',
language::ru => 'минут',
default => 'minutes'
}
}
);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
}

View File

@ -1,586 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\controllers\core as controller,
mirzaev\arming_bot\models\catalog,
mirzaev\arming_bot\models\suspension,
mirzaev\arming_bot\models\account;
// Framework for Telegram
use Zanzara\Zanzara,
Zanzara\Context,
Zanzara\Telegram\Type\Input\InputFile,
Zanzara\Telegram\Type\File\Document as telegram_document,
Zanzara\Middleware\MiddlewareNode as Node;
/**
* Model of chat (telegram)
*
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class telegram extends core
{
/**
* Экранирование символов для Markdown
*
* @param string $text Текст для экранирования
* @param array $exception Символы которые будут исключены из списка для экранирования
*
* @return string Экранированный текст
*/
public static function unmarkdown(string $text, array $exceptions = []): string
{
// Инициализация реестра символом для конвертации
$from = array_diff(
[
'#',
'*',
'_',
'=',
'.',
'[',
']',
'(',
')',
'-',
'>',
'<',
'!',
'`'
],
$exceptions
);
// Инициализация реестра целей для конвертации
$to = [];
foreach ($from as $symbol) $to[] = "\\$symbol";
// Конвертация и выход (успех)
return str_replace($from, $to, $text);
}
/**
* Инициализация запчасти
*
* Проверяет существование запчасти
*
* @param string $spare Запчасть
*
* @return string|bool Запчасть, если найдена, иначе false
*/
public static function spares(string $spare): string|bool
{
// Поиск запчастей и выход (успех)
return match (mb_strtolower($spare)) {
'цевьё' => 'Цевьё',
default => false
};
}
/**
* Главное меню
*
* Команда: /start
*
* @param Context $ctx
*
* @return void
*/
public static function menu(Context $ctx): void
{
// Инициализация клавиатуры
$keyboard = [
[
['text' => '🛒 Каталог', 'web_app' => ['url' => 'https://arming.dev.mirzaev.sexy']]
],
[
['text' => '🏛️ О компании'],
['text' => '💬 Контакты']
],
[
['text' => '🎯 Сообщество']
]
];
if ($ctx->get('account')?->access['settings']) $keyboard[] = [['text' => '⚙️ Настройки']];
// Отправка сообщения
$ctx->sendMessage(
static::unmarkdown(<<<TXT
Это сообщение будет отображаться (оно должно быть обязательно) при вызове главного меню командой /start (создаёт кнопки меню снизу)
TXT),
[
'reply_markup' => [
'keyboard' => $keyboard,
'resize_keyboard' => true
],
'disable_notification' => true
]
);
}
/**
* Начало работы с чат-роботом
*
* Команда: /start
*
* @param Context $ctx
*
* @return void
*/
public static function start(Context $ctx): void
{
// Главное меню
static::menu($ctx);
}
/**
* Контакты
*
* Команда: /contacts
*
* @param Context $ctx
*
* @return void
*/
public static function contacts(Context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(static::unmarkdown(<<<TXT
Здесь придумать текст для раздела "Контакты"
TXT), [
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '⚡ Связь с менеджером', 'url' => 'https://t.me/iarming'],
],
[
['text' => '📨 Почта', 'callback_data' => 'mail']
],
[
['text' => '🪖 Сайт', 'url' => 'https://arming.ru'],
['text' => '🛒 Wildberries', 'url' => 'https://www.wildberries.ru/seller/137386'],
['text' => '🛒 Ozon', 'url' => 'https://www.ozon.ru/seller/arming-1086587/products/?miniapp=seller_1086587'],
]
]
],
'link_preview_options' => [
'is_disabled' => true
],
'disable_notification' => true
]);
}
/**
* Почта
*
* @param Context $ctx
*
* @return void
*/
public static function _mail(Context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(static::unmarkdown(<<<TXT
[info@arming.ru](mailto::info@arming.ru)
TXT, ['[', ']', '(', ')']), [
'link_preview_options' => [
'is_disabled' => true
],
'disable_notification' => true
]);
}
/**
* Компания
*
* Команда: /company
*
* @param Context $ctx
*
* @return void
*/
public static function company(Context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(
static::unmarkdown(<<<TXT
Здесь придумать текст для раздела "Компания"
TXT),
/* [
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '⚡ Связь с менеджером', '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
*/
public static function community(Context $ctx): void
{
// Отправка сообщения
$ctx->sendMessage(static::unmarkdown(<<<TXT
Здесь придумать текст для раздела "Сообщество"
TXT), [
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '💬 Основной чат', 'url' => 'https://t.me/arming_zone'],
]
]
],
'link_preview_options' => [
'is_disabled' => true
],
'disable_notification' => true
]);
}
/**
* Настройки (доступ только авторизованным)
*
* Команда: /settings
*
* @param Context $ctx
*
* @return void
*/
public static function settings(Context $ctx): void
{
if ($ctx->get('account')?->access['settings']) {
// Авторизован доступ к настройкам
// Отправка сообщения
$ctx->sendMessage(
static::unmarkdown(<<<TXT
Панель управления чат-роботом ARMING
TXT),
[
'reply_markup' => [
'inline_keyboard' => [
[
['text' => '📦 Импорт товаров', 'callback_data' => 'import_request'],
]
]
],
'link_preview_options' => [
'is_disabled' => true
],
'disable_notification' => true
]
);
} else {
// Не авторизован доступ к настройкам
// Отправка сообщения
$ctx->sendMessage('⛔ *Нет доступа*');
}
}
/**
* Запросить файл для импорта товаров (доступ только авторизованным)
*
* @param Context $ctx
*
* @return void
*/
public static function import_request(Context $ctx): void
{
if ($ctx->get('account')?->access['settings']) {
// Авторизован доступ к настройкам
// Отправка сообщения
$ctx->sendMessage(static::unmarkdown('Отправьте документ в формате xlsx со списком товаров'))
->then(function ($message) use ($ctx) {
// Отправка файла
$ctx->sendDocument(new InputFile(CATALOG_EXAMPLE), ['disable_notification' => true]);
// Импорт файла
$ctx->nextStep([static::class, 'import'], true);
});
} else {
// Не авторизован доступ к настройкам
// Отправка сообщения
$ctx->sendMessage('⛔ *Нет доступа*');
}
}
/**
* Импорт товаров (доступ только авторизованным)
*
* @param Context $ctx
*
* @return void
*/
public static function import(Context $ctx): void
{
if (($account = $ctx->get('account'))?->access['settings']) {
// Авторизован доступ к настройкам
// Инициализация документа
$document = $ctx->getMessage()?->getDocument();
if ($document instanceof telegram_document) {
// Инициализирован документ
// Инициализация файла
$ctx->getFile($document->getFileId())->then(function ($file) use ($ctx, $document, $account) {
if ($file->getFileSize() <= 50000000) {
// Не превышает 50 мегабайт (50 000 000 байт) размер файла
if (pathinfo(parse_url($file->getFilePath())['path'], PATHINFO_EXTENSION) === 'xlsx') {
// Имеет расширение xlsx файл
// Initializing the directory in the storage
if (!file_exists($storage = STORAGE . DIRECTORY_SEPARATOR . 'import' . DIRECTORY_SEPARATOR . $account->getKey() . DIRECTORY_SEPARATOR . time()))
mkdir($storage, 0775, true);
// Сохранение файла
file_put_contents(
$import = $storage . DIRECTORY_SEPARATOR . 'import.xlsx',
file_get_contents('https://api.telegram.org/file/bot' . KEY . '/' . parse_url($file->getFilePath())['path'])
);
// Отправка сообщения
$ctx->sendMessage(sprintf(
<<<'TXT'
🔬 *Выполняется анализ:* %s \(%s байт\)
TXT,
static::unmarkdown($document->getFileName()),
static::unmarkdown((string) $file->getFileSize())
))
->then(function ($message) use ($ctx, $import) {
// Инициализация счётчика загруженных товаров
$categories_loaded
= $products_loaded
= $categories_created
= $products_created
= $categories_updated
= $products_updated
= $categories_deleted
= $products_deleted
= $categories_old
= $products_old
= $categories_new
= $products_new
= 0;
// Import
catalog::import(
$import,
$categories_loaded,
$categories_created,
$categories_updated,
$categories_deleted,
$categories_old,
$categories_new,
$products_loaded,
$products_created,
$products_updated,
$products_deleted,
$products_old,
$products_new,
language: $account->language ?? settings::active()?->language ?? 'en'
);
// Отправка сообщения
$ctx->sendMessage(<<<TXT
🏷 *Категории*
*Загружено:* $categories_loaded
*Добавлено:* $categories_created
*Обновлено:* $categories_updated
*Удалено:* $categories_deleted
*Было:* $categories_old
*Стало:* $categories_new
TXT)
->then(function ($message) use ($ctx, $products_loaded, $products_created, $products_updated, $products_deleted, $products_old, $products_new) {
$ctx->sendMessage(<<<TXT
📦 *Товары*
*Загружено:* $products_loaded
*Добавлено:* $products_created
*Обновлено:* $products_updated
*Удалено:* $products_deleted
*Было:* $products_old
*Стало:* $products_new
TXT)
->then(function ($message) use ($ctx) {
// Завершение диалога
$ctx->endConversation();
});
});
});
} else {
// Не имеет расширение xlsx файл
// Отправка сообщения
$ctx->sendMessage(static::unmarkdown('Файл должен иметь расширение xlsx'));
}
} else {
// Превышает 50 мегабайт (50000000 байт) размер файла
// Отправка сообщения
$ctx->sendMessage(static::unmarkdown('Размер файла не должен превышать 50 мегабайт'));
}
});
} else {
// Не инициализирован документ
// Отправка сообщения
$ctx->sendMessage(static::unmarkdown('Отправьте документ в формате xlsx со списком товаров'));
}
} else {
// Не авторизован доступ к настройкам
// Отправка сообщения
$ctx->sendMessage('⛔ *Нет доступа*');
}
}
/**
* Инициализация аккаунта (middleware)
*
* @param Context $ctx
* @param Node $next
*
* @return void
*/
public static function account(Context $ctx, Node $next): void
{
// Выполнение заблокировано?
if ($ctx->get('stop')) return;
// Инициализация аккаунта Telegram
$telegram = $ctx->getEffectiveUser();
// Инициализация аккаунта
$account = account::initialize($telegram->getId(), $telegram);
if ($account) {
// Инициализирован аккаунт
if ($account->banned) {
// Заблокирован аккаунт
// Отправка сообщения
$ctx->sendMessage('⛔ *Ты заблокирован*')
->then(function ($message) use ($ctx) {
// Завершение диалога
$ctx->endConversation();
});
// Блокировка дальнейшего выполнения
$ctx->set('stop', true);
} else {
// Не заблокирован аккаунт
// Запись в буфер
$ctx->set('account', $account);
// Продолжение выполнения
$next($ctx);
}
} else {
// Не инициализирован аккаунт
}
}
/**
* Инициализация статуса технических работ (middleware)
*
* @param Context $ctx
* @param Node $next
*
* @return void
*/
public static function suspension(Context $ctx, Node $next): void
{
// Выполнение заблокировано?
if ($ctx->get('stop')) return;
// Поиск технических работ
$suspension = suspension::search();
if ($suspension && $suspension->targets['telegram-robot']) {
// Найдена активная приостановка
// Инициализация аккаунта
$account = $ctx->get('account');
if ($account) {
// Инициализирован аккаунт
foreach ($suspension->access as $type => $status) {
// Перебор статусов доступа
if ($status && $account->{$type}) {
// Авторизован аккаунт
// Продолжение выполнения
$next($ctx);
// Выход (успех)
return;
}
}
}
// Инициализация сообщения
$message = "⚠️ *Работа приостановлена*\n*Оставшееся время\:* " . $suspension->message($account->language ?? settings::active()?->language ?? 'en');
// Добавление описания причины приостановки, если найдена
if (!empty($suspension->description))
$message .= "\n\n" . $suspension->description[$account->language ?? settings::active()?->language ?? 'en'] ?? array_values($suspension->description)[0];
// Отправка сообщения
$ctx->sendMessage($message)
->then(function ($message) use ($ctx) {
// Завершение диалога
$ctx->endConversation();
});
// Блокировка дальнейшего выполнения
$ctx->set('stop', true);
} else {
// Не найдена активная приостановка
// Продолжение выполнения
$next($ctx);
}
}
}

View File

@ -1,150 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models\traits;
// Files of the project
use mirzaev\arming_bot\models\core;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
// Framework for ArangoDB
use mirzaev\arangodb\connection as arangodb;
// Built-in libraries
use exception;
/**
* Trait for implementing a document instance from ArangoDB
*
* @var protected readonly _document|null $document An instance of the ArangoDB document
*
* @package mirzaev\arming_bot\models\traits
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
trait document
{
/**
* An instance of the ArangoDB document from ArangoDB
*/
protected readonly _document $document;
/**
* Constructor of an instance
*
* @param bool $initialize Initialize a model?
* @param ?arangodb $arangodb Instance of a session of ArangoDB
* @param _document|null|false $document An instance of the ArangoDB document
*
* @return void
*/
public function __construct(
bool $initialize = true,
?arangodb $arangodb = null,
_document|null|false $document = false
) {
// For the extends system
parent::__construct($initialize, $arangodb);
// Writing to the property
if ($document instanceof _document) $this->__document($document);
else if ($document === null) throw new exception('Failed to initialize an instance of the document from ArangoDB');
}
/**
* Write or read document
*
* @param _document|null $document Instance of document from ArangoDB
*
* @return _document|null Instance of document from ArangoDB
*/
public function __document(?_document $document = null): ?_document
{
// Writing a property storing a document instance to ArangoDB
if ($document) $this->document ??= $document;
// Read a property storing a document instance to ArangoDB and exit (success)
return $this->document ?? null;
}
/**
* Write
*
* Write a property into an instance of the ArangoDB document
*
* @param string $name Name of the property
* @param mixed $value Content of the property
*
* @return void
*/
public function __set(string $name, mixed $value = null): void
{
// Writing to the property into an instance of the ArangoDB document and exit (success)
$this->document->{$name} = $value;
}
/**
* Read
*
* Read a property from an instance of the ArangoDB docuemnt
*
* @param string $name Name of the property
*
* @return mixed Content of the property
*/
public function __get(string $name): mixed
{
// Read a property from an instance of the ArangoDB document and exit (success)
return match ($name) {
default => $this->document->{$name}
};
}
/**
* Delete
*
* Deinitialize the property in an instance of the ArangoDB document
*
* @param string $name Name of the property
*
* @return void
*/
public function __unset(string $name): void
{
// Delete the property in an instance of the ArangoDB document and exit (success)
unset($this->document->{$name});
}
/**
* Check of initialization
*
* Check of initialization of the property into an instance of the ArangoDB document
*
* @param string $name Name of the property
*
* @return bool The property is initialized?
*/
public function __isset(string $name): bool
{
// Check of initializatio nof the property and exit (success)
return isset($this->document->{$name});
}
/**
* Execute a method
*
* Execute a method from an instance of the ArangoDB document
*
* @param string $name Name of the method
* @param array $arguments Arguments for the method
*
* @return mixed Result of execution of the method
*/
public function __call(string $name, array $arguments = []): mixed
{
// Execute the method and exit (success)
return method_exists($this->document, $name) ? $this->document->{$name}($arguments) ?? null : null;
}
}

View File

@ -1,58 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models\traits;
// Built-in libraries
use exception;
/**
* Trait for initialization of files handlers
*
* @package mirzaev\arming_bot\models\traits
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
trait files
{
/**
* Delete files recursively
*
* @param string $directory Directory
* @param array &$errors Registry of errors
*
* @return void
*/
private static function delete(string $directory, array &$errors = []): void
{
try {
if (file_exists($directory)) {
// Directory exists
// Deleting descendant files and directories (enter to the recursion)
foreach (scandir($directory) as $file) {
if ($file === '.' || $file === '..') continue;
else if (is_dir("$directory/$file")) static::delete("$directory/$file", $errors);
else unlink("$directory/$file");
}
// Deleting the directory
rmdir($directory);
// Exit (success)
return;
} else throw new exception('Directory does not exist');
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return;
}
}

View File

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models\traits;
// Built-in libraries
use exception;
/**
* Trait for initialization of a status
*
* @package mirzaev\arming_bot\models\traits
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
trait status
{
/**
* Initialize of a status
*
* @param array &$errors Registry of errors
*
* @return ?bool Status, if they are found
*/
public function status(array &$errors = []): ?bool
{
try {
// Read from ArangoDB and exit (success)
return $this->document->active ?? false;
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
}

View File

@ -1,70 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models\traits\yandex;
// Built-in libraries
use exception;
/**
* Trait for "Yandex Disk"
*
* @package mirzaev\arming_bot\models\traits\yandex
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
trait disk
{
/**
* Download file from "Yandex Disk"
*
* @param string $uri URI of the file from "Yandex Disk"
* @param string $destination Destination to write the file
* @param array &$errors Registry of errors
*
* @return bool The file is downloaded?
*/
private static function download(
string $uri,
string $destination,
array &$errors = []
): bool {
try {
if (!empty($uri)) {
// Not empty URI
if (!empty($destination)) {
// Not empty destination
// Initializing URL of the file
$url = "https://cloud-api.yandex.net/v1/disk/public/resources/download?public_key=$uri";
// Checking if the file is available for download
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($code === 200) {
// The file is available for download
// Downloading the file and exit (success)
return file_put_contents($destination, file_get_contents(json_decode(file_get_contents($url))?->href)) > 0;
} else throw new exception("File not available for download: $uri");
} else throw new exception("Empty destination");
} else throw new exception("Empty URI");
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return false;
}
}

View File

@ -1,72 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot;
// Files of the project
use mirzaev\arming_bot\controllers\core as controller,
mirzaev\arming_bot\models\core as model;
// Framework for PHP
use mirzaev\minimal\core,
mirzaev\minimal\router;
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
// Версия робота
define('ROBOT_VERSION', '1.0.0');
define('VIEWS', realpath('..' . DIRECTORY_SEPARATOR . 'views'));
define('STORAGE', realpath('..' . DIRECTORY_SEPARATOR . 'storage'));
define('SETTINGS', realpath('..' . DIRECTORY_SEPARATOR . 'settings'));
define('INDEX', __DIR__);
define('THEME', 'default');
// Инициализация библиотек
require __DIR__ . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. 'vendor' . DIRECTORY_SEPARATOR
. 'autoload.php';
// Инициализация маршрутизатора
$router = new router;
// Initializing of routes
$router
->write('/', 'catalog', 'index', 'GET')
->write('/search', 'catalog', 'search', 'POST')
->write('/session/connect/telegram', 'session', 'telegram', 'POST')
->write('/category/$identifier', 'catalog', 'index', 'POST')
->write('/category', 'catalog', 'index', 'POST')
->write('/product/$identifier', 'catalog', 'product', 'POST');
/*
// Initializing of routes
$router
->write('/', 'catalog', 'index', 'GET')
->write('/$sex', 'catalog', 'search', 'POST')
->write('/$search', 'catalog', 'search', 'POST')
->write('/search', 'catalog', 'search', 'POST')
->write('/search/$asdasdasd', 'catalog', 'search', 'POST')
->write('/ebala/$sex/$categories...', 'catalog', 'index', 'POST')
->write('/$sex/$categories...', 'catalog', 'index', 'POST')
->write('/$categories...', 'catalog', 'index', 'POST')
->write('/ebala/$categories...', 'catalog', 'index', 'POST');
var_dump($router->routes);
echo "\n\n\n\n\n\n";
$router
->sort();
var_dump($router->routes); */
// Инициализация ядра
$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));
// Обработка запроса
echo $core->start();

View File

@ -1,65 +0,0 @@
"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() {
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;
}
}
});
}
});
})
);

View File

@ -1,35 +0,0 @@
"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.cart === "undefined") {
// Not initialized
// Write to the core
core.cart = class cart {
/**
* Products in cart ["product/148181", "product/148181", "product/148181"...]
*/
static cart = [];
};
}
}
})
);

View File

@ -1,716 +0,0 @@
"use strict";
// Import dependencies
import("/js/core.js").then(() =>
import("/js/damper.js").then(() => {
import("/js/telegram.js").then(() => {
import("/js/hotline.js").then(() => {
const dependencies = setInterval(() => {
if (
typeof core === "function" &&
typeof core.damper === "function" &&
typeof core.telegram === "function" &&
typeof core.hotline === "function"
) {
clearInterval(dependencies);
clearTimeout(timeout);
initialization();
}
}, 10);
const timeout = setTimeout(() => {
clearInterval(dependencies);
initialization();
}, 5000);
function initialization() {
if (typeof core.catalog === "undefined") {
// Not initialized
// Write to the core
core.catalog = class catalog {
/**
* Registry of filters (instead of cookies)
*/
static filters = new Map([
["brand", null],
]);
/**
* Select a category (interface)
*
* @param {HTMLElement} button Button of category <a>
* @param {bool} clean Clear search bar?
* @param {bool} force Ignore the damper?
*
* @return {void}
*/
static category(button, clean = true, force = false) {
// Initializing identifier of the category
const identifier = button.getAttribute(
"data-category-identifier",
);
this._category(identifier, clean, force);
}
/**
* Select a category (damper)
*
* @param {HTMLElement} button Button of category <a>
* @param {bool} clean Clear search bar?
* @param {bool} force Ignore the damper?
*
* @return {void}
*/
static _category = core.damper(
(...variables) => this.__category(...variables),
400,
2,
);
/**
* Select a category (system)
*
* @param {string} identifier Identifier of the category
* @param {bool} clean Clear search bar?
*
* @return {Promise} Request to the server
*/
static __category(category, clean = true) {
if (typeof category === "string") {
// Received required parameters
return core.request(
"/category" +
("/" + category).replace(/^\/*/, "/").trim().replace(
/\/*$/,
"",
),
)
.then((json) => {
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Errors received
} else {
// Errors not received
if (clean) {
// Clearing the search bar
const search = core.main.querySelector(
'search[data-section="search"]>input',
);
if (search instanceof HTMLElement) search.value = "";
}
if (
typeof json.title === "string" &&
json.title.length > 0
) {
// Received the page title
// Initialize a link to the categories list
const title = core.main.getElementsByTagName("h2")[0];
// Write the title
title.innerText = json.title;
}
if (
typeof json.html.categories === "string" &&
json.html.categories.length > 0
) {
// Received categories (reinitialization of the categories)
const categories = core.main.querySelector(
'section[data-catalog-type="categories"]',
);
if (categories instanceof HTMLElement) {
// Found list of categories
categories.outerHTML = json.html.categories;
} else {
// Not found list of categories
const element = document.createElement("section");
const search = core.main.querySelector(
'search[data-section="search"]',
);
if (search instanceof HTMLElement) {
core.main.insertBefore(
element,
search.nextSibling,
);
element.outerHTML = json.html.categories;
}
}
} else {
// Not received categories (deinitialization of the categories)
const categories = core.main.querySelector(
'section[data-catalog-type="categories"',
);
if (categories instanceof HTMLElement) {
categories.remove();
}
}
if (
typeof json.html.products === "string" &&
json.html.products.length > 0
) {
// Received products (reinitialization of the products)
const products = core.main.querySelector(
'section[data-catalog-type="products"]',
);
if (products instanceof HTMLElement) {
// Found list of products
products.outerHTML = json.html.products;
} else {
// 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;
}
}
}
} else {
// Not received products (deinitialization of the products)
const products = core.main.querySelector(
'section[data-catalog-type="products"',
);
if (products instanceof HTMLElement) {
products
.remove();
}
}
}
});
}
}
/**
* Select a category (interface)
*
* @param {Event} event Event (keyup)
* @param {HTMLElement} element Search bar <input>
* @param {bool} force Ignore the damper?
*
* @return {void}
*/
static search(event, element, force = false) {
element.classList.remove("error");
if (element.innerText.length === 1) {
return;
} else if (event.keyCode === 13) {
// Button: "enter"
element.setAttribute("disabled", true);
this.__search(element);
} else {
// Button: any
this._search(element, force);
}
}
/**
* Search in the catalog (damper)
*
* @param {HTMLElement} button Button of category <a>
* @param {bool} clean Clear search bar?
* @param {bool} force Ignore the damper?
*
* @return {void}
*/
static _search = core.damper(
(...variables) => this.__search(...variables),
1400,
2,
);
/**
* Search in the catalog (system)
*
* @param {HTMLElement} element Search bar <input>
*
* @return {Promise} Request to the server
*
* @todo add animations of errors
*/
static __search(element) {
return this.__category("/", false)
.then(function () {
core.request("/search", `text=${element.value}`)
.then((json) => {
element.removeAttribute("disabled");
element.focus();
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Errors received
element.classList.add("error");
} else {
// Errors not received
if (
typeof json.title === "string" &&
json.title.length > 0
) {
// Received the page title
// Initialize a link to the categories list
const title =
core.main.getElementsByTagName("h2")[0];
// Write the title
title.innerText = json.title;
}
// Deinitialization of the categories
const categories = core.main.querySelector(
'section[data-catalog-type="categories"]',
);
// if (categories instanceof HTMLElement) categories.remove();
if (
typeof json.html.products === "string" &&
json.html.products.length > 0
) {
// Received products (reinitialization of the products)
const products = core.main.querySelector(
'section[data-catalog-type="products"]',
);
if (products instanceof HTMLElement) {
// Found list of products
products.outerHTML = json.html.products;
} else {
// 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;
}
}
}
} else {
// Not received products (deinitialization of the products)
const products = core.main.querySelector(
'section[data-catalog-type="products"]',
);
if (products instanceof HTMLElement) {
products.remove();
}
}
}
});
});
}
/**
* Open product card (interface)
*
* @param {string} identifier Identifier of a product
* @param {bool} force Ignore the damper?
*
* @return {void}
*/
static product(identifier, force = false) {
this._product(identifier, force);
}
/**
* Open product card (damper)
*
* @param {string} identifier Identifier of a product
* @param {bool} force Ignore the damper?
*
* @return {void}
*/
static _product = core.damper(
(...variables) => this.__product(...variables),
400,
1,
);
/**
* Open product card (system)
*
* @param {string} identifier Identifier of a product
*
* @return {Promise} Request to the server
*/
static __product(identifier) {
if (typeof identifier === "number") {
//
return core.request(`/product/${identifier}`)
.then((json) => {
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Errors received
} else {
// Errors not received
if (
json.product !== null &&
typeof json.product === "object"
) {
// Received data of the product
// Deinitializing of the old winow
const old = document.getElementById("window");
if (old instanceof HTMLElement) old.remove();
const wrap = document.createElement("section");
wrap.setAttribute("id", "window");
const card = document.createElement("div");
// card.classList.add("product", "card");
card.classList.add("card", "unselectable");
const h3 = document.createElement("h3");
const name = document.createElement("span");
name.classList.add("name");
name.setAttribute("title", json.product.identifier);
name.innerText = json.product.name;
const exit = document.createElement("a");
exit.classList.add("exit");
exit.setAttribute("type", "button");
const exit_icon = document.createElement("i");
exit_icon.classList.add("icon", "close");
const images = document.createElement("div");
images.classList.add("images", "unselectable");
const button = core.telegram.api.isVisible;
// блокировка закрытия изображений
let images_from;
const _images_from = (event) => images_from = event;
images.addEventListener("mousedown", _images_from);
images.addEventListener("touchstart", _images_from);
const _open = (event) => {
if (
event.type === "touchstart" ||
event.button === 0
) {
{
let x = event.pageX || event.touches[0].pageX;
let y = event.pageY || event.touches[0].pageY;
let _x = images_from.pageX ||
images_from.touches[0].pageX;
let _y = images_from.pageY ||
images_from.touches[0].pageY;
if (
_x - x < 10 &&
_x - x > -10 &&
_y - y < 10 &&
_y - y > -10
) {
images.classList.add("extend");
if (button) {
core.telegram.api.MainButton.hide();
}
setTimeout(() => {
images.hotline.step = 0;
images.hotline.wheel = false;
images.hotline.start();
images.addEventListener("mouseup", _close);
images.addEventListener("touchend", _close);
}, 300);
images.removeEventListener("mouseup", _open);
images.removeEventListener("touchend", _open);
}
}
}
};
const _close = (event) => {
let x = event.pageX || event.touches[0].pageX;
let y = event.pageY || event.touches[0].pageY;
let _x = images_from.pageX ||
images_from.touches[0].pageX;
let _y = images_from.pageY ||
images_from.touches[0].pageY;
if (
event.type === "touchstart" ||
event.button === 0
) {
if (
_x - x < 10 &&
_x - x > -10 &&
_y - y < 10 &&
_y - y > -10
) {
// Курсор не был сдвинут на 10 квардратных пикселей
// (в идеале надо переделать на таймер 2 секунды есди зажата кнопка то ничего не делать а просто двигать)
// пох абсолютно сейчас заказчик слишком охуевший для этого
images.hotline.step = -0.3;
images.hotline.wheel = true;
if (width < card.offsetWidth) {
images.hotline.stop();
}
images.classList.remove("extend");
if (button) core.telegram.api.MainButton.show();
images.removeEventListener("mouseup", _close);
images.removeEventListener("touchend", _close);
images.addEventListener("mousedown", _start);
images.addEventListener("touchstart", _start);
}
}
};
const _start = (event) => {
if (
event.type === "touchstart" ||
event.button === 0
) {
images.removeEventListener("mousedown", _start);
images.removeEventListener("touchstart", _start);
images.addEventListener("mouseup", _open);
images.addEventListener("touchend", _open);
}
};
images.addEventListener("mousedown", _start);
images.addEventListener("touchstart", _start);
for (const uri of json.product.images) {
const image = document.createElement("img");
image.setAttribute("src", uri);
image.setAttribute("ondragstart", "return false;");
images.append(image);
}
const header = document.createElement("p");
header.classList.add("header");
const brand = document.createElement("small");
brand.classList.add("brand");
brand.innerText = json.product.brand;
const description = document.createElement("p");
description.classList.add("description");
description.innerText = json.product.description;
const compatibility = document.createElement("p");
compatibility.classList.add("compatibility");
compatibility.innerText = json.product.compatibility;
const footer = document.createElement("div");
footer.classList.add("footer");
footer.classList.add("footer");
const dimensions = document.createElement("small");
dimensions.classList.add("dimensions");
const x = json.product.dimensions.x;
const y = json.product.dimensions.y;
const z = json.product.dimensions.z;
let formatted = "";
if (x !== "") formatted = x;
if (y !== "") {
if (formatted.length === 0) formatted = y;
else formatted += "x" + y;
}
if (z !== "") {
if (formatted.length === 0) formatted = z;
else formatted += "x" + z;
}
dimensions.innerText = formatted;
const weight = document.createElement("small");
weight.classList.add("weight");
weight.innerText = json.product.weight + "г";
const cost = document.createElement("p");
cost.classList.add("cost");
cost.innerText = json.product.cost + "р";
h3.append(name);
exit.append(exit_icon);
h3.append(exit);
card.append(h3);
card.append(images);
header.append(brand);
card.append(header);
card.append(description);
card.append(compatibility);
footer.append(dimensions);
footer.append(weight);
footer.append(cost);
card.append(footer);
wrap.append(card);
core.main.append(wrap);
let width = 0;
let buffer;
[...images.children].forEach((child) =>
width += child.offsetWidth + (isNaN(
buffer = parseFloat(
getComputedStyle(child).marginRight,
),
)
? 0
: buffer)
);
history.pushState(
{ product_card: json.product.identifier },
json.product.name,
);
// блокировка закрытия карточки
let from;
const _from = (event) => from = event.target;
wrap.addEventListener("mousedown", _from);
wrap.addEventListener("touchstart", _from);
const remove = () => {
wrap.remove();
images.removeEventListener(
"mousedown",
_images_from,
);
images.removeEventListener(
"touchstart",
_images_from,
);
wrap.removeEventListener("mousedown", _from);
wrap.removeEventListener("touchstart", _from);
exit.removeEventListener("click", remove);
exit.removeEventListener("touch", remove);
document.removeEventListener("click", close);
document.removeEventListener("touch", close);
window.removeEventListener("popstate", remove);
};
const close = (event) => {
if (
from === wrap &&
!card.contains(event.target) &&
!!card &&
!!(card.offsetWidth ||
card.offsetHeight ||
card.getClientRects().length)
) {
remove();
}
from = undefined;
};
exit.addEventListener("click", remove);
exit.addEventListener("touch", remove);
document.addEventListener("click", close);
document.addEventListener("touch", close);
window.addEventListener("popstate", remove);
images.hotline = new core.hotline(
json.product.identfier,
images,
);
images.hotline.step = -0.3;
images.hotline.wheel = true;
images.hotline.touch = true;
if (width > card.offsetWidth) {
images.hotline.start();
}
}
}
});
}
}
};
}
}
});
});
})
);

View File

@ -1,52 +0,0 @@
"use strict";
// Initialize of the class in global namespace
const core = class core {
// Domain
static domain = window.location.hostname;
// Language
static language = "ru";
// Label for the "loding" element
static loading = document.getElementById("loading");
// Label for the <header> element
static header = document.body.getElementsByTagName("header")[0];
// Label for the <aside> element
static aside = document.body.getElementsByTagName("aside")[0];
// Label for the "menu" element
static menu = document.body.querySelector("section[data-section='menu']");
// Label for the <main> element
static main = document.body.getElementsByTagName("main")[0];
// Label for the <footer> element
static footer = document.body.getElementsByTagName("footer")[0];
/**
* Request
*
* @param {string} address
* @param {string} body
* @param {string} method POST, GET...
* @param {object} headers
* @param {string} type Format of response (json, text...)
*
* @return {Promise}
*/
static async request(
address = "/",
body,
method = "POST",
headers = {
"Content-Type": "application/x-www-form-urlencoded",
},
type = "json",
) {
return await fetch(encodeURI(address), { method, headers, body })
.then((response) => response[type]());
}
};

View File

@ -1,71 +0,0 @@
"use strict";
// Import dependencies
import("/js/core.js").then(() => {
const dependencies = setInterval(() => {
if (typeof core === "function") {
clearInterval(dependencies);
clearTimeout(timeout);
initialization();
}
}, 10);
const timeout = setTimeout(() => {
clearInterval(dependencies);
initialization();
}, 5000);
function initialization() {
if (typeof core.damper === "undefined") {
// Not initialized
/**
* Damper
*
* @param {function} function Function to execute after damping
* @param {number} timeout Timer in milliseconds (ms)
* @param {number} force Argument number storing the status of enforcement execution (see @example)
*
* @example
* $a = damper(
* async (
* a, // 0
* b, // 1
* c, // 2
* force = false, // 3
* d // 4
* ) => {
* // Body of function
* },
* 500,
* 3, // 3 -> "force" argument
* );
*
* $a('for a', 'for b', 'for c', true, 'for d'); // Force execute is enabled
*
* @return {void}
*/
core.damper = (func, timeout = 300, force) => {
// Initializing of the timer
let timer;
return (...args) => {
// Deinitializing of the timer
clearTimeout(timer);
if (typeof force === "number" && args[force]) {
// Force execution (ignoring the timer)
func.apply(this, args);
} else {
// Normal execution
// Execute the handled function (entry into recursion)
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
}
};
};
}
}
});

View File

@ -1,775 +0,0 @@
"use strict";
// Import dependencies
import("/js/core.js").then(() => {
const dependencies = setInterval(() => {
if (typeof core === "function") {
clearInterval(dependencies);
clearTimeout(timeout);
initialization();
}
}, 10);
const timeout = setTimeout(() => {
clearInterval(dependencies);
initialization();
}, 5000);
function initialization() {
if (typeof core.hotline === "undefined") {
// Not initialized
/**
* Бегущая строка
*
* @description
* Простой, но мощный класс для создания бегущих строк. Поддерживает
* перемещение мышью и прокрутку колесом, полностью настраивается очень гибок
* для настроек в CSS и подразумевается, что отлично индексируется поисковыми роботами.
* Имеет свой препроцессор, благодаря которому можно создавать бегущие строки
* без программирования - с помощью HTML-аттрибутов, а так же возможность
* изменять параметры (data-hotline-* аттрибуты) на лету. Есть возможность вызывать
* события при выбранных действиях для того, чтобы пользователь имел возможность
* дорабатывать функционал без изучения и изменения моего кода
*
* @example
* сonst hotline = new hotline();
* hotline.step = '-5';
* hotline.start();
*
* @todo
* 1. Бесконечный режим - элементы не удаляются если видны на экране (будут дубликаты).
* Сейчас при БЫСТРОМ прокручивании можно заметит как элементы "появляются" в начале и конце строки.
* 2. "gap" and "padding" in wrap should be removed! or added here to the calculations
*
* @copyright WTFPL
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
core.hotline = class hotline {
// Идентификатор
#id = 0;
// Оболочка (instanceof HTMLElement)
#shell = document.getElementById("hotline");
// Инстанция горячей строки
#instance = null;
// Перемещение
#transfer = true;
// Движение
#move = true;
// Наблюдатель
#observer = null;
// Реестр запрещённых к изменению параметров
#block = new Set(["events"]);
// Status (null, active, inactive)
#status = null;
// Настраиваемые параметры
transfer = null;
move = null;
delay = 10;
step = 1;
hover = true;
movable = true;
sticky = false;
wheel = false;
delta = null;
vertical = false;
button = 0; // button for grabbing. 0 is main mouse button (left)
observe = false;
events = new Map([
["start", false],
["stop", false],
["move", false],
["move.block", false],
["move.unblock", false],
["offset", false],
["transfer.start", true],
["transfer.end", true],
["mousemove", false],
["touchmove", false],
]);
// Is hotline currently moving due to "onmousemove" or "ontouchmove"?
moving = false;
constructor(id, shell) {
// Запись идентификатора
if (typeof id === "string" || typeof id === "number") this.#id = id;
// Запись оболочки
if (shell instanceof HTMLElement) this.#shell = shell;
}
start() {
if (this.#instance === null) {
// Нет запущенной инстанции бегущей строки
// Инициализация ссылки на ядро
const _this = this;
// Запуск движения
this.#instance = setInterval(function () {
if (_this.#shell.childElementCount > 1) {
// Найдено содержимое бегущей строки (2 и более)
// Инициализация буфера для временных данных
let buffer;
// Инициализация данных первого элемента в строке
const first = {
element: (buffer = _this.#shell.firstElementChild),
coords: buffer.getBoundingClientRect(),
};
if (_this.vertical) {
// Вертикальная бегущая строка
// Инициализация сдвига у первого элемента (движение)
first.offset = isNaN(
buffer = parseFloat(first.element.style.marginTop),
)
? 0
: buffer;
// Инициализация отступа до второго элемента у первого элемента (разделение)
first.separator = isNaN(
buffer = parseFloat(
getComputedStyle(first.element).marginBottom,
),
)
? 0
: buffer;
// Инициализация крайнего с конца ребра первого элемента в строке
first.end = first.coords.y + first.coords.height +
first.separator;
} else {
// Горизонтальная бегущая строка
// Инициализация отступа у первого элемента (движение)
first.offset = isNaN(
buffer = parseFloat(first.element.style.marginLeft),
)
? 0
: buffer;
// Инициализация отступа до второго элемента у первого элемента (разделение)
first.separator = isNaN(
buffer = parseFloat(
getComputedStyle(first.element).marginRight,
),
)
? 0
: buffer;
// Инициализация крайнего с конца ребра первого элемента в строке
first.end = first.coords.x + first.coords.width +
first.separator;
}
if (
(_this.vertical &&
Math.round(first.end) < _this.#shell.offsetTop) ||
(!_this.vertical &&
Math.round(first.end) < _this.#shell.offsetLeft)
) {
// Элемент (вместе с отступом до второго элемента) вышел из области видимости (строки)
if (
(_this.transfer === null && _this.#transfer) ||
_this.transfer === true
) {
// Перенос разрешен
if (_this.vertical) {
// Вертикальная бегущая строка
// Удаление отступов (движения)
first.element.style.marginTop = null;
} else {
// Горизонтальная бегущая строка
// Удаление отступов (движения)
first.element.style.marginLeft = null;
}
// Копирование первого элемента в конец строки
_this.#shell.appendChild(first.element);
if (_this.events.get("transfer.end")) {
// Запрошен вызов события: "перемещение в конец"
// Вызов события: "перемещение в конец"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.transfer.end`, {
detail: {
element: first.element,
offset: -(
(_this.vertical
? first.coords.height
: first.coords.width) + first.separator
),
},
}),
);
}
}
} else if (
(_this.vertical &&
Math.round(first.coords.y) > _this.#shell.offsetTop) ||
(!_this.vertical &&
Math.round(first.coords.x) > _this.#shell.offsetLeft)
) {
// Передняя (движущая) граница первого элемента вышла из области видимости
if (
(_this.transfer === null && _this.#transfer) ||
_this.transfer === true
) {
// Перенос разрешен
// Инициализация отступа у последнего элемента (разделение)
const separator = (buffer = isNaN(
buffer = parseFloat(
getComputedStyle(_this.#shell.lastElementChild)[
_this.vertical ? "marginBottom" : "marginRight"
],
),
)
? 0
: buffer) === 0
? first.separator
: buffer;
// Инициализация координат первого элемента в строке
const coords = _this.#shell.lastElementChild
.getBoundingClientRect();
if (_this.vertical) {
// Вертикальная бегущая строка
// Удаление отступов (движения)
_this.#shell.lastElementChild.style.marginTop =
-coords.height - separator + "px";
} else {
// Горизонтальная бегущая строка
// Удаление отступов (движения)
_this.#shell.lastElementChild.style.marginLeft =
-coords.width - separator + "px";
}
// Копирование последнего элемента в начало строки
_this.#shell.insertBefore(
_this.#shell.lastElementChild,
first.element,
);
// Удаление отступов у второго элемента в строке (движения)
_this.#shell.children[1].style[
_this.vertical ? "marginTop" : "marginLeft"
] = null;
if (_this.events.get("transfer.start")) {
// Запрошен вызов события: "перемещение в начало"
// Вызов события: "перемещение в начало"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.transfer.start`, {
detail: {
element: _this.#shell.lastElementChild,
offset:
(_this.vertical ? coords.height : coords.width) +
separator,
},
}),
);
}
}
} else {
// Элемент в области видимости
if (
(_this.move === null && _this.#move) || _this.move === true
) {
// Движение разрешено
// Запись новых координат сдвига
const offset = first.offset + _this.step;
// Запись сдвига (движение)
_this.offset(offset);
if (_this.events.get("move")) {
// Запрошен вызов события: "движение"
// Вызов события: "движение"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.move`, {
detail: {
from: first.offset,
to: offset,
},
}),
);
}
}
}
}
}, _this.delay);
if (this.hover) {
// Запрошена возможность останавливать бегущую строку
// Инициализация сдвига
let offset = 0;
// Инициализация слушателя события при перемещении элемента в бегущей строке
const listener = function (e) {
// Увеличение сдвига
offset += e.detail.offset ?? 0;
};
// Объявление переменной в области видимости обработки остановки бегущей строки
let move;
// Инициализация обработчика наведения курсора (остановка движения)
this.#shell.onmouseover = function (e) {
// Курсор наведён на бегущую строку
// Блокировка движения
_this.#move = false;
if (_this.events.get("move.block")) {
// Запрошен вызов события: "блокировка движения"
// Вызов события: "блокировка движения"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.move.block`),
);
}
};
if (this.movable) {
// Запрошена возможность двигать бегущую строку
_this.#shell.onmousedown =
_this.#shell.ontouchstart =
function (
start,
) {
// Handling a "mousedown" and a "touchstart" on hotline
if (
start.type === "touchstart" ||
start.button === _this.button
) {
const x = start.pageX || start.touches[0].pageX;
const y = start.pageY || start.touches[0].pageY;
// Блокировка движения
_this.#move = false;
if (_this.events.get("move.block")) {
// Запрошен вызов события: "блокировка движения"
// Вызов события: "блокировка движения"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.move.block`),
);
}
// Инициализация слушателей события перемещения элемента в бегущей строке
document.addEventListener(
`hotline.${_this.#id}.transfer.start`,
listener,
);
document.addEventListener(
`hotline.${_this.#id}.transfer.end`,
listener,
);
// Инициализация буфера для временных данных
let buffer;
// Инициализация данных первого элемента в строке
const first = {
offset: isNaN(
buffer = parseFloat(
_this.vertical
? _this.#shell.firstElementChild.style
.marginTop
: _this.#shell.firstElementChild.style
.marginLeft,
),
)
? 0
: buffer,
};
move = (move) => {
// Обработка движения курсора
if (_this.#status === "active") {
// Запись статуса ручного перемещения
_this.moving = true;
const _x = move.pageX || move.touches[0].pageX;
const _y = move.pageY || move.touches[0].pageY;
if (_this.vertical) {
// Вертикальная бегущая строка
// Инициализация буфера местоположения
const from =
_this.#shell.firstElementChild.style.marginTop;
const to = _y - (y + offset - first.offset);
// Движение
_this.#shell.firstElementChild.style.marginTop =
to +
"px";
} else {
// Горизонтальная бегущая строка
// Инициализация буфера местоположения
const from =
_this.#shell.firstElementChild.style.marginLeft;
const to = _x - (x + offset - first.offset);
// Движение
_this.#shell.firstElementChild.style.marginLeft =
to +
"px";
}
if (_this.events.get(move.type)) {
// Запрошен вызов события: "перемещение" (мышью или касанием)
// Вызов события: "перемещение" (мышью или касанием)
document.dispatchEvent(
new CustomEvent(
`hotline.${_this.#id}.${move.type}`,
{
detail: { from, to },
},
),
);
}
// Запись курсора
_this.#shell.style.cursor = "grabbing";
}
};
// Запуск обработки движения
document.addEventListener("mousemove", move);
document.addEventListener("touchmove", move);
}
};
// Перещапись событий браузера (чтобы не дёргалось)
_this.#shell.ondragstart = null;
_this.#shell.onmouseup = _this.#shell.ontouchend = function () {
// Курсор деактивирован
// Запись статуса ручного перемещения
_this.moving = false;
// Остановка обработки движения
document.removeEventListener("mousemove", move);
document.removeEventListener("touchmove", move);
// Сброс сдвига
offset = 0;
document.removeEventListener(
`hotline.${_this.#id}.transfer.start`,
listener,
);
document.removeEventListener(
`hotline.${_this.#id}.transfer.end`,
listener,
);
// Разблокировка движения
_this.#move = true;
if (_this.events.get("move.unblock")) {
// Запрошен вызов события: "разблокировка движения"
// Вызов события: "разблокировка движения"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.move.unblock`),
);
}
// Восстановление курсора
_this.#shell.style.cursor = null;
};
}
// Инициализация обработчика отведения курсора (остановка движения)
this.#shell.onmouseleave = function (onmouseleave) {
// Курсор отведён от бегущей строки
if (!_this.sticky) {
// Отключено прилипание
// Запись статуса ручного перемещения
_this.moving = false;
// Остановка обработки движения
document.removeEventListener("mousemove", move);
document.removeEventListener("touchmove", move);
document.removeEventListener(
`hotline.${_this.#id}.transfer.start`,
listener,
);
document.removeEventListener(
`hotline.${_this.#id}.transfer.end`,
listener,
);
// Восстановление курсора
_this.#shell.style.cursor = null;
}
// Сброс сдвига
offset = 0;
// Разблокировка движения
_this.#move = true;
if (_this.events.get("move.unblock")) {
// Запрошен вызов события: "разблокировка движения"
// Вызов события: "разблокировка движения"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.move.unblock`),
);
}
};
}
if (this.wheel) {
// Запрошена возможность прокручивать колесом мыши
// Инициализация обработчика наведения курсора (остановка движения)
this.#shell.onwheel = function (e) {
// Курсор наведён на бегущую
// Инициализация буфера для временных данных
let buffer;
// Перемещение
_this.offset(
(isNaN(
buffer = parseFloat(
_this.#shell.firstElementChild.style[
_this.vertical ? "marginTop" : "marginLeft"
],
),
)
? 0
: buffer) +
(_this.delta === null
? e.wheelDelta
: e.wheelDelta > 0
? _this.delta
: -_this.delta),
);
};
}
this.#status = "active";
}
if (this.observe) {
// Запрошено наблюдение за изменениями аттрибутов элемента бегущей строки
if (this.#observer === null) {
// Отсутствует наблюдатель
// Инициализация ссылки на ядро
const _this = this;
// Инициализация наблюдателя
this.#observer = new MutationObserver(function (mutations) {
for (const mutation of mutations) {
if (mutation.type === "attributes") {
// Запись параметра в инстанцию бегущей строки
_this.configure(mutation.attributeName);
}
}
// Перезапуск бегущей строки
_this.restart();
});
// Активация наблюдения
this.#observer.observe(this.#shell, {
attributes: true,
});
}
} else if (this.#observer instanceof MutationObserver) {
// Запрошено отключение наблюдения
// Деактивация наблюдения
this.#observer.disconnect();
// Удаление наблюдателя
this.#observer = null;
}
if (this.events.get("start")) {
// Запрошен вызов события: "запуск"
// Вызов события: "запуск"
document.dispatchEvent(
new CustomEvent(`hotline.${this.#id}.start`),
);
}
return this;
}
stop() {
this.#status = "inactive";
// Остановка бегущей строки
clearInterval(this.#instance);
// Удаление инстанции интервала
this.#instance = null;
if (this.events.get("stop")) {
// Запрошен вызов события: "остановка"
// Вызов события: "остановка"
document.dispatchEvent(new CustomEvent(`hotline.${this.#id}.stop`));
}
return this;
}
restart() {
// Остановка бегущей строки
this.stop();
// Запуск бегущей строки
this.start();
}
configure(attribute) {
// Инициализация названия параметра
const parameter =
(/^data-hotline-(\w+)$/.exec(attribute) ?? [, null])[1];
if (typeof parameter === "string") {
// Параметр найден
// Проверка на разрешение изменения
if (this.#block.has(parameter)) return;
// Инициализация значения параметра
const value = this.#shell.getAttribute(attribute);
if (typeof value !== undefined || typeof value !== null) {
// Найдено значение
// Инициализация буфера для временных данных
let buffer;
// Запись параметра
this[parameter] = isNaN(buffer = parseFloat(value))
? value === "true" ? true : value === "false" ? false : value
: buffer;
}
}
return this;
}
offset(value) {
// Запись отступа
this.#shell.firstElementChild.style[
this.vertical ? "marginTop" : "marginLeft"
] = value + "px";
if (this.events.get("offset")) {
// Запрошен вызов события: "сдвиг"
// Вызов события: "сдвиг"
document.dispatchEvent(
new CustomEvent(`hotline.${this.#id}.offset`, {
detail: {
to: value,
},
}),
);
}
return this;
}
static preprocessing(event = false) {
// Инициализация счётчиков инстанций горячей строки
const success = new Set();
let error = 0;
for (
const element of document.querySelectorAll('*[data-hotline="true"]')
) {
// Перебор элементов для инициализации бегущих строк
if (typeof element.id === "string") {
// Найден идентификатор
// Инициализация инстанции бегущей строки
const hotline = new this(element.id, element);
for (const attribute of element.getAttributeNames()) {
// Перебор аттрибутов
// Запись параметра в инстанцию бегущей строки
hotline.configure(attribute);
}
// Запуск бегущей строки
hotline.start();
// Запись инстанции бегущей строки в элемент
element.hotline = hotline;
// Запись в счётчик успешных инициализаций
success.add(hotline);
} else ++error;
}
if (event) {
// Запрошен вызов события: "предварительная подготовка"
// Вызов события: "предварительная подготовка"
document.dispatchEvent(
new CustomEvent(`hotline.preprocessed`, {
detail: {
success,
error,
},
}),
);
}
}
};
}
}
});

View File

@ -1,45 +0,0 @@
"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.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,
);
}
};
}
}
})
);

View File

@ -1,43 +0,0 @@
"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.telegram === "undefined") {
// Not initialized
// Write to the core
core.telegram = class telegram {
/**
* Telegram WebApp API
*
* @see {@link https://core.telegram.org/bots/webapps#initializing-mini-apps}
*/
static api = window.Telegram.WebApp;
};
/* telegram.MainButton.text =
typeof core === "object" && core.language === "ru"
? "Корзина"
: "Cart";
telegram.MainButton.show(); */
}
}
})
);

View File

@ -1,88 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot;
// Files of the project
use mirzaev\arming_bot\controllers\core as controller,
mirzaev\arming_bot\models\core as model,
mirzaev\arming_bot\models\telegram;
// Фреймворк Telegram
use Zanzara\Zanzara,
Zanzara\Context,
Zanzara\Config;
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
// Версия робота
define('ROBOT_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';
// Инициализация ядра контроллеров MINIMAL
new controller(false);
// Инициализация ядра моделей MINIMAL
new model(true);
$config = new Config();
$config->setParseMode(Config::PARSE_MODE_MARKDOWN);
$config->useReactFileSystem(true);
$bot = new Zanzara(KEY, $config);
/* $bot->onUpdate(function (Context $ctx): void {
var_dump($ctx->getMessage()->getWebAppData());
var_dump($ctx->getEffectiveUser() );
}); */
$bot->onCommand('start', fn($ctx) => telegram::start($ctx));
$bot->onCommand('contacts', fn($ctx) => telegram::contacts($ctx));
$bot->onCommand('company', fn($ctx) => telegram::company($ctx));
$bot->onCommand('community', fn($ctx) => telegram::community($ctx));
$bot->onCommand('settings', fn($ctx) => telegram::settings($ctx));
$bot->onText('💬 Контакты', fn($ctx) => telegram::contacts($ctx));
$bot->onText('🏛️ О компании', fn($ctx) => telegram::company($ctx));
$bot->onText('🎯 Сообщество', fn($ctx) => telegram::community($ctx));
$bot->onText('⚙️ Настройки', fn($ctx) => telegram::settings($ctx));
$bot->onCbQueryData(['mail'], fn($ctx) => telegram::_mail($ctx));
$bot->onCbQueryData(['import_request'], fn($ctx) => telegram::import_request($ctx));
$bot->onCbQueryData(['tuning'], fn($ctx) => telegram::tuning($ctx));
$bot->onCbQueryData(['brands'], fn($ctx) => telegram::brands($ctx));
// Инициализация middleware с обработкой аккаунта
$bot->middleware([telegram::class, "account"]);
// Инициализация middleware с обработкой технических работ разных уровней
$bot->middleware([telegram::class, "suspension"]);
// Запуск чат-робота
$bot->run();

View File

@ -1,160 +0,0 @@
@charset "UTF-8";
main>section[data-section="catalog"] {
width: var(--width);
height: 100%;
display: flex;
flex-flow: row wrap;
gap: var(--gap, 5px);
/* justify-content: space-between; */
/* justify-content: space-evenly; */
/* background-color: var(--tg-theme-secondary-bg-color); */
}
main>section[data-section="catalog"]:last-child {
margin-bottom: unset;
}
main>section[data-section="catalog"]>a.category[type="button"] {
height: 23px;
padding: 8px 16px;
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
border-radius: 0.75rem;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
main>section[data-section="catalog"]>article.product {
/* --product-height: 200px; */
--product-height: 220px;
--title-font-size: 0.9rem;
--title-height: 1.5rem;
--button-height: 33px;
position: relative;
/* width: calc((100% - var(--gap) * 2) / 3); */
width: calc((100% - var(--gap)) / 2);
height: var(--product-height);
display: flex;
flex-grow: 0;
flex-direction: column;
white-space: nowrap;
border-radius: 0.75rem;
overflow: clip;
backdrop-filter: brightness(0.7);
cursor: pointer;
}
main>section[data-section="catalog"]>article.product:hover {
/* flex-grow: 0.1; */
/* background-color: var(--tg-theme-secondary-bg-color); */
}
main>section[data-section="catalog"]>article.product:hover>* {
transition: 0s;
}
main>section[data-section="catalog"]>article.product:not(:hover)>* {
transition: 0.2s ease-out;
}
main>section[data-section="catalog"]>article.product>img:first-of-type {
z-index: -50;
position: absolute;
bottom: var(--button-height);
/* bottom: calc(var(--button-height) + var(--title-height)); */
width: 100%;
/* height: 100%; */
height: calc(var(--product-height) - var(--button-height));
object-fit: cover;
}
main>section[data-section="catalog"]>article.product>img:first-of-type+* {
margin-top: auto;
}
main>section[data-section="catalog"]>article.product>a {
padding: 4px 8px 4px 8px;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
font-weight: bold;
backdrop-filter: brightness(0.4) contrast(1.2);
color: var(--text-light, var(--tg-theme-text-color));
}
main>section[data-section="catalog"]>article.product>a.title {
padding: 0 8px 0 8px;
height: var(--title-height);
min-height: var(--title-height);
line-height: var(--title-height);
font-size: var(--title-font-size);
}
main>section[data-section="catalog"]>article.product:hover>a.title {
backdrop-filter: brightness(0.35) contrast(1.2);
}
main>section[data-section="catalog"]>article.product>a+ :is(a, small) {
padding: 0px 8px 0 8px;
}
main>section[data-section="catalog"]>article.product>small {
padding: 3px 8px 0 8px;
white-space: normal;
word-break: break-all;
font-size: 0.7rem;
backdrop-filter: brightness(0.4) contrast(1.2);
color: var(--text-light, var(--tg-theme-text-color));
}
main>section[data-section="catalog"]>article.product>small.description {
overflow: hidden;
}
main>section[data-section="catalog"]>article.product:hover>small.description {
height: calc(var(--product-height) - var(--title-height) - var(--button-height) - 6px);
}
main>section[data-section="catalog"]>article.product:not(:hover)>small.description {
height: 0;
padding: 0 8px 0px 8px !important;
}
main>section[data-section="catalog"]>article.product>*:has(+ button:last-of-type) {
--offset-before-button: 9px;
padding: 4px 8px 13px 8px;
}
main>section[data-section="catalog"]>article.product>button:last-of-type {
height: var(--button-height);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
main>section[data-section="cart"] {
--diameter: 4rem;
z-index: 999;
right: 5vw;
bottom: 5vw;
position: fixed;
width: var(--diameter);
height: var(--diameter);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 100%;
background-color: var(--tg-theme-button-color);
}
main>section[data-section="cart"]>i.icon.shopping.cart {
top: -1px;
color: var(--tg-theme-button-text-color);
}

View File

@ -1,181 +0,0 @@
@charset "UTF-8";
main>section[data-section="catalog"] {
width: var(--width);
display: flex;
flex-flow: row wrap;
gap: var(--gap, 5px);
}
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"] {
--padding: 0.7rem;
position: relative;
height: 23px;
padding: var(--padding);
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
overflow: hidden;
border-radius: 0.75rem;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
main>section[data-section="catalog"][data-catalog-type="categories"]:last-child {
/* margin-bottom: unset; */
}
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:has(>img) {
min-width: calc(50% - var(--padding) * 2);
height: 180px;
padding: unset;
}
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]>img {
position: absolute;
left: -5%;
top: -5%;
width: 110%;
height: 110%;
object-fit: cover;
/* filter: blur(1px); */
filter: brightness(60%);
}
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:hover>img {
filter: unset;
}
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:has(>img)>p {
position: absolute;
left: var(--padding);
bottom: var(--padding);
right: var(--padding);
margin: unset;
width: min-content;
border-radius: 0.75rem;
background: var(--tg-theme-secondary-bg-color);
}
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]>p {
z-index: 100;
padding: 8px 16px;
}
main>section[data-section="catalog"][data-catalog-type="products"] {
--column: calc((100% - var(--gap)) / 2);
width: var(--width);
display: grid;
grid-gap: var(--gap);
grid-template-columns: repeat(2, var(--column));
grid-auto-flow: row dense;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--gap);
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product {
position: relative;
width: 100%;
display: flex;
flex-grow: 0;
flex-direction: column;
border-radius: 0.75rem;
overflow: clip;
backdrop-filter: brightness(0.7);
cursor: pointer;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover {
/* 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>* {
transition: 0s;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:not(:hover)>* {
transition: 0.2s ease-out;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a {
display: contents;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a>img:first-of-type {
width: 100%;
height: 100%;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a>img:first-of-type+* {
margin-top: auto;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a>p.title {
z-index: 50;
margin: unset;
padding: 4px 8px;
font-size: 0.9rem;
font-weight: bold;
overflow-wrap: anywhere;
hyphens: auto;
color: var(--tg-theme-text-color);
background-color: var(--tg-theme-secondary-bg-color);
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>button:last-of-type {
z-index: 100;
height: 33px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
main>section[data-section="cart"] {
--diameter: 4rem;
z-index: 999;
right: 5vw;
bottom: 5vw;
position: fixed;
width: var(--diameter);
height: var(--diameter);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 100%;
background-color: var(--tg-theme-button-color);
}
main>section[data-section="cart"]>i.icon.shopping.cart {
top: -1px;
color: var(--tg-theme-button-text-color);
}
main>section[data-section="filters"] {
--diameter: 4rem;
z-index: 999;
right: 5vw;
bottom: 5vw;
position: fixed;
width: var(--diameter);
height: var(--diameter);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 100%;
background-color: var(--tg-theme-button-color);
}
main>section[data-section="filters"][data-filter="brand"] {}

View File

@ -1,117 +0,0 @@
@charset "UTF-8";
main>section[data-section="catalog"] {
width: var(--width);
display: flex;
flex-flow: row wrap;
gap: var(--gap, 5px);
}
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"] {
height: 23px;
padding: 8px 16px;
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
border-radius: 0.75rem;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
main>section[data-section="catalog"][data-catalog-type="categories"]:last-child {
/* margin-bottom: unset; */
}
main>section[data-section="catalog"][data-catalog-type="products"] {
--column: calc((100% - var(--gap) * 2) / 3);
width: var(--width);
display: grid;
grid-gap: var(--gap);
grid-template-columns: repeat(3, var(--column));
grid-auto-flow: row dense;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--gap);
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product {
position: relative;
width: 100%;
display: flex;
flex-grow: 0;
flex-direction: column;
border-radius: 0.75rem;
overflow: clip;
backdrop-filter: brightness(0.7);
cursor: pointer;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover {
/* 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>* {
transition: 0s;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:not(:hover)>* {
transition: 0.2s ease-out;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>img:first-of-type {
z-index: -50;
width: 100%;
max-height: 120px;
object-fit: cover;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>img:first-of-type+* {
margin-top: auto;
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a.title {
padding: 4px 8px;
font-size: 0.9rem;
font-weight: bold;
word-break: break-word;
hyphens: auto;
color: var(--tg-theme-text-color);
background-color: var(--tg-theme-secondary-bg-color);
}
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>button:last-of-type {
height: 33px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
main>section[data-section="cart"] {
--diameter: 4rem;
z-index: 999;
right: 5vw;
bottom: 5vw;
position: fixed;
width: var(--diameter);
height: var(--diameter);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 100%;
background-color: var(--tg-theme-button-color);
}
main>section[data-section="cart"]>i.icon.shopping.cart {
top: -1px;
color: var(--tg-theme-button-text-color);
}

View File

@ -1,36 +0,0 @@
@charset "UTF-8";
@font-face {
font-family: 'DejaVu';
src: url("/themes/default/fonts/dejavu/DejaVuLGCSans-ExtraLight.ttf");
font-weight: 200;
font-style: normal;
}
@font-face {
font-family: 'DejaVu';
src: url("/themes/default/fonts/dejavu/DejaVuLGCSans.ttf");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'DejaVu';
src: url("/themes/default/fonts/dejavu/DejaVuLGCSans-Oblique.ttf");
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'DejaVu';
src: url("/themes/default/fonts/dejavu/DejaVuLGCSans-Bold.ttf");
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'DejaVu';
src: url("/themes/default/fonts/dejavu/DejaVuLGCSans-BoldOblique.ttf");
font-weight: 500;
font-style: italic;
}

View File

@ -1,36 +0,0 @@
@charset "UTF-8";
@font-face {
font-family: 'Kabrio';
src: url("/themes/default/fonts/kabrio/Kabrio-Light.ttf");
font-weight: 200;
font-style: normal;
}
@font-face {
font-family: 'Kabrio';
src: url("/themes/default/fonts/kabrio/Kabrio-Regular.ttf");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Kabrio';
src: url("/themes/default/fonts/kabrio/Kabrio-Italic.ttf");
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Kabrio';
src: url("/themes/default/fonts/kabrio/Kabrio-Heavy.ttf");
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Kabrio';
src: url("/themes/default/fonts/kabrio/Kabrio-HeavyItalic.ttf");
font-weight: 500;
font-style: italic;
}

View File

@ -1,30 +0,0 @@
@charset "UTF-8";
i.icon.close {
--diameter: 22px;
box-sizing: border-box;
position: relative;
display: block;
width: var(--diameter);
height: var(--diameter);
border: 2px solid transparent;
border-radius: 40px;
}
i.icon.close::after,
i.icon.close::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 16px;
height: 2px;
background: currentColor;
transform: rotate(45deg);
border-radius: 5px;
top: 8px;
left: 1px;
}
i.icon.close::after {
transform: rotate(-45deg);
}

View File

@ -1,26 +0,0 @@
@charset "UTF-8";
i.icon.hashtag {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(1);
width: 8px;
height: 16px;
border-left: 2px solid;
border-right: 2px solid;
}
i.icon.hashtag::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 16px;
height: 8px;
border-top: 2px solid;
border-bottom: 2px solid;
left: -6px;
top: 4px;
}

View File

@ -1,32 +0,0 @@
@charset "UTF-8";
@keyframes loading_spinner {
0% {
transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
}
i.icon.loading.spinner,
i.icon.loading.spinner::before {
box-sizing: border-box;
position: relative;
display: block;
width: 20px;
height: 20px;
}
i.icon.loading.spinner::before {
content: "";
position: absolute;
border-radius: 100px;
border: 3px solid transparent;
border-top-color: currentColor;
}
i.icon.loading.spinner.animated::before {
animation: loading_spinner 1s cubic-bezier(0.6, 0, 0.4, 1) infinite;
}

View File

@ -1,29 +0,0 @@
@charset "UTF-8";
i.icon.search {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(1);
width: 16px;
height: 16px;
border: 2px solid;
border-radius: 100%;
margin-left: -4px;
margin-top: -4px;
}
i.icon.search::after {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
border-radius: 3px;
width: 2px;
height: 8px;
background: currentColor;
transform: rotate(-45deg);
top: 10px;
left: 12px;
}

View File

@ -1,37 +0,0 @@
@charset "UTF-8";
i.icon.shopping.cart {
display: block;
box-sizing: border-box;
position: relative;
transform: scale(var(--ggs, 1));
width: 20px;
height: 21px;
background:
linear-gradient(to left, currentColor 12px, transparent 0) no-repeat -1px 6px/18px 2px,
linear-gradient(to left, currentColor 12px, transparent 0) no-repeat 6px 14px/11px 2px,
linear-gradient(to left, currentColor 12px, transparent 0) no-repeat 0 2px/4px 2px,
radial-gradient(circle, currentColor 60%, transparent 40%) no-repeat 12px 17px/4px 4px,
radial-gradient(circle, currentColor 60%, transparent 40%) no-repeat 6px 17px/4px 4px;
}
i.icon.shopping.cart::after,
i.icon.shopping.cart::before {
content: "";
display: block;
position: absolute;
box-sizing: border-box;
width: 2px;
height: 14px;
background: currentColor;
top: 2px;
left: 4px;
transform: skew(12deg);
}
i.icon.shopping.cart::after {
height: 10px;
top: 6px;
left: 16px;
transform: skew(-12deg);
}

View File

@ -1,16 +0,0 @@
@charset "UTF-8";
@keyframes initialization {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
*:not(#loading) {
animation: 0.2s ease-in 2s 1 normal forwards running initialization;
/* animation: 0.2s ease-in 1s forwards initialization; */
}

View File

@ -1,19 +0,0 @@
@charset "UTF-8";
section#loading {
z-index: 9999;
position: fixed;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: brightness(50%) contrast(120%) grayscale(80%) blur(3px);
cursor: progress;
}
section#loading[disabled] {
opacity: 0;
pointer-events: none;
transition: 0.3s ease-out;
}

View File

@ -1,155 +0,0 @@
@charset "UTF-8";
:root {
--text-light: #fafaff;
}
* {
text-decoration: none;
outline: none;
border: none;
font-family: "DejaVu";
color: var(--tg-theme-text-color);
transition: 0.1s ease-out;
}
a {
cursor: pointer;
color: var(--tg-theme-link-color);
}
body {
width: 100%;
height: 100%;
margin: 0;
min-height: 100vh;
padding: 0;
display: flex;
flex-direction: column;
overflow-x: clip;
background-color: var(--tg-theme-bg-color);
}
aside {}
header {}
main {
--offset-x: 2%;
padding: 0 var(--offset-x);
display: flex;
flex-direction: column;
align-items: center;
gap: 26px;
transition: 0s;
}
main>*[data-section] {
--gap: 16px;
--width: calc(100% - var(--gap) * 2);
width: var(--width);
}
main>section[data-section]>p {
margin: 0;
}
main>search {
--gap: 16px;
--border-width: 1px;
width: var(--width);
display: flex;
flex-flow: row;
border-radius: 1.375rem;
backdrop-filter: contrast(0.8);
border: 2px solid transparent;
overflow: clip;
}
search:has(input:is(:focus, :active)) {
border-color: var(--tg-theme-accent-text-color);
transition: unset;
}
search>label {
margin-inline-start: 0.75rem;
width: 1.5rem;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
search>label>i.icon {
color: var(--tg-theme-subtitle-text-color);
}
search:has(input:is(:focus, :active))>label>i.icon {
color: var(--tg-theme-accent-text-color);
transition: unset;
}
search>input {
width: 100%;
max-width: calc(100% - 3.25rem);
height: 2.5rem;
touch-action: manipulation;
padding: calc(.4375rem - var(--border-width)) calc(.625rem - var(--border-width)) calc(.5rem - var(--border-width)) calc(.75rem - var(--border-width));
background-color: transparent;
}
search>input:disabled {
cursor: progress;
color: var(--tg-theme-subtitle-text-color);
}
search:has(input:disabled) {
backdrop-filter: contrast(0.5);
}
*[type="button"] {
cursor: pointer;
}
:is(button, a[type="button"]) {
padding: 8px 16px;
display: flex;
justify-content: center;
align-items: center;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
button {
height: 33px;
}
a[type="button"] {
height: 23px;
}
:is(button, a[type="button"]):is(:hover) {
filter: brightness(120%);
}
:is(button, a[type="button"]):active {
filter: brightness(80%);
transition: 0s;
}
h1,
h2 {
margin: 28px 0 0;
}
footer {}
.unselectable {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

View File

@ -1,14 +0,0 @@
@charset "UTF-8";
main>section[data-section="suspension"]>p.remain {
width: 100%;
display: flex;
}
main>section[data-section="suspension"]>p.remain>span.time {
margin-left: auto;
}
main>section[data-section="suspension"]>p.description {
color: var(--tg-theme-hint-color);
}

View File

@ -1,160 +0,0 @@
@charset "UTF-8";
section#window {
z-index: 1500;
position: fixed;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: brightness(50%) contrast(120%) grayscale(60%) blur(1.2px);
}
section#window>div.card {
width: 85vw;
max-height: 90vh;
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: clip;
background-color: var(--tg-theme-bg-color);
}
section#window>div.card>h3 {
margin: 0 0 0.5rem 0;
height: 23px;
padding: 1rem 0;
display: inline-flex;
align-items: center;
gap: 1rem;
white-space: nowrap;
background-color: var(--tg-theme-header-bg-color);
}
section#window>div.card>h3>span.name {
margin-left: 1.5rem;
overflow: hidden;
text-overflow: ellipsis;
}
/* section#window>div.card>h3>small.brand {
margin-left: auto;
font-size: 0.8rem;
font-weight: normal;
color: var(--tg-theme-section-header-text-color);
} */
section#window>div.card>h3>a.exit[type="button"] {
margin-left: auto;
margin-right: 0.5rem;
padding: 0.6rem;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
color: var(--tg-theme-section-header-text-color);
background-color: unset;
}
section#window>div.card>div.images {
--image-width: 10rem;
height: 10rem;
display: flex;
overflow: clip;
cursor: zoom-in;
transition: 0s;
}
section#window>div.card>div.images.extend {
--image-width: max(30rem, 40vw);
z-index: 9999999;
left: 0;
top: 0;
margin: unset !important;
position: absolute;
width: 100vw;
max-width: unset;
height: 100vh;
align-items: center;
cursor: normal;
transition: 0s;
}
section#window>div.card>div.images>img {
margin-right: 0.5rem;
width: var(--image-width);
max-width: var(--image-width);
height: 100%;
flex-shrink: 0;
flex-grow: 0;
object-fit: cover;
border-radius: 0.5rem;
transition: 0s;
}
section#window>div.card>div.images.extend>img {
margin-right: 2vw;
height: max-content;
object-fit: unset;
border-radius: unset;
}
section#window>div.card>div.images>img:last-child {
margin-right: unset;
}
section#window>div.card>div.images>img.extend {
z-index: 9999999;
left: 0;
top: 0;
margin: unset !important;
position: absolute;
width: 100vw;
max-width: unset;
height: 100vh;
object-fit: contain;
border-radius: unset;
transition: 0s;
cursor: zoom-out;
}
section#window>div.card>p {
margin-bottom: unset;
min-height: 1rem;
padding: 0 1rem;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
}
section#window>div.card>p:last-of-type {
margin-bottom: revert;
}
section#window>div.card>div.footer {
display: flex;
flex-flow: row wrap;
align-items: baseline;
gap: 0 0.8rem;
background-color: var(--tg-theme-header-bg-color);
}
section#window>div.card>div.footer>small.dimensions {
margin-left: 1.5rem;
color: var(--tg-theme-section-header-text-color);
}
section#window>div.card>div.footer>small.weight {
color: var(--tg-theme-section-header-text-color);
}
section#window>div.card>div.footer>p.cost {
margin-left: auto;
margin-right: 1.5rem;
font-weight: bold;
}
section#window>div.card>div.footer>button.buy {
width: 100%;
height: 3.5rem;
}

Some files were not shown because too many files have changed in this diff Show More