Compare commits

..

No commits in common. "stable" and "1.12.0" have entirely different histories.

271 changed files with 7323 additions and 20222 deletions

1
.gitignore vendored
View File

@ -1,2 +1 @@
node_modules
vendor

393
README.md
View File

@ -1,160 +1,42 @@
# huesos
Base for creating shop chat-robots using Web App technology for [Telegram](https://telegram.org)
## Functions
1. Tree-structured catalog
2. Product cards with images carousel ([mirzaev/hotline.mjs](https://git.svoboda.works/mirzaev/hotline.mjs))
3. Cart (CRUD with limits and session binding)
4. Saving user data (and session) for all devices
5. Deliveries settings (with interactive maps and automatic geolocation detection on smartphones)
6. Real time price generation
7. Interface according to all Telegram standards
8. Public offer, dynamic settings and suspensions
9. Multi-language and easy to add new languages
10. Multi-currency and easy to add new currencies
11. Loading products and categories from an excel-file with automatic updating of existing ones
12. Flag authorization system, separate access for testers
13. Sending the generated order directly to the chat-robot
14. Intelligent search by titles, descriptions and other parameters (Levenshtein algorithm + separate settings for different languages)
15. Asynchronous chat-robot and Web App based on dynamic queries (AJAX)
16. Modern non-relational database ready for scaling and integration with third-party CRM
17. Fully documented code in English
18. Customizable menu buttons
19. Responsive design with built-in Telegram buttons and haptic functions
20. Automatic download and compression of images in 4 sizes (currently only from Yandex.Disk, but the system is ready to add new sources)
21. Commercially approved fonts and pure CSS icons
22. Product filter panel using pure CSS
23. Damper technology on all user interaction functions ([mirzaev/damper.mjs](https://git.svoboda.works/mirzaev/damper.mjs))
24. Two-step registration system (entering other data after creating an order)
25. Delivery company selection system (ready for scaling)
26. Acquiring company selection system (ready for scaling)
27. Sending paid orders to the operators chat with the customer contacts
## Integrations
### Import
*Methods for importing products into the shop*<br>
1. Excel-file (products and categories)
### Images download
*Methods of transferring images when importing products into the shop*<br>
1. [Yandex.Disk](https://360.yandex.ru/disk/) (russian) ([API](https://yandex.com/dev/disk/))
### Delivery companies
*Companies that deliver products from the shop*<br>
1. [CDEK](https://www.cdek.ru/) (russian) ([API](https://api-docs.cdek.ru/29923741.html)) ([PHP library](https://github.com/TTATPuOT/cdek-sdk2.0))
### Acquiring companies
*Companies that provide acquiring for the shop*<br>
1. [Robokassa](https://robokassa.com) (russian) (no swift) ([API](https://docs.robokassa.ru/pay-interface/))
## Dependencies
1. [PHP 8.4](https://www.php.net/releases/8.4/en.php)
2. [Composer](https://getcomposer.org/) (php package manager)
3. [MINIMAL](https://git.svoboda.works/mirzaev/minimal) (PHP framework)
4. [Twig](https://twig.symfony.com/) (HTML templater)
5. [Zanzara](https://github.com/badfarm/zanzara) (Telegram framework + ReactPHP)
6. [ArangoDB](https://docs.arangodb.com/3.11/about-arangodb/) (non-relational database)
7. [NGINX](https://nginx.org/en/) (web server) *(can be replaced)*
8. [SystemD](https://systemd.io/) (service manager) *(can be replaced)*
<small>You can find other dependencies in the file `/composer.json`</small>
Basis for developing chat-robots with "Web App" technology for Telegram
## Installation
### AnangoDB
1. **Configure unix-socket**<br>
Edit the file `/etc/arangodb3/arangod.conf`<br>
`endpoint = tcp://127.0.0.1:8529` -> `endpoint = unix:///var/run/arangodb3/arango.sock` (this will disable the web panel)<br>
<br>
To make the web panel work, you can add this to the NGINX server settings:
```lua
server {
...
server_name arangodb.domain.zone;
...
allow YOUR_IP_ADDRESS;
allow 192.168.1.1/24;
allow 127.0.0.1;
deny all;
# ArangoDB
location / {
proxy_pass http://arangodb;
}
}
upstream arangodb {
server unix:/var/run/arangodb3/arango.sock;
}
```
[here is my solution for "permission denied" problem on Ubuntu (accepted by ArangoDB maintainer)](https://github.com/arangodb/arangodb/issues/17302)<br>
1. **Configure TCP (instead of unix-socket)**<br>
Edit the file `/etc/arangodb3/arangod.conf`<br>
`endpoint = tcp://127.0.0.1:8529` -> `endpoint = tcp://0.0.0.0:8529`<br>
Edit the file `mirzaev/huesos/system/settings/arangodb.php`<br>
`unix:///var/run/arangodb3/arango.sock` -> `tcp://YOUR_IP_ADDRESS:8529` (it is slow and not secure)
---
2. **Create a Graph with the specified values**<br>
**Name:** catalog<br>
* Relation 1<br>
**edgeDefinition:** entry<br>
**fromCollections:** category, product<br>
1. Create a Graph with the specified values
**Name:** catalog<br/>
<br/>
**edgeDefinition:** entry<br/>
**fromCollections:** categoy, product<br/>
**toCollections:** category
* Relation 2<br>
**edgeDefinition:** reservation<br>
**fromCollections:** product<br>
**toCollections:** cart
---
3. **Create a Graph with the specified values**<br>
**Name:** users<br>
* Relation 1<br>
**edgeDefinition:** connect<br>
**fromCollections:** cart, session<br>
**toCollections:** account, session<br>
* Orphan Collections<br>
product
---
4. **Create indexes for the "product" collection**<br>
**Type:** "Inverted Index"<br>
**Fields:** name.ru<br>
**Analyzer:** "text_ru"<br>
**Search field:** true<br>
**Name:** name_ru<br><br>
*Add indexes for all search parameters and for all languages (search language is selected based on the user's language, <br>
otherwise from the default language specified in the active settings from **settings** collection document)*<br>
<br>
*See fields in the `mirzaev/arming_bot/models/product`<br>
**name.ru**, **description.ru** and **compatibility.ru***<br>
---
5. **Create a View with the specified values**<br>
**type:** search-alias (you can also use "arangosearch")<br>
**name:** **product**s_search<br>
**indexes:**<br><br>
You can copy an example of view file from here: `/examples/arangodb/views/products_search.json`
2. Create a Graph with the specified values
**Name:** sessions<br/>
<br/>
**edgeDefinition:** connect<br/>
**fromCollections:** account<br/>
**toCollections:** session
3. Create indexes for the "product" collection
**Type:** "Inverted Index"<br/>
**Fields:** name.ru<br/>
**Analyzer:** "text_ru"<br/>
**Search field:** true<br/>
**Name:** name_ru<br/>
<br/>
*Add indexes for all search parameters and for all languages (search language is selected based on the user's language, <br/>
otherwise from the default language specified in the active settings from **settings** collection document)*<br/>
<br/>
*See fields in the `mirzaev/arming_bot/models/product`<br/>
**name.ru**, **description.ru** and **compatibility.ru***
4. Create a View with the specified values
**type:** search-alias (you can also use "arangosearch")<br/>
**name:** **product**s_search<br/>
**indexes:**
```json
"indexes": [
{
@ -166,189 +48,70 @@ You can copy an example of view file from here: `/examples/arangodb/views/produc
### NGINX
1. **Create a NGINX server**<br>
You can copy an example of server file from here: `/examples/nginx/server.conf`
1. Example of NGINX server file
```nginx
location / {
try_files $uri $uri/ /index.php;
}
2. **Add support for javascript modules**<br>
Edit the file `/etc/nginx/mime.types`<br>
`application/javascript js;` -> `application/javascript js mjs;`
location ~ /(?<type>categories|products) {
root /var/www/arming_bot/mirzaev/arming_bot/system/storage;
try_files $uri =404;
}
3. **Generate a TLS/SSL sertificate** (via [certbot](http://certbot.eff.org/) for [ubuntu](https://ubuntu.com/))<br>
3.1. `sudo apt install certbot python3-certbot-nginx`<br>
3.2. `sudo certbot certonly --nginx` (The **domain** must already be **bound** to the **IP-address** of the server by `A-record` or `AAAA-record`)
5. **Firewall rules for HTTP and HTTPS** (for [ubuntu](https://ubuntu.com/))<br>
4.1 `sudo ufw allow "NGINX Full"`<br>
4.1.1 `sudo ufw allow 22` (make sure that the port for SSH connection is open)<br>
4.2 `sudo ufw enable`
location ~ \.php$ {
...
}
```
### SystemD (or any alternative you like)
You can copy an example of systemd file from here: `/examples/systemd/huesos.service`<br>
1. `sudo cp huesos.service /etc/systemd/system/huesos.service && sudo chmod +x /etc/systemd/system/huesos.service`
2. `sudo systemctl daemon-reload`
3. `sudo systemctl enable huesos`<br>
*before you execute the command think about **what it does** and whether the **paths** are specified correctly*<br>
1. Execute: `sudo cp telegram-huesos.service /etc/systemd/system/telegram-huesos.service`
*before you execute the command think about **what it does** and whether the **paths** are specified correctly*<br/>
*the configuration file is very simple and you can remake it for any alternative to SystemD that you like*
## Menu
*Menu inside the Web App*<br><br>
Make sure you have a **menu** collection (can be created automatically)<br>
You can copy a clean menu documents without comments from here: `/examples/arangodb/collections/menu`
```json
{
"urn": "/", // Link
"name": {
"en": "Main page",
"ru": "Главная страница"
},
"style": { // The `style` attribute
"order": 0
},
"class": "",
"icon": { // Icon from `/themes/default/css/icons`
"style": { // The `style` attribute
"rotate": "-135deg"
},
"class": "arrow circle" // Classes of the icon
},
"image": { // Image at the background @deprecated?
"storage": null
}
}
```
## Settings
*Settings of chat-robot and Web App*<br><br>
Make sure you have a **settings** collection (can be created automatically) and at least one document with the "status" parameter set to "active"<br>
You can copy a clean settings document without comments from here: `/examples/arangodb/collections/settings.json`
Settings of chat-robot and Web App<br/>
<br/>
Make sure you have a **settings** collection (can be created automatically) and at least one document with the "status" parameter set to "active"
```json
{
"status": "active", // Values: "active", "inactive" (string) Status of the settings document?
"status": "active",
"project": {
"name": "PROJECT" // Name of the projext (string)
"name": "NAME_OF_THE_PROJECT"
},
"language": "en", // Will be converted to an instance of enumeration `mirzaev\arming_bot\models\enumerations\language`
"currency": "usd", // Will be converted to an instance of enumeration `mirzaev\arming_bot\models\enumerations\currency`
"company": {
"identifier": null, // Example: "000000000000000" (string|null) (if `null` it will not be displayed)
"tax": null, // Example: "000000000000" (string|null) (if `null` it will not be displayed)
"name": null, // Example: "COMPANY" (string|null) (if `null` it will not be displayed)
"offer": false, // Display the data of a public offer in the footer? (bool) (does not affect the `/offer` page generation)
"sim": null, // Examples: "+7 000 000-00-00", "70000000000" (string|null) (if `null` it will not be displayed)
"mail": null // Example: "name@domain.zone" (string|null) (if `null` it will not be displayed)
},
"search": {
"enabled": true, // Enable the search input field?
"position": "fixed" // Values: "fixed", "relative"
},
"catalog": {
"categories": {
"display": "column", // Values: "row" (flex wrap), "column" (rows) Type of the catalog display
"structure": "lists", // Values: "pages" (pages with categories and products), "lists" (all categories as tree lists on the main page)
"buttons": {
"height": "120px", // Examples: "80px", "120px", "180px" (string|null) Height of buttons
"background": "#fafafa", // Examples: "#fafafa", "yellow" (string|null) Color of buttons background
"separator": {
"enabled": true, // Enable separators?
"width": "60%" // Exaples: "100%", "80%", "60%" (string|null) Width of separators over images (relative to image width from the left)
},
"lists": {
"height": "800px", // Examples: "500px", "100%" (string|null) Maximum height of lists (`max-height` for animations working)
"background": null, // Examples: "#fafafa", "yellow" (string|null) Color of lists
"separator": null, // Examples: "#fafafa", "yellow" (string|null) Color of separators between rows
"separated": true, // Separate lists from its buttons?
"blocks": true, // Blocks instead of plain text?
"arrow": true // Add arrow at the right?
},
"texts": {
"position": {
"vertical": "center" // Values: "top", "center", "bottom" (string|null) Position of texts of ascendants categories
},
"width": "max(8rem, 20%)", // Examples: "60%", "5rem", "max(8rem, 20%)" (string|null) Width of the text section (left side of buttons)
"background": false, // Enable wrapping element for texts?
"title": {
"color": "#020202" // Examples: "#fafafa", "yellow" (string|null) Color of titles
},
"description": {
"color": "#121212" // Examples: "#fafafa", "yellow" (string|null) Color of descriptions
}
},
"images": {
"filter": "contrast(1.2)" // Example: "contrast(1.2)" (string|null) Filter for images
}
}
}
},
"cart": {
"enabled": true // Enable the cart button?
},
"css": {
"catalog-button-cart-background": "#40a7e3",
"product-button-cart-background": "#40a7e3"
"catalog-button-cart-added-background": "#90be36",
"product-button-cart-added-background": "#90be36"
},
"account": {
"enabled": false // Enable the account section? (works only when opened from telegram `inline-button`)
},
"menu": {
"enabled": true, // Enable the main menu?
"position": "fixed" // Values: "fixed" (fixed to the bottom as a solid line), "relative" (at the top as separated buttons) (stirng) Position of the main menu
},
"header": {
"enabled": true, // Enable the header?
"position": "fixed" // Values: "fixed" (fixed to the bottom), "relative" (at the top) (stirng) Position of the header
},
"acquirings": {
"robokassa": {
"enabled": true, // Enable the Robokassa acquiring?
"mode": "test" // Values: "work", "test" (string) Mode of the Robokassa acquiring
}
},
"input": {
"deliveries": {
"site": [], // Values: "sim", "name", "destination", "address", "commentary"
"chat": [
"sim",
"name",
"destination",
"address",
"commentary"
] // Values: "sim", "name", "destination", "address", "commentary"
}
},
"deliveries": {
"cdek": {
"enabled": true, // Enable CDEK delivery?
"label": "CDEK" // Name of the CDEK delivery
}
},
"chats": [
{
"id": null, // Example: -1002599391893 (int) (negative number) The telegram chat identifier
"orders": true // Send orders? (for moderators)
}
]
"language": "en",
"currency": "usd"
}
```
### Language
Language for render of interface, if account or session language is not initialized<br/>
<br/>
**Value:** en<br/>
**⚠️ The value will be converted to an instance of enumeration** `mirzaev\arming_bot\models\enumerations\language`
### Currency
Currency for calculations and render of interface, if account or session currency is not initialized<br/>
<br/>
**Value:** usd<br/>
**⚠️ The value will be converted to an instance of enumeration** `mirzaev\arming_bot\models\enumerations\currency`
## Suspensions
*System of suspensions of chat-robot and Web App*<br><br>
Make sure you have a **suspension** collection (can be created automatically)<br>
You can copy a clean suspension document without comments from here: `/examples/arangodb/collections/suspension.json`
System of suspensions of chat-robot and Web App<br/>
<br/>
Make sure you have a **suspension** collection (can be created automatically)
```json
{
"end": 1726068961, // Unixtime
"end": 1726068961,
"targets": {
"chat-robot": true, // Block chat-robot
"web app": true // Block "Web App"
},
"chat-robot": true,
"web app": true
}
"access": {
"tester": true, // Account with `"tester": true`
"developer": true // Account with `"developer": true`
"tester": true,
"developer": true
},
"description": {
"ru": "Разрабатываю каталог, поиск и корзину",
@ -356,13 +119,3 @@ You can copy a clean suspension document without comments from here: `/examples/
}
}
```
## Used by
*List of projects created on the basis of [huesos](https://git.svoboda.works/mirzaev/huesos)*
- ARMING [@arming_bot](https://t.me/arming_bot)<br>
*Russian weapons tuning shop* ([repository](https://git.svoboda.works/mirzaev/arming))

View File

@ -1,51 +1,48 @@
{
"name": "mirzaev/huesos",
"description": "Chat-robot for tuning weapons",
"homepage": "https://t.me/huesos",
"type": "chat-robot",
"keywords": [
"telegram",
"chat-robot",
"military",
"shop"
],
"readme": "README.md",
"license": "WTFPL",
"authors": [
{
"name": "Arsen Mirzaev Tatyano-Muradovich",
"email": "arsen@mirzaev.sexy"
}
],
"require": {
"php": "^8.4",
"ext-gd": "^8.4",
"ext-intl": "^8.4",
"triagens/arangodb": "^3.8",
"mirzaev/minimal": "^3.4.0",
"mirzaev/arangodb": "^2",
"badfarm/zanzara": "^0.9.1",
"nyholm/psr7": "^1.8",
"react/filesystem": "^0.1.2",
"twig/twig": "^3.10",
"twig/extra-bundle": "^3.7",
"twig/intl-extra": "^3.10",
"avadim/fast-excel-reader": "^2.19",
"ttatpuot/cdek-sdk2.0": "^1.2",
"guzzlehttp/guzzle": "^7.9",
"php-http/guzzle7-adapter": "^1.0",
"react/async": "^4.3"
},
"autoload": {
"psr-4": {
"mirzaev\\huesos\\": "mirzaev/huesos/system/"
}
},
"minimum-stability": "stable",
"config": {
"allow-plugins": {
"php-http/discovery": true,
"wyrihaximus/composer-update-bin-autoload-path": true
}
}
"name": "mirzaev/arming_bot",
"description": "Chat-robot for tuning weapons",
"homepage": "https://t.me/arming_bot",
"type": "chat-robot",
"keywords": [
"telegram",
"chat-robot",
"military",
"shop"
],
"readme": "README.md",
"license": "WTFPL",
"authors": [
{
"name": "Arsen Mirzaev Tatyano-Muradovich",
"email": "arsen@mirzaev.sexy"
}
],
"require": {
"triagens/arangodb": "^3.8",
"mirzaev/minimal": "^2.2",
"mirzaev/arangodb": "^1.3",
"badfarm/zanzara": "^0.9.1",
"nyholm/psr7": "^1.8",
"react/filesystem": "^0.1.2",
"twig/twig": "^3.10",
"twig/extra-bundle": "^3.7",
"twig/intl-extra": "^3.10",
"avadim/fast-excel-reader": "^2.19",
"openswoole/core": "22.1.5",
"ttatpuot/cdek-sdk2.0": "^1.2",
"guzzlehttp/guzzle": "^7.9",
"php-http/guzzle7-adapter": "^1.0"
},
"autoload": {
"psr-4": {
"mirzaev\\arming_bot\\": "mirzaev/arming_bot/system/"
}
},
"minimum-stability": "stable",
"config": {
"allow-plugins": {
"php-http/discovery": true,
"wyrihaximus/composer-update-bin-autoload-path": true
}
}
}

1048
composer.lock generated Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +0,0 @@
{
"urn": "/account",
"name": {
"en": "Account",
"ru": "Аккаунт"
},
"identifier": "account",
"style": {
"order": 1
},
"class": "",
"icon": {
"style": {},
"class": "loading spinner animated"
},
"image": {
"storage": null
}
}

View File

@ -1,18 +0,0 @@
{
"urn": "/cart",
"name": {
"en": "Cart",
"ru": "Корзина"
},
"style": {
"order": 999
},
"class": "cart",
"icon": {
"style": {},
"class": "shopping cart"
},
"image": {
"storage": null
}
}

View File

@ -1,18 +0,0 @@
{
"urn": "/",
"name": {
"en": "Main page",
"ru": "Главная страница"
},
"style": {
"order": 0
},
"class": "",
"icon": {
"style": {},
"class": "house"
},
"image": {
"storage": null
}
}

View File

@ -1,27 +0,0 @@
{
"status": "active",
"project": {
"name": "PROJECT"
},
"language": "en",
"currency": "usd",
"company": {
"identifier": null,
"tax": null,
"name": null,
"offer": false,
"sim": null,
"mail": null
},
"search": {
"enabled": true,
"position": "fixed"
},
"cart": {
"enabled": true
},
"css": {
"catalog-button-cart-background": "#40a7e3",
"product-button-cart-background": "#40a7e3"
}
}

View File

@ -1,15 +0,0 @@
{
"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,20 +0,0 @@
{
"type": "search-alias",
"name": "products_search",
"indexes": [
{
"collection": "product",
"index": "name_ru"
},
{
"collection": "product",
"index": "description_ru"
},
{
"collection": "product",
"index": "compatibility_ru"
}
],
"id": "1368785",
"globallyUniqueId": "hB561949FBEF8/1368785"
}

View File

@ -1,52 +0,0 @@
#
# This section is commented out to make it possible to run NGINX without errors
# to generate TLS/SSL certificate via CertBot (see README.md)
#
# server {
# listen 443 default_server ssl;
# listen [::]:443 ssl default_server;
# server_name domain.zone;
# root /var/www/huesos/mirzaev/huesos/system/public;
# index index.php;
# ssl_certificate /etc/letsencrypt/live/domain.zone/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/domain.zone/privkey.pem;
# include /etc/letsencrypt/options-ssl-nginx.conf;
# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# location / {
# try_files $uri $uri/ /index.php?$query_string;
# }
# location /api/cdek {
# rewrite ^/api/cdek(.*)$ /$1 break;
# index cdek.php;
# }
# location ~ /(?<type>categories|products) {
# root /var/www/huesos/mirzaev/huesos/system/storage;
# try_files $uri =404;
# }
# location ~ \.php$ {
# include snippets/fastcgi-php.conf;
# fastcgi_pass unix:/run/php/php8.4-fpm.sock;
# }
# }
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name domain.zone;
if ($host = domain.zone) {
return 301 https://$host$request_uri;
}
return 404;
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\session,
mirzaev\arming_bot\models\account as model;
// Framework for ArangoDB
use mirzaev\arangodb\document;
/**
* Controller of account
*
* @package mirzaev\arming_bot\controllers
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class account extends core
{
/**
* Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => []
];
/**
* Write to the buffer
*
* @param array $parameters Parameters of the request (POST + GET)
*
* @return void
*/
public function write(array $parameters = []): void
{
if (!empty($parameters) && $this->account instanceof model) {
// Received parameters and initialized account
// Declaring the buffer of deserialized parameters
$deserialized = [];
foreach ($parameters as $name => $value) {
// Iterate over parameters
// Validation of the parameter value
if (mb_strlen($value) > 4096) continue;
// Declaring the buffer of deserialized parameter
$parameter = null;
// Deserializing name to multidimensional array
foreach (array_reverse(explode('_', $name)) as $key)
$parameter = [$key => $parameter ?? (json_validate($value) ? json_decode($value, true, 10) : $value)];
// Writing into the buffer of deserialized parameters
$deserialized = array_merge_recursive($parameter, $deserialized);
}
// Write to the account document from ArangoDB
if (!empty($deserialized)) $this->account->write($deserialized, $this->errors['account']);
}
}
}

View File

@ -0,0 +1,344 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\cart as model,
mirzaev\arming_bot\models\product,
mirzaev\arming_bot\models\menu,
mirzaev\arming_bot\models\enumerations\language;
// Framework for ArangoDB
use mirzaev\arangodb\document;
/**
* Controller of cart
*
* @package mirzaev\arming_bot\controllers
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class cart extends core
{
/**
* Instance of the cart
*/
protected readonly ?model $cart;
/**
* Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => [],
'cart' => [],
'menu' => [],
];
/**
* Cart
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function index(array $parameters = []): ?string
{
if (isset($menu)) {
//
} else {
// Not received ... menu
// Search for filters and write to the buffer of global variables of view templater
$this->view->menu = menu::_read(
return: 'MERGE(d, { name: d.name.@language })',
sort: 'd.style.order ASC, d.created DESC, d._key DESC',
amount: 4,
parameters: ['language' => $this->language->name],
errors: $this->errors['menu']
);
}
// Initializing the cart
$this->cart = $this->account?->cart() ?? $this->session?->cart();
// Initializing the cart data
$this->view->cart = [
'summary' => $this->cart?->summary(currency: $this->currency),
'products' => $this->cart?->products(language: $this->language, currency: $this->currency)
];
// Initializing types of avaiabld deliveries
$this->view->deliveries = [
'cdek' => [
'label' => 'CDEK'
]
];
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// GET request
// Exit (success)
return $this->view->render('cart/page.html', [
'h2' => $this->language === language::ru ? 'Корзина' : 'Cart' // @see https://git.mirzaev.sexy/mirzaev/huesos/issues/1
]);
} 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(
[
'main' => '',
'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
*
* Write or delete the product in the cart
*
* @param array $parameters Parameters of the request (POST + GET)
*
* @todo
* 1. Add a limit on adding products to the cart based on the number of products in stock
*/
public function product(array $parameters = []): ?string
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// Declaring of the buffer with amount of the product in the cart
$amount = 0;
// Validating @todo add throwing errors
$identifier = null;
if (isset($parameters['identifier']) && preg_match('/[\d]+/', urldecode($parameters['identifier']), $matches)) $identifier = (int) $matches[0];
if (isset($identifier)) {
// Received and validated identfier of the product
// Search for the product
$product = product::read(
filter: "d.identifier == @identifier && d.deleted != true && d.hidden != true",
sort: 'd.created DESC',
amount: 1,
parameters: ['identifier' => $identifier],
errors: $this->errors['cart']
);
if ($product instanceof product) {
// Initialized the product
// Initializing the cart
$this->cart = $this->account?->cart() ?? $this->session?->cart();
if ($this->cart instanceof model) {
// Initialized the cart
// Initializing the buffer with amount of the product in the cart
$amount = $this->cart->count(product: $product, limit: 100, errors: $this->errors['cart']) ?? 0;
if ($this->cart instanceof model) {
// Initialized the cart
// Validating @todo add throwing errors
$type = null;
if (isset($parameters['type']) && preg_match('/[\w]+/', urldecode($parameters['type']), $matches)) $type = $matches[0];
if (isset($type)) {
// Received and validated type of action with the product
if ($type === 'toggle') {
// Write the product to the cart if is not in the cart and vice versa
if ($amount > 0) {
// The cart has the product
// Deleting the product from the cart
$this->cart->delete(product: $product, amount: $amount, errors: $this->errors['cart']);
// Reinitializing the buffer with amount of the product in the cart
$amount = 0;
} else {
// The cart has no the product
// Writing the product to the cart
$this->cart->write(product: $product, amount: 1, errors: $this->errors['cart']);
// Reinitializing the buffer with amount of the product in the cart
$amount = 1;
}
} else {
// Received not the "toggle" command
// Validating @todo add throwing errors
$_amount = null;
if (isset($parameters['amount']) && preg_match('/[\d]+/', urldecode($parameters['amount']), $matches)) $_amount = (int) $matches[0];
if (isset($_amount)) {
// Received and validated amount parameter for action with the product
if ($type === 'write') {
// Increase amount of the product in the cart
if ($amount + $_amount < 101) {
// Validated amount to wrting
// Writing the product to the cart
$this->cart->write(product: $product, amount: $_amount, errors: $this->errors['cart']);
// Reinitialize the buffer with amount of the product in the cart
$amount += $_amount;
}
} else if ($type === 'delete') {
// Decrease amount of the product in the cart
if ($amount - $_amount > -1) {
// Validated amount to deleting
// Deleting the product from the cart
$this->cart->delete(product: $product, amount: $_amount, errors: $this->errors['cart']);
// Reinitialize the buffer with amount of the product in the cart
$amount -= $_amount;
}
} else if ($type === 'set') {
// Set amount of the product in the cart
if ($_amount > -1 && $_amount < 101) {
// Validated amount to setting
if ($_amount > $amount) {
// Requested amount more than actual amount of the product in the cart
// Writing the product from the cart
$this->cart->write(product: $product, amount: $_amount - $amount, errors: $this->errors['cart']);
} else {
// Requested amount less than actual amount of the product in the cart
// Deleting the product from the cart
$this->cart->delete(product: $product, amount: $amount - $_amount, errors: $this->errors['cart']);
}
// Reinitializing the buffer with amount of the product in the cart
$amount = $_amount;
}
}
}
}
}
}
}
}
}
// 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(
[
'amount' => $amount, // $amount does not store a real value, but is calculated without a repeated request to ArangoDB
'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;
}
/**
* Summary
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function summary(array $parameters = []): ?string
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// Initializing the cart
$this->cart = $this->account?->cart() ?? $this->session?->cart();
if ($this->cart instanceof model) {
// Initialized the cart
// Initializing summary data of the cart
$summary = $this->cart?->summary(currency: $this->currency, errors: $this->errors['cart']);
// Initializing 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(
[
'cost' => $summary['cost'] ?? 0,
'amount' => $summary['amount'] ?? 0,
'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,435 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\entry,
mirzaev\arming_bot\models\category,
mirzaev\arming_bot\models\product,
mirzaev\arming_bot\models\menu;
// Library for ArangoDB
use ArangoDBClient\Document as _document;
/**
* Controller of catalog
*
* @package mirzaev\arming_bot\controllers
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class catalog extends core
{
/**
* Registry of errors
*/
protected array $errors = [
'session' => [],
'account' => [],
'menu' => [],
'catalog' => []
];
/**
* Catalog
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function index(array $parameters = []): ?string
{
// validating
if (isset($parameters['product']) && preg_match('/[\d]+/', $parameters['product'], $matches)) $product = (int) $matches[0];
if (isset($product)) {
// Received and validated identifier of the product
// Search for the product data and write to the buffer of global variables of view templater
$this->view->product = product::read(
filter: "d.identifier == @identifier && d.deleted != true && d.hidden != true",
sort: 'd.created DESC',
amount: 1,
return: '{identifier: d.identifier, name: d.name.@language, description: d.description.@language, cost: d.cost.@currency, weight: d.weight, dimensions: d.dimensions, brand: d.brand.@language, compatibility: d.compatibility.@language, cost: d.cost.@currency, images: d.images[*].storage}',
language: $this->language,
currency: $this->currency,
parameters: ['identifier' => $product],
errors: $this->errors['catalog']
)?->getAll();
}
// Intializing buffer of query parameters
$_parameters = [];
// Initializing buffer of filters query (AQL)
$_filters = 'd.deleted != true && d.hidden != true';
// Validating
if (isset($parameters['brand']) && preg_match('/[\w\s]+/u', urldecode($parameters['brand']), $matches)) $brand = $matches[0];
if (isset($brand)) {
// Received and validated filter by brand
// Writing to the account buffer
$this->account?->write(
[
'catalog' => [
'filters' => [
'brand' => $brand
]
]
]
);
// Writing to the session buffer
$this->session?->write(
[
'catalog' => [
'filters' => [
'brand' => $brand
]
]
]
);
// Writing to the buffer of filters query (AQL)
$_filters .= ' && d.brand.@language == @brand';
// Writing to the buffer of query parameters
$_parameters['brand'] = $brand;
} else {
// Not received or not validated filter by brand
// Writing to the account buffer
$this->account?->write(
[
'catalog' => [
'filters' => [
'brand' => null
]
]
]
);
// Writing to the session buffer
$this->session?->write(
[
'catalog' => [
'filters' => [
'brand' => null
]
]
]
);
}
// Initialize buffer of filters query (AQL)
$_sort = 'd.position ASC, d.name ASC, d.created DESC';
// Validating
if (isset($parameters['sort']) && preg_match('/[\w\s]+/u', $parameters['sort'], $matches)) $sort = $matches[0];
if (isset($sort)) {
// Received and validated sort
// Writing to the account buffer
$this->account?->write(
[
'catalog' => [
'sort' => $sort
]
]
);
// Writing to the session buffer
$this->session?->write(
[
'catalog' => [
'sort' => $sort
]
]
);
// Write to the buffer of sort query (AQL)
$_sort = "d.@sort DESC, $_sort";
// Write to the buffer of query parameters
$_parameters['sort'] = $sort;
} else {
// Not received or not validated filter by brand
// Writing to the account buffer
$this->account?->write(
[
'catalog' => [
'sort' => null
]
]
);
// Writing to the session buffer
$this->session?->write(
[
'catalog' => [
'sort' => null
]
]
);
}
// Validating @todo add throwing errors
if (isset($parameters['text']) && preg_match('/[\w\s]+/u', urldecode($parameters['text']), $matches) && mb_strlen($matches[0]) > 2) $text = $matches[0];
if (isset($text)) {
// Received and validated text
// Writing to the account buffer (useless becouse rewrite itself to null with every request)
$this->account?->write(
[
'catalog' => [
'search' => [
'text' => $text
]
]
]
);
// Writing to the session buffer (useless becouse rewrite itself to null with every request)
$this->session?->write(
[
'catalog' => [
'search' => [
'text' => $text
]
]
]
);
} else {
// Not received or not validated filter by brand
// Writing to the account buffer
$this->account?->write(
[
'catalog' => [
'search' => [
'text' => null
]
]
]
);
// Writing to the session buffer
$this->session?->write(
[
'catalog' => [
'search' => [
'text' => null
]
]
]
);
}
// Validating
if (isset($parameters['category']) && preg_match('/[\d]+/', $parameters['category'], $matches)) $category = (int) $matches[0];
if (isset($category)) {
// Received and validated identifier of the category
// Initialize of category
$category = category::_read('d.identifier == @identifier', parameters: ['identifier' => $category], errors: $this->errors['catalog']);
if ($category instanceof category) {
// Found the category
// Write to the response buffer
$response['category'] = ['name' => $category->name ?? null];
// Search for categories that are descendants of $category
$entries = entry::search(
document: $category,
amount: 50,
categories_merge: 'name: v.name.@language',
/* products_merge: 'DISTINCT MERGE(d, {name: d.name.@language, description: d.description.@language, compatibility: d.compatibility.@language, brand: d.brand.@language, cost: d.cost.@currency})', */
parameters: ['language' => $this->language->name],
errors: $this->errors['catalog']
);
// Initialize buffers of entries (in singular, by parameter from ArangoDB)
$category = $product = [];
foreach ($entries as $entry) {
// Iterate over entries (descendands)
// Write entry to the buffer of entries (sort by $category and $product)
${$entry->_type}[] = $entry;
}
// Write to the buffer of global variables of view templater
$this->view->categories = $category;
// Write to the buffer of global variables of view templater
$this->view->products = $product;
if (isset($this->view->products) && count($this->view->products) > 0) {
// Amount of rendered products is more than 0
// Write to the buffer of filters query (AQL)
$_filters .= ' && POSITION(["' . implode('", "', array_map(fn(_document $document): string => $document->getId(), $this->view->products)) . '"], d._id)';
}
// Deleting buffers
unset($category, $product);
}
} else if (!isset($parameters['category'])) {
// Not received identifier of the category
// search for root ascendants categories
$this->view->categories = entry::ascendants(
descendant: new category,
return: 'DISTINCT MERGE(ascendant, { name: ascendant.name.@language})',
parameters: ['language' => $this->language->name],
errors: $this->errors['catalog']
) ?? null;
}
if (isset($brand) || isset($text) || (isset($this->view->products) && count($this->view->products) > 0)) {
// Received and validated at least one of filters or amount of rendered products is more than 0
// Search for filters and write to the buffer of global variables of view templater
$this->view->filters = [
'brands' => product::collect(
return: 'd.brand.@language',
products: array_map(fn(_document $document): string => $document->getId(), $this->view->products ?? []),
language: $this->language,
errors: $this->errors['catalog']
)
];
}
// Search among products in the $category
if (isset($text) || isset($this->view->products) && count($this->view->products) > 0) {
// Amount of rendered products is more than 0
// Search for products and write to the buffer of global variables of view templater
$this->view->products = product::read(
search: $text ?? null,
filter: $_filters,
sort: $_sort,
amount: 50, // @todo pagination
return: 'DISTINCT MERGE(d, {name: d.name.@language, description: d.description.@language, compatibility: d.compatibility.@language, brand: d.brand.@language, cost: d.cost.@currency})',
language: $this->language,
currency: $this->currency,
parameters: $_parameters,
errors: $this->errors['catalog']
);
}
if (isset($menu)) {
//
} else {
// Not received ... menu
// Search for filters and write to the buffer of global variables of view templater
$this->view->menu = menu::_read(
return: 'MERGE(d, { name: d.name.@language })',
sort: 'd.style.order ASC, d.created DESC, d._key DESC',
amount: 4,
parameters: ['language' => $this->language->name],
errors: $this->errors['menu']
);
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// GET request
if (!empty($this->view->product)) {
// Initialized the product data
// Writing javascript code that has been executed after core and damper has been loaded
$this->view->javascript = [
sprintf(
<<<javascript
if (typeof _window === 'undefined') {
_window = setTimeout(() => core.catalog.product.system('%s'), 500);
}
javascript,
$this->view->product['identifier']
)
] + ($this->view->javascript ?? []);
}
// Exit (success)
return $this->view->render('catalog/page.html', [
'h2' => $this->language === language::ru ? 'Каталог' : 'Catalog' // @see https://git.mirzaev.sexy/mirzaev/huesos/issues/1
]);
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
// Initializing the buffer of response
$response = [
'title' => $this->language === language::ru ? 'Каталог' : 'Catalog' // @see https://git.mirzaev.sexy/mirzaev/huesos/issues/1
];
if (isset($this->view->categories)) {
// Initialized categories
// Render HTML-code of categories and write to the response buffer
$response['categories'] = $this->view->render('catalog/elements/categories.html');
}
if (isset($this->view->product)) {
// Initialized product
// Writing data of the product to the response buffer @todo GENERATE THIS ON THE SERVER
$response['product'] = $this->view->product;
}
if (isset($this->view->products)) {
// Initialized products
// Render HTML-code of products and write to the response buffer
$response['products'] = $this->view->render('catalog/elements/products.html');
}
if (isset($this->view->filters)) {
// Initialized filters
// Render HTML-code of filters and write to the response buffer
$response['filters'] = $this->view->render('catalog/elements/filters.html');
}
// 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(
$response + [
'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

@ -2,40 +2,26 @@
declare(strict_types=1);
namespace mirzaev\huesos\controllers;
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\huesos\views\templater,
mirzaev\huesos\models\core as models,
mirzaev\huesos\models\account,
mirzaev\huesos\models\session,
mirzaev\huesos\models\settings,
mirzaev\huesos\models\cart,
mirzaev\huesos\models\suspension,
mirzaev\huesos\models\enumerations\language,
mirzaev\huesos\models\enumerations\currency;
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\cart,
mirzaev\arming_bot\models\suspension,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for PHP
use mirzaev\minimal\core as minimal,
mirzaev\minimal\controller,
mirzaev\minimal\http\response,
mirzaev\minimal\http\enumerations\status;
use mirzaev\minimal\controller;
/**
* Core of controllers
*
* @package mirzaev\huesos\controllers
*
* @param settings $settings Instance of the settings
* @param session $session Instance of the session
* @param account|null $account Instance of the account
* @param cart|null $cart Instance of the cart
* @param language $language Language
* @param currency $currency Currency
* @param response $response Response
* @param array $errors Registry of errors
*
* @method void __construct(minimal $core, bool $initialize) Constructor
* @package mirzaev\arming_bot\controllers
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -43,63 +29,42 @@ use mirzaev\minimal\core as minimal,
class core extends controller
{
/**
* Settings
*
* @var settings $settings Instance of the settings
* Postfix for name of controllers files
*/
final public const string POSTFIX = '';
/**
* Instance of the settings
*/
protected readonly settings $settings;
/**
* Session
*
* @var session|null $session Instance of the session
* Instance of the session
*/
protected readonly session $session;
/**
* Account
*
* @var account|null $account Instance of the account
* Instance of the account
*/
protected readonly ?account $account;
/**
* Cart
*
* @var cart|null $cart Instance of the cart
* Instance of the cart
*/
protected readonly ?cart $cart;
/**
* Language
*
* @var language $language Language
*/
protected language $language = language::en;
/**
* Currency
*
* @var currency $currency Currency
*/
protected currency $currency = currency::usd;
/**
* Response
*
* @see https://wiki.php.net/rfc/property-hooks (find a table about backed and virtual hooks)
*
* @var response $response Response
*/
protected response $response {
// Read
get => $this->response ??= $this->request->response();
}
/**
* Errors
*
* @var array $errors Registry of errors
* Registry of errors
*/
protected array $errors = [
'session' => [],
@ -107,9 +72,8 @@ class core extends controller
];
/**
* Constructor
* Constructor of an instance
*
* @param minimal $core Initialize a controller?
* @param bool $initialize Initialize a controller?
*
* @return void
@ -118,13 +82,13 @@ class core extends controller
* 1. settings account и session не имеют проверок на возврат null
* 2. TRANSIT EVERYTHING TO MIDDLEWARES
*/
public function __construct(minimal $core, bool $initialize = true)
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 status::bruh->label;
if (isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] === 'nginx-ssl early hints') return;
// For the extends system
parent::__construct(core: $core);
parent::__construct($initialize);
if ($initialize) {
// Initializing is requested
@ -143,7 +107,9 @@ class core extends controller
// Handle a problems with initializing a session
if (!empty($this->errors['session'])) exit(1);
else if ($_COOKIE["session"] !== $this->session->hash) {
// телеграм не сохраняет куки
/* 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
@ -158,16 +124,13 @@ class core extends controller
'samesite' => 'strict'
]
);
}
// Initializing registry of account errors
$this->errors['account'];
} */
// Initializing of the account
$this->account = $this->session->account($this->errors['account']);
// Initializing of the settings
$this->settings = settings::active(create: SETTINGS_PROJECT);
$this->settings = settings::active();
// Initializing of the language
$this->language = $this->account?->language ?? $this->session?->buffer['language'] ?? $this->settings?->language ?? language::en;
@ -241,4 +204,21 @@ class core extends controller
}
}
}
/**
* 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,78 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\cart,
mirzaev\arming_bot\models\product,
mirzaev\arming_bot\models\menu;
// Framework for ArangoDB
use mirzaev\arangodb\document;
/**
* Controller of cdek
*
* @package mirzaev\arming_bot\controllers
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class cdek extends core
{
/**
* Registry of errors
*/
protected array $errors = [
'delivery' => []
];
/**
* Calculate
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function calculate(array $parameters = []): ?string
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST request
$this->model::calculate();
die;
// 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(
[
'main' => '',
'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,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
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class index extends core
{
/**
* Render the main page
*
* @param array $parameters Parameters of the request (POST + GET)
*/
public function index(array $parameters = []): ?string
{
// Exit (success)
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render('index.html');
// Exit (fail)
return null;
}
}

View File

@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\controllers;
// Files of the project
use mirzaev\arming_bot\controllers\core,
mirzaev\arming_bot\models\session as model,
mirzaev\arming_bot\models\account;
// Framework for ArangoDB
use mirzaev\arangodb\document;
/**
* Controller of session
*
* @package mirzaev\arming_bot\controllers
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class session extends core
{
/**
* 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
// Declaring variables in the correct scope
$identifier = $domain = $language = null;
if ($connected = isset($this->account)) {
// Found the account
// Initializing identifier of the account
$identifier = $this->account->identifier;
// Initializing language of the account
$language = $this->account->language;
// Initializing domain of the account
$domain = $this->account->domain;
} else {
// Not found the account
if (count($parameters) > 1 && isset($parameters['hash'])) {
$buffer = $parameters;
unset($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::initialize(
$data->id,
[
'identifier' => $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 identifier of the account
$identifier = $account->identifier;
// Initializing language of the account
$language = $account->language;
// Initializing domain of the account
$domain = $account->domain;
}
}
}
}
}
// 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,
'identifier' => $identifier ?? null,
'domain' => $domain ?? null,
'language' => $language?->name ?? 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;
}
/**
* Write to the buffer
*
* @param array $parameters Parameters of the request (POST + GET)
*
* @return void
*
* @todo переделать под trait buffer
*/
public function write(array $parameters = []): void
{
if (!empty($parameters) && $this->session instanceof model) {
// Received parameters and initialized session
// Declaring the buffer of deserialized parameters
$deserialized = [];
foreach ($parameters as $name => $value) {
// Iterate over parameters
// Validation of the parameter value
if (mb_strlen($value) > 4096) continue;
// Declaring the buffer of deserialized parameter
$parameter = null;
// Deserializing name to multidimensional array
foreach (array_reverse(explode('_', $name)) as $key)
$parameter = [$key => $parameter ?? (json_validate($value) ? json_decode($value, true, 10) : $value)];
// Writing into the buffer of deserialized parameters
$deserialized = array_merge_recursive($parameter, $deserialized);
}
// Write to the session document from ArangoDB
if (!empty($deserialized)) $this->session->write($deserialized, $this->errors['session']);
}
}
}

View File

@ -2,18 +2,18 @@
declare(strict_types=1);
namespace mirzaev\huesos\models;
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\huesos\models\core,
mirzaev\huesos\models\traits\status,
mirzaev\huesos\models\traits\buffer,
mirzaev\huesos\models\traits\cart,
mirzaev\huesos\models\traits\document as document_trait,
mirzaev\huesos\models\interfaces\document as document_interface,
mirzaev\huesos\models\interfaces\collection as collection_interface,
mirzaev\huesos\models\enumerations\language,
mirzaev\huesos\models\enumerations\currency;
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\status,
mirzaev\arming_bot\models\traits\buffer,
mirzaev\arming_bot\models\traits\cart,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
@ -31,7 +31,7 @@ use exception;
/**
* Model of account
*
* @package mirzaev\huesos\models
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -86,7 +86,7 @@ final class account extends core implements document_interface, collection_inter
if (method_exists($account, '__document')) {
// Object can implement a document from ArangoDB
// Implementinf parameters
// Abstractioning of parameters
if (isset($result->language)) $result->language = language::{$result->language};
if (isset($result->currency)) $result->currency = currency::{$result->currency};
@ -121,7 +121,7 @@ final class account extends core implements document_interface, collection_inter
'messages' => $registration->getCanReadAllGroupMessages()
],
'premium' => $registration->isPremium(),
'language' => language::{$registration->getLanguageCode() ?? 'en'}?->name ?? 'en',
'language' => language::{$registration->getLanguageCode()}->name ?? 'en',
'queries' => [
'inline' => $registration->getSupportsInlineQueries()
]
@ -145,7 +145,7 @@ final class account extends core implements document_interface, collection_inter
return static::initialize($identifier, errors: $errors);
} else throw new exception('Failed to register account');
} else throw new exception('Failed to find account');
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [

View File

@ -0,0 +1,337 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\reservation,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Library for ArangoDB
use ArangoDBClient\Document as _document;
// Built-in libraries
use exception;
/**
* Model of cart
*
* @uses reservation
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class cart extends core implements document_interface, collection_interface
{
use document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'cart';
/**
* Search for all products
*
* Search for all products in the cart
*
* @param language|null $language Language
* @param currency|null $currency Currency
* @param array &$errors Registry of errors
*
* @return array|null Array with implementing objects of documents from ArangoDB, if found
*/
public function products(
?language $language = language::en,
?currency $currency = currency::usd,
array &$errors = []
): ?array {
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
if (collection::initialize(reservation::COLLECTION, reservation::TYPE, errors: $errors)) {
if (collection::initialize(product::COLLECTION, product::TYPE, errors: $errors)) {
// Initialized collections
// Search for all products in the cart
$result = @collection::execute(
<<<AQL
FOR v IN 1..1 INBOUND @cart GRAPH @graph
FILTER IS_SAME_COLLECTION(@collection, v._id)
COLLECT d = v WITH COUNT INTO amount
RETURN {
[d._id]: {
amount,
document: MERGE(d, {
name: d.name.@language,
description: d.description.@language,
compatibility: d.compatibility.@language,
brand: d.brand.@language,
cost: d.cost.@currency
})
}
}
AQL,
[
'graph' => 'catalog',
'cart' => $this->getId(),
'collection' => product::COLLECTION,
'language' => $language->name,
'currency' => $currency->name
],
flat: true,
errors: $errors
);
/*
* МеНЯ ЭТО РАЗДРАЖАЕТ
*/
$products = [];
foreach ($result ?? [] as $raw) {
foreach ($raw as $key => $value) {
$products[$key] = $value;
}
}
// Exit (success)
return isset($products['amount']) ? [$products['document']['_id'] => $products] : $products;
} else throw new exception('Failed to initialize ' . product::TYPE . ' collection: ' . product::COLLECTION);
} else throw new exception('Failed to initialize ' . reservation::TYPE . ' collection: ' . reservation::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Search for summary of all products
*
* Search for summary of all products in the cart
*
* @param currency|null $currency Currency
* @param array &$errors Registry of errors
*
* @return array|null Array with implementing objects of documents from ArangoDB, if found
*/
public function summary(
?currency $currency = currency::usd,
array &$errors = []
): ?array {
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
if (collection::initialize(reservation::COLLECTION, reservation::TYPE, errors: $errors)) {
if (collection::initialize(product::COLLECTION, product::TYPE, errors: $errors)) {
// Initialized collections
// Search for all products in the cart
$result = @collection::execute(
<<<AQL
FOR v IN 1..1 INBOUND @cart GRAPH @graph
FILTER IS_SAME_COLLECTION(@collection, v._id)
COLLECT AGGREGATE amount = COUNT(v), cost = SUM(v.cost.@currency)
RETURN { amount, cost }
AQL,
[
'graph' => 'catalog',
'cart' => $this->getId(),
'collection' => product::COLLECTION,
'currency' => $currency->name
],
flat: true,
errors: $errors
);
// Exit (success)
return $result;
} else throw new exception('Failed to initialize ' . product::TYPE . ' collection: ' . product::COLLECTION);
} else throw new exception('Failed to initialize ' . reservation::TYPE . ' collection: ' . reservation::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/**
* Count
*
* Count of the product in the cart
*
* @param product $product The product
* @param int $limit Limit for counting
* @param array &$errors Registry of errors
*
* @return int|null Amount of the product in the cart, if counted
*/
public function count(product $product, int $limit = 100, array &$errors = []): ?int
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
if (collection::initialize(reservation::COLLECTION, reservation::TYPE, errors: $errors)) {
if (collection::initialize(product::COLLECTION, product::TYPE, errors: $errors)) {
// Initialized collections
// Search for the products in the cart and count them
return (int) collection::execute(
<<<AQL
FOR v IN 1..1 INBOUND @cart GRAPH @graph
FILTER IS_SAME_COLLECTION(@collection, v._id) && v._id == @product
LIMIT @limit
COLLECT WITH COUNT INTO length
RETURN length
AQL,
[
'graph' => 'catalog',
'cart' => $this->getId(),
'collection' => product::COLLECTION,
'product' => $product->getId(),
'limit' => $limit
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . product::TYPE . ' collection: ' . product::COLLECTION);
} else throw new exception('Failed to initialize ' . reservation::TYPE . ' collection: ' . reservation::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
/*
* Write
*
* Write the product in the cart
*
* @param product $product The product
* @param int $amount Amount of writings
* @param array &$errors Registry of errors
*
* @return void
*/
public function write(product $product, int $amount = 1, array &$errors = []): void
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
if (collection::initialize(reservation::COLLECTION, reservation::TYPE, errors: $errors)) {
if (collection::initialize(product::COLLECTION, product::TYPE, errors: $errors)) {
// Initialized collections
// Writing the product to the cart
collection::execute(
<<<AQL
FOR i IN 1..@amount
INSERT { _from: @product, _to: @cart, created: @created, updated: @created, active: true } INTO @@edge
AQL,
[
'cart' => $this->getId(),
'product' => $product->getId(),
'@edge' => reservation::COLLECTION,
'amount' => $amount,
'created' => time()
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . product::TYPE . ' collection: ' . product::COLLECTION);
} else throw new exception('Failed to initialize ' . reservation::TYPE . ' collection: ' . reservation::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
}
/*
* Delete
*
* Delete the product from the cart
*
* @param product $product The product
* @param int $amount Amount of deletings
* @param array &$errors Registry of errors
*
* @return void
*/
public function delete(product $product, int $amount = 1, array &$errors = []): void
{
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
if (collection::initialize(reservation::COLLECTION, reservation::TYPE, errors: $errors)) {
if (collection::initialize(product::COLLECTION, product::TYPE, errors: $errors)) {
// Initialized collections
// Deleting the product from the cart
collection::execute(
<<<AQL
FOR v, e IN 1..1 INBOUND @cart GRAPH @graph
FILTER IS_SAME_COLLECTION(@collection, v._id) && v._id == @product
LIMIT @amount
REMOVE e._key IN @@reservation
AQL,
[
'graph' => 'catalog',
'cart' => $this->getId(),
'collection' => product::COLLECTION,
'product' => $product->getId(),
'@reservation' => reservation::COLLECTION,
'amount' => $amount
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . product::TYPE . ' collection: ' . product::COLLECTION);
} else throw new exception('Failed to initialize ' . reservation::TYPE . ' collection: ' . reservation::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
}
}

View File

@ -2,20 +2,17 @@
declare(strict_types=1);
namespace mirzaev\huesos\models;
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\huesos\models\core,
mirzaev\huesos\models\product,
mirzaev\huesos\models\category,
mirzaev\huesos\models\entry,
mirzaev\huesos\models\traits\files,
mirzaev\huesos\models\enumerations\language,
mirzaev\huesos\models\enumerations\currency,
mirzaev\huesos\models\traits\yandex\disk as yandex;
// Framework for PHP
use mirzaev\minimal\http\enumerations\content;
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\product,
mirzaev\arming_bot\models\category,
mirzaev\arming_bot\models\entry,
mirzaev\arming_bot\models\traits\files,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency,
mirzaev\arming_bot\models\traits\yandex\disk as yandex;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
@ -27,13 +24,10 @@ use avadim\FastExcelReader\Excel as excel;
// Built-in libraries
use exception;
// GD library
use GdImage as image;
/**
* Model of the catalog
*
* @package mirzaev\huesos\models
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -41,64 +35,7 @@ use GdImage as image;
final class catalog extends core
{
use yandex, files {
yandex::download as file;
yandex::list as folder;
}
/**
* Search
*
* @param category|product $document Ascendant document
* @param string|null $filter Expression for filtering (AQL)
* @param string|null $sort Expression for sorting (AQL)
* @param int $depth Amount of nodes for traversal search (subcategories/products inside subcategories...)
* @param int $page Page
* @param int $amount Amount of documents per page
* @param string|null $categories_merge Expression with paremeters to return for categories (AQL)
* @param string|null $products_merge Expression with paremeters to return for products (AQL)
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
* @param array &$errors Registry of errors
*
* @return array [categories => [document::class...], $products => [document::class...]]
*/
public static function search(
category|product $document,
?string $filter = 'v.deleted != true && v.hidden != true',
?string $sort = 'v.position ASC, v.created DESC',
int $depth = 1,
int $page = 1,
int $amount = 100,
?string $categories_merge = null,
?string $products_merge = null,
array $parameters = [],
array &$errors = []
): array {
// Search for entries that are descendants of $category
$entries = entry::search(
document: $document,
filter: $filter,
sort: $sort,
depth: $depth,
page: $page,
amount: $amount,
categories_merge: $categories_merge,
products_merge: $products_merge,
parameters: $parameters,
errors: $errors
);
// Initialize buffers of entries (in singular, by parameter from ArangoDB)
$category = $product = [];
foreach ($entries as $entry) {
// Iterate over entries (descendands)
// Write entry to the buffer of entries (sort by $category and $product)
${$entry->_type}[] = $entry;
}
// Exit (success)
return ['categories' => $category, 'products' => $product];
yandex::download as yandex;
}
/**
@ -177,7 +114,7 @@ final class catalog extends core
// Iterate over categories
try {
if (!empty($row['identifier']) && is_int($row['identifier']) && $row['identifier'] > 0 && !empty($row['name'])) {
if (!empty($row['identifier']) && !empty($row['name'])) {
// Required cells are filled in
// Incrementing the counter of loaded categories
@ -206,19 +143,10 @@ final class catalog extends core
// Not initialized the category
// Creating the category
$_id = $created = category::write(
identifier: (int) $row['identifier'],
name: [$language->name => $row['name']],
position: (int) $row['position'] ?? null,
errors: $errors
);
$_id = $created = category::write((int) $row['identifier'], [$language->name => $row['name']], $row['position'] ?? null, $errors);
// Initializing the category
$category = category::_read(
filter: 'd._id == @_id',
parameters: ['_id' => $_id],
errors: $errors
);
$category = category::_read('d._id == @_id', parameters: ['_id' => $_id], errors: $errors);
// Incrementing the counter of created categories
if ($created) ++$categories_created;
@ -231,11 +159,7 @@ final class catalog extends core
// Received the ascendant category
// Initializing the ascendant category
$ascendant = category::_read(
filter: 'd.identifier == @identifier',
parameters: ['identifier' => (int) $row['category']],
errors: $errors
);
$ascendant = category::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $row['category']], errors: $errors);
if ($ascendant instanceof category) {
// Found the ascendant category
@ -252,17 +176,13 @@ final class catalog extends core
// Received images
// Initializing new images of the category
$images = static::folder(
uri: explode(' ', mb_trim($row['images']))[0],
errors: $errors
);
$images = explode(' ', trim($row['images']));
// Reinitialize images? (true, if no images found or their amount does not match)
/* $reinitialize = !$category->images || count($category->images) !== count($images); */
$reinitialize = true;
$reinitialize = !$category->images || count($category->images) !== count($images);
// Checking the identity of existing images with new images (if reinitialization of images has not yet been requested)
/* if (!$reinitialize) foreach ($category->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break; */
if (!$reinitialize) foreach ($category->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break;
if ($reinitialize) {
// Requested reinitialization of images
@ -270,98 +190,35 @@ final class catalog extends core
// Initializing the buffer of images
$buffer = [];
foreach (is_array($images) ? $images : [] as $image) {
foreach ($images as $index => $image) {
// Iterating over new images
// Initializing identifier of the image
$identifier = preg_replace('/\.\w+$/', '', $image->name);
// Skipping empty URI`s
if (empty($image = trim($image))) continue;
// Initializing path to directory of images in storage
$directory = DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $row['identifier'] . DIRECTORY_SEPARATOR . $identifier;
// Initializing path to directory of the image in storage
$directory = DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $row['identifier'];
// Initializing URL of the image in storage
$url = STORAGE . $directory;
// Initializing URN of the image in storage
$urn = $index . '.jpg';
// Initializing URI of the image in storage
$uri = $url . DIRECTORY_SEPARATOR . $urn;
// Initializing the directory in storage
if (!file_exists($url)) mkdir($url, 0775, true);
if ($downloaded = static::file(
uri: $image->public_url ?? $image->public_key,
destination: $url,
name: 'source',
errors: $errors
)) {
// The image is downloaded and initialized data of the image in storage
if (static::yandex($image, $uri, errors: $errors)) {
// The image is downloaded
// Initializing URI of the image
$uri = $downloaded['destination'] . DIRECTORY_SEPARATOR . $downloaded['name'] . '.' . $downloaded['content']->extension();
// Initializing size of the image
$size = getimagesize($uri);
if ($downloaded['content'] === content::png) {
// PNG
// Initializing implementator of the image
$boba = imagecreatefrompng($uri);
} else if ($downloaded['content'] === content::jpeg) {
// JPEG
// Initializing implementator of the image
$boba = imagecreatefromjpeg($uri);
} else if ($downloaded['content'] === content::webp) {
// WEBP
// Initializing implementator of the image
$boba = imagecreatefromwebp($uri);
}
// Enabling better antialiasing
imageantialias($boba, true);
if ($boba instanceof image) {
// Initialized implementator of the image
// Initializing buffer of resized images
$resized = [];
foreach ([1400, 800, 400, 200] as $resize) {
// Iterating over sizes for creating images
if ($size[0] >= $size[1]) {
// The width ($size[0]) is longer than the height ($size[1])
$width = $resize;
$height = (int) ($resize * $size[1] / $size[0]);
} else {
// The height ($size[1]) is longer than the width ($size[0])
$width = (int) ($resize * $size[0] / $size[1]);
$height = $resize;
}
// Resizing the image
$biba = imagecreatetruecolor($width, $height);
imagecopyresampled($biba, $boba, 0, 0, 0, 0, $width, $height, $size[0], $size[1]);
// Initializing URI of the resized image
$uri = $directory . DIRECTORY_SEPARATOR . "$resize.webp";
// Saving the image
imagewebp($biba, STORAGE . $uri);
// Writing the resized image to the buffer of resized images
$resized[$resize] = $uri;
}
// Writing the image to the buffer if images
$buffer[] = [
'source' => $image->public_url ?? null,
'storage' => [
'source' => $directory . DIRECTORY_SEPARATOR . $downloaded['name'] . '.' . $downloaded['content']->extension(),
] + $resized
];
}
// Writing the image to the buffer if images
$buffer[] = [
'source' => $image,
'storage' => $directory . DIRECTORY_SEPARATOR . $urn
];
}
}
@ -375,7 +232,7 @@ final class catalog extends core
// Incrementing the counter of updated categories
if ($updated && !$created) ++$categories_updated;
} else throw new exception('Failed to initialize category: ' . $row['name'] . " ($number)");
} else throw new exception("Failed to initialize category: {$row['name']} ($number)");
}
// Writing to the registry of handled categories and products
@ -414,7 +271,7 @@ final class catalog extends core
// Iterate over products
try {
if (!empty($row['identifier']) && is_int($row['identifier']) && $row['identifier'] > 0 && !empty($row['name'])) {
if (!empty($row['identifier']) && !empty($row['name'])) {
// Required cells are filled in
// Incrementing the counter of loaded products
@ -450,21 +307,21 @@ final class catalog extends core
// Initializing position of the product
if (empty($product->position) || $product->position !== $row['position'])
$product->position = isset($row['position']) ? (int) $row['position'] : 0;
$product->position = $row['position'];
} else {
// Not initialized the product
// Creating the product
$_id = product::write(
identifier: (int) $row['identifier'],
name: [$language->name => $row['name']],
description: [$language->name => $row['description']],
cost: [$currency->name => (float) $row['cost']],
weight: (float) $row['weight'],
dimensions: ['x' => $row['x'], 'y' => $row['y'], 'z' => $row['z']],
brand: [$language->name => $row['brand']],
compatibility: [$language->name => $row['compatibility']],
position: isset($row['position']) ? (int) $row['position'] : 0,
(int) $row['identifier'],
[$language->name => $row['name']],
[$language->name => $row['description']],
[$currency->name => (float) $row['cost']],
(float) $row['weight'],
['x' => $row['x'], 'y' => $row['y'], 'z' => $row['z']],
[$language->name => $row['brand']],
[$language->name => $row['compatibility']],
$row['position'] ?? null,
errors: $errors
);
@ -482,10 +339,7 @@ final class catalog extends core
// Received the category
// Initializing the category
$category = category::_read(
filter: sprintf('d.identifier == %u', (int) $row['category']),
errors: $errors
);
$category = category::_read(sprintf('d.identifier == %u', (int) $row['category']), errors: $errors);
if ($category instanceof category) {
// Found the ascendant category
@ -501,18 +355,14 @@ final class catalog extends core
if (!empty($row['images'])) {
// Received images
// Initializing new images of the product
$images = static::folder(
uri: explode(' ', mb_trim($row['images']))[0],
errors: $errors
);
// Initializing new images of the category
$images = explode(' ', trim($row['images']));
// Reinitialize images? (true, if no images found or their amount does not match)
/* $reinitialize = !$product->images || count($product->images) !== count($images); */
$reinitialize = true;
$reinitialize = !$product->images || count($product->images) !== count($images);
// Checking the identity of existing images with new images (if reinitialization of images has not yet been requested)
/* if (!$reinitialize) foreach ($product->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break; */
if (!$reinitialize) foreach ($product->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break;
if ($reinitialize) {
// Requested reinitialization of images
@ -520,102 +370,39 @@ final class catalog extends core
// Initializing the buffer of images
$buffer = [];
foreach (is_array($images) ? $images : [] as $image) {
foreach ($images as $index => $image) {
// Iterating over new images
// Initializing identifier of the image
$identifier = preg_replace('/\.\w+$/', '', $image->name);
// Skipping empty URI`s
if (empty($image = trim($image))) continue;
// Initializing path to directory of images in storage
$directory = DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $row['identifier'] . DIRECTORY_SEPARATOR . $identifier;
// Initializing path to directory of the image in storage
$directory = DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $row['identifier'];
// Initializing URL of the image in storage
$url = STORAGE . $directory;
// Initializing URN of the image in storage
$urn = $index . '.jpg';
// Initializing URI of the image in storage
$uri = $url . DIRECTORY_SEPARATOR . $urn;
// Initializing the directory in storage
if (!file_exists($url)) mkdir($url, 0775, true);
if ($downloaded = static::file(
uri: $image->public_url ?? $image->public_key,
destination: $url,
name: 'source',
errors: $errors
)) {
// The image is downloaded and initialized data of the image in storage
if (static::yandex($image, $uri, errors: $errors)) {
// The image is downloaded
// Initializing URI of the image
$uri = $downloaded['destination'] . DIRECTORY_SEPARATOR . $downloaded['name'] . '.' . $downloaded['content']->extension();
// Initializing size of the image
$size = getimagesize($uri);
if ($downloaded['content'] === content::png) {
// PNG
// Initializing implementator of the image
$boba = imagecreatefrompng($uri);
} else if ($downloaded['content'] === content::jpeg) {
// JPEG
// Initializing implementator of the image
$boba = imagecreatefromjpeg($uri);
} else if ($downloaded['content'] === content::webp) {
// WEBP
// Initializing implementator of the image
$boba = imagecreatefromwebp($uri);
}
// Enabling better antialiasing
imageantialias($boba, true);
if ($boba instanceof image) {
// Initialized implementator of the image
// Initializing buffer of resized images
$resized = [];
foreach ([1400, 800, 400, 200] as $resize) {
// Iterating over sizes for creating images
if ($size[0] >= $size[1]) {
// The width ($size[0]) is longer than the height ($size[1])
$width = $resize;
$height = (int) ($resize * $size[1] / $size[0]);
} else {
// The height ($size[1]) is longer than the width ($size[0])
$width = (int) ($resize * $size[0] / $size[1]);
$height = $resize;
}
// Resizing the image
$biba = imagecreatetruecolor($width, $height);
imagecopyresampled($biba, $boba, 0, 0, 0, 0, $width, $height, $size[0], $size[1]);
// Initializing URI of the resized image
$uri = $directory . DIRECTORY_SEPARATOR . "$resize.webp";
// Saving the image
imagewebp($biba, STORAGE . $uri);
// Writing the resized image to the buffer of resized images
$resized[$resize] = $uri;
}
// Writing the image to the buffer if images
$buffer[] = [
'source' => $image->public_url ?? null,
'storage' => [
'source' => $directory . DIRECTORY_SEPARATOR . $downloaded['name'] . '.' . $downloaded['content']->extension(),
] + $resized
];
}
// Writing the image to the buffer if images
$buffer[] = [
'source' => $image,
'storage' => $directory . DIRECTORY_SEPARATOR . $urn
];
}
}
// Initializing images of the product
// Initializing images of the category
$product->images = $buffer;
}
}
@ -623,9 +410,9 @@ final class catalog extends core
// Writing in ArangoDB
$updated = document::update($product->__document(), errors: $errors);
// Incrementing the counter of updated products
// Incrementing the counter of updated categories
if ($updated && !$created) ++$products_updated;
} else throw new exception('Failed to initialize product: ' . $row['name'] . " ($number)");
} else throw new exception("Failed to initialize product: {$row['name']} ($number)");
}
// Writing to the registry of handled categories and products
@ -659,25 +446,16 @@ final class catalog extends core
$category instanceof category
&& array_search($category->identifier, $handled['categories']) === false
) {
// Not found identifier of the category in the buffer of handled categories and products
// Not found identifier of the product in the buffer of handled categories and products
// Deleting images of the category from storage
static::delete(
directory: STORAGE . DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $category->identifier,
errors: $errors
);
static::delete(STORAGE . DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $category->identifier, errors: $errors);
// Deleting entries of the category in ArangoDB
entry::banish(
document: $category,
errors: $errors
);
entry::banish($category, errors: $errors);
// Deleting the category in ArangoDB
document::delete(
document: $category->__document(),
errors: $errors
);
document::delete($category->__document(), errors: $errors);
// Incrementing the counter of deleted categories
++$categories_deleted;
@ -705,22 +483,13 @@ final class catalog extends core
// Not found identifier of the product in the buffer of handled categories and products
// Deleting images of the product from storage
static::delete(
directory: STORAGE . DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $product->identifier,
errors: $errors
);
static::delete(STORAGE . DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $product->identifier, errors: $errors);
// Deleting entries of the product in ArangoDB
entry::banish(
document: $product,
errors: $errors
);
entry::banish($product, errors: $errors);
// Deleting the product in ArangoDB
document::delete(
document: $product->__document(),
errors: $errors
);
document::delete($product->__document(), errors: $errors);
// Incrementing the counter of deleted products
++$products_deleted;
@ -728,8 +497,8 @@ final class catalog extends core
}
// Counting new documents
$categories_new = collection::count(collection: category::COLLECTION, errors: $errors);
$products_new = collection::count(collection: product::COLLECTION, errors: $errors);
$categories_new = collection::count(category::COLLECTION, errors: $errors);
$products_new = collection::count(product::COLLECTION, errors: $errors);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [

View File

@ -2,13 +2,13 @@
declare(strict_types=1);
namespace mirzaev\huesos\models;
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\huesos\models\core,
mirzaev\huesos\models\traits\document as document_trait,
mirzaev\huesos\models\interfaces\document as document_interface,
mirzaev\huesos\models\interfaces\collection as collection_interface;
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
@ -21,7 +21,7 @@ use exception;
/**
* Model of category
*
* @package mirzaev\huesos\models
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -69,7 +69,7 @@ final class category extends core implements document_interface, collection_inte
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [

View File

@ -2,13 +2,13 @@
declare(strict_types=1);
namespace mirzaev\huesos\models;
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\huesos\models\core,
mirzaev\huesos\models\traits\document as document_trait,
mirzaev\huesos\models\interfaces\document as document_interface,
mirzaev\huesos\models\interfaces\collection as collection_interface;
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface;
// Framework for ArangoDB
use mirzaev\arangodb\enumerations\collection\type;
@ -16,7 +16,7 @@ use mirzaev\arangodb\enumerations\collection\type;
/**
* Model of connect
*
* @package mirzaev\huesos\models
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace mirzaev\huesos\models;
namespace mirzaev\arming_bot\models;
// Framework for PHP
use mirzaev\minimal\model;
@ -20,13 +20,18 @@ use exception;
/**
* Core of models
*
* @package mirzaev\huesos\models
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class core extends model
{
/**
* Postfix for name of models files
*/
final public const string POSTFIX = '';
/**
* Path to the file with settings of connecting to the ArangoDB
*/
@ -42,7 +47,7 @@ class core extends model
/**
* Constructor of an instance
*
* @param bool $initialize Initialize ...?
* @param bool $initialize Initialize a model?
* @param ?arangodb $arangodb Instance of a session of ArangoDB
*
* @return void
@ -127,7 +132,7 @@ class core extends model
// Exit (success)
return $result;
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to registry of errors
$errors[] = [
@ -142,6 +147,7 @@ class core extends model
return null;
}
/**
* Write
*

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot\models\deliveries;
// Files of the project
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface;
// The HTTP PSR-18 adapter for Guzzle HTTP client
use Http\Adapter\Guzzle7\Client as guzzle;
// Framework for CDEK
use CdekSDK2\Client as client,
CdekSDK2\Dto\CityList as cities;
// Built-in libraries
use exception;
/**
* Model of CDEK
*
* @package mirzaev\arming_bot\models\deliveries
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class cdek extends core implements document_interface, collection_interface
{
use document_trait;
/**
* Name of the collection in ArangoDB
*/
final public const string COLLECTION = 'delivery';
/**
* Calculate
*
* Calculate delivery by CDEK
*
* @param array &$errors Registry of errors
*
* @return
*/
public static function calculate(array &$errors = []): static|null
{
try {
//
/* $client = new client(new guzzle, 'account', 'secure'); */
$client = new client(new guzzle);
$client->setTest(true);
$result = $client->cities()->getFiltered(['country_codes' => 'RU', 'city' => 'зеленогорск']);
if ($result->isOk()) {
//
//Запрос успешно выполнился
$cities = $client->formatResponseList($result, cities::class);
foreach ($cities->items as $city) {
var_dump($city);
}
die;
}
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
// Exit (fail)
return null;
}
}

View File

@ -2,13 +2,13 @@
declare(strict_types=1);
namespace mirzaev\huesos\models;
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\huesos\models\core,
mirzaev\huesos\models\traits\document as document_trait,
mirzaev\huesos\models\interfaces\document as document_interface,
mirzaev\huesos\models\interfaces\collection as collection_interface;
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface;
// Library for ArangoDB
use ArangoDBClient\Document as _document;
@ -24,7 +24,7 @@ use exception;
/**
* Model of entry
*
* @package mirzaev\huesos\models
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -73,9 +73,9 @@ final class entry extends core implements document_interface, collection_interfa
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $to::TYPE->name . ' collection: ' . $to::COLLECTION);
} else throw new exception('Failed to initialize ' . $from::TYPE->name . ' collection: ' . $from::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $to::TYPE . ' collection: ' . $to::COLLECTION);
} else throw new exception('Failed to initialize ' . $from::TYPE . ' collection: ' . $from::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
@ -111,21 +111,16 @@ final class entry extends core implements document_interface, collection_interfa
try {
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
// Initialized the collection
// Search for ascendants
// Search for ascendants
if ($result = collection::execute(
sprintf(
<<<'AQL'
let from = (
FOR e IN @@edge
RETURN DISTINCT e._from
)
FOR d in @@collection
FILTER !POSITION(from, d._id)
RETURN %s
FOR d IN @@collection
FOR ascendant IN OUTBOUND d @@edge
RETURN %s
AQL,
empty($return) ? 'DISTINCT d' : $return
empty($return) ? 'DISTINCT ascendant' : $return
),
[
'@collection' => $descendant::COLLECTION,
@ -138,7 +133,7 @@ final class entry extends core implements document_interface, collection_interfa
// Exit (success)
return is_array($result) ? $result : [$result];
} else return [];
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
@ -194,9 +189,9 @@ final class entry extends core implements document_interface, collection_interfa
// Exit (success)
return is_array($entry) ? $entry[0] : $entry;
} else return null;
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $to::TYPE->name . ' collection: ' . $to::COLLECTION);
} else throw new exception('Failed to initialize ' . $from::TYPE->name . ' collection: ' . $from::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $to::TYPE . ' collection: ' . $to::COLLECTION);
} else throw new exception('Failed to initialize ' . $from::TYPE . ' collection: ' . $from::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
@ -223,9 +218,8 @@ final class entry extends core implements document_interface, collection_interfa
* @param category|product $document Ascendant document
* @param string|null $filter Expression for filtering (AQL)
* @param string|null $sort Expression for sorting (AQL)
* @param int $depth Amount of nodes for traversal search (subcategories/products inside subcategories...)
* @param int $page Page
* @param int $amount Amount of documents per page
* @param int $page Страница
* @param int $amount Количество товаров на странице
* @param string|null $categories_merge Expression with paremeters to return for categories (AQL)
* @param string|null $products_merge Expression with paremeters to return for products (AQL)
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
@ -237,7 +231,6 @@ final class entry extends core implements document_interface, collection_interfa
category|product $document,
?string $filter = 'v.deleted != true && v.hidden != true',
?string $sort = 'v.position ASC, v.created DESC',
int $depth = 1,
int $page = 1,
int $amount = 100,
?string $categories_merge = null,
@ -254,13 +247,12 @@ final class entry extends core implements document_interface, collection_interfa
return is_array($result = collection::execute(
sprintf(
<<<'AQL'
FOR v IN 1..%u INBOUND @document GRAPH @graph
FOR v IN 1..1 INBOUND @document GRAPH @graph
%s
%s
LIMIT @offset, @amount
RETURN DISTINCT IS_SAME_COLLECTION(@category, v._id) ? MERGE(v, {_type: @category%s}) : MERGE(v, {_type: @product%s})
AQL,
$depth,
empty($filter) ? '' : "FILTER $filter",
empty($sort) ? '' : "SORT $sort",
empty($categories_merge) ? '' : ", $categories_merge",
@ -276,8 +268,8 @@ final class entry extends core implements document_interface, collection_interfa
] + $parameters,
errors: $errors
)) ? $result : [$result];
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $document::TYPE->name . ' collection: ' . $document::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $document::TYPE . ' collection: ' . $document::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
@ -323,8 +315,8 @@ final class entry extends core implements document_interface, collection_interfa
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $document::TYPE->name . ' collection: ' . $document::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $document::TYPE . ' collection: ' . $document::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [

View File

@ -2,15 +2,15 @@
declare(strict_types=1);
namespace mirzaev\huesos\models\enumerations;
namespace mirzaev\arming_bot\models\enumerations;
// Files of the project
use mirzaev\huesos\models\enumerations\language;
use mirzaev\arming_bot\models\enumerations\language;
/**
* Types of currencies by ISO 4217 standart
*
* @package mirzaev\huesos\models\enumerations
* @package mirzaev\arming_bot\models\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>

View File

@ -2,12 +2,12 @@
declare(strict_types=1);
namespace mirzaev\huesos\models\enumerations;
namespace mirzaev\arming_bot\models\enumerations;
/**
* Types of languages by ISO 639-1 standart
*
* @package mirzaev\huesos\models\enumerations
* @package mirzaev\arming_bot\models\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>

View File

@ -2,12 +2,12 @@
declare(strict_types=1);
namespace mirzaev\huesos\models\enumerations;
namespace mirzaev\arming_bot\models\enumerations;
/**
* Types of session verification
*
* @package mirzaev\huesos\models\enumerations
* @package mirzaev\arming_bot\models\enumerations
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace mirzaev\huesos\models\interfaces;
namespace mirzaev\arming_bot\models\interfaces;
// Framework for ArangoDB
use mirzaev\arangodb\enumerations\collection\type;
@ -10,7 +10,7 @@ use mirzaev\arangodb\enumerations\collection\type;
/**
* Interface for implementing a collection from ArangoDB
*
* @package mirzaev\huesos\models\traits
* @package mirzaev\arming_bot\models\traits
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace mirzaev\huesos\models\interfaces;
namespace mirzaev\arming_bot\models\interfaces;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
@ -12,7 +12,7 @@ use ArangoDBClient\Document as _document;
*
* @param _document $document An instance of the ArangoDB document from ArangoDB (protected readonly)
*
* @package mirzaev\huesos\models\traits
* @package mirzaev\arming_bot\models\traits
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>

View File

@ -2,18 +2,18 @@
declare(strict_types=1);
namespace mirzaev\huesos\models;
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\huesos\models\core,
mirzaev\huesos\models\traits\document as document_trait,
mirzaev\huesos\models\interfaces\document as document_interface,
mirzaev\huesos\models\interfaces\collection as collection_interface;
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface;
/**
* Model of menu
*
* @package mirzaev\huesos\models
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>

View File

@ -2,15 +2,15 @@
declare(strict_types=1);
namespace mirzaev\huesos\models;
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\huesos\models\core,
mirzaev\huesos\models\traits\document as document_trait,
mirzaev\huesos\models\interfaces\document as document_interface,
mirzaev\huesos\models\interfaces\collection as collection_interface,
mirzaev\huesos\models\enumerations\language,
mirzaev\huesos\models\enumerations\currency;
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
@ -25,7 +25,7 @@ use exception;
/**
* Model of a product
*
* @package mirzaev\huesos\models
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -100,7 +100,7 @@ final class product extends core implements document_interface, collection_inter
] + $data,
errors: $errors
);
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
@ -225,7 +225,7 @@ final class product extends core implements document_interface, collection_inter
// Exit (success)
return is_array($result) ? $result : [$result];
}
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
@ -281,7 +281,7 @@ final class product extends core implements document_interface, collection_inter
// Exit (success)
return is_array($result) ? $result : [$result];
} else return [];
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [

View File

@ -2,14 +2,14 @@
declare(strict_types=1);
namespace mirzaev\huesos\models;
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\huesos\models\core,
mirzaev\huesos\models\cart,
mirzaev\huesos\models\traits\document as document_trait,
mirzaev\huesos\models\interfaces\document as document_interface,
mirzaev\huesos\models\interfaces\collection as collection_interface;
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\cart,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface;
// Framework for ArangoDB
use mirzaev\arangodb\enumerations\collection\type;
@ -18,7 +18,7 @@ use mirzaev\arangodb\enumerations\collection\type;
* Model of reservtion
*
* @used-by cart
* @package mirzaev\huesos\models
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>

View File

@ -2,20 +2,20 @@
declare(strict_types=1);
namespace mirzaev\huesos\models;
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\huesos\models\account,
mirzaev\huesos\models\connect,
mirzaev\huesos\models\enumerations\session as verification,
mirzaev\huesos\models\traits\status,
mirzaev\huesos\models\traits\buffer,
mirzaev\huesos\models\traits\cart,
mirzaev\huesos\models\traits\document as document_trait,
mirzaev\huesos\models\interfaces\document as document_interface,
mirzaev\huesos\models\interfaces\collection as collection_interface,
mirzaev\huesos\models\enumerations\language,
mirzaev\huesos\models\enumerations\currency;
use mirzaev\arming_bot\models\account,
mirzaev\arming_bot\models\connect,
mirzaev\arming_bot\models\enumerations\session as verification,
mirzaev\arming_bot\models\traits\status,
mirzaev\arming_bot\models\traits\buffer,
mirzaev\arming_bot\models\traits\cart,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
@ -30,7 +30,7 @@ use exception;
/**
* Model of a session
*
* @package mirzaev\huesos\models
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -53,15 +53,15 @@ final class session extends core implements document_interface, collection_inter
final public const verification VERIFICATION = verification::hash_else_address;
/**
* Constructor
* Constructor of instance
*
* Initialize session and write into the $this->document property
* 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 void
* @return static
*/
public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
{
@ -71,7 +71,7 @@ final class session extends core implements document_interface, collection_inter
if (isset($hash) && $document = $this->hash($hash, errors: $errors)) {
// Found the instance of the ArangoDB document of session and received a session hash
// Writing document instance of the session from ArangoDB to the property of the implementing object
$this->__document($document);
} else if (static::VERIFICATION === verification::hash_else_address && $document = $this->address($_SERVER['REMOTE_ADDR'], errors: $errors)) {
@ -114,14 +114,14 @@ final class session extends core implements document_interface, collection_inter
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
if (document::update($session, errors: $errors)) {
// Writed into ArangoDB
// Update is writed to ArangoDB
// Writing instance of the session document from ArangoDB to the property of the implementing object
$this->__document($session);
} else throw new exception('Failed to write the session data');
} else throw new exception('Failed to create or find just created session');
}
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
@ -185,9 +185,9 @@ final class session extends core implements document_interface, collection_inter
return $account;
} else throw new exception('Class ' . account::class . ' does not implement a document from ArangoDB');
} else return null;
} else throw new exception('Failed to initialize ' . account::TYPE->name . ' collection: ' . account::COLLECTION);
} else throw new exception('Failed to initialize ' . connect::TYPE->name . ' collection: ' . connect::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . account::TYPE . ' collection: ' . account::COLLECTION);
} else throw new exception('Failed to initialize ' . connect::TYPE . ' collection: ' . connect::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
@ -222,7 +222,7 @@ final class session extends core implements document_interface, collection_inter
return collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d.hash == @hash && d.expires > @time && d.active == true
FILTER d.hash == @hash && d.expires > $time && d.active == true
RETURN d
AQL,
[
@ -232,7 +232,7 @@ final class session extends core implements document_interface, collection_inter
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [
@ -268,18 +268,16 @@ final class session extends core implements document_interface, collection_inter
<<<'AQL'
FOR d IN @@collection
FILTER d.address == @address && d.expires > @time && d.active == true
SORT d.updated DESC
LIMIT 1
RETURN d
AQL,
[
'@collection' => static::COLLECTION,
'@collection' => static::COLLECTION,
'address' => $address,
'time' => time()
],
errors: $errors
);
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [

View File

@ -2,15 +2,15 @@
declare(strict_types=1);
namespace mirzaev\huesos\models;
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\huesos\models\core,
mirzaev\huesos\models\traits\document as document_trait,
mirzaev\huesos\models\interfaces\document as document_interface,
mirzaev\huesos\models\interfaces\collection as collection_interface,
mirzaev\huesos\models\enumerations\language,
mirzaev\huesos\models\enumerations\currency;
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
@ -25,7 +25,7 @@ use exception;
/**
* Model of settings
*
* @package mirzaev\huesos\models
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -96,7 +96,7 @@ final class settings extends core implements document_interface, collection_inte
// Re-search (without creating) and exit (success || fail)
return static::active(errors: $errors);
} else throw new exception('Active settings not found');
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [

View File

@ -2,14 +2,14 @@
declare(strict_types=1);
namespace mirzaev\huesos\models;
namespace mirzaev\arming_bot\models;
// Files of the project
use mirzaev\huesos\models\core,
mirzaev\huesos\models\traits\document as document_trait,
mirzaev\huesos\models\interfaces\document as document_interface,
mirzaev\huesos\models\interfaces\collection as collection_interface,
mirzaev\huesos\models\enumerations\language;
use mirzaev\arming_bot\models\core,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface,
mirzaev\arming_bot\models\enumerations\language;
// Framework for ArangoDB
use mirzaev\arangodb\collection,
@ -25,7 +25,7 @@ use exception,
/**
* Model of a suspension
*
* @package mirzaev\huesos\models
* @package mirzaev\arming_bot\models
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -84,7 +84,7 @@ final class suspension extends core implements document_interface, collection_in
return $suspension;
} else throw new exception('Class ' . static::class . ' does not implement a document from ArangoDB');
} else return null;
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [

View File

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

View File

@ -2,12 +2,12 @@
declare(strict_types=1);
namespace mirzaev\huesos\models\traits;
namespace mirzaev\arming_bot\models\traits;
// Files of the project
use mirzaev\huesos\models\interfaces\collection as collection_interface,
mirzaev\huesos\models\enumerations\language,
mirzaev\huesos\models\enumerations\currency;
use mirzaev\arming_bot\models\interfaces\collection as collection_interface,
mirzaev\arming_bot\models\enumerations\language,
mirzaev\arming_bot\models\enumerations\currency;
// Library for ArangoDB
use ArangoDBClient\Document as _document;
@ -28,7 +28,7 @@ use exception;
* @param static TYPE Type of the collection in ArangoDB
*
* @uses collection_interface
* @package mirzaev\huesos\models\traits
* @package mirzaev\arming_bot\models\traits
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -50,21 +50,21 @@ trait buffer
// Initialized the collection
// Is the instance of the document from ArangoDB are initialized?
if (!($this->__document() instanceof _document)) throw new exception('The instance of the document from ArangoDB is not initialized');
if (!isset($this->document)) throw new exception('The instance of the sessoin document from ArangoDB is not initialized');
// Writing data into buffer of the instance of the document from ArangoDB
$this->__document()->buffer = array_replace_recursive($this->document->buffer ?? [], $data);
$this->document->buffer = array_replace_recursive($this->document->buffer ?? [], $data);
// Is the buffer of the instance of the document from ArangoDB exceed 10 megabytes?
if (mb_strlen(json_encode($this->__document()->buffer)) > 10485760) throw new exception('The buffer size exceeds 10 megabytes');
if (mb_strlen(json_encode($this->document->buffer)) > 10485760) throw new exception('The buffer size exceeds 10 megabytes');
// Serializing parameters @todo ЗАЧЕМУ ЭТО ЗДЕСЬ?
/* if ($this->__document()->language instanceof language) $this->__document()->language = $this->__document()->language->name;
if ($this->__document()->currency instanceof currency) $this->__document()->currency = $this->__document()->currency->name; */
// Serializing parameters
if ($this->document->language instanceof language) $this->document->language = $this->document->language->name;
if ($this->document->currency instanceof currency) $this->document->currency = $this->document->currency->name;
// Writing to ArangoDB and exit (success)
return document::update($this->__document(), errors: $errors);
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
return document::update($this->document, errors: $errors);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [

View File

@ -2,14 +2,14 @@
declare(strict_types=1);
namespace mirzaev\huesos\models\traits;
namespace mirzaev\arming_bot\models\traits;
// Files of the project
use mirzaev\huesos\models\interfaces\collection as collection_interface,
mirzaev\huesos\models\interfaces\document as document_interface,
mirzaev\huesos\models\traits\document as document_trait,
mirzaev\huesos\models\connect,
mirzaev\huesos\models\cart as model;
use mirzaev\arming_bot\models\interfaces\collection as collection_interface,
mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\traits\document as document_trait,
mirzaev\arming_bot\models\connect,
mirzaev\arming_bot\models\cart as model;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
@ -28,7 +28,7 @@ use exception;
*
* @uses collection_interface
* @uses document_interface
* @package mirzaev\huesos\models\traits
* @package mirzaev\arming_bot\models\traits
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -132,9 +132,9 @@ trait cart
} else throw new exception('Class ' . model::class . ' does not implement a document from ArangoDB');
} else throw new exception('Failed to create or find just created ' . static::class);
}
} else throw new exception('Failed to initialize ' . model::TYPE->name . ' collection: ' . model::COLLECTION);
} else throw new exception('Failed to initialize ' . connect::TYPE->name . ' collection: ' . connect::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . model::TYPE . ' collection: ' . model::COLLECTION);
} else throw new exception('Failed to initialize ' . connect::TYPE . ' collection: ' . connect::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [

View File

@ -2,12 +2,12 @@
declare(strict_types=1);
namespace mirzaev\huesos\models\traits;
namespace mirzaev\arming_bot\models\traits;
// Files of the project
use mirzaev\huesos\models\interfaces\document as document_interface,
mirzaev\huesos\models\interfaces\collection as collection_interface,
mirzaev\huesos\models\connect;
use mirzaev\arming_bot\models\interfaces\document as document_interface,
mirzaev\arming_bot\models\interfaces\collection as collection_interface,
mirzaev\arming_bot\models\connect;
// Library для ArangoDB
use ArangoDBClient\Document as _document;
@ -26,7 +26,7 @@ use exception;
* @var protected readonly _document|null $document An instance of the ArangoDB document
*
* @uses document_interface
* @package mirzaev\huesos\models\traits
* @package mirzaev\arming_bot\models\traits
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -79,12 +79,10 @@ trait document
/**
* Connect
*
* Searches for a connection document, otherwise creates one
*
* @param collecton_interface $document Document
* @param array &$errors Registry of errors
*
* @return string|null The identifier of the "connect" edge collection, if created or found
* @return string|null The identifier of the created edge of the "connect" collection, if created
*/
public function connect(collection_interface $document, array &$errors = []): ?string
{
@ -97,50 +95,19 @@ trait document
if ($this->document instanceof _document) {
// Initialized instance of the document from ArangoDB
// Searching for a connection
$found = collection::execute(
<<<'AQL'
FOR d IN @@collection
FILTER d._from == @_from && d._to == @_to && d.active == true
RETURN d
AQL,
// Writing document and exit (success)
return framework_document::write(
connect::COLLECTION,
[
'@collection' => connect::COLLECTION,
'_from' => $document->getId(),
'_to' => $this->document->getId()
],
errors: $errors
);
if ($found) {
// Found the connection document
// Exit (success)
return $found->getId();
} else {
// Not found the connection document
// Creting the connection document
$created = framework_document::write(
connect::COLLECTION,
[
'_from' => $document->getId(),
'_to' => $this->document->getId()
],
errors: $errors
);
if ($created) {
// Created the connection document
// Exit (success)
return $created;
}
}
} else throw new exception('The instance of the document from ArangoDB is not initialized');
} else throw new exception('Failed to initialize ' . $document::TYPE->name . ' collection: ' . $document::COLLECTION);
} else throw new exception('Failed to initialize ' . connect::TYPE->name . ' collection: ' . connect::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
} else throw new exception('Failed to initialize ' . $document::TYPE . ' collection: ' . $document::COLLECTION);
} else throw new exception('Failed to initialize ' . connect::TYPE . ' collection: ' . connect::COLLECTION);
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
} catch (exception $e) {
// Writing to the registry of errors
$errors[] = [

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace mirzaev\huesos\models\traits;
namespace mirzaev\arming_bot\models\traits;
// Built-in libraries
use exception;
@ -10,7 +10,7 @@ use exception;
/**
* Trait for initialization of files handlers
*
* @package mirzaev\huesos\models\traits
* @package mirzaev\arming_bot\models\traits
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace mirzaev\huesos\models\traits;
namespace mirzaev\arming_bot\models\traits;
// Built-in libraries
use exception;
@ -10,7 +10,7 @@ use exception;
/**
* Trait for initialization of a status
*
* @package mirzaev\huesos\models\traits
* @package mirzaev\arming_bot\models\traits
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>

View File

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

View File

@ -0,0 +1,80 @@
<?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');
// Initialize dependencies
require __DIR__ . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. 'vendor' . DIRECTORY_SEPARATOR
. 'autoload.php';
// Initialize the router
$router = new router;
// Initialize routes
$router
->write('/', 'catalog', 'index', 'GET')
->write('/', 'catalog', 'index', 'POST')
->write('/cart', 'cart', 'index', 'GET')
->write('/cart', 'cart', 'index', 'POST')
->write('/cart/product', 'cart', 'product', 'POST')
->write('/cart/summary', 'cart', 'summary', 'POST')
->write('/account/write', 'account', 'write', 'POST')
->write('/session/write', 'session', 'write', 'POST')
->write('/session/connect/telegram', 'session', 'telegram', 'POST')
->write('/cdek/calculate', 'deliveries\\cdek', 'calculate', 'POST')
/* ->write('/category/$identifier', 'catalog', 'index', 'POST') */
/* ->write('/category', 'catalog', 'index', 'POST') */
/* ->write('/product/$identifier', 'catalog', 'product', 'POST') */
;
/*
// Initializing of routes
$router
->write('/', 'catalog', 'index', 'GET')
->write('/$sex', 'catalog', 'search', 'POST')
->write('/$search', 'catalog', 'search', 'POST')
->write('/search', 'catalog', 'search', 'POST')
->write('/search/$asdasdasd', 'catalog', 'search', 'POST')
->write('/ebala/$sex/$categories...', 'catalog', 'index', 'POST')
->write('/$sex/$categories...', 'catalog', 'index', 'POST')
->write('/$categories...', 'catalog', 'index', 'POST')
->write('/ebala/$categories...', 'catalog', 'index', 'POST');
var_dump($router->routes);
echo "\n\n\n\n\n\n";
$router
->sort();
var_dump($router->routes); */
// Initialize the core
$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));
// Handle the request
echo $core->start();

View File

@ -0,0 +1,20 @@
"use strict";
core.modules.connect(["session", "account", "telegram"])
.then(() => {
//
const { initData, initDataUnsafe, ...data } = core.telegram.api;
//
core.session.buffer.write("telegram_program", JSON.stringify(data));
if (core.telegram.api.initData.length > 0) {
//
//
core.account.authentication();
}
//
core.telegram.api.ready();
});

View File

@ -17,11 +17,11 @@ class core {
// Window
static window;
// Account
static account;
// The "loading" element
static loading = document.getElementById("loading");
static status_loading = document.getElementById("loading");
// The "account" element
static status_account = document.getElementById("account");
// The <header> element
static header = document.body.getElementsByTagName("header")[0];
@ -52,7 +52,7 @@ class core {
static async request(
uri = "/",
body,
method = "GET",
method = "POST",
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
@ -320,7 +320,7 @@ Object.assign(
// Downloading, importing and writing the module into a core property and into registry of loaded modules
core[module] =
loaded[module] =
await (await import(`./modules/${module}.mjs`)).default;
await (await import(`./modules/${module}.js`)).default;
}
// Exit (success)
@ -351,7 +351,7 @@ core.modules.connect("damper").then(() => {
damper: core.damper(
(...variables) => core.buffer.write.system(...variables),
300,
2,
3,
),
},
);
@ -382,14 +382,14 @@ Object.assign(
// Imported the session module
// Write to the session buffer
core.session.buffer?.write(name, value);
session.default.buffer?.write(name, value);
});
core.modules.connect("account").then(() => {
// Imported the account module
// Write to the account buffer
core.account.buffer?.write(name, value);
account.default.buffer?.write(name, value);
});
// Exit (success)

View File

@ -0,0 +1,750 @@
"use strict";
/**
* @name Бегущая строка
*
* @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
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
export default 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

@ -30,11 +30,11 @@ export default class account {
* @return {void}
*/
static authentication() {
core.loading.removeAttribute("disabled");
core.status_loading.removeAttribute("disabled");
const timer_for_response = setTimeout(() => {
core.loading.setAttribute("disabled", true);
}, 200);
core.status_loading.setAttribute("disabled", true);
}, 3000);
core.modules.connect("telegram").then(() => {
// Imported the telegram module
@ -44,7 +44,6 @@ export default class account {
.request(
"/session/connect/telegram",
core.telegram.api.initData,
"PUT",
)
.then((json) => {
if (json) {
@ -55,74 +54,25 @@ export default class account {
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
// Exit (fail)
reject(json);
// Errors received
} else {
// Success (not received errors)
// Errors not received
if (json.connected === true) {
// Deactivating the loading screen
core.loading.setAttribute("disabled", true);
core.status_loading.setAttribute("disabled", true);
clearTimeout(timer_for_response);
core.account = {
identifier: json.identifier
};
// Initializing the account element
const account = document.getElementById("account");
if (account instanceof HTMLElement) {
// Initialized the account element
// Initializing the account link
const a = account.getElementsByTagName("a")[0];
if (a instanceof HTMLElement) {
// Initialized the account link
a.setAttribute("onclick", "core.account.profile()");
a.innerText = json.domain.length > 0
? "@" + json.domain
: "ERROR";
} else {
// Not initialized the account link
if (json.avatar) {
// Received the avatar image
// Initialize the menu button icon
const icon = account.getElementsByTagName("i")[0];
if (icon instanceof HTMLElement) {
// Initialized the menu button icon
setTimeout(function () {
// Hiding the menu button icon
icon.classList.add("hidden");
}, 3000);
}
// Initializing the avatar image element
const image = document.createElement("img");
image.setAttribute("src", json.avatar);
image.style.setProperty("opacity", "0");
image.style.setProperty("--animation-delay", "2s");
image.classList.add("opacity", "animated");
// Writing the avatar image element
account.appendChild(image);
}
}
}
const a = core.status_account.getElementsByTagName("a")[0];
a.setAttribute("onclick", "core.account.profile()");
a.innerText = json.domain.length > 0
? "@" + json.domain
: "ERROR";
}
if (
json.language !== null &&
typeof json.language === "string" &&
json.language.length === 2
json.langiage.length === 2
) {
core.language = json.language;
}
@ -147,7 +97,7 @@ export default class account {
* @param {string} value Value of the parameter (it can be JSON)
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Did the execution complete without errors?
* @return {bool} Execution completed with an error?
*/
static write = (name, value, force = false) => {
core.modules.connect("damper").then(
@ -166,7 +116,7 @@ export default class account {
);
// Exit (success)
return true;
return false;
};
};
}
@ -178,12 +128,10 @@ core.modules.connect("damper").then(() => {
account.buffer.write,
{
/**
* @name Write (damper)
* @name Write
*
* @description
* Write to the account buffer
*
* @memberof account.buffer.write
* Write to the account buffer (damper)
*
* @param {string} name Name of the parameter
* @param {string} value Value of the parameter (it can be JSON)
@ -194,7 +142,7 @@ core.modules.connect("damper").then(() => {
damper: core.damper(
(...variables) => account.buffer.write.system(...variables),
300,
2,
3,
),
},
);
@ -204,12 +152,10 @@ Object.assign(
account.buffer.write,
{
/**
* @name Write (system)
* @name Write
*
* @description
* Write to the account buffer
*
* @memberof account.buffer.write
* Write to the account buffer (system)
*
* @param {string} name Name of the parameter
* @param {string} value Value of the parameter (it can be JSON)
@ -223,38 +169,22 @@ Object.assign(
reject = () => {},
) {
try {
if (
typeof name === "string" &&
(typeof value === "string" || typeof value === "number")
) {
if (typeof name === "string" && typeof value === "string") {
// Received and validated required arguments
// Sending request to the server
return await core.request(
"/account/write",
`${name}=${value}`,
"PATCH",
"POST",
)
.then(
(json) => {
if (json) {
// Received a JSON-response
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
// Exit (fail)
reject(json);
} else {
// Success (not received errors)
// Exit (success)
resolve(json);
}
// Exit (success)
resolve(json);
}
},
() => reject(),

View File

@ -0,0 +1,528 @@
"use strict";
/**
* @name Cart
*
* @description
* Implements actions with cart
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
export default class cart {
/**
* @name Type of the program
*/
static type = "module";
/**
* Toggle
*
* Toggle the product in the cart (interface)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
*/
static toggle(element, product, remove = false, force = false) {
// Blocking the element
element.setAttribute("disabled", "true");
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
"toggle",
undefined,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
"toggle",
undefined,
remove,
force,
);
},
);
// Exit (success)
return false;
}
/**
* Write
*
* Write the product in the cart (interface)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {number} amount Amount of writings
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
*/
static write(
element,
product,
amount = 1,
remove = false,
force = false,
) {
// Blocking the element
element.setAttribute("disabled", "true");
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
"write",
amount,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
"write",
amount,
remove,
force,
);
},
);
// Exit (success)
return false;
}
/**
* Delete
*
* Delete the product from the cart (interface)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {number} amount Amount of deletings
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
*/
static delete(
element,
product,
amount = 1,
remove = false,
force = false,
) {
// Blocking the element
element.setAttribute("disabled", "true");
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
"delete",
amount,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
"delete",
amount,
remove,
force,
);
},
);
// Exit (success)
return false;
}
/**
* Set
*
* Set amount of the product in the cart (interface)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {number} amount Amount of the product in the cart to be setted
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
*/
static set(
element,
product,
amount = 1,
remove = false,
force = false,
) {
// Blocking the element
element.setAttribute("disabled", "true");
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
"set",
amount,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
"set",
amount,
remove,
force,
);
},
);
// Exit (success)
return false;
}
/**
* The product
*
* Handle the product in the cart (system)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler element of the product
* @param {HTMLElement} product The product element
* @param {string} type Type of action with the product
* @param {number} amount Amount of product to handle
* @param {bool} remove Remove the product element if json.amount === 0?
*
* @return {bool} Execution completed with an error?
*/
static product(
element,
product,
type,
amount = null,
remove = false,
) {
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.product.damper(
element,
product,
type,
amount,
remove,
force,
);
},
() => {
// Not imported the damper module
// Execute
this.product.system(
element,
product,
type,
amount,
remove,
force,
);
},
);
// Exit (success)
return false;
}
/**
* @name Summary
*
* @description
* Initialize summary of products the cart (system)
*
* @return {Promise}
*/
static async summary() {
// Request
return await core.request("/cart/summary")
.then((json) => {
if (json) {
// Received a JSON-response
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
} else {
// Success (not received errors)
// Initializing the summary amount <span> element
const amount = document.getElementById("amount");
if (amount instanceof HTMLElement) {
// Initialized the summary amount element
// Writing summmary amount into the summary amount element
amount.innerText = json.amount;
}
// Initializing the summary cost <span> element
const cost = document.getElementById("cost");
if (cost instanceof HTMLElement) {
// Initialized the summary cost element
// Writing summmary cost into the summary cost element
cost.innerText = json.cost;
}
}
}
});
}
}
core.modules.connect("damper").then(() => {
// Imported the damper module
Object.assign(
cart.product,
{
/**
* Toggle
*
* Toggle the product in the cart (damper)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler elememnt of the product
* @param {HTMLElement} product The product element
* @param {bool} remove Remove the product element if json.amount === 0?
* @param {bool} force Ignore the damper? (false)
*
* @return {Promise}
*/
damper: core.damper(
(...variables) => cart.product.system(...variables).then(cart.summary),
300,
6,
),
},
);
});
Object.assign(
cart.product,
{
/**
* The product
*
* Handle the product in the cart (system)
*
* @param {HTMLButtonElement|HTMLInputElement} element Handler element of the product
* @param {HTMLElement} product The product element
* @param {string} type Type of action with the product
* @param {number} amount Amount of product to handle
* @param {bool} remove Remove the product element if json.amount === 0?
*
* @return {Promise|null}
*/
async system(
element,
product,
type,
amount = null,
remove = false,
resolve = () => {},
reject = () => {},
) {
try {
if (
(element instanceof HTMLButtonElement ||
element instanceof HTMLInputElement) &&
product instanceof HTMLElement
) {
// Validated
// Initializing the buffer of request body
let request = "";
// Initializing of identifier of the product
const identifier = +product.getAttribute(
"data-product-identifier",
);
if (typeof identifier === "number") {
// Validated identifier
// Writing to the buffer of request body
request += "&identifier=" + identifier;
if (
type === "toggle" ||
type === "write" ||
type === "delete" ||
type === "set"
) {
// Validated type
// Writing to the buffer of request body
request += "&type=" + type;
console.log(type, amount);
if (
(type === "toggle" &&
amount === null ||
typeof amount === "undefined") ||
(type === "set" &&
amount === 0 ||
amount === 100) ||
typeof amount === "number" &&
amount > 0 &&
amount < 100
) {
// Validated amount
console.log(amount);
if (type !== "toggle") {
// Not a toggle request
// Writing to the buffer of request body
request += "&amount=" + amount;
}
// Request
return await core.request(
"/cart/product",
request,
)
.then(
(json) => {
if (json) {
// Received a JSON-response
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
} else {
// Success (not received errors)
if (remove && json.amount === 0) {
// Requested deleting of the product element when there is no the product in the cart
// Deleting the product element
product.remove();
} else {
// Not requested deleting the product element when there is no the product in the cart
// Unblocking the element
element.removeAttribute("disabled");
// Writing offset of hue-rotate to indicate that the product is in the cart
product.style.setProperty(
"--hue-rotate-offset",
json.amount + "0deg",
);
// Writing attribute with amount of the product in the cart
product.setAttribute(
"data-product-amount",
json.amount,
);
// Initializing the amount <span> element
const amounts = product.querySelectorAll(
'[data-product-parameter="amount"]',
);
for (const amount of amounts) {
// Iterating over an amount elements
if (amount instanceof HTMLInputElement) {
// The <input> element
// Writing amount of the product in the cart
amount.value = json.amount;
} else {
// Not the <input> element
// Writing amount of the product in the cart
amount.innerText = json.amount;
}
}
}
// Exit (success)
resolve();
}
}
},
() => reject(),
);
}
}
}
}
} catch (e) {
// Exit (fail)
reject(e);
}
},
},
);
// Connecting to the core
if (!core.cart) core.cart = cart;

File diff suppressed because it is too large Load Diff

View File

@ -49,10 +49,7 @@ export default function damper(func, timeout = 300, force) {
// Requested execution with ignoring the timer
// Deleting the force argument
if (typeof force === "number") args = [
...args.splice(0, force),
...args.splice(force + 1)
];
if (typeof force === "number") delete args[force - 1];
// Writing promise handlers into the arguments variable
args.push(resolve, reject);
@ -63,10 +60,7 @@ export default function damper(func, timeout = 300, force) {
// Normal execution
// Deleting the force argument
if (typeof force === "number") args = [
...args.splice(0, force),
...args.splice(force + 1)
];
if (typeof force === "number") delete args[force - 1];
// Writing promise handlers into the arguments variable
args.push(resolve, reject);

View File

@ -0,0 +1,161 @@
"use strict";
/**
* @name Delivery
*
* @description
* Implements actions with delivery
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
export default class delivery {
/**
* @name Type of the program
*/
static type = "module";
/**
* @name Company
*
* @description
* Choose a delivery company (interface)
*
* @param {HTNLElement} element Handler element
* @param {string} identifier Identifier of the delivery company
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
*/
static company = (element, identifier, force = false) => {
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.company.damper(element, identifier, force);
},
() => {
// Not imported the damper module
// Execute
this.company.system(element, identifier, force);
},
);
// Exit (success)
return false;
};
/**
* @name City
*
* @description
* Write name of a city for delivery (interface)
*
* @param {HTNLInputElement} element Handler element <input>
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Execution completed with an error?
*/
static company = (element, force = false) => {
core.modules.connect("damper").then(
() => {
// Imported the damper module
// Execute under damper
this.city.damper(element, identifier, force);
},
() => {
// Not imported the damper module
// Execute
this.company.system(element, identifier, force);
},
);
// Exit (success)
return false;
};
}
core.modules.connect("damper").then(() => {
// Imported the damper module
Object.assign(
delivery.company,
{
/**
* @name Write
*
* @description
* Choose a delivery company (damper)
*
* @param {HTNLElement} element Handler element
* @param {string} identifier Identifier of the delivery company
* @param {bool} force Ignore the damper? (false)
*
* @return {Promise}
*/
damper: core.damper(
(...variables) => delivery.company.system(...variables),
300,
3,
),
},
);
});
Object.assign(
delivery.company,
{
/**
* @name Write
*
* @description
* Choose a delivery company (system)
*
* @param {string} identifier Identifier of the delivery company
*
* @return {Promise}
*/
async system(
type,
value,
resolve = () => {},
reject = () => {},
) {
try {
if (
typeof type === "string" &&
(typeof value === "string" || typeof valye === "number")
) {
// Received and validated required arguments
// Sending request to the server
return await core.buffer.write(`delivery_${type}`, value)
.then(
(json) => {
try {
// Exit (success)
resolve(json);
} catch (e) {
// Exit (fail)
reject(e);
}
},
() => reject(),
);
}
} catch (e) {
// Exit (fail)
reject(e);
}
},
},
);
// Connecting to the core
if (!core.delivery) core.delivery = delivery;

View File

@ -15,11 +15,6 @@ export default class loader {
*/
static type = "module";
/**
* @name Actial URI
*/
static uri;
/**
* @name Load
*
@ -28,7 +23,7 @@ export default class loader {
*
* @return {Promise}
*/
static async load(uri = "/", body, back = false) {
static async load(uri = "/", body) {
if (typeof uri === "string") {
// Received and validated uri
@ -36,7 +31,7 @@ export default class loader {
.request(
uri,
body,
"GET",
"POST",
{
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
@ -53,33 +48,11 @@ export default class loader {
json.errors.length > 0
) {
// Fail (received errors)
// Exit (fail)
reject(json);
} else {
// Success (not received errors)
if (back) {
// Requested to go back
// Writing actual URI
this.uri = history.state?.previous;
// Deletimg from the browser history
history.back();
} else {
// Requested to go forward
// Writing to the browser history
history.pushState(
{ previous: this.uri },
json.title ?? uri,
uri,
);
// Writing actual URI
this.uri = uri;
}
// Writing to the browser history
history.pushState({}, json.title ?? uri, uri);
/**
* The <title>
@ -174,7 +147,7 @@ export default class loader {
// Writing the <header> element after the last <section> element inside the <body> element
body.insertBefore(
core.header,
section.nextElementSibling,
section.nextSibling,
);
} else {
// Not found section elements <section> inside the <body> element
@ -227,7 +200,7 @@ export default class loader {
// Writing the <main> element after the <header> element
body.insertBefore(
core.main,
core.header.nextElementSibling,
core.header.nextSibling,
);
} else if (core.footer instanceof HTMLElement) {
// Fount the <footer> element
@ -246,10 +219,7 @@ export default class loader {
// Found the last <section> element inside the <body> element
// Writing the <main> element after the last <section> element inside the <body> element
body.insertBefore(
core.main,
section.nextElementSibling,
);
body.insertBefore(core.main, section.nextSibling);
} else {
// Not found section elements <section> inside the <body> element

View File

@ -29,7 +29,7 @@ export default class session {
* @param {string} value Value of the parameter (it can be JSON)
* @param {bool} force Ignore the damper? (false)
*
* @return {bool} Did the execution complete without errors?
* @return {bool} Execution completed with an error?
*/
static write = (name, value, force = false) => {
core.modules.connect("damper").then(
@ -48,7 +48,7 @@ export default class session {
);
// Exit (success)
return true;
return false;
};
};
}
@ -60,12 +60,10 @@ core.modules.connect("damper").then(() => {
session.buffer.write,
{
/**
* @name Write (damper)
* @name Write
*
* @description
* Write to the session buffer
*
* @memberof session.buffer.write
* Write to the session buffer (damper)
*
* @param {string} name Name of the parameter
* @param {string} value Value of the parameter (it can be JSON)
@ -76,7 +74,7 @@ core.modules.connect("damper").then(() => {
damper: core.damper(
(...variables) => session.buffer.write.system(...variables),
300,
2,
3,
),
},
);
@ -86,12 +84,10 @@ Object.assign(
session.buffer.write,
{
/**
* @name Write (system)
* @name Write
*
* @description
* Write to the session buffer
*
* @memberof session.buffer.write
* Write to the session buffer (system)
*
* @param {string} name Name of the parameter
* @param {string} value Value of the parameter (it can be JSON)
@ -104,38 +100,22 @@ Object.assign(
resolve = () => {},
reject = () => {},
) {
if (
typeof name === "string" &&
(typeof value === "string" || typeof value === "number")
) {
if (typeof name === "string" && typeof value === "string") {
// Received and validated required arguments
// Sending request to the server
return await core.request(
"/session/write",
`${name}=${value}`,
"PATCH",
"POST",
)
.then(
(json) => {
if (json) {
// Received a JSON-response
if (
json.errors !== null &&
typeof json.errors === "object" &&
json.errors.length > 0
) {
// Fail (received errors)
// Exit (fail)
reject(json);
} else {
// Success (not received errors)
// Exit (success)
resolve(json);
}
// Exit (success)
resolve(json);
}
},
() => reject(),
@ -146,4 +126,4 @@ Object.assign(
);
// Connecting to the core
// if (!core.session) core.session = session;
if (!core.session) core.session = session;

View File

@ -21,11 +21,6 @@ export default class telegram {
* @see {@link https://core.telegram.org/bots/webapps#initializing-mini-apps}
*/
static api = window.Telegram.WebApp;
/**
* @name List of BackButton events
*/
static back = [];
}
// Connecting to the core

View File

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace mirzaev\arming_bot;
// Files of the project
use mirzaev\arming_bot\controllers\core as controller,
mirzaev\arming_bot\models\core as model,
mirzaev\arming_bot\models\telegram;
// Фреймворк Telegram
use Zanzara\Zanzara,
Zanzara\Context,
Zanzara\Config;
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
// Версия робота
define('ROBOT_VERSION', '1.0.0');
// Путь до настроек
define('SETTINGS', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'settings');
// Путь до хранилища
define('STORAGE', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'storage');
// Файл в формате xlsx с примером excel-документа для импорта каталога
define('CATALOG_EXAMPLE', STORAGE . DIRECTORY_SEPARATOR . 'example.xlsx');
// Файл в формате xlsx для импорта каталога
define('CATALOG_IMPORT', STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx');
/**
* Ключ чат-робота Telegram
* @deprecated
*/
define('KEY', require(SETTINGS . DIRECTORY_SEPARATOR . 'telegram.php'));
define('TELEGRAM_KEY', require(SETTINGS . DIRECTORY_SEPARATOR . 'telegram.php'));
// Initialize dependencies
require __DIR__ . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR
. 'vendor' . DIRECTORY_SEPARATOR
. 'autoload.php';
// Инициализация ядра контроллеров MINIMAL
new controller(false);
// Инициализация ядра моделей MINIMAL
new model(true);
$config = new Config();
$config->setParseMode(Config::PARSE_MODE_MARKDOWN);
$config->useReactFileSystem(true);
$bot = new Zanzara(TELEGRAM_KEY, $config);
/* $bot->onUpdate(function (Context $ctx): void {
var_dump($ctx->getMessage()->getWebAppData());
var_dump($ctx->getEffectiveUser() );
}); */
$bot->onCommand('start', fn($ctx) => telegram::start($ctx));
$bot->onCommand('contacts', fn($ctx) => telegram::contacts($ctx));
$bot->onCommand('company', fn($ctx) => telegram::company($ctx));
$bot->onCommand('community', fn($ctx) => telegram::community($ctx));
$bot->onCommand('settings', fn($ctx) => telegram::settings($ctx));
$bot->onText('💬 Контакты', fn($ctx) => telegram::contacts($ctx));
$bot->onText('🏛️ О компании', fn($ctx) => telegram::company($ctx));
$bot->onText('🎯 Сообщество', fn($ctx) => telegram::community($ctx));
$bot->onText('⚙️ Настройки', fn($ctx) => telegram::settings($ctx));
$bot->onCbQueryData(['mail'], fn($ctx) => telegram::_mail($ctx));
$bot->onCbQueryData(['import_request'], fn($ctx) => telegram::import_request($ctx));
$bot->onCbQueryData(['tuning'], fn($ctx) => telegram::tuning($ctx));
$bot->onCbQueryData(['brands'], fn($ctx) => telegram::brands($ctx));
// Инициализация middleware с обработкой аккаунта
$bot->middleware([telegram::class, "account"]);
// Инициализация middleware с обработкой технических работ разных уровней
$bot->middleware([telegram::class, "suspension"]);
// Запуск чат-робота
$bot->run();

View File

@ -2,12 +2,12 @@
declare(strict_types=1);
namespace mirzaev\huesos;
namespace mirzaev\arming_bot;
// Files of the project
use mirzaev\huesos\controllers\core as controller,
mirzaev\huesos\models\core as model,
mirzaev\huesos\models\socket;
use mirzaev\arming_bot\controllers\core as controller,
mirzaev\arming_bot\models\core as model,
mirzaev\arming_bot\models\socket;
// Framework for PHP
use mirzaev\minimal\core;

View File

@ -0,0 +1,11 @@
@charset "UTF-8";
section#account {
z-index: 999;
position: fixed;
bottom: 20px;
left: 20px;
height: 16px;
display: flex;
}

View File

@ -0,0 +1,222 @@
@charset "UTF-8";
main>section:is(#summary, #products, #delivery) {
width: var(--width);
gap: var(--gap);
overflow: hidden;
}
main>section#delivery>div.column {
padding: 1rem;
gap: var(--gap);
background-color: var(--tg-theme-secondary-bg-color);
}
main>section#delivery>div.column>div#deliveries>input {
display: none;
}
main>section#delivery>div.column>div#deliveries>input+label {
/* backdrop-filter: brightness(1.2); */
/* background-color: unset; */
}
main>section#delivery>div.column>div#deliveries>input+label:is(:hover, :focus) {
filter: brightness(1.1) hue-rotate(30deg);
/* filter: unset; */
/* backdrop-filter: brightness(1.4); */
}
main>section#delivery>div.column>div#deliveries>input+label:active {
filter: unset;
/* backdrop-filter: brightness(0.8); */
}
main>section#delivery>div.column>div#deliveries>input:checked+label {
filter: hue-rotate(30deg);
}
main>section#delivery>div.column>div#deliveries>input:checked+label:is(:hover, :focus) {
filter: brightness(1.1) hue-rotate(30deg);
}
main>section#delivery>div.column>div#deliveries>input:checked+label:active {
filter: brightness(0.9) hue-rotate(30deg);
}
main>section#delivery>div.column>div#address {
flex-flow: row wrap;
gap: 0.7rem;
}
main>section#delivery>div.column>div#address>input#city {
min-width: max(6rem, 20%);
width: min(6rem, 100%);
flex-grow: 1;
}
main>section#delivery>div.column>div#address>input#street {
min-width: max(10rem, 30%);
width: min(10rem, 100%);
flex-grow: 10;
}
main:has(section#summary.disabled:hover)>section#delivery>div {
filter: brightness(1.2);
}
main>section#summary>div {
container-type: inline-size;
container-name: summary;
padding-left: 1rem;
align-items: center;
gap: 0.4rem;
overflow: hidden;
pointer-events: auto;
border-radius: 1.375rem;
background-color: var(--tg-theme-secondary-bg-color);
}
main>section#summary>div>span {
flex-shrink: 0;
}
main>section#summary>div>span:first-of-type {
/* margin-left: auto; */
}
main>section#summary>div>button#order {
/* margin-left: 0.3rem; */
margin-left: auto;
padding-left: 0.7rem;
}
main>section#summary.disabled,
main>section#summary>div:has(button#order:disabled),
main:not(:has(section#delivery>div.column>div#deliveries>input:checked))>section#summary>div:has(button#order:enabled) {
cursor: not-allowed;
}
main>section#summary.disabled,
main>section#summary>div>button#order:disabled,
main:not(:has(section#delivery>div.column>div#deliveries>input:checked))>section#summary>div>button#order:enabled {
pointer-events: none;
filter: grayscale(1);
}
main>section#products>article.product {
position: relative;
width: 100%;
min-height: 5rem;
max-height: 10rem;
display: flex;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--tg-theme-secondary-bg-color);
}
main>section#products>article.product[data-product-amount]:not([data-product-amount="0"]) * {
backdrop-filter: hue-rotate(calc(120deg + var(--hue-rotate-offset, 0deg)));
}
main>section#products>article.product:is(:hover, :focus)>* {
transition: 0s;
}
main>section#products>article.product:not(:is(:hover, :focus))>* {
transition: 0.2s ease-out;
}
main>section#products>article.product>a {
display: contents;
}
main>section#products>article.product>a>img:first-of-type {
width: 5rem;
min-width: 5rem;
min-height: 100%;
object-fit: cover;
image-rendering: auto;
}
main>section#products>article.product>div {
width: 100%;
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
overflow: hidden;
}
main>section#products>article.product>div>div {
display: inline-flex;
align-items: center;
}
main>section#products>article.product>div>div.head {
z-index: 50;
padding: 0 0.4rem;
gap: 1rem;
}
main>section#products>article.product>div>div>button:first-of-type {
margin-left: auto;
}
main>section#products>article.product>div>div>button {
padding: 0.4rem;
background: unset;
}
main>section#products>article.product>div>div.head>button+button {
margin-left: 0.4rem;
}
main>section#products>article.product>div>div.head>button>i.icon.trash {
color: var(--tg-theme-destructive-text-color);
}
main>section#products>article.product>div>div.body {
z-index: 30;
flex-grow: 1;
display: inline-flex;
flex-flow: row wrap;
align-items: start;
gap: 0.3rem;
font-size: 0.6rem;
overflow: hidden;
}
main>section#products>article.product>div>div.body>span {
padding: 0.2rem 0.4rem;
border-radius: 0.75rem;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
main>section#products>article.product>div>div.footer {
z-index: 100;
padding: 0 0.4rem;
display: flex;
overflow: hidden;
}
main>section#products>article.product>div>div.footer>span[data-product-parameter] {
font-size: 0.8rem;
}
main>section#products>article.product>div>div.footer>span[data-product-parameter]+span[data-product-parameter="currency"] {
margin-left: 0.1rem;
}
main>section#products>article.product>div>div.footer>input {
width: 2rem;
padding: 0 0.3rem;
text-align: center;
}
@container summary (max-width: 350px) {
main>section#summary>div>span:not(#cost) {
display: none;
}
}

View File

@ -0,0 +1,181 @@
@charset "UTF-8";
main>section#categories {
width: var(--width);
display: flex;
flex-flow: row wrap;
gap: var(--gap, 5px);
}
main>section#categories>a.category[type="button"] {
--padding: 0.7rem;
position: relative;
height: 23px;
padding: var(--padding);
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
overflow: hidden;
border-radius: 0.75rem;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
main>section#categories:last-child {
/* margin-bottom: unset; */
}
main>section#categories>a.category[type="button"]:has(>img) {
min-width: calc(50% - var(--padding) * 2);
height: 180px;
padding: unset;
}
main>section#categories>a.category[type="button"]>img {
position: absolute;
left: -5%;
top: -5%;
width: 110%;
height: 110%;
object-fit: cover;
/* filter: blur(1px); */
filter: brightness(60%);
}
main>section#categories>a.category[type="button"]:is(:hover, :focus)>img {
filter: unset;
}
main>section#categories>a.category[type="button"]:has(>img)>p {
position: absolute;
left: var(--padding);
bottom: var(--padding);
right: var(--padding);
margin: unset;
width: min-content;
border-radius: 0.75rem;
background: var(--tg-theme-secondary-bg-color);
}
main>section#categories>a.category[type="button"]>p {
z-index: 100;
padding: 8px 16px;
}
main>section#filters {
width: var(--width);
max-height: 2.5rem;
display: flex;
align-items: start;
}
main>section#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#products>div.column {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--gap);
}
main>section#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#products>div.column>article.product:is(:hover, :focus) {
/* flex-grow: 0.1; */
/* background-color: var(--tg-theme-secondary-bg-color); */
}
main>section#products>div.column>article.product:is(:hover, :focus)>* {
transition: 0s;
}
main>section#products>div.column>article.product:not(:is(:hover, :focus))>* {
transition: 0.2s ease-out;
}
main>section#products>div.column>article.product>a {
display: contents;
}
main>section#products>div.column>article.product>a>img:first-of-type {
width: 100%;
height: 100%;
image-rendering: auto;
}
main>section#products>div.column>article.product>a>img:first-of-type+* {
margin-top: auto;
}
main>section#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#products>div.column>article.product>div[data-product="buttons"]:last-of-type {
z-index: 100;
height: 33px;
display: flex;
justify-content: center;
align-items: center;
}
main>section#products>div.column>article.product[data-product-amount]:not(:is([data-product-amount="0"], [data-product-amount="1"]))>div[data-product="buttons"]:last-of-type {
container-type: inline-size;
container-name: product-buttons;
}
main>section#products>div.column>article.product>div[data-product="buttons"]>button[data-product-button="toggle"] {
padding: 0;
flex-grow: 1;
}
main>section#products>div.column>article.product:is([data-product-amount="0"], [data-product-amount="1"])>div[data-product="buttons"]>button[data-product-button="toggle"]>span[data-product-parameter="amount"],
main>section#products>div.column>article.product[data-product-amount="0"]>div[data-product="buttons"]>button:is([data-product-button="write"], [data-product-button="delete"]) {
display: none;
}
main>section#products>div.column>article.product>div[data-product="buttons"]>button[data-product-button="toggle"]>span[data-product-parameter="amount"]:after {
content: '*';
margin: 0 0.2rem;
}
main>section#products>div.column>article.product[data-product-amount]:not([data-product-amount="0"])>div[data-product="buttons"] {
filter: hue-rotate(calc(120deg + var(--hue-rotate-offset, 0deg)));
}
@container product-buttons (max-width: 200px) {
main>section#products>div.column>article.product>div[data-product="buttons"]>button[data-product-button="toggle"]>span:is([data-product-parameter="cost"], [data-product-parameter="currency"]) {
display: none;
}
main>section#products>div.column>article.product>div[data-product="buttons"]>button[data-product-button="toggle"]>span[data-product-parameter="amount"]:after {
content: unset;
}
}

View File

@ -26,7 +26,7 @@ i.icon.arrow:not(.circle, .square)::after {
bottom: 7px;
}
i.icon.arrow:not(.circle, .square, .short)::before {
i.icon.arrow:not(.circle, .square)::before {
width: 16px;
height: 2px;
bottom: 10px;

View File

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

View File

@ -0,0 +1,115 @@
@charset "UTF-8";
section[data-type="select"] {
--width: max(14rem, 20vw);
--height-element: 2rem;
--height-close: var(--height-element);
--height-open: max-content;
position: relative;
width: var(--width);
height: var(--height-close);
display: flex;
flex-direction: column;
cursor: context-menu;
border-radius: 0.75rem;
overflow-x: hidden;
background-color: var(--tg-theme-button-color);
transition: 0s;
}
section[data-type="select"]:not(:focus):after {
z-index: 30;
content: '';
top: calc(50% - 2.5px);
right: 1rem;
position: absolute;
width: 0;
height: 0;
pointer-events: none;
border-left: 5px solid transparent;
border-top: 5px solid var(--tg-theme-button-text-color, black);
border-right: 5px solid transparent;
}
section[data-type="select"]>input {
left: -99999px;
position: absolute;
opacity: 0;
}
section[data-type="select"]>label {
z-index: 10;
order: 2;
top: 0;
position: absolute;
width: 100%;
height: var(--height-element);
padding: 0 1rem;
display: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
pointer-events: none;
color: var(--tg-theme-button-text-color);
transition: 0s;
}
section[data-type="select"]:is([data-select="open"], :focus)>input:not(:checked)+label[for$='title'] {
display: none;
}
section[data-type="select"]:is([data-select="open"], :focus)>input:not(:checked)+label[for$='all']:not(:hover, :active, :focus) {
filter: brightness(90%);
}
section[data-type="select"]>input:not(:checked)+label {
cursor: pointer;
filter: brightness(80%);
}
section[data-type="select"]:is([data-select="open"], :focus)>input+label:hover {
filter: brightness(110%);
}
section[data-type="select"]:is([data-select="open"], :focus)>input:checked+label:hover {
filter: brightness(120%);
}
section[data-type="select"]:is([data-select="open"], :focus)>input+label:is(:active, :focus) {
filter: brightness(60%);
}
section[data-type="select"]:is([data-select="open"], :focus)>input:checked+label:is(:active, :focus) {
filter: brightness(70%);
}
section[data-type="select"]>input:checked+label {
order: 1;
max-width: calc(var(--width) - 2rem - 10px);
display: inline;
line-height: var(--height-element);
padding-right: 0;
}
section[data-type="select"]:is([data-select="open"], :focus)>input:checked+label {
max-width: initial;
padding-right: initial;
}
section[data-type="select"]:is([data-select="open"], :focus) {
height: var(--height-open, max-content);
}
section[data-type="select"]:is([data-select="open"], :focus)>label {
position: relative;
display: inline;
line-height: var(--height-element);
pointer-events: all;
}
@media only screen and (max-width: 500px) {
section[data-type="select"]:only-child {
--width: 100%
}
}

View File

@ -0,0 +1,204 @@
@charset "UTF-8";
:root {
--text-light: #fafaff;
--socket-connected: #2be851;
--socket-disconnected: #8e8181;
--socket-text: #b09999;
}
* {
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 {
--gap: 16px;
--width: calc(100% - var(--gap) * 2);
--offset-x: 2%;
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 {
container-type: inline-size;
container-name: header;
margin: 2rem 0 1rem;
padding: 0 var(--offset-x);
display: flex;
flex-direction: column;
align-items: center;
gap: 26px;
}
main {
container-type: inline-size;
container-name: main;
padding: 0 var(--offset-x);
display: flex;
flex-direction: column;
align-items: center;
gap: 26px;
transition: 0s;
}
main>section:last-child {
margin-bottom: 5rem;
}
main>*[data-section] {
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;
}
footer {}
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);
}
button,
*[type="button"] {
cursor: pointer;
}
:is(button, :is(a, label)[type="button"]) {
padding: 8px 16px;
display: flex;
justify-content: center;
align-items: center;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
button {
height: 33px;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
a[type="button"] {
height: 23px;
}
:is(button, :is(a, label)[type="button"]):is(:hover, :focus) {
filter: brightness(120%);
}
:is(button, :is(a, label)[type="button"]):active {
filter: brightness(80%);
transition: 0s;
}
h1,
h2 {
margin: 1rem 0 0;
}
input {
background: unset;
}
.kabrio {
font-family: "Kabrio";
}
.cost.currency:after {
content: var(--currency);
margin-left: var(--currency-offset, 0.1rem);
}
.rounded {
border-radius: 0.75rem;
}
.row {
display: flex;
flex-direction: row;
}
.column {
display: flex;
flex-direction: column;
}
.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,54 @@
@charset "UTF-8";
header>nav#menu {
container-type: inline-size;
container-name: menu;
margin-bottom: 1rem;
width: var(--width);
min-height: 3rem;
display: flex;
flex-flow: row wrap;
gap: 1rem;
border-radius: 1.375rem;
overflow: hidden;
}
header>nav#menu>a[type="button"] {
height: 3rem;
padding: unset;
border-radius: 1.375rem;
color: var(--tg-theme-button-text-color);
background-color: var(--tg-theme-button-color);
}
header>nav#menu>a[type=button]>:first-child {
margin-left: 1rem;
}
header>nav#menu>a[type="button"]>* {
margin-right: 1rem;
}
@container header (max-width: 450px) {
header>nav#menu>a[type="button"]:nth-child(1)>i.icon+span {
display: none;
}
}
@container header (max-width: 350px) {
header>nav#menu>a[type="button"]:nth-child(2)>i.icon+span {
display: none;
}
}
@container header (max-width: 250px) {
header>nav#menu>a[type="button"]:nth-child(3)>i.icon+span {
display: none;
}
}
@container header (max-width: 150px) {
header>nav#menu>a[type="button"]>i.icon+span {
display: none;
}
}

View File

@ -8,18 +8,7 @@ section#window {
display: flex;
justify-content: center;
align-items: center;
/* backdrop-filter: brightness(40%) contrast(120%) grayscale(60%) blur(1.2px); */
}
section#window:before {
content: '';
z-index: -100;
position: absolute;
width: 100vw;
height: 100vh;
opacity: 0.7;
background: #000;
filter: brightness(50%);
backdrop-filter: brightness(50%) contrast(120%) grayscale(60%) blur(1.2px);
}
section#window>div.card {
@ -69,45 +58,34 @@ section#window>div.card>h3>a.exit[type="button"] {
}
section#window>div.card>div.images {
--image-width: 10rem;
height: 10rem;
min-height: 10rem;
max-height: 10rem;
display: flex;
overflow: hidden;
overflow: clip;
cursor: zoom-in;
transition: 0s;
}
section#window>div.card>div.images:not(.extend):has(> img:last-child:nth-child(2)) {
padding: 0rem 1rem;
}
section#window>div.card>div.images.extend {
--image-width: max(30rem, 40vw);
z-index: 9999999;
left: 10vw;
top: 10vh;
left: 0;
top: 0;
margin: unset !important;
position: absolute;
width: 100vw;
max-width: unset;
height: 80vh;
min-height: 80vh;
max-height: 80vh;
height: 100vh;
align-items: center;
cursor: default;
overflow: unset;
cursor: normal;
transition: 0s;
}
section#window>div.card:has(>div.images.extend)>:not(div.images.extend) {
filter: brightness(50%);
}
section#window>div.card>div.images>img {
margin-right: 0.5rem;
min-width: 150px;
width: auto;
height: 10rem;
width: var(--image-width);
max-width: var(--image-width);
height: 100%;
flex-shrink: 0;
flex-grow: 0;
object-fit: cover;
@ -117,12 +95,10 @@ section#window>div.card>div.images>img {
}
section#window>div.card>div.images.extend>img {
margin-right: 3vw;
width: 80vw;
height: auto;
max-height: 100%;
object-fit: contain;
border-radius: 0.75rem;
margin-right: 2vw;
height: max-content;
object-fit: unset;
border-radius: unset;
}
section#window>div.card>div.images>img:last-child {
@ -186,6 +162,7 @@ section#window>div.card>div.footer>button.buy {
}
@container window-footer (max-width: 350px) {
section#window>div.card>div.footer>small:first-of-type {
margin-left: auto;
}

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