Compare commits

...

1 Commits

Author SHA1 Message Date
Arsen Mirzaev Tatyano-Muradovich 7d794b16a5 процесс идёт 2024-09-30 14:29:41 +03:00
145 changed files with 9341 additions and 4947 deletions

View File

@ -1,3 +1,98 @@
# Telegram-robot for registering for tasks
# huesos
Synchronizes accounts with the site, displays a list of published applications with a selection by date, and also register to tasks
Basis for developing chat-robots with "Web App" technology for Telegram
## Installation
### AnangoDB
1. Create a View in ArangoDB for the document "product"
`
"links": {
"product": {
"fields": {
"description": {
"analyzers": [
"text_ru"
]
}
}
}
}
`
2. Create a Graph with the specified values
**Name:** hierarchy
**edgeDefinition:** entry
**fromCollections:** part, product...
**toCollections:** category, part...
3. Create indexes for the "product" collection
**Type:** "Inverted Index"
**Fields:** title.RU
**Analyzer:** "text_ru"
**Search field:** true
**Name:** title_ru
4. Create a View with the specified values
**Name:** products_search
**type:** search-alias (you can also use "arangosearch")
**indexes:**
`
"indexes": [
{
"collection": "product",
"index": "title_ru" (THIS IS AN EXAMPLE)
}
]
`
### NGINX
1. Add this to a NGINX config: `try_files $uri $uri/ /index.php;`
2. Add this to a NGINX config
`
location /images {
alias /PATH/TO/public/themes/default/images;
}
`
## Settings
Settings of chat-robot and Web App
Make sure you have a "settings" collection (can be created automatically) and at least one document with the "status" parameter set to "active"
`
{
"status": "active"
}
`
### language
Language for system messages if user language could not be determined
**Value:** en
## Suspensions
System of suspensions of chat-robot and Web App
Make sure you have a "suspension" collection (can be created automatically)
`
{
"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,31 +1,39 @@
{
"name": "mirzaev/arming_bot",
"type": "robot",
"tags": [
"description": "Chat-robot for tuning weapons",
"homepage": "https://t.me/arming_bot",
"type": "chat-robot",
"keywords": [
"telegram",
"chat-robot",
"military",
"shop"
],
"require": {
"triagens/arangodb": "^3.8",
"mirzaev/arangodb": "^1.0",
"badfarm/zanzara": "^0.9.1",
"nyholm/psr7": "^1.8",
"react/filesystem": "^0.1.2"
},
"license": "WTFPL",
"autoload": {
"psr-4": {
"mirzaev\\arming_bot\\": "mirzaev/arming_bot/system/"
}
},
"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",
"phpoffice/phpspreadsheet": "^2.1"
},
"autoload": {
"psr-4": {
"mirzaev\\arming_bot\\": "mirzaev/arming_bot/system/"
}
},
"minimum-stability": "stable",
"config": {
"allow-plugins": {

3100
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\categories,
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
{
if (!empty($parameters['categories']) && $parameters['categories'] !== ['/']) {
// Переданы категории ["category1", "category2", "category3"] (иерархия)
// Инициализация актуальной категории
$category = end($parameters['categories']);
if ($model = categories::category($category)) {
// Найдена модель обработки актуальной категории
if (method_exists($model, 'entries')) {
// Найден метод поиска вхождений
// Поиск категорий или товаров входящих в актуальную категорию
$entries = $model::entries(
category: $model->getId(),
filter: 'v.deleted != true && v.hidden != true',
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"
// @todo сделать автоматический поиск "самой верхней" категории
$this->view->categories = category::_read(
filter: 'd.deleted != true && d.hidden != true',
sort: 'd.position ASC, d.name ASC, d.created DESC',
amount: 30,
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']
); */
}
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')
],
'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,
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.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 of text fore search
preg_match('/[\d]+/', $parameters['id'] ?? '', $matches);
$_key = $matches[0] ?? null;
if (!empty($_key)) {
// Received id of prouct (_key)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// Search for products
$product = product::read(
filter: "d._key == \"$_key\" && d.deleted != true && d.hidden != true",
sort: 'd.created DESC',
amount: 1,
return: '{id: d._key, title: d.title.ru, description: d.description.ru, cost: d.cost, weight: d.weight, dimensions: d.dimensions, brand: d.brand.ru, compatibility: d.compatibility.ru}',
errors: $this->errors['catalog']
)[0]?->getAll();
if (!empty($product)) {
// Found the product
// Initializing buffer of images
$images = [];
foreach (
glob(INDEX .
DIRECTORY_SEPARATOR .
'themes' .
DIRECTORY_SEPARATOR .
(THEME ?? 'default') .
DIRECTORY_SEPARATOR .
'images' .
DIRECTORY_SEPARATOR .
$_key .
DIRECTORY_SEPARATOR .
"*.{jpg,png,gif}", GLOB_BRACE) as $file
) {
// Iterate over images of the product
// Write to buffer of images
$images[] = "/images/$_key/" . basename($file);
}
$product = $product + ['images' => $images];
}
// 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

@ -0,0 +1,193 @@
<?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;
// Library for ArangoDB
use ArangoDBClient\Document as _document;
// 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
*/
public static settings $settings;
/**
* Instance of a session
*/
protected readonly session $session;
/**
* Instance of an account
*/
protected readonly ?account $account;
/**
* Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => []
];
/**
* Constructor of an instance
*
* @param bool $initialize Initialize a controller?
*
* @return void
*/
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();
// 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
self::$settings = settings::active();
// Initializing of preprocessor of views
$this->view = new templater($this->session, $this->account);
// @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 ($account?->language ?? self::$settings?->language) {
'ru' => 'Приостановлено',
'en' => 'Suspended',
default => 'Suspended'
};
// Write description of the suspension to templater global variables
$this->view->description = $suspension->description[$account?->language ?? self::$settings?->language] ?? array_values($suspension->description)[0];
// Write message of remaining time of the suspension to templater global variables
$this->view->remain = [
'title' => match ($account?->language ?? self::$settings?->language) {
'ru' => 'Осталось времени: ',
'en' => 'Time remaining: ',
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

@ -0,0 +1,35 @@
<?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
{
// Поиск товаров
/* $this->view->products = product::read(); */
// Exit (success)
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render('catalog.html');
// Exit (fail)
return null;
}
}

View File

@ -0,0 +1,134 @@
<?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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,124 @@
<?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;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
// Framework for Telegram
use Zanzara\Telegram\Type\User as telegram;
// Built-in libraries
use exception;
/**
* Model of an account
*
* @package mirzaev\arming_bot\models
* @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';
/**
* Инициализация
*
* @param int $id Идентификатор Telegram
* @param telegram|array|null $registration Данные для регистрация, если аккаунт не найден
* @param array &$errors Registry of errors
*
* @return static|null Объект аккаунта, если найден
*/
public static function initialization(int $id, telegram|array|null $registration = null, array &$errors = []): static|null
{
try {
if (collection::init(core::$arangodb->session, self::COLLECTION)) {
if ($document = collection::search(core::$arangodb->session, sprintf("FOR d IN %s FILTER d.id == %u RETURN d", self::COLLECTION, $id))) {
// Найден аккаунт
// Инициализация объекта аккаунта
$account = new static;
// Запись инстанции документа в объект
$account->document = $document;
// Возврат (успех)
return $account;
} else if ($registration) {
// Не найден аккаунт и запрошена его регистрация
// Создание аккаунта
document::write(
core::$arangodb->session,
self::COLLECTION,
(is_array($registration)
? $registration :
[
'id' => $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' => $registration->getLanguageCode(),
'queries' => [
'inline' => $registration->getSupportsInlineQueries()
]
]) + [
'version' => ROBOT_VERSION,
'active' => true
]
);
// Инициализация (без регистрации)
return static::initialization($id, errors: $errors);
} else throw new exception('Account not found');
} else throw new exception('Failed to initialize document collection: ' . self::COLLECTION);
} catch (exception $e) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Выход (провал)
return null;
}
}

View File

@ -0,0 +1,294 @@
<?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;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
// Built-in libraries
use exception;
/**
* Model of a category
*
* @package mirzaev\arming_bot\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class categories extends core implements arangodb_document_interface
{
use arangodb_document_trait;
/**
* Name of the collection in ArangoDB
*/
public const string COLLECTION = 'THIS_COLLECTION_SHOULD_NOT_EXIST';
/**
* Запись категории
*
* @param string $name Название
* @param arrau $labels Ярлыки для отображения в интерфейсе ['EN' => "Label"]
* @param array $hierarchy Иерархия вложенности (от родителей к потомкам: [ model, model ... ])
* @param array &$errors Registry of errors
*
* @return string|null Идентификатор (_id) документа, если создан
*/
public static function write(
string $name,
array $labels = ['RU' => 'Без названия'],
array $hierarchy = [],
array &$errors = []
): string|null {
try {
if (collection::init(core::$arangodb->session, static::COLLECTION)) {
// Инициализирована коллекция
// Создание категории
$category = document::write(
core::$arangodb->session,
static::COLLECTION,
[
'name' => $name,
'labels' => $labels,
'version' => ROBOT_VERSION
]
);
if ($category) {
// Создана категория
/* if (collection::init(core::$arangodb->session, 'entry', true)) {
// Инициализирована коллекция
foreach ($hierarchy as $model) {
// Перебор иерархической структуры категорий
// Инициализация вложенной категории (следующей в массиве)
$next = current($hierarchy);
// Поиск ребра описывающего иерархическую связь
document::write(core::$arangodb->session, 'entry');
}
} else throw new exception('Failed to initialize document collection: ' . static::COLLECTION); */
}
} else throw new exception('Failed to initialize edge collection: entry');
} catch (exception $e) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Выход (провал)
return null;
}
/**
* Поиск вхождений (подкатегории или товары)
*
* Находит вхождения через ребро entry
* Генерирует _type со значениями "category" и "product"
* относительно того есть ли у документа ещё вложения (у product вложений быть не может)
* Объединяет возвращаемые объекты документа с переменной _type
*
* @param string|null $category Category identifier (_id)
* @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 entries(
?string $category = null,
?string $filter = null,
?string $sort = 'v.promotion DESC, v.position ASC, v.created DESC',
int $page = 1,
int $amount = 100,
array &$errors = []
): array {
try {
if (collection::init(core::$arangodb->session, static::COLLECTION)) {
// Инициализирована коллекция
if ($documents = collection::search(
core::$arangodb->session,
sprintf(
<<<AQL
FOR v IN 1..1 INBOUND %s GRAPH hierarchy
%s
%s
LIMIT %u, %u
LET _type = (FOR v2 IN INBOUND v._id GRAPH hierarchy RETURN v2)[0] ? "category" : "product"
RETURN MERGE(v, {_type})
AQL,
empty($category) ? '(FOR d IN ' . ${static::COLLECTION} . 'LIMIT 1 RETURN d._id)[0]' : "\"$category\"",
empty($filter) ? '' : "FILTER $filter",
empty($sort) ? '' : "SORT $sort",
--$page <= 0 ? $page = 0 : $page * $amount,
$amount,
)
)) {
// Найдены вхождения
// Возврат (успех)
return is_array($documents) ? $documents : [$documents];
} else return [];
} else throw new exception('Failed to initialize document collection: ' . static::COLLECTION);
} catch (exception $e) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Выход (провал)
return [];
}
/**
* Поиск категорий и товаров
*
* Ищет категории и товары по коллекции рёбер entry из _from и _to
*
* @param array &$errors Registry of errors
*
* @return array Массив с уникализированными найденными коллекциями (может быть пустым)
*/
protected static function collections(
array &$errors = []
): array {
try {
if (collection::init(core::$arangodb->session, $collection = 'entry')) {
// Инициализирована коллекция
if ($names = collection::search(
core::$arangodb->session,
sprintf(
<<<AQL
FOR e IN %s
COLLECT AGGREGATE
from_category = UNIQUE(PARSE_IDENTIFIER(e._from).collection),
to_category = UNIQUE(PARSE_IDENTIFIER(e._to).collection)
RETURN UNIQUE(UNION(from_category, to_category))
AQL,
$collection
)
)) {
// Найдены коллекции
// Возврат (успех)
return $names->getAll();
} else return [];
} else throw new exception('Failed to initialize edge collection: entry');
} catch (exception $e) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Выход (провал)
return [];
}
/**
* Поиск категории по названияю
*
* Перебирает все коллекции из self::collections() и ищет в них соответствие с параметром name
* ВНИМАНИЕ: НЕЛЬЗЯ ДОПУСКАТЬ ОДИНАКОВЫЕ НАЗВАНИЯ СРЕДИ РАЗНЫХ КАТЕГОРИЙ
*
* @param string $name Name of a collection
* @param array &$errors Registry of errors
*
* @return categories|null Модель имплементирующая документ с категорией, если была найдена
*/
public static function category(
string $name,
array &$errors = []
): ?categories {
try {
// Инициалиация списка коллекций
$collections = self::collections($errors);
if (count($collections) > 0) {
// Найдены коллекции
// Инициализация буфера части запроса со списком коллекций для аргументов UNION()
$union = [];
foreach ($collections as $collection) $union[] = "FOR d IN $collection RETURN d";
unset($collection);
if ($document = collection::search(
core::$arangodb->session,
sprintf(
<<<AQL
FOR u IN UNION(%s)
FILTER u.name == "%s"
SORT u.created ASC
LIMIT 1
RETURN u
AQL,
implode(', ', $union),
$name
)
)) {
// Найдена категория
// Инициализация названия коллекции
$collection = explode('/', $document->getId())[0];
if (class_exists($model = "mirzaev\\arming_bot\\models\\$collection")) {
// Найдена модель имплементирующая документы из этой коллекции
// Инициализация объекта модели
$object = new $model;
if ($object instanceof categories) {
// Объект является инстанцией категории (то есть не товара)
// Запись инстанции документа в объект модели
$object->document = $document;
// Возврат (успех)
return $object;
}
}
} else return null;
}
} catch (exception $e) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Выход (провал)
return null;
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\categories;
/**
* Model of a category
*
* @package mirzaev\arming_bot\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class category extends categories
{
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'category';
}

View File

@ -0,0 +1,602 @@
<?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\account;
// Фреймворк Telegram
use Zanzara\Zanzara,
Zanzara\Context,
Zanzara\Telegram\Type\Input\InputFile,
Zanzara\Telegram\Type\File\Document as telegram_document,
Zanzara\Telegram\Type\File\File,
Zanzara\Middleware\MiddlewareNode as Node;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
/**
* Model of a chat
*
* @package mirzaev\arming_bot\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class chat 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://arming.ru']
]
]
],
'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("import");
});
} else {
// Не авторизован доступ к настройкам
// Отправка сообщения
$ctx->sendMessage('⛔ *Нет доступа*');
}
}
/**
* Импорт товаров (доступ только авторизованным)
*
* @param Context $ctx
*
* @return void
*/
public static function import(Context $ctx): void
{
if ($ctx->get('account')?->access['settings']) {
// Авторизован доступ к настройкам
// Инициализация документа
$document = $ctx->getMessage()?->getDocument();
if ($document instanceof telegram_document) {
// Инициализирован документ
// Инициализация файла
$ctx->getFile($document->getFileId())->then(function ($file) use ($ctx) {
if ($file->getFileSize() <= 50000000) {
// Не превышает 50 мегабайт (50000000 байт) размер файла
if ($file->getFilePath()['extension'] === 'xlsx') {
// Имеет расширение xlsx файл
// Сохранение файла
file_put_contents(STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx', file_get_contents('https://api.telegram.org/file/bot' . KEY . '/' . $file->getFilePath()));
// Инициализация счётчика загруженных товаров
$loaded = $created = $updated = $deleted = $old = $new = 0;
// Отправка сообщения
$ctx->sendMessage(<<<TXT
*Загружено для обработки:* $loaded
*Добавлено:* $created
*Обновлено:* $updated
*Удалено:* $deleted
*Опубликовано в магазине:* $old \-\> *$new*
TXT)
->then(function ($message) use ($ctx) {
// Завершение диалога
$ctx->endConversation();
});
} else {
// Не имеет расширение xlsx файл
// Отправка сообщения
$ctx->sendMessage(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::initialization($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['chat-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 ?? controller::$settings?->language);
// Добавление описания причины приостановки, если найдена
if (!empty($suspension->description)) $message .= "\n\n" . $suspension->description[$account->language ?? controller::$settings?->language] ?? array_values($suspension->description)[0];
// Отправка сообщения
$ctx->sendMessage($message)
->then(function ($message) use ($ctx) {
// Завершение диалога
$ctx->endConversation();
});
// Блокировка дальнейшего выполнения
$ctx->set('stop', true);
} else {
// Не найдена активная приостановка
// Продолжение выполнения
$next($ctx);
}
}
/**
* 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
{
// Write 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) {
'arangodb' => $this::$arangodb,
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)
if (method_exists($this->document, $name)) return $this->document->{$name}($arguments);
}
}

View File

@ -0,0 +1,291 @@
<?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\document;
// Libraries for ArangoDB
use ArangoDBClient\Document as _document,
ArangoDBClient\DocumentHandler as _document_handler;
// Built-in libraries
use exception;
/**
* Core of models
*
* @package mirzaev\arming_bot\models
* @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
*/
protected static arangodb $arangodb;
/**
* Name of the collection in ArangoDB
*/
public const string COLLECTION = 'THIS_COLLECTION_SHOULD_NOT_EXIST';
/**
* 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 = true, ?arangodb $arangodb = null)
{
// For the extends system
parent::__construct($initialize);
if ($initialize) {
// Initializing is requested
if (isset($arangodb)) {
// Recieved an instance of a session of ArangoDB
// Write an instance of a session of ArangoDB to the property
$this->__set('arangodb', $arangodb);
} else {
// Not recieved an instance of a session of ArangoDB
// Initializing of an instance of a session of ArangoDB
$this->__get('arangodb');
}
}
}
/**
* Read 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 &$errors The registry on 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 &$errors = []
): _document|array|null {
try {
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
// Initialized the collection
// Read from ArangoDB and exit (success)
$result = collection::search(
static::$arangodb->session,
sprintf(
<<<'AQL'
FOR d IN %s
%s
%s
LIMIT %d, %d
RETURN %s
AQL,
static::COLLECTION,
empty($filter) ? '' : "FILTER $filter",
empty($sort) ? '' : "SORT $sort",
--$page <= 0 ? 0 : $page * $amount,
$amount,
$return
)
);
// Выход (успех)
return is_array($result) ? $result : [$result];
} else throw new exception('Failed to initialize the collection');
} catch (exception $e) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Update in ArangoDB
*
* @param _document $instance Instance of the document from ArangoDB
*
* @return bool Writed to ArangoDB without errors?
*/
public static function _update(_document $instance): bool
{
// Update in ArangoDB and exit (success)
return document::update(static::$arangodb->session, $instance);
}
/**
* Delete from ArangoDB
*
* @param _document $instance Instance of the document from ArangoDB
* @param array &$errors The registry on errors
*
* @return bool Deleted from ArangoDB without errors?
*/
public static function _delete(_document $instance, array &$errors = []): bool
{
try {
if (collection::init(static::$arangodb->session, static::COLLECTION)) {
// Initialized the collection
// Delete from ArangoDB and exit (success)
return (new _document_handler(static::$arangodb->session))->remove($instance);
} else throw new exception('Failed to initialize the collection');
} catch (exception $e) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return false;
}
/**
* 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 ($this->__isset('arangodb')) {
// Is alredy initialized
// Exit (fail)
throw new exception('Forbidden to reinitialize the session of ArangoDB ($this::$arangodb)', 500);
} else {
// Is not already initialized
if ($value instanceof arangodb) {
// Recieved an appropriate value
// Write the property and exit (success)
self::$arangodb = $value;
} else {
// Recieved an inappropriate value
// Exit (fail)
throw new exception('Session of 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) {
'arangodb' => (function () {
try {
if (!$this->__isset('arangodb')) {
// Is not initialized
// Initializing of a default value from settings
$this->__set('arangodb', new arangodb(require static::ARANGODB));
}
// Exit (success)
return self::$arangodb;
} catch (exception) {
// Exit (fail)
return null;
}
})(),
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

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models\enumerations;
enum session
{
case hash_only;
case hash_else_address;
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models\interfaces;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
// Framework for ArangoDB
use mirzaev\arangodb\connection as arangodb;
/**
* 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

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\categories;
/**
* Model of a part
*
* @package mirzaev\arming_bot\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class part extends categories
{
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'part';
}

View File

@ -0,0 +1,218 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core;
// 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 product
*
* @package mirzaev\arming_bot\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class product extends core
{
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'product';
/**
* Чтение товаров
*
* @param string|null $search Поиск
* @param string|null $filter Фильтр
* @param string|null $sort Сортировка
* @param int $page Страница
* @param int $amount Количество товаров на странице
* @param string|null $return
* @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.promotion DESC, d.position ASC, d.created DESC',
int $page = 1,
int $amount = 100,
?string $return = 'd',
array &$errors = []
): array {
try {
if (collection::init(core::$arangodb->session, self::COLLECTION)) {
// Инициализирована коллекция
// Инициализация строки запроса
$aql = sprintf(
<<<AQL
FOR d IN %s
AQL,
$search ? self::COLLECTION . 's_search' : self::COLLECTION
);
if ($search) {
// Search
$aql .= sprintf(
<<<AQL
SEARCH
LEVENSHTEIN_MATCH(
d.title.ru,
TOKENS("%s", "text_ru")[0],
1,
false
) OR
levenshtein_match(
d.description.ru,
tokens("%s", "text_ru")[0],
1,
false
) OR
levenshtein_match(
d.compatibility.ru,
tokens("%s", "text_ru")[0],
1,
false
)
AQL,
$search,
$search,
$search
);
// Adding sorting
if ($sort) $sort = "BM25(d) DESC, $sort";
else $sort = "BM25(d) DESC";
}
if ($documents = collection::search(
core::$arangodb->session,
sprintf(
$aql . <<<AQL
%s
%s
LIMIT %u, %u
RETURN %s
AQL,
empty($filter) ? '' : "FILTER $filter",
empty($sort) ? '' : "SORT $sort",
--$page <= 0 ? $page = 0 : $page * $amount,
$amount,
empty($return) ? 'd' : $return
)
)) {
// Найдены товары
// Возврат (успех)
return is_array($documents) ? $documents : [$documents];
} else return [];
} else throw new exception('Failed to initialize document collection: ' . self::COLLECTION);
} catch (exception $e) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Выход (провал)
return [];
}
/**
* Запись товара
*
* @param string $title Заголовок
* @param string|null $description Описание
* @param float $cost Цена
* @param float $weight Вес
* @param array $dimensions Габариты (float)
* @param array $images Изображения (первое - обложка) (https, путь в storage, иначе будет поиск в storage)
* @param array $hierarchy Иерархия вложенности (от родителей к потомкам: [ model, model ... ])
* @param array $data Дополнительные данные
* @param array &$errors Registry of errors
*
* @return string|null Идентификатор (_id) документа (товара), если создан
*/
public static function write(
string $title,
?string $description = null,
float $cost = 0,
float $weight = 0,
array $dimensions = ['x' => 0, 'y' => 0, 'z' => 0],
array $images = [],
array $hierarchy = [],
array $data = [],
array &$errors = []
): string|null {
try {
if (collection::init(core::$arangodb->session, self::COLLECTION)) {
// Инициализирована коллекция
// Создание товара
$product = document::write(
core::$arangodb->session,
self::COLLECTION,
[
'title' => $title,
'description' => $description,
'cost' => $cost ?? 0,
'weight' => $weight ?? 0,
'dimensions' => [
'x' => $dimensions['x'] ?? 0,
'y' => $dimensions['y'] ?? 0,
'z' => $dimensions['z'] ?? 0,
],
'images' => $images,
'version' => ROBOT_VERSION
] + $data
);
if ($product) {
// Создан товар
if (collection::init(core::$arangodb->session, 'entry', true)) {
// Инициализирована коллекция
foreach ($hierarchy as $model) {
// Перебор иерархической структуры категорий
// Инициализация вложенной категории (следующей в массиве)
$next = current($hierarchy);
// Поиск ребра описывающего иерархическую связь
document::write(core::$arangodb->session, 'entry');
}
} else throw new exception('Failed to initialize document collection: ' . self::COLLECTION);
}
} else throw new exception('Failed to initialize document collection: entry');
} catch (exception $e) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Выход (провал)
return null;
}
}

View File

@ -0,0 +1,338 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\account,
mirzaev\arming_bot\models\enumerations\session as verification,
mirzaev\arming_bot\models\traits\status,
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
// 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
* @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 session verification(
*/
public const verification VERIFICATION = verification::hash_else_address;
/**
* Constructor of an 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 instance of the ArangoDB document of session
*/
public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
{
try {
if (collection::init(static::$arangodb->session, self::COLLECTION)) {
// Initialized the collection
if (isset($hash) && $document = $this->hash($hash, $errors)) {
// Found an 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)) {
// Found an 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 an instance of the ArangoDB document of session
// Initializing a new session and write they into ArangoDB
$_id = document::write($this::$arangodb->session, self::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::search($this::$arangodb->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d._id == '%s' && d.expires > %d && d.active == true
RETURN d
AQL,
self::COLLECTION,
$_id,
time()
))) {
// Found an instance of just created new session
// Generate a hash and write into an instance of the ArangoDB document of session property
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
if (document::update($this::$arangodb->session, $session)) {
// Is writed update
// Writing document instance of the session from ArangoDB to the property of the implementing object
$this->document = $session;
} else throw new exception('Could not write the session data');
} else throw new exception('Could not create or find just created session');
}
} else throw new exception('Could not initialize the collection');
} catch (exception $e) {
// Write 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 implementing the account instance from the database, if found
*/
public function account(array &$errors = []): ?account
{
try {
if (collection::init(core::$arangodb->session, static::COLLECTION)) {
if (collection::init(core::$arangodb->session, 'connect', true)) {
if (collection::init(core::$arangodb->session, account::COLLECTION)) {
// Инициализирована коллекция
if ($document = collection::search(
core::$arangodb->session,
sprintf(
<<<AQL
FOR v IN INBOUND "%s" GRAPH sessions
SORT v.created DESC
LIMIT 1
RETURN v
AQL,
$this->getId(),
)
)) {
// Найден аккаунт
// Инициализация объекта аккаунта
$account = new account;
// Запись инстанции документа в объект
$account->__document($document);
// Exit (success)
return $account;
} else return null;
} else throw new exception('Failed to initialize document collection: ' . account::COLLECTION);
} else throw new exception('Failed to initialize edge collection: connect');
} else throw new exception('Failed to initialize document collection: ' . static::COLLECTION);
} catch (exception $e) {
// Write 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::init(core::$arangodb->session, static::COLLECTION)) {
if (collection::init(core::$arangodb->session, 'connect', true)) {
if (collection::init(core::$arangodb->session, account::COLLECTION)) {
// Collections initialized
// Writing document and exit (success)
return document::write(
core::$arangodb->session,
'connect',
[
'_from' => $account->getId(),
'_to' => $this->document->getId()
]
);
} else throw new exception('Failed to initialize document collection: ' . account::COLLECTION);
} else throw new exception('Failed to initialize edge collection: connect');
} else throw new exception('Failed to initialize document collection: ' . static::COLLECTION);
} catch (exception $e) {
// Write 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::init(core::$arangodb->session, static::COLLECTION)) {
// Collection initialized
// Search the session data in ArangoDB
return collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d.hash == '%s' && d.expires > %d && d.active == true
RETURN d
AQL,
static::COLLECTION,
$hash,
time()
));
} else throw new exception('Failed to initialize document collection: ' . static::COLLECTION);
} catch (exception $e) {
// Write 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::init(core::$arangodb->session, static::COLLECTION)) {
// Collection initialized
// Search the session data in ArangoDB
return collection::search(static::$arangodb->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d.address == '%s' && d.expires > %d && d.active == true
RETURN d
AQL,
static::COLLECTION,
$address,
time()
));
} else throw new exception('Failed to initialize document collection: ' . static::COLLECTION);
} catch (exception $e) {
// Write 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 buffer?
*/
public function write(array $data, array &$errors = []): bool
{
try {
if (collection::init($this::$arangodb->session, self::COLLECTION)) {
// Initialized the collection
// An instance of the ArangoDB document of session is initialized?
if (!isset($this->document)) throw new exception('An instance of the ArangoDB document of session is not initialized');
// Write data into buffwer of an instance of the ArangoDB document of session
$this->document->buffer = array_replace_recursive(
$this->document->buffer ?? [],
[$_SERVER['INTERFACE'] => array_replace_recursive($this->document->buffer[$_SERVER['INTERFACE']] ?? [], $data)]
);
// Write to ArangoDB and exit (success)
return document::update($this::$arangodb->session, $this->document) ? true : throw new exception('Не удалось записать данные в буфер сессии');
} else throw new exception('Could not initialize the collection');
} catch (exception $e) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return false;
}
}

View File

@ -0,0 +1,83 @@
<?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;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
// Built-in libraries
use exception;
/**
* Model of settings
*
* @package mirzaev\arming_bot\models
* @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 an instance of settngs document from ArangoDB
*/
public static function active(array|null $create = null, array &$errors = []): static|null
{
try {
if (collection::init(core::$arangodb->session, self::COLLECTION)) {
if ($document = collection::search(core::$arangodb->session, sprintf("FOR d IN %s FILTER d.status == 'active' SORT d.updated DESC LIMIT 1 RETURN d", self::COLLECTION))) {
// Найдены активные настройки
// Инициализация объекта настроек
$settings = new static;
// Запись инстанции документа в объект
$settings->document = $document;
// Возврат (успех)
return $settings;
} else if ($create) {
// Не найдены активные настройки и запрошено создание
// Создание настроек
document::write(core::$arangodb->session, self::COLLECTION, ['status' => 'active'] + $create);
// Инициализация (без создания)
return static::active(errors: $errors);
} else throw new exception('Settings not found');
} else throw new exception('Failed to initialize document collection: ' . self::COLLECTION);
} catch (exception $e) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Выход (провал)
return null;
}
}

View File

@ -0,0 +1,166 @@
<?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\settings,
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
* @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 an instance of suspension from ArangoDB
*/
public static function search(array &$errors = []): static|null
{
try {
if (collection::init(core::$arangodb->session, self::COLLECTION)) {
if ($document = collection::search(core::$arangodb->session, sprintf("FOR d IN %s FILTER d.end > %u SORT d.end DESC LIMIT 1 RETURN d", self::COLLECTION, time()))) {
// Найдены активные настройки
// Инициализация объекта настроек
$suspension = new static;
// Запись инстанции документа в объект
$suspension->document = $document;
// Возврат (успех)
return $suspension;
} else return null;
} else throw new exception('Failed to initialize document collection: ' . self::COLLECTION);
} catch (exception $e) {
// Write 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 string|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(?string $language = null, array &$errors = []): ?string
{
try {
// Initializing default value
$language ??= controller::$settings?->language ?? 'en';
// 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) {
'ru' => 'день',
'en' => 'day',
default => 'day'
},
2, 3, 4 => match ($language) {
'ru' => 'дня',
'en' => 'days',
default => 'days'
},
default => match ($language) {
'ru' => 'дней',
'en' => 'days',
default => 'days'
}
},
$difference->h,
match ($difference->h > 20 ? $difference->h % 10 : $difference->h % 100) {
1 => match ($language) {
'ru' => 'час',
'en' => 'hours',
default => 'hour'
},
2, 3, 4 => match ($language) {
'ru' => 'часа',
'en' => 'hours',
default => 'hours'
},
default => match ($language) {
'ru' => 'часов',
'en' => 'hours',
default => 'hours'
}
},
$difference->i,
match ($difference->i > 20 ? $difference->i % 10 : $difference->i % 100) {
1 => match ($language) {
'ru' => 'минута',
'en' => 'minute',
default => 'minute'
},
2, 3, 4 => match ($language) {
'ru' => 'минуты',
'en' => 'minutes',
default => 'minutes'
},
default => match ($language) {
'ru' => 'минут',
'en' => 'minutes',
default => 'minutes'
}
}
);
} catch (exception $e) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
}

View File

@ -0,0 +1,121 @@
<?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;
/**
* Trait for implementing a document instance from ArangoDB
*
* @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;
/**
* 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): ?_document
{
// Write 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;
}
/**
* 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
{
// Write 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) {
'arangodb' => core::$arangodb,
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

@ -0,0 +1,44 @@
<?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) {
// Write to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
}

View File

@ -0,0 +1,71 @@
<?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('/product/$id', 'catalog', 'product', 'POST')
->write('/$categories...', 'catalog', 'index', '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

@ -0,0 +1,62 @@
"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), 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

@ -0,0 +1,30 @@
"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), 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

@ -0,0 +1,628 @@
"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(() => {
console.log(typeof core, typeof core.damper, typeof core.telegram, typeof core.hotline);
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), 5000);
function initialization() {
if (typeof core.catalog === "undefined") {
// Not initialized
// Write to the core
core.catalog = class catalog {
/**
* Current position in hierarchy of the categories
*/
static categories = [];
/**
* 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) {
// Initialize of the new category name
const category = button.getAttribute("data-category-name");
this._category(category, 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 {HTMLElement} button Button of category <a>
* @param {bool} clean Clear search bar?
*
* @return {Promise} Request to the server
*/
static __category(category = "", clean = true) {
if (typeof category === "string") {
//
let urn;
if (category === "/" || category === "") urn = "/";
else {urn = this.categories.length > 0
? `/${this.categories.join("/")}/${category}`
: `/${category}`;}
return core.request(urn)
.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 = "";
}
// Write the category to position in the categories hierarchy
if (category !== "/" && category !== "") {
this.categories.push(category);
}
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) {
// Deinitialization of position in the categories hierarchy
this.categories = [];
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} id Identifier of a product
* @param {bool} force Ignore the damper?
*
* @return {void}
*/
static product(id, force = false) {
this._product(id, force);
}
/**
* Open product card (damper)
*
* @param {string} id 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} id Identifier of a product
*
* @return {Promise} Request to the server
*/
static __product(id) {
if (typeof id === "number") {
//
return core.request(`/product/${id}`)
.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");
h3.setAttribute("title", json.product.id);
const title = document.createElement("span");
title.classList.add("title");
title.innerText = json.product.title;
const brand = document.createElement("small");
brand.classList.add("brand");
brand.innerText = json.product.brand;
const images = document.createElement("div");
images.classList.add("images", "unselectable");
for (const uri of json.product.images) {
const image = document.createElement("img");
image.setAttribute("src", uri);
image.setAttribute("ondragstart", "return false;");
const button = core.telegram.api.isVisible;
const open = (event) => {
if (event.target === from) {
if (typeof images.hotline === "object") {
if (images.hotline.moving) return;
images.hotline.stop();
}
image.classList.add("extend");
if (button) core.telegram.api.MainButton.hide();
setTimeout(() => {
image.addEventListener("click", close);
image.addEventListener("touch", close);
}, 300);
image.removeEventListener("mouseup", open);
image.removeEventListener("touchend", open);
}
};
const close = () => {
if (typeof images.hotline === "object") {
images.hotline.start();
}
image.classList.remove("extend");
if (button) core.telegram.api.MainButton.show();
image.removeEventListener("click", close);
image.removeEventListener("touch", close);
image.addEventListener("mousedown", start);
image.addEventListener("touchstart", start);
};
const start = (event) => {
if (
event.type === "touchstart" ||
event.button === 0
) {
image.removeEventListener("mousedown", start);
image.removeEventListener("touchstart", start);
image.addEventListener("mouseup", open);
image.addEventListener("touchend", open);
}
};
image.addEventListener("mousedown", start);
image.addEventListener("touchstart", start);
images.append(image);
}
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");
dimensions.innerText = json.product.dimensions.x +
"x" +
json.product.dimensions.y + "x" +
json.product.dimensions.z;
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(title);
h3.append(brand);
card.append(h3);
card.append(images);
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.id },
json.product.title,
);
// блокировка закрытия карточки
let from;
const _from = (event) => from = event.target;
wrap.addEventListener("mousedown", _from);
wrap.addEventListener("touchstart", _from);
const remove = () => {
wrap.remove();
wrap.removeEventListener("mousedown", _from);
wrap.removeEventListener("touchstart", _from);
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;
};
document.addEventListener("click", close);
document.addEventListener("touch", close);
window.addEventListener("popstate", remove);
if (width > card.offsetWidth) {
images.hotline = new core.hotline(
json.product.id,
images,
);
images.hotline.step = -0.3;
images.hotline.wheel = true;
images.hotline.touch = true;
images.hotline.start();
}
}
}
});
}
}
};
}
}
});
});
})
);

View File

@ -0,0 +1,52 @@
"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

@ -0,0 +1,68 @@
"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), 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

@ -0,0 +1,772 @@
"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), 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

@ -0,0 +1,40 @@
"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), 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

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

View File

@ -0,0 +1,160 @@
@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

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

View File

@ -0,0 +1,117 @@
@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

@ -0,0 +1,36 @@
@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

@ -0,0 +1,36 @@
@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

@ -0,0 +1,32 @@
@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

@ -0,0 +1,29 @@
@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

@ -0,0 +1,37 @@
@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

@ -0,0 +1,16 @@
@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

@ -0,0 +1,19 @@
@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

@ -0,0 +1,152 @@
@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);
}
:is(button, a[type="button"]) {
padding: 8px 16px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
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

@ -0,0 +1,14 @@
@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

@ -0,0 +1,123 @@
@charset "UTF-8";
section#window {
z-index: 1500;
position: absolute;
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 1.5rem;
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.title {
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>div.images {
height: 10rem;
display: flex;
overflow: clip;
}
section#window>div.card>div.images>img {
margin-right: 0.5rem;
width: 10rem;
max-width: 10rem;
height: 100%;
flex-shrink: 0;
flex-grow: 0;
object-fit: cover;
border-radius: 0.5rem;
transition: 0s;
cursor: zoom-in;
}
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