Compare commits
38 Commits
|
@ -1 +1,2 @@
|
||||||
|
node_modules
|
||||||
vendor
|
vendor
|
||||||
|
|
359
README.md
359
README.md
|
@ -1,42 +1,160 @@
|
||||||
# huesos
|
# huesos
|
||||||
|
Base for creating shop chat-robots using Web App technology for [Telegram](https://telegram.org)
|
||||||
|
|
||||||
Basis for developing chat-robots with "Web App" technology for Telegram
|
## 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>
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### AnangoDB
|
### AnangoDB
|
||||||
|
|
||||||
1. Create a Graph with the specified values
|
1. **Configure unix-socket**<br>
|
||||||
**Name:** catalog<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>
|
<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>
|
**edgeDefinition:** entry<br>
|
||||||
**fromCollections:** categoy, product<br>
|
**fromCollections:** category, product<br>
|
||||||
**toCollections:** category
|
**toCollections:** category
|
||||||
|
|
||||||
2. Create a Graph with the specified values
|
* Relation 2<br>
|
||||||
**Name:** sessions<br>
|
**edgeDefinition:** reservation<br>
|
||||||
<br>
|
**fromCollections:** product<br>
|
||||||
**edgeDefinition:** connect<br>
|
**toCollections:** cart
|
||||||
**fromCollections:** account<br>
|
|
||||||
**toCollections:** session
|
|
||||||
|
|
||||||
3. Create indexes for the "product" collection
|
---
|
||||||
|
|
||||||
|
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>
|
**Type:** "Inverted Index"<br>
|
||||||
**Fields:** name.ru<br>
|
**Fields:** name.ru<br>
|
||||||
**Analyzer:** "text_ru"<br>
|
**Analyzer:** "text_ru"<br>
|
||||||
**Search field:** true<br>
|
**Search field:** true<br>
|
||||||
**Name:** name_ru<br>
|
**Name:** name_ru<br><br>
|
||||||
<br>
|
|
||||||
*Add indexes for all search parameters and for all languages (search language is selected based on the user's language, <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>
|
otherwise from the default language specified in the active settings from **settings** collection document)*<br>
|
||||||
<br>
|
<br>
|
||||||
*See fields in the `mirzaev/arming_bot/models/product`<br>
|
*See fields in the `mirzaev/arming_bot/models/product`<br>
|
||||||
**name.ru**, **description.ru** and **compatibility.ru***
|
**name.ru**, **description.ru** and **compatibility.ru***<br>
|
||||||
|
|
||||||
4. Create a View with the specified values
|
---
|
||||||
|
|
||||||
|
5. **Create a View with the specified values**<br>
|
||||||
**type:** search-alias (you can also use "arangosearch")<br>
|
**type:** search-alias (you can also use "arangosearch")<br>
|
||||||
**name:** **product**s_search<br>
|
**name:** **product**s_search<br>
|
||||||
**indexes:**
|
**indexes:**<br><br>
|
||||||
|
|
||||||
|
You can copy an example of view file from here: `/examples/arangodb/views/products_search.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
|
@ -48,58 +166,189 @@ otherwise from the default language specified in the active settings from **sett
|
||||||
|
|
||||||
### NGINX
|
### NGINX
|
||||||
|
|
||||||
1. Example of NGINX server file
|
1. **Create a NGINX server**<br>
|
||||||
```nginx
|
You can copy an example of server file from here: `/examples/nginx/server.conf`
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.php;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ /(?<type>categories|products) {
|
2. **Add support for javascript modules**<br>
|
||||||
root /var/www/arming_bot/mirzaev/arming_bot/system/storage;
|
Edit the file `/etc/nginx/mime.types`<br>
|
||||||
try_files $uri =404;
|
`application/javascript js;` -> `application/javascript js mjs;`
|
||||||
}
|
|
||||||
|
|
||||||
location ~ \.php$ {
|
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`
|
||||||
|
|
||||||
### SystemD (or any alternative you like)
|
### SystemD (or any alternative you like)
|
||||||
|
You can copy an example of systemd file from here: `/examples/systemd/huesos.service`<br>
|
||||||
1. Execute: `sudo cp telegram-huesos.service /etc/systemd/system/telegram-huesos.service`
|
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>
|
*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*
|
*the configuration file is very simple and you can remake it for any alternative to SystemD that you like*
|
||||||
|
|
||||||
## Settings
|
## Menu
|
||||||
Settings of chat-robot and Web App<br>
|
*Menu inside the Web App*<br><br>
|
||||||
<br>
|
Make sure you have a **menu** collection (can be created automatically)<br>
|
||||||
Make sure you have a **settings** collection (can be created automatically) and at least one document with the "status" parameter set to "active"
|
You can copy a clean menu documents without comments from here: `/examples/arangodb/collections/menu`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "active"
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### language
|
## Settings
|
||||||
Language for system messages if user language could not be determined<br>
|
*Settings of chat-robot and Web App*<br><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>
|
||||||
**Value:** en
|
You can copy a clean settings document without comments from here: `/examples/arangodb/collections/settings.json`
|
||||||
|
|
||||||
## Suspensions
|
|
||||||
System of suspensions of chat-robot and Web App<br>
|
|
||||||
<br>
|
|
||||||
Make sure you have a **suspension** collection (can be created automatically)
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"end": 1726068961,
|
"status": "active", // Values: "active", "inactive" (string) Status of the settings document?
|
||||||
"targets": {
|
"project": {
|
||||||
"chat-robot": true,
|
"name": "PROJECT" // Name of the projext (string)
|
||||||
"web app": true
|
},
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"end": 1726068961, // Unixtime
|
||||||
|
"targets": {
|
||||||
|
"chat-robot": true, // Block chat-robot
|
||||||
|
"web app": true // Block "Web App"
|
||||||
|
},
|
||||||
"access": {
|
"access": {
|
||||||
"tester": true,
|
"tester": true, // Account with `"tester": true`
|
||||||
"developer": true
|
"developer": true // Account with `"developer": true`
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"ru": "Разрабатываю каталог, поиск и корзину",
|
"ru": "Разрабатываю каталог, поиск и корзину",
|
||||||
|
@ -107,3 +356,13 @@ Make sure you have a **suspension** collection (can be created automatically)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "mirzaev/arming_bot",
|
"name": "mirzaev/huesos",
|
||||||
"description": "Chat-robot for tuning weapons",
|
"description": "Chat-robot for tuning weapons",
|
||||||
"homepage": "https://t.me/arming_bot",
|
"homepage": "https://t.me/huesos",
|
||||||
"type": "chat-robot",
|
"type": "chat-robot",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"telegram",
|
"telegram",
|
||||||
|
@ -18,20 +18,27 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
|
"php": "^8.4",
|
||||||
|
"ext-gd": "^8.4",
|
||||||
|
"ext-intl": "^8.4",
|
||||||
"triagens/arangodb": "^3.8",
|
"triagens/arangodb": "^3.8",
|
||||||
"mirzaev/minimal": "^2.2",
|
"mirzaev/minimal": "^3.4.0",
|
||||||
"mirzaev/arangodb": "^1.3",
|
"mirzaev/arangodb": "^2",
|
||||||
"badfarm/zanzara": "^0.9.1",
|
"badfarm/zanzara": "^0.9.1",
|
||||||
"nyholm/psr7": "^1.8",
|
"nyholm/psr7": "^1.8",
|
||||||
"react/filesystem": "^0.1.2",
|
"react/filesystem": "^0.1.2",
|
||||||
"twig/twig": "^3.10",
|
"twig/twig": "^3.10",
|
||||||
"twig/extra-bundle": "^3.7",
|
"twig/extra-bundle": "^3.7",
|
||||||
"twig/intl-extra": "^3.10",
|
"twig/intl-extra": "^3.10",
|
||||||
"avadim/fast-excel-reader": "^2.19"
|
"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": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"mirzaev\\arming_bot\\": "mirzaev/arming_bot/system/"
|
"mirzaev\\huesos\\": "mirzaev/huesos/system/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"urn": "/account",
|
||||||
|
"name": {
|
||||||
|
"en": "Account",
|
||||||
|
"ru": "Аккаунт"
|
||||||
|
},
|
||||||
|
"identifier": "account",
|
||||||
|
"style": {
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
"class": "",
|
||||||
|
"icon": {
|
||||||
|
"style": {},
|
||||||
|
"class": "loading spinner animated"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"storage": null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"urn": "/cart",
|
||||||
|
"name": {
|
||||||
|
"en": "Cart",
|
||||||
|
"ru": "Корзина"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"order": 999
|
||||||
|
},
|
||||||
|
"class": "cart",
|
||||||
|
"icon": {
|
||||||
|
"style": {},
|
||||||
|
"class": "shopping cart"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"storage": null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"urn": "/",
|
||||||
|
"name": {
|
||||||
|
"en": "Main page",
|
||||||
|
"ru": "Главная страница"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"order": 0
|
||||||
|
},
|
||||||
|
"class": "",
|
||||||
|
"icon": {
|
||||||
|
"style": {},
|
||||||
|
"class": "house"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"storage": null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
#
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Telegram-robot - @arming_bot
|
Description=Telegram chat-robot: @domain_of_your_robot_here
|
||||||
|
|
||||||
Wants=network.target
|
Wants=network.target
|
||||||
After=syslog.target network-online.target
|
After=syslog.target network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=sudo -u www-data /usr/bin/php /var/www/arming_bot/mirzaev/arming_bot/system/public/robot.php
|
ExecStart=sudo -u www-data /usr/bin/php /var/www/huesos/mirzaev/huesos/system/public/robot.php
|
||||||
PIDFile=/var/run/php/telegram-arming_bot.pid
|
PIDFile=/var/run/php/huesos.pid
|
||||||
RemainAfterExit=no
|
RemainAfterExit=no
|
||||||
RuntimeMaxSec=3600s
|
RuntimeMaxSec=3600s
|
||||||
Restart=always
|
Restart=always
|
|
@ -1,276 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\controllers;
|
|
||||||
|
|
||||||
// Files of the project
|
|
||||||
use mirzaev\arming_bot\controllers\core,
|
|
||||||
mirzaev\arming_bot\models\catalog as model,
|
|
||||||
mirzaev\arming_bot\models\entry,
|
|
||||||
mirzaev\arming_bot\models\category,
|
|
||||||
mirzaev\arming_bot\models\product;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controller of catalog
|
|
||||||
*
|
|
||||||
* @package mirzaev\arming_bot\controllers
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
|
||||||
*/
|
|
||||||
final class catalog extends core
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Registry of errors
|
|
||||||
*/
|
|
||||||
protected array $errors = [
|
|
||||||
'session' => [],
|
|
||||||
'account' => [],
|
|
||||||
'catalog' => []
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Catalog
|
|
||||||
*
|
|
||||||
* @param array $parameters Parameters of the request (POST + GET)
|
|
||||||
*/
|
|
||||||
public function index(array $parameters = []): ?string
|
|
||||||
{
|
|
||||||
if (!empty($parameters['category'])) {
|
|
||||||
// Передана категория (идентификатор)
|
|
||||||
|
|
||||||
// Инициализация актуальной категории
|
|
||||||
$category = new category(document: category::_read('d.identifier == @identifier', parameters: ['identifier' => $parameters['category']]));
|
|
||||||
|
|
||||||
if ($category instanceof category) {
|
|
||||||
// Found the category
|
|
||||||
|
|
||||||
// Поиск категорий или товаров входящих в актуальную категорию
|
|
||||||
$entries = entry::search(
|
|
||||||
document: $category,
|
|
||||||
amount: 30,
|
|
||||||
errors: $this->errors['catalog']
|
|
||||||
);
|
|
||||||
|
|
||||||
// Объявление буферов категорий и товаров (важно - в единственном числе, по параметру из базы данных)
|
|
||||||
$category = $product = [];
|
|
||||||
|
|
||||||
foreach ($entries as $entry) {
|
|
||||||
// Перебор вхождений
|
|
||||||
|
|
||||||
// Запись массивов категорий и товаров ($category и $product) в буфер глобальной переменной шаблонизатора
|
|
||||||
${$entry->_type}[] = $entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запись категорий из буфера в глобальную переменную шаблонизатора
|
|
||||||
$this->view->categories = $category;
|
|
||||||
|
|
||||||
// Запись товаров из буфера в глобальную переменную шаблонизатора
|
|
||||||
$this->view->products = $product;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Не передана категория
|
|
||||||
|
|
||||||
// Поиск категорий: "categories" (самый верхний уровень)
|
|
||||||
$this->view->categories = entry::ascendants(descendant: new category, errors: $this->errors['catalog']);
|
|
||||||
|
|
||||||
// Search for products
|
|
||||||
/* $this->view->products = product::read(
|
|
||||||
filter: 'd.deleted != true && d.hidden != true',
|
|
||||||
sort: 'd.promoting ASC, d.position ASC, d.created DESC',
|
|
||||||
amount: 6,
|
|
||||||
errors: $this->errors['catalog']
|
|
||||||
); */
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generating filters
|
|
||||||
$this->view->filters = [
|
|
||||||
'brands' => product::collect('d.brand.ru', $this->errors['catalog'])
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
|
||||||
// GET request
|
|
||||||
|
|
||||||
// Exit (success)
|
|
||||||
return $this->view->render('catalog/page.html');
|
|
||||||
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
// POST request
|
|
||||||
|
|
||||||
// Initializing a response headers
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
header('Content-Encoding: none');
|
|
||||||
header('X-Accel-Buffering: no');
|
|
||||||
|
|
||||||
// Initializing of the output buffer
|
|
||||||
ob_start();
|
|
||||||
|
|
||||||
// Generating the reponse
|
|
||||||
echo json_encode(
|
|
||||||
[
|
|
||||||
'title' => $title ?? '',
|
|
||||||
'html' => [
|
|
||||||
'categories' => $this->view->render('catalog/elements/categories.html'),
|
|
||||||
'products' => $this->view->render('catalog/elements/products/2columns.html'),
|
|
||||||
'filters' => $this->view->render('catalog/elemments/filters.html')
|
|
||||||
],
|
|
||||||
'errors' => $this->errors
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initializing a response headers
|
|
||||||
header('Content-Length: ' . ob_get_length());
|
|
||||||
|
|
||||||
// Sending and deinitializing of the output buffer
|
|
||||||
ob_end_flush();
|
|
||||||
flush();
|
|
||||||
|
|
||||||
// Exit (success)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit (fail)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search
|
|
||||||
*
|
|
||||||
* @param array $parameters Parameters of the request (POST + GET)
|
|
||||||
*/
|
|
||||||
public function search(array $parameters = []): ?string
|
|
||||||
{
|
|
||||||
// Initializing of text fore search
|
|
||||||
preg_match('/[\w\s]+/u', $parameters['text'] ?? '', $matches);
|
|
||||||
$text = $matches[0] ?? null;
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
// POST request
|
|
||||||
|
|
||||||
// Search for products
|
|
||||||
$this->view->products = isset($text) ? product::read(
|
|
||||||
search: $text,
|
|
||||||
filter: 'd.deleted != true && d.hidden != true',
|
|
||||||
sort: 'd.position ASC, d.name ASC, d.created DESC',
|
|
||||||
amount: 30,
|
|
||||||
errors: $this->errors['catalog']
|
|
||||||
) : [];
|
|
||||||
|
|
||||||
// Initializing a response headers
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
header('Content-Encoding: none');
|
|
||||||
header('X-Accel-Buffering: no');
|
|
||||||
|
|
||||||
// Initializing of the output buffer
|
|
||||||
ob_start();
|
|
||||||
|
|
||||||
// Generating the reponse
|
|
||||||
echo json_encode(
|
|
||||||
[
|
|
||||||
'title' => $title ?? '',
|
|
||||||
'html' => [
|
|
||||||
'products' => $this->view->render('catalog/elements/products.html')
|
|
||||||
],
|
|
||||||
'errors' => $this->errors
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initializing a response headers
|
|
||||||
header('Content-Length: ' . ob_get_length());
|
|
||||||
|
|
||||||
// Sending and deinitializing of the output buffer
|
|
||||||
ob_end_flush();
|
|
||||||
flush();
|
|
||||||
|
|
||||||
// Exit (success)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit (fail)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product
|
|
||||||
*
|
|
||||||
* @param array $parameters Parameters of the request (POST + GET)
|
|
||||||
*/
|
|
||||||
public function product(array $parameters = []): ?string
|
|
||||||
{
|
|
||||||
// Initializing of text fore search
|
|
||||||
preg_match('/[\d]+/', $parameters['id'] ?? '', $matches);
|
|
||||||
$_key = $matches[0] ?? null;
|
|
||||||
|
|
||||||
if (!empty($_key)) {
|
|
||||||
// Received id of prouct (_key)
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
// POST request
|
|
||||||
|
|
||||||
// Search for products
|
|
||||||
$product = product::read(
|
|
||||||
filter: "d._key == \"$_key\" && d.deleted != true && d.hidden != true",
|
|
||||||
sort: 'd.created DESC',
|
|
||||||
amount: 1,
|
|
||||||
return: '{id: d._key, title: d.title.ru, description: d.description.ru, cost: d.cost, weight: d.weight, dimensions: d.dimensions, brand: d.brand.ru, compatibility: d.compatibility.ru}',
|
|
||||||
errors: $this->errors['catalog']
|
|
||||||
)[0]?->getAll();
|
|
||||||
|
|
||||||
if (!empty($product)) {
|
|
||||||
// Found the product
|
|
||||||
|
|
||||||
// Initializing buffer of images
|
|
||||||
$images = [];
|
|
||||||
|
|
||||||
foreach (
|
|
||||||
glob(INDEX .
|
|
||||||
DIRECTORY_SEPARATOR .
|
|
||||||
'themes' .
|
|
||||||
DIRECTORY_SEPARATOR .
|
|
||||||
(THEME ?? 'default') .
|
|
||||||
DIRECTORY_SEPARATOR .
|
|
||||||
'images' .
|
|
||||||
DIRECTORY_SEPARATOR .
|
|
||||||
$_key .
|
|
||||||
DIRECTORY_SEPARATOR .
|
|
||||||
"*.{jpg,png,gif}", GLOB_BRACE) as $file
|
|
||||||
) {
|
|
||||||
// Iterate over images of the product
|
|
||||||
|
|
||||||
// Write to buffer of images
|
|
||||||
$images[] = "/images/$_key/" . basename($file);
|
|
||||||
}
|
|
||||||
|
|
||||||
$product = $product + ['images' => $images];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializing a response headers
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
header('Content-Encoding: none');
|
|
||||||
header('X-Accel-Buffering: no');
|
|
||||||
|
|
||||||
// Initializing of the output buffer
|
|
||||||
ob_start();
|
|
||||||
|
|
||||||
// Generating the reponse
|
|
||||||
echo json_encode(
|
|
||||||
[
|
|
||||||
'product' => $product,
|
|
||||||
'errors' => $this->errors
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initializing a response headers
|
|
||||||
header('Content-Length: ' . ob_get_length());
|
|
||||||
|
|
||||||
// Sending and deinitializing of the output buffer
|
|
||||||
ob_end_flush();
|
|
||||||
flush();
|
|
||||||
|
|
||||||
// Exit (success)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit (fail)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\controllers;
|
|
||||||
|
|
||||||
// Files of the project
|
|
||||||
use mirzaev\arming_bot\controllers\core,
|
|
||||||
mirzaev\arming_bot\models\product;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Index controller
|
|
||||||
*
|
|
||||||
* @package mirzaev\arming_bot\controllers
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
|
||||||
*/
|
|
||||||
final class index extends core
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the main page
|
|
||||||
*
|
|
||||||
* @param array $parameters Parameters of the request (POST + GET)
|
|
||||||
*/
|
|
||||||
public function index(array $parameters = []): ?string
|
|
||||||
{
|
|
||||||
// Exit (success)
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render('index.html');
|
|
||||||
|
|
||||||
// Exit (fail)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\controllers;
|
|
||||||
|
|
||||||
// Files of the project
|
|
||||||
use mirzaev\arming_bot\controllers\core,
|
|
||||||
mirzaev\arming_bot\models\account;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controller of session
|
|
||||||
*
|
|
||||||
* @package mirzaev\arming_bot\controllers
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
|
||||||
*/
|
|
||||||
final class session extends core
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Registry of errors
|
|
||||||
*/
|
|
||||||
protected array $errors = [
|
|
||||||
'session' => [],
|
|
||||||
'account' => []
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect session to the telegram account
|
|
||||||
*
|
|
||||||
* @param array $parameters Parameters of the request (POST + GET)
|
|
||||||
*/
|
|
||||||
public function telegram(array $parameters = []): ?string
|
|
||||||
{
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
// POST request
|
|
||||||
|
|
||||||
if ($connected = isset($this->account)) {
|
|
||||||
// Found the account
|
|
||||||
|
|
||||||
// Initializing language of the account
|
|
||||||
$language = $this->account->language;
|
|
||||||
} else {
|
|
||||||
// Not found the account
|
|
||||||
|
|
||||||
if (count($parameters) > 1 && isset($parameters['hash'])) {
|
|
||||||
|
|
||||||
$buffer = $parameters;
|
|
||||||
|
|
||||||
unset($buffer['authentication'], $buffer['hash']);
|
|
||||||
ksort($buffer);
|
|
||||||
|
|
||||||
$prepared = [];
|
|
||||||
foreach ($buffer as $key => $value) {
|
|
||||||
if (is_array($value)) {
|
|
||||||
$prepared[] = $key . '=' . json_encode($value, JSON_UNESCAPED_UNICODE);
|
|
||||||
} else {
|
|
||||||
$prepared[] = $key . '=' . $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$key = hash_hmac('sha256', require(SETTINGS . DIRECTORY_SEPARATOR . 'key.php'), 'WebAppData', true);
|
|
||||||
$hash = bin2hex(hash_hmac('sha256', implode(PHP_EOL, $prepared), $key, true));
|
|
||||||
|
|
||||||
if (hash_equals($hash, $parameters['hash'])) {
|
|
||||||
// Data confirmed (according to telegram documentation)
|
|
||||||
|
|
||||||
if (time() - $parameters['auth_date'] < 86400) {
|
|
||||||
// Authorization date less than 1 day ago
|
|
||||||
|
|
||||||
// Initializing data of the account
|
|
||||||
$data = json_decode($parameters['user']);
|
|
||||||
|
|
||||||
// Initializing of the account
|
|
||||||
$account = account::initialization(
|
|
||||||
$data->id,
|
|
||||||
[
|
|
||||||
'id' => $data->id,
|
|
||||||
'name' => [
|
|
||||||
'first' => $data->first_name,
|
|
||||||
'last' => $data->last_name
|
|
||||||
],
|
|
||||||
'domain' => $data->username,
|
|
||||||
'language' => $data->language_code,
|
|
||||||
'messages' => $data->allows_write_to_pm
|
|
||||||
],
|
|
||||||
$this->errors['account']
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($account instanceof account) {
|
|
||||||
// Initialized the account
|
|
||||||
|
|
||||||
// Connecting the account to the session
|
|
||||||
$connected = $this->session->connect($account, $this->errors['session']);
|
|
||||||
|
|
||||||
// Initializing language of the account
|
|
||||||
$language = $account->language;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializing a response headers
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
header('Content-Encoding: none');
|
|
||||||
header('X-Accel-Buffering: no');
|
|
||||||
|
|
||||||
// Initializing of the output buffer
|
|
||||||
ob_start();
|
|
||||||
|
|
||||||
// Generating the reponse
|
|
||||||
echo json_encode(
|
|
||||||
[
|
|
||||||
'connected' => (bool) $connected,
|
|
||||||
'language' => $language ?? null,
|
|
||||||
'errors' => $this->errors
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initializing a response headers
|
|
||||||
header('Content-Length: ' . ob_get_length());
|
|
||||||
|
|
||||||
// Sending and deinitializing of the output buffer
|
|
||||||
ob_end_flush();
|
|
||||||
flush();
|
|
||||||
|
|
||||||
// Exit (success)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit (fail)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models;
|
|
||||||
|
|
||||||
// Files of the project
|
|
||||||
use mirzaev\arming_bot\models\core,
|
|
||||||
mirzaev\arming_bot\models\traits\status,
|
|
||||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
|
||||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
|
||||||
|
|
||||||
// Framework for ArangoDB
|
|
||||||
use mirzaev\arangodb\collection,
|
|
||||||
mirzaev\arangodb\document;
|
|
||||||
|
|
||||||
// Framework for Telegram
|
|
||||||
use Zanzara\Telegram\Type\User as telegram;
|
|
||||||
|
|
||||||
// Built-in libraries
|
|
||||||
use exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Model of account
|
|
||||||
*
|
|
||||||
* @package mirzaev\arming_bot\models
|
|
||||||
*
|
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
|
||||||
*/
|
|
||||||
final class account extends core implements arangodb_document_interface
|
|
||||||
{
|
|
||||||
use status, arangodb_document_trait;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the collection in ArangoDB
|
|
||||||
*/
|
|
||||||
final public const string COLLECTION = 'account';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize
|
|
||||||
*
|
|
||||||
* @param int $identifier Identifier of the account
|
|
||||||
* @param telegram|array|null $registration Данные для регистрация, если аккаунт не найден
|
|
||||||
* @param array &$errors Registry of errors
|
|
||||||
*
|
|
||||||
* @return static|null Объект аккаунта, если найден
|
|
||||||
*/
|
|
||||||
public static function initialize(int $identifier, telegram|array|null $registration = null, array &$errors = []): static|null
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
|
||||||
// Initialized the collection
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Initializing the account and exit (success)
|
|
||||||
return new static(
|
|
||||||
document: collection::execute(
|
|
||||||
<<<'AQL'
|
|
||||||
FOR d IN @@collection
|
|
||||||
FILTER d.identifier == @identifier
|
|
||||||
RETURN d
|
|
||||||
AQL,
|
|
||||||
[
|
|
||||||
'@collection' => static::COLLECTION,
|
|
||||||
'identifier' => $identifier
|
|
||||||
],
|
|
||||||
errors: $errors
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} catch (exception $e) {
|
|
||||||
if ($registration) {
|
|
||||||
// Not found the account and registration is requested
|
|
||||||
|
|
||||||
// Creating account
|
|
||||||
$account = document::write(
|
|
||||||
static::COLLECTION,
|
|
||||||
(is_array($registration)
|
|
||||||
? $registration :
|
|
||||||
[
|
|
||||||
'identifier' => $registration->getId(),
|
|
||||||
'name' => [
|
|
||||||
'first' => $registration->getFirstName(),
|
|
||||||
'last' => $registration->getLastName()
|
|
||||||
],
|
|
||||||
'domain' => $registration->getUsername(),
|
|
||||||
'robot' => $registration->isBot(),
|
|
||||||
'banned' => false,
|
|
||||||
'tester' => false,
|
|
||||||
'developer' => false,
|
|
||||||
'access' => [
|
|
||||||
'settings' => false
|
|
||||||
],
|
|
||||||
'menus' => [
|
|
||||||
'attachments' => $registration->getAddedToAttachmentMenu()
|
|
||||||
],
|
|
||||||
'messages' => true,
|
|
||||||
'groups' => [
|
|
||||||
'join' => $registration->getCanJoinGroups(),
|
|
||||||
'messages' => $registration->getCanReadAllGroupMessages()
|
|
||||||
],
|
|
||||||
'premium' => $registration->isPremium(),
|
|
||||||
'language' => $registration->getLanguageCode(),
|
|
||||||
'queries' => [
|
|
||||||
'inline' => $registration->getSupportsInlineQueries()
|
|
||||||
]
|
|
||||||
]) + [
|
|
||||||
'version' => ROBOT_VERSION,
|
|
||||||
'active' => true
|
|
||||||
],
|
|
||||||
errors: $errors
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($account) {
|
|
||||||
// Created account
|
|
||||||
|
|
||||||
// Initializing of the account (without registration request)
|
|
||||||
return static::initialize($identifier, errors: $errors);
|
|
||||||
} else throw new exception('Failed to register account');
|
|
||||||
} else throw new exception('Failed to find account');
|
|
||||||
}
|
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
|
||||||
} catch (exception $e) {
|
|
||||||
// Writing to the registry of errors
|
|
||||||
$errors[] = [
|
|
||||||
'text' => $e->getMessage(),
|
|
||||||
'file' => $e->getFile(),
|
|
||||||
'line' => $e->getLine(),
|
|
||||||
'stack' => $e->getTrace()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit (fail)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,487 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models;
|
|
||||||
|
|
||||||
// Files of the project
|
|
||||||
use mirzaev\arming_bot\models\core,
|
|
||||||
mirzaev\arming_bot\models\product,
|
|
||||||
mirzaev\arming_bot\models\category,
|
|
||||||
mirzaev\arming_bot\models\entry,
|
|
||||||
mirzaev\arming_bot\models\traits\files,
|
|
||||||
mirzaev\arming_bot\models\traits\yandex\disk as yandex;
|
|
||||||
|
|
||||||
// Framework for ArangoDB
|
|
||||||
use mirzaev\arangodb\collection,
|
|
||||||
mirzaev\arangodb\document;
|
|
||||||
|
|
||||||
// Framework for Excel
|
|
||||||
use avadim\FastExcelReader\Excel as excel;
|
|
||||||
|
|
||||||
// Built-in libraries
|
|
||||||
use exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Model of the catalog
|
|
||||||
*
|
|
||||||
* @package mirzaev\arming_bot\models
|
|
||||||
*
|
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
|
||||||
*/
|
|
||||||
final class catalog extends core
|
|
||||||
{
|
|
||||||
use yandex, files {
|
|
||||||
yandex::download as yandex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect parameter from all products
|
|
||||||
*
|
|
||||||
* @param string $documment Path to the EXCEL-document
|
|
||||||
* @param int &$categories_loaded Counter of loaded categories
|
|
||||||
* @param int &$categories_created Counter of created categories
|
|
||||||
* @param int &$categories_updated Counter of updated categories
|
|
||||||
* @param int &$categories_deleted Counter of deleted categories
|
|
||||||
* @param int &$categories_new Counter of new categories
|
|
||||||
* @param int &$categories_old Counter of old categories
|
|
||||||
* @param int &$products_loaded Counter of loaded products
|
|
||||||
* @param int &$products_created Counter of created products
|
|
||||||
* @param int &$products_updated Counter of updated products
|
|
||||||
* @param int &$products_deleted Counter of deleted products
|
|
||||||
* @param int &$products_new Counter of new products
|
|
||||||
* @param int &$products_old Counter of old products
|
|
||||||
* @param string &$language Language (en, ru...)
|
|
||||||
* @param array &$errors Registry of errors
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*
|
|
||||||
* @todo
|
|
||||||
* 1. Сначала создать все категории и затем снова по циклу пройтись уже создавать entry между ними
|
|
||||||
* 2. Сжимать изображения
|
|
||||||
*/
|
|
||||||
public static function import(
|
|
||||||
string $document,
|
|
||||||
int &$categories_loaded = 0,
|
|
||||||
int &$categories_created = 0,
|
|
||||||
int &$categories_updated = 0,
|
|
||||||
int &$categories_deleted = 0,
|
|
||||||
int &$categories_old = 0,
|
|
||||||
int &$categories_new = 0,
|
|
||||||
int &$products_loaded = 0,
|
|
||||||
int &$products_created = 0,
|
|
||||||
int &$products_updated = 0,
|
|
||||||
int &$products_deleted = 0,
|
|
||||||
int &$products_old = 0,
|
|
||||||
int &$products_new = 0,
|
|
||||||
string $language = 'en',
|
|
||||||
array &$errors = []
|
|
||||||
): void {
|
|
||||||
try {
|
|
||||||
// Initializing the spreadsheet
|
|
||||||
$spreadsheet = excel::open($document);
|
|
||||||
|
|
||||||
// Inititalizing worksheets
|
|
||||||
$categories = $spreadsheet->getSheet('Категории');
|
|
||||||
$products = $spreadsheet->getSheet('Товары');
|
|
||||||
|
|
||||||
// Counting old documents
|
|
||||||
$categories_old = collection::count(category::COLLECTION, errors: $errors);
|
|
||||||
$products_old = collection::count(product::COLLECTION, errors: $errors);
|
|
||||||
|
|
||||||
// Initializing the buffer of handler categories and products
|
|
||||||
$handled = [
|
|
||||||
'categories' => [],
|
|
||||||
'products' => []
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach (
|
|
||||||
$categories->nextRow(
|
|
||||||
[
|
|
||||||
'A' => 'identifier',
|
|
||||||
'B' => 'name',
|
|
||||||
'C' => 'category',
|
|
||||||
'D' => 'images',
|
|
||||||
'E' => 'position'
|
|
||||||
],
|
|
||||||
excel::KEYS_FIRST_ROW
|
|
||||||
) as $number => $row
|
|
||||||
) {
|
|
||||||
// Iterate over categories
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!empty($row['identifier']) && !empty($row['name'])) {
|
|
||||||
// All required cells are filled in
|
|
||||||
|
|
||||||
// Incrementing the counter of loaded categories
|
|
||||||
++$categories_loaded;
|
|
||||||
|
|
||||||
// Declaring the variable with the status that a new category has been created
|
|
||||||
$created = false;
|
|
||||||
|
|
||||||
// Declaring the variable with the category
|
|
||||||
$category = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Initializing the category
|
|
||||||
$category = new category(document: category::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $row['identifier']], errors: $errors)[0] ?? null);
|
|
||||||
|
|
||||||
// Initializing name of the category
|
|
||||||
$category->name[$language] === $row['name'] || $category->name = [[$language => $row['name']]] + $category->name ?? [];
|
|
||||||
|
|
||||||
// Initializing position of the category
|
|
||||||
$category->position === $row['position'] || $category->position = $row['position'];
|
|
||||||
} catch (exception $e) {
|
|
||||||
// Not found the category
|
|
||||||
|
|
||||||
// Creating the category
|
|
||||||
$_id = category::write((int) $row['identifier'], [$language => $row['name']], $row['position'] ?? null, $errors);
|
|
||||||
|
|
||||||
// Initializing the category
|
|
||||||
$category = new category(document: $created = category::_read('d._id == @_id', parameters: ['_id' => $_id], errors: $errors)[0] ?? null);
|
|
||||||
|
|
||||||
// Incrementing the counter of created categories
|
|
||||||
if ($created) ++$categories_created;
|
|
||||||
};
|
|
||||||
|
|
||||||
if ($category instanceof category) {
|
|
||||||
// Found the category
|
|
||||||
|
|
||||||
if (!empty($row['category'])) {
|
|
||||||
// Received the ascendant category
|
|
||||||
|
|
||||||
// Initializing the ascendant category
|
|
||||||
$ascendant = new category(document: category::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $row['category']], errors: $errors)[0] ?? null);
|
|
||||||
|
|
||||||
if ($ascendant instanceof category) {
|
|
||||||
// Found the ascendant category
|
|
||||||
|
|
||||||
// Deleting entries of the category in ArangoDB
|
|
||||||
entry::banish($category, $errors);
|
|
||||||
|
|
||||||
// Writing the category as an entry to the ascendant category in ArangoDB
|
|
||||||
entry::write($category, $ascendant, $errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($row['images'])) {
|
|
||||||
// Received images
|
|
||||||
|
|
||||||
// Initializing new images of the category
|
|
||||||
$images = explode(' ', trim($row['images']));
|
|
||||||
|
|
||||||
// Reinitialize images? (true, if no images found or their amount does not match)
|
|
||||||
$reinitialize = !$category->images || count($category->images) !== count($images);
|
|
||||||
|
|
||||||
// Checking the identity of existing images with new images (if reinitialization of images has not yet been requested)
|
|
||||||
if (!$reinitialize) foreach ($category->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break;
|
|
||||||
|
|
||||||
if ($reinitialize) {
|
|
||||||
// Requested reinitialization of images
|
|
||||||
|
|
||||||
// Initializing the buffer of images
|
|
||||||
$buffer = [];
|
|
||||||
|
|
||||||
foreach ($images as $index => $image) {
|
|
||||||
// Iterating over new images
|
|
||||||
|
|
||||||
// Skipping empty URI`s
|
|
||||||
if (empty($image = trim($image))) continue;
|
|
||||||
|
|
||||||
// Initializing path to directory of the image in storage
|
|
||||||
$directory = DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $row['identifier'];
|
|
||||||
|
|
||||||
// Initializing URL of the image in storage
|
|
||||||
$url = STORAGE . $directory;
|
|
||||||
|
|
||||||
// Initializing URN of the image in storage
|
|
||||||
$urn = $index . '.jpg';
|
|
||||||
|
|
||||||
// Initializing URI of the image in storage
|
|
||||||
$uri = $url . DIRECTORY_SEPARATOR . $urn;
|
|
||||||
|
|
||||||
// Initializing the directory in storage
|
|
||||||
if (!file_exists($url)) mkdir($url, 0775, true);
|
|
||||||
|
|
||||||
if (static::yandex($image, $uri, errors: $errors)) {
|
|
||||||
// The image is downloaded
|
|
||||||
|
|
||||||
// Writing the image to the buffer if images
|
|
||||||
$buffer[] = [
|
|
||||||
'source' => $image,
|
|
||||||
'storage' => $directory . DIRECTORY_SEPARATOR . $urn
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializing images of the category
|
|
||||||
$category->images = $buffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writing in ArangoDB
|
|
||||||
$updated = document::update($category->__document(), errors: $errors);
|
|
||||||
|
|
||||||
// Incrementing the counter of updated categories
|
|
||||||
if ($updated && !$created) ++$categories_updated;
|
|
||||||
} else throw new exception("Failed to initialize category: {$row['name']} ($number)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writing to the registry of handled categories and products
|
|
||||||
$handled['categories'][] = $row['identifier'];
|
|
||||||
} catch (exception $e) {
|
|
||||||
// Writing to the registry of errors
|
|
||||||
$errors[] = [
|
|
||||||
'text' => $e->getMessage(),
|
|
||||||
'file' => $e->getFile(),
|
|
||||||
'line' => $e->getLine(),
|
|
||||||
'stack' => $e->getTrace()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (
|
|
||||||
$products->nextRow(
|
|
||||||
[
|
|
||||||
'A' => 'identifier',
|
|
||||||
'B' => 'name',
|
|
||||||
'C' => 'category',
|
|
||||||
'D' => 'description',
|
|
||||||
'E' => 'cost',
|
|
||||||
'F' => 'weight',
|
|
||||||
'G' => 'x',
|
|
||||||
'H' => 'y',
|
|
||||||
'I' => 'z',
|
|
||||||
'J' => 'brand',
|
|
||||||
'K' => 'compatibility',
|
|
||||||
'L' => 'images',
|
|
||||||
'M' => 'position'
|
|
||||||
],
|
|
||||||
excel::KEYS_FIRST_ROW
|
|
||||||
) as $number => $row
|
|
||||||
) {
|
|
||||||
// Iterate over products
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!empty($row['identifier']) && !empty($row['name'])) {
|
|
||||||
// All required cells are filled in
|
|
||||||
|
|
||||||
// Incrementing the counter of loaded products
|
|
||||||
++$products_loaded;
|
|
||||||
|
|
||||||
// Declaring the variable with the status that a new product has been created
|
|
||||||
$created = false;
|
|
||||||
|
|
||||||
// Declaring the variable with the product
|
|
||||||
$product = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Initializing the product
|
|
||||||
$product = new product(document: product::_read('d.identifier == %u', parameters: ['identifier' => (int) $row['identifier']], errors: $errors)[0] ?? null);
|
|
||||||
|
|
||||||
// Initializing name of the category
|
|
||||||
$product->name[$language] === $row['name'] || $product->name = [[$language => $row['name']]] + $product->name ?? [];
|
|
||||||
|
|
||||||
// Initializing position of the product
|
|
||||||
$product->position === $row['position'] || $product->position = $row['position'];
|
|
||||||
} catch (exception $e) {
|
|
||||||
// Not found the product
|
|
||||||
|
|
||||||
// Creating the product
|
|
||||||
$_id = product::write(
|
|
||||||
(int) $row['identifier'],
|
|
||||||
[$language => $row['name']],
|
|
||||||
[$language => $row['description']],
|
|
||||||
(float) $row['cost'],
|
|
||||||
(float) $row['weight'],
|
|
||||||
['x' => $row['x'], 'y' => $row['y'], 'z' => $row['z']],
|
|
||||||
$row['position'] ?? null,
|
|
||||||
errors: $errors
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initializing the product
|
|
||||||
$product = new product(document: $created = product::_read(sprintf('d._id == "%s"', $_id), errors: $errors)[0] ?? null);
|
|
||||||
|
|
||||||
// Incrementing the counter of created products
|
|
||||||
if ($created) ++$products_created;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($product instanceof product) {
|
|
||||||
// Found the product
|
|
||||||
|
|
||||||
if (!empty($row['category'])) {
|
|
||||||
// Received the category
|
|
||||||
|
|
||||||
// Initializing the category
|
|
||||||
$category = new category(document: category::_read(sprintf('d.identifier == %u', (int) $row['category']), errors: $errors)[0] ?? null);
|
|
||||||
|
|
||||||
if ($category instanceof category) {
|
|
||||||
// Found the ascendant category
|
|
||||||
|
|
||||||
// Deleting entries of the product in ArangoDB
|
|
||||||
entry::banish($product, $errors);
|
|
||||||
|
|
||||||
// Writing the product as an entry to the ascendant category in ArangoDB
|
|
||||||
entry::write($product, $category, $errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($row['images'])) {
|
|
||||||
// Received images
|
|
||||||
|
|
||||||
// Initializing new images of the category
|
|
||||||
$images = explode(' ', trim($row['images']));
|
|
||||||
|
|
||||||
// Reinitialize images? (true, if no images found or their amount does not match)
|
|
||||||
$reinitialize = !$product->images || count($product->images) !== count($images);
|
|
||||||
|
|
||||||
// Checking the identity of existing images with new images (if reinitialization of images has not yet been requested)
|
|
||||||
if (!$reinitialize) foreach ($product->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break;
|
|
||||||
|
|
||||||
if ($reinitialize) {
|
|
||||||
// Requested reinitialization of images
|
|
||||||
|
|
||||||
// Initializing the buffer of images
|
|
||||||
$buffer = [];
|
|
||||||
|
|
||||||
foreach ($images as $index => $image) {
|
|
||||||
// Iterating over new images
|
|
||||||
|
|
||||||
// Skipping empty URI`s
|
|
||||||
if (empty($image = trim($image))) continue;
|
|
||||||
|
|
||||||
// Initializing path to directory of the image in storage
|
|
||||||
$directory = DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $row['identifier'];
|
|
||||||
|
|
||||||
// Initializing URL of the image in storage
|
|
||||||
$url = STORAGE . $directory;
|
|
||||||
|
|
||||||
// Initializing URN of the image in storage
|
|
||||||
$urn = $index . '.jpg';
|
|
||||||
|
|
||||||
// Initializing URI of the image in storage
|
|
||||||
$uri = $url . DIRECTORY_SEPARATOR . $urn;
|
|
||||||
|
|
||||||
// Initializing the directory in storage
|
|
||||||
if (!file_exists($url)) mkdir($url, 0775, true);
|
|
||||||
|
|
||||||
if (static::yandex($image, $uri, errors: $errors)) {
|
|
||||||
// The image is downloaded
|
|
||||||
|
|
||||||
// Writing the image to the buffer if images
|
|
||||||
$buffer[] = [
|
|
||||||
'source' => $image,
|
|
||||||
'storage' => $directory . DIRECTORY_SEPARATOR . $urn
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializing images of the category
|
|
||||||
$product->images = $buffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writing in ArangoDB
|
|
||||||
$updated = document::update($product->__document(), errors: $errors);
|
|
||||||
|
|
||||||
// Incrementing the counter of updated categories
|
|
||||||
if ($updated && !$created) ++$products_updated;
|
|
||||||
} else throw new exception("Failed to initialize product: {$row['name']} ($number)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writing to the registry of handled categories and products
|
|
||||||
$handled['products'][] = $row['identifier'];
|
|
||||||
} catch (exception $e) {
|
|
||||||
// Writing to the registry of errors
|
|
||||||
$errors[] = [
|
|
||||||
'text' => $e->getMessage(),
|
|
||||||
'file' => $e->getFile(),
|
|
||||||
'line' => $e->getLine(),
|
|
||||||
'stack' => $e->getTrace()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deleting old categories
|
|
||||||
foreach (
|
|
||||||
category::_read(
|
|
||||||
/* filter: sprintf('%s - d.updated > 3600', time()), */
|
|
||||||
sort: 'd.updated DESC',
|
|
||||||
amount: 100000,
|
|
||||||
errors: $errors
|
|
||||||
) as $document
|
|
||||||
) {
|
|
||||||
// Iterating over categories
|
|
||||||
|
|
||||||
// Initializing the category
|
|
||||||
$category = new category(document: $document);
|
|
||||||
|
|
||||||
if (
|
|
||||||
$category instanceof category
|
|
||||||
&& array_search($category->identifier, $handled['categories']) === false
|
|
||||||
) {
|
|
||||||
// Not found identifier of the product in the buffer of handled categories and products
|
|
||||||
|
|
||||||
// Deleting images of the category from storage
|
|
||||||
static::delete(STORAGE . DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $category->identifier, errors: $errors);
|
|
||||||
|
|
||||||
// Deleting entries of the category in ArangoDB
|
|
||||||
entry::banish($category, errors: $errors);
|
|
||||||
|
|
||||||
// Deleting the category in ArangoDB
|
|
||||||
document::delete($category->__document(), errors: $errors);
|
|
||||||
|
|
||||||
// Incrementing the counter of deleted categories
|
|
||||||
++$categories_deleted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deleting old products
|
|
||||||
foreach (
|
|
||||||
product::_read(
|
|
||||||
/* filter: sprintf('%s - d.updated > 3600', time()), */
|
|
||||||
sort: 'd.updated DESC',
|
|
||||||
amount: 100000,
|
|
||||||
errors: $errors
|
|
||||||
) as $document
|
|
||||||
) {
|
|
||||||
// Iterating over products
|
|
||||||
|
|
||||||
// Initializing the category
|
|
||||||
$product = new product(document: $document);
|
|
||||||
|
|
||||||
if (
|
|
||||||
$product instanceof product
|
|
||||||
&& array_search($product->identifier, $handled['products']) === false
|
|
||||||
) {
|
|
||||||
// Not found identifier of the product in the buffer of handled categories and products
|
|
||||||
|
|
||||||
// Deleting images of the product from storage
|
|
||||||
static::delete(STORAGE . DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $product->identifier, errors: $errors);
|
|
||||||
|
|
||||||
// Deleting entries of the product in ArangoDB
|
|
||||||
entry::banish($product, errors: $errors);
|
|
||||||
|
|
||||||
// Deleting the product in ArangoDB
|
|
||||||
document::delete($product->__document(), errors: $errors);
|
|
||||||
|
|
||||||
// Incrementing the counter of deleted products
|
|
||||||
++$products_deleted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Counting new documents
|
|
||||||
$categories_new = collection::count(category::COLLECTION, errors: $errors);
|
|
||||||
$products_new = collection::count(product::COLLECTION, errors: $errors);
|
|
||||||
} catch (exception $e) {
|
|
||||||
// Writing to the registry of errors
|
|
||||||
$errors[] = [
|
|
||||||
'text' => $e->getMessage(),
|
|
||||||
'file' => $e->getFile(),
|
|
||||||
'line' => $e->getLine(),
|
|
||||||
'stack' => $e->getTrace()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models\enumerations;
|
|
||||||
|
|
||||||
enum session
|
|
||||||
{
|
|
||||||
case hash_only;
|
|
||||||
case hash_else_address;
|
|
||||||
}
|
|
|
@ -1,586 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models;
|
|
||||||
|
|
||||||
// Files of the project
|
|
||||||
use mirzaev\arming_bot\models\core,
|
|
||||||
mirzaev\arming_bot\controllers\core as controller,
|
|
||||||
mirzaev\arming_bot\models\catalog,
|
|
||||||
mirzaev\arming_bot\models\suspension,
|
|
||||||
mirzaev\arming_bot\models\account;
|
|
||||||
|
|
||||||
// Framework for Telegram
|
|
||||||
use Zanzara\Zanzara,
|
|
||||||
Zanzara\Context,
|
|
||||||
Zanzara\Telegram\Type\Input\InputFile,
|
|
||||||
Zanzara\Telegram\Type\File\Document as telegram_document,
|
|
||||||
Zanzara\Middleware\MiddlewareNode as Node;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Model of chat (telegram)
|
|
||||||
*
|
|
||||||
* @package mirzaev\arming_bot\models
|
|
||||||
*
|
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
|
||||||
*/
|
|
||||||
final class telegram extends core
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Экранирование символов для Markdown
|
|
||||||
*
|
|
||||||
* @param string $text Текст для экранирования
|
|
||||||
* @param array $exception Символы которые будут исключены из списка для экранирования
|
|
||||||
*
|
|
||||||
* @return string Экранированный текст
|
|
||||||
*/
|
|
||||||
public static function unmarkdown(string $text, array $exceptions = []): string
|
|
||||||
{
|
|
||||||
// Инициализация реестра символом для конвертации
|
|
||||||
$from = array_diff(
|
|
||||||
[
|
|
||||||
'#',
|
|
||||||
'*',
|
|
||||||
'_',
|
|
||||||
'=',
|
|
||||||
'.',
|
|
||||||
'[',
|
|
||||||
']',
|
|
||||||
'(',
|
|
||||||
')',
|
|
||||||
'-',
|
|
||||||
'>',
|
|
||||||
'<',
|
|
||||||
'!',
|
|
||||||
'`'
|
|
||||||
],
|
|
||||||
$exceptions
|
|
||||||
);
|
|
||||||
|
|
||||||
// Инициализация реестра целей для конвертации
|
|
||||||
$to = [];
|
|
||||||
foreach ($from as $symbol) $to[] = "\\$symbol";
|
|
||||||
|
|
||||||
// Конвертация и выход (успех)
|
|
||||||
return str_replace($from, $to, $text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Инициализация запчасти
|
|
||||||
*
|
|
||||||
* Проверяет существование запчасти
|
|
||||||
*
|
|
||||||
* @param string $spare Запчасть
|
|
||||||
*
|
|
||||||
* @return string|bool Запчасть, если найдена, иначе false
|
|
||||||
*/
|
|
||||||
public static function spares(string $spare): string|bool
|
|
||||||
{
|
|
||||||
// Поиск запчастей и выход (успех)
|
|
||||||
return match (mb_strtolower($spare)) {
|
|
||||||
'цевьё' => 'Цевьё',
|
|
||||||
default => false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Главное меню
|
|
||||||
*
|
|
||||||
* Команда: /start
|
|
||||||
*
|
|
||||||
* @param Context $ctx
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function menu(Context $ctx): void
|
|
||||||
{
|
|
||||||
// Инициализация клавиатуры
|
|
||||||
$keyboard = [
|
|
||||||
[
|
|
||||||
['text' => '🛒 Каталог', 'web_app' => ['url' => 'https://arming.dev.mirzaev.sexy']]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
['text' => '🏛️ О компании'],
|
|
||||||
['text' => '💬 Контакты']
|
|
||||||
],
|
|
||||||
[
|
|
||||||
['text' => '🎯 Сообщество']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($ctx->get('account')?->access['settings']) $keyboard[] = [['text' => '⚙️ Настройки']];
|
|
||||||
|
|
||||||
// Отправка сообщения
|
|
||||||
$ctx->sendMessage(
|
|
||||||
static::unmarkdown(<<<TXT
|
|
||||||
Это сообщение будет отображаться (оно должно быть обязательно) при вызове главного меню командой /start (создаёт кнопки меню снизу)
|
|
||||||
TXT),
|
|
||||||
[
|
|
||||||
'reply_markup' => [
|
|
||||||
'keyboard' => $keyboard,
|
|
||||||
'resize_keyboard' => true
|
|
||||||
],
|
|
||||||
'disable_notification' => true
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Начало работы с чат-роботом
|
|
||||||
*
|
|
||||||
* Команда: /start
|
|
||||||
*
|
|
||||||
* @param Context $ctx
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function start(Context $ctx): void
|
|
||||||
{
|
|
||||||
// Главное меню
|
|
||||||
static::menu($ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Контакты
|
|
||||||
*
|
|
||||||
* Команда: /contacts
|
|
||||||
*
|
|
||||||
* @param Context $ctx
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function contacts(Context $ctx): void
|
|
||||||
{
|
|
||||||
// Отправка сообщения
|
|
||||||
$ctx->sendMessage(static::unmarkdown(<<<TXT
|
|
||||||
Здесь придумать текст для раздела "Контакты"
|
|
||||||
TXT), [
|
|
||||||
'reply_markup' => [
|
|
||||||
'inline_keyboard' => [
|
|
||||||
[
|
|
||||||
['text' => '⚡ Связь с менеджером', 'url' => 'https://t.me/iarming'],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
['text' => '📨 Почта', 'callback_data' => 'mail']
|
|
||||||
],
|
|
||||||
[
|
|
||||||
['text' => '🪖 Сайт', 'url' => 'https://arming.ru'],
|
|
||||||
['text' => '🛒 Wildberries', 'url' => 'https://www.wildberries.ru/seller/137386'],
|
|
||||||
['text' => '🛒 Ozon', 'url' => 'https://www.ozon.ru/seller/arming-1086587/products/?miniapp=seller_1086587'],
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'link_preview_options' => [
|
|
||||||
'is_disabled' => true
|
|
||||||
],
|
|
||||||
'disable_notification' => true
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Почта
|
|
||||||
*
|
|
||||||
* @param Context $ctx
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function _mail(Context $ctx): void
|
|
||||||
{
|
|
||||||
// Отправка сообщения
|
|
||||||
$ctx->sendMessage(static::unmarkdown(<<<TXT
|
|
||||||
[info@arming.ru](mailto::info@arming.ru)
|
|
||||||
TXT, ['[', ']', '(', ')']), [
|
|
||||||
'link_preview_options' => [
|
|
||||||
'is_disabled' => true
|
|
||||||
],
|
|
||||||
'disable_notification' => true
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Компания
|
|
||||||
*
|
|
||||||
* Команда: /company
|
|
||||||
*
|
|
||||||
* @param Context $ctx
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function company(Context $ctx): void
|
|
||||||
{
|
|
||||||
// Отправка сообщения
|
|
||||||
$ctx->sendMessage(
|
|
||||||
static::unmarkdown(<<<TXT
|
|
||||||
Здесь придумать текст для раздела "Компания"
|
|
||||||
TXT),
|
|
||||||
/* [
|
|
||||||
'reply_markup' => [
|
|
||||||
'inline_keyboard' => [
|
|
||||||
[
|
|
||||||
['text' => '⚡ Связь с менеджером', 'url' => 'https://git.mirzaev.sexy/mirzaev/mashtrash'],
|
|
||||||
['text' => '📨 Почта', 'text' => ''],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
['text' => '🪖 Сайт', 'url' => '']
|
|
||||||
['text' => '🛒 Wildberries', 'url' => '']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'link_preview_options' => [
|
|
||||||
'is_disabled' => true
|
|
||||||
]
|
|
||||||
] */
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Сообщество
|
|
||||||
*
|
|
||||||
* Команда: /community
|
|
||||||
*
|
|
||||||
* @param Context $ctx
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function community(Context $ctx): void
|
|
||||||
{
|
|
||||||
// Отправка сообщения
|
|
||||||
$ctx->sendMessage(static::unmarkdown(<<<TXT
|
|
||||||
Здесь придумать текст для раздела "Сообщество"
|
|
||||||
TXT), [
|
|
||||||
'reply_markup' => [
|
|
||||||
'inline_keyboard' => [
|
|
||||||
[
|
|
||||||
['text' => '💬 Основной чат', 'url' => 'https://t.me/arming_zone'],
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'link_preview_options' => [
|
|
||||||
'is_disabled' => true
|
|
||||||
],
|
|
||||||
'disable_notification' => true
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Настройки (доступ только авторизованным)
|
|
||||||
*
|
|
||||||
* Команда: /settings
|
|
||||||
*
|
|
||||||
* @param Context $ctx
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function settings(Context $ctx): void
|
|
||||||
{
|
|
||||||
if ($ctx->get('account')?->access['settings']) {
|
|
||||||
// Авторизован доступ к настройкам
|
|
||||||
|
|
||||||
// Отправка сообщения
|
|
||||||
$ctx->sendMessage(
|
|
||||||
static::unmarkdown(<<<TXT
|
|
||||||
Панель управления чат-роботом ARMING
|
|
||||||
TXT),
|
|
||||||
[
|
|
||||||
'reply_markup' => [
|
|
||||||
'inline_keyboard' => [
|
|
||||||
[
|
|
||||||
['text' => '📦 Импорт товаров', 'callback_data' => 'import_request'],
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'link_preview_options' => [
|
|
||||||
'is_disabled' => true
|
|
||||||
],
|
|
||||||
'disable_notification' => true
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Не авторизован доступ к настройкам
|
|
||||||
|
|
||||||
// Отправка сообщения
|
|
||||||
$ctx->sendMessage('⛔ *Нет доступа*');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запросить файл для импорта товаров (доступ только авторизованным)
|
|
||||||
*
|
|
||||||
* @param Context $ctx
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function import_request(Context $ctx): void
|
|
||||||
{
|
|
||||||
if ($ctx->get('account')?->access['settings']) {
|
|
||||||
// Авторизован доступ к настройкам
|
|
||||||
|
|
||||||
// Отправка сообщения
|
|
||||||
$ctx->sendMessage(static::unmarkdown('Отправьте документ в формате xlsx со списком товаров'))
|
|
||||||
->then(function ($message) use ($ctx) {
|
|
||||||
// Отправка файла
|
|
||||||
$ctx->sendDocument(new InputFile(CATALOG_EXAMPLE), ['disable_notification' => true]);
|
|
||||||
|
|
||||||
// Импорт файла
|
|
||||||
$ctx->nextStep([static::class, 'import'], true);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Не авторизован доступ к настройкам
|
|
||||||
|
|
||||||
// Отправка сообщения
|
|
||||||
$ctx->sendMessage('⛔ *Нет доступа*');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Импорт товаров (доступ только авторизованным)
|
|
||||||
*
|
|
||||||
* @param Context $ctx
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function import(Context $ctx): void
|
|
||||||
{
|
|
||||||
if (($account = $ctx->get('account'))?->access['settings']) {
|
|
||||||
// Авторизован доступ к настройкам
|
|
||||||
|
|
||||||
// Инициализация документа
|
|
||||||
$document = $ctx->getMessage()?->getDocument();
|
|
||||||
|
|
||||||
if ($document instanceof telegram_document) {
|
|
||||||
// Инициализирован документ
|
|
||||||
|
|
||||||
// Инициализация файла
|
|
||||||
$ctx->getFile($document->getFileId())->then(function ($file) use ($ctx, $document, $account) {
|
|
||||||
|
|
||||||
if ($file->getFileSize() <= 50000000) {
|
|
||||||
// Не превышает 50 мегабайт (50 000 000 байт) размер файла
|
|
||||||
|
|
||||||
if (pathinfo(parse_url($file->getFilePath())['path'], PATHINFO_EXTENSION) === 'xlsx') {
|
|
||||||
// Имеет расширение xlsx файл
|
|
||||||
|
|
||||||
// Initializing the directory in the storage
|
|
||||||
if (!file_exists($storage = STORAGE . DIRECTORY_SEPARATOR . 'import' . DIRECTORY_SEPARATOR . $account->getKey() . DIRECTORY_SEPARATOR . time()))
|
|
||||||
mkdir($storage, 0775, true);
|
|
||||||
|
|
||||||
// Сохранение файла
|
|
||||||
file_put_contents(
|
|
||||||
$import = $storage . DIRECTORY_SEPARATOR . 'import.xlsx',
|
|
||||||
file_get_contents('https://api.telegram.org/file/bot' . KEY . '/' . parse_url($file->getFilePath())['path'])
|
|
||||||
);
|
|
||||||
|
|
||||||
// Отправка сообщения
|
|
||||||
$ctx->sendMessage(sprintf(
|
|
||||||
<<<'TXT'
|
|
||||||
🔬 *Выполняется анализ:* %s \(%s байт\)
|
|
||||||
TXT,
|
|
||||||
static::unmarkdown($document->getFileName()),
|
|
||||||
static::unmarkdown((string) $file->getFileSize())
|
|
||||||
))
|
|
||||||
->then(function ($message) use ($ctx, $import) {
|
|
||||||
// Инициализация счётчика загруженных товаров
|
|
||||||
$categories_loaded
|
|
||||||
= $products_loaded
|
|
||||||
= $categories_created
|
|
||||||
= $products_created
|
|
||||||
= $categories_updated
|
|
||||||
= $products_updated
|
|
||||||
= $categories_deleted
|
|
||||||
= $products_deleted
|
|
||||||
= $categories_old
|
|
||||||
= $products_old
|
|
||||||
= $categories_new
|
|
||||||
= $products_new
|
|
||||||
= 0;
|
|
||||||
|
|
||||||
// Import
|
|
||||||
catalog::import(
|
|
||||||
$import,
|
|
||||||
$categories_loaded,
|
|
||||||
$categories_created,
|
|
||||||
$categories_updated,
|
|
||||||
$categories_deleted,
|
|
||||||
$categories_old,
|
|
||||||
$categories_new,
|
|
||||||
$products_loaded,
|
|
||||||
$products_created,
|
|
||||||
$products_updated,
|
|
||||||
$products_deleted,
|
|
||||||
$products_old,
|
|
||||||
$products_new,
|
|
||||||
language: 'ru'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Отправка сообщения
|
|
||||||
$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::initialization($telegram->getId(), $telegram);
|
|
||||||
|
|
||||||
if ($account) {
|
|
||||||
// Инициализирован аккаунт
|
|
||||||
|
|
||||||
if ($account->banned) {
|
|
||||||
// Заблокирован аккаунт
|
|
||||||
|
|
||||||
// Отправка сообщения
|
|
||||||
$ctx->sendMessage('⛔ *Ты заблокирован*')
|
|
||||||
->then(function ($message) use ($ctx) {
|
|
||||||
// Завершение диалога
|
|
||||||
$ctx->endConversation();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Блокировка дальнейшего выполнения
|
|
||||||
$ctx->set('stop', true);
|
|
||||||
} else {
|
|
||||||
// Не заблокирован аккаунт
|
|
||||||
|
|
||||||
// Запись в буфер
|
|
||||||
$ctx->set('account', $account);
|
|
||||||
|
|
||||||
// Продолжение выполнения
|
|
||||||
$next($ctx);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Не инициализирован аккаунт
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Инициализация статуса технических работ (middleware)
|
|
||||||
*
|
|
||||||
* @param Context $ctx
|
|
||||||
* @param Node $next
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function suspension(Context $ctx, Node $next): void
|
|
||||||
{
|
|
||||||
// Выполнение заблокировано?
|
|
||||||
if ($ctx->get('stop')) return;
|
|
||||||
|
|
||||||
// Поиск технических работ
|
|
||||||
$suspension = suspension::search();
|
|
||||||
|
|
||||||
if ($suspension && $suspension->targets['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 ?? controller::$settings?->language);
|
|
||||||
|
|
||||||
// Добавление описания причины приостановки, если найдена
|
|
||||||
if (!empty($suspension->description))
|
|
||||||
$message .= "\n\n" . $suspension->description[$account->language ?? controller::$settings?->language] ?? array_values($suspension->description)[0];
|
|
||||||
|
|
||||||
// Отправка сообщения
|
|
||||||
$ctx->sendMessage($message)
|
|
||||||
->then(function ($message) use ($ctx) {
|
|
||||||
// Завершение диалога
|
|
||||||
$ctx->endConversation();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Блокировка дальнейшего выполнения
|
|
||||||
$ctx->set('stop', true);
|
|
||||||
} else {
|
|
||||||
// Не найдена активная приостановка
|
|
||||||
|
|
||||||
// Продолжение выполнения
|
|
||||||
$next($ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models\traits\yandex;
|
|
||||||
|
|
||||||
// Built-in libraries
|
|
||||||
use exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trait for "Yandex Disk"
|
|
||||||
*
|
|
||||||
* @package mirzaev\arming_bot\models\traits\yandex
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
|
||||||
*/
|
|
||||||
trait disk
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Download file from "Yandex Disk"
|
|
||||||
*
|
|
||||||
* @param string $uri URI of the file from "Yandex Disk"
|
|
||||||
* @param string $destination Destination to write the file
|
|
||||||
* @param array &$errors Registry of errors
|
|
||||||
*
|
|
||||||
* @return bool The file is downloaded?
|
|
||||||
*/
|
|
||||||
private static function download(
|
|
||||||
string $uri,
|
|
||||||
string $destination,
|
|
||||||
array &$errors = []
|
|
||||||
): bool {
|
|
||||||
try {
|
|
||||||
if (!empty($uri)) {
|
|
||||||
// Not empty URI
|
|
||||||
|
|
||||||
if (!empty($destination)) {
|
|
||||||
// Not empty destination
|
|
||||||
|
|
||||||
// Initializing URL of the file
|
|
||||||
$url = "https://cloud-api.yandex.net/v1/disk/public/resources/download?public_key=$uri";
|
|
||||||
|
|
||||||
// Checking if the file is available for download
|
|
||||||
$ch = curl_init($url);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_exec($ch);
|
|
||||||
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($code === 200) {
|
|
||||||
// The file is available for download
|
|
||||||
|
|
||||||
// Downloading the file and exit (success)
|
|
||||||
return file_put_contents($destination, file_get_contents(json_decode(file_get_contents($url))?->href)) > 0;
|
|
||||||
} else throw new exception("File not available for download: $uri");
|
|
||||||
} else throw new exception("Empty destination");
|
|
||||||
} else throw new exception("Empty URI");
|
|
||||||
} catch (exception $e) {
|
|
||||||
// Writing to the registry of errors
|
|
||||||
$errors[] = [
|
|
||||||
'text' => $e->getMessage(),
|
|
||||||
'file' => $e->getFile(),
|
|
||||||
'line' => $e->getLine(),
|
|
||||||
'stack' => $e->getTrace()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit (fail)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace mirzaev\arming_bot;
|
|
||||||
|
|
||||||
// Files of the project
|
|
||||||
use mirzaev\arming_bot\controllers\core as controller,
|
|
||||||
mirzaev\arming_bot\models\core as model;
|
|
||||||
|
|
||||||
// Framework for PHP
|
|
||||||
use mirzaev\minimal\core,
|
|
||||||
mirzaev\minimal\router;
|
|
||||||
|
|
||||||
ini_set('error_reporting', E_ALL);
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
|
|
||||||
// Версия робота
|
|
||||||
define('ROBOT_VERSION', '1.0.0');
|
|
||||||
define('VIEWS', realpath('..' . DIRECTORY_SEPARATOR . 'views'));
|
|
||||||
define('STORAGE', realpath('..' . DIRECTORY_SEPARATOR . 'storage'));
|
|
||||||
define('SETTINGS', realpath('..' . DIRECTORY_SEPARATOR . 'settings'));
|
|
||||||
define('INDEX', __DIR__);
|
|
||||||
define('THEME', 'default');
|
|
||||||
|
|
||||||
// Инициализация библиотек
|
|
||||||
require __DIR__ . DIRECTORY_SEPARATOR
|
|
||||||
. '..' . DIRECTORY_SEPARATOR
|
|
||||||
. '..' . DIRECTORY_SEPARATOR
|
|
||||||
. '..' . DIRECTORY_SEPARATOR
|
|
||||||
. '..' . DIRECTORY_SEPARATOR
|
|
||||||
. 'vendor' . DIRECTORY_SEPARATOR
|
|
||||||
. 'autoload.php';
|
|
||||||
|
|
||||||
// Инициализация маршрутизатора
|
|
||||||
$router = new router;
|
|
||||||
|
|
||||||
// Initializing of routes
|
|
||||||
$router
|
|
||||||
->write('/', 'catalog', 'index', 'GET')
|
|
||||||
->write('/search', 'catalog', 'search', 'POST')
|
|
||||||
->write('/session/connect/telegram', 'session', 'telegram', 'POST')
|
|
||||||
->write('/product/$id', 'catalog', 'product', 'POST')
|
|
||||||
->write('/$categories...', 'catalog', 'index', 'POST');
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
// Initializing of routes
|
|
||||||
$router
|
|
||||||
->write('/', 'catalog', 'index', 'GET')
|
|
||||||
->write('/$sex', 'catalog', 'search', 'POST')
|
|
||||||
->write('/$search', 'catalog', 'search', 'POST')
|
|
||||||
->write('/search', 'catalog', 'search', 'POST')
|
|
||||||
->write('/search/$asdasdasd', 'catalog', 'search', 'POST')
|
|
||||||
->write('/ebala/$sex/$categories...', 'catalog', 'index', 'POST')
|
|
||||||
->write('/$sex/$categories...', 'catalog', 'index', 'POST')
|
|
||||||
->write('/$categories...', 'catalog', 'index', 'POST')
|
|
||||||
->write('/ebala/$categories...', 'catalog', 'index', 'POST');
|
|
||||||
|
|
||||||
var_dump($router->routes);
|
|
||||||
echo "\n\n\n\n\n\n";
|
|
||||||
$router
|
|
||||||
->sort();
|
|
||||||
var_dump($router->routes); */
|
|
||||||
|
|
||||||
// Инициализация ядра
|
|
||||||
$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));
|
|
||||||
|
|
||||||
// Обработка запроса
|
|
||||||
echo $core->start();
|
|
|
@ -1,65 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Import dependencies
|
|
||||||
import("/js/core.js").then(() =>
|
|
||||||
import("/js/damper.js").then(() => {
|
|
||||||
import("/js/telegram.js").then(() => {
|
|
||||||
const dependencies = setInterval(() => {
|
|
||||||
if (
|
|
||||||
typeof core === "function" &&
|
|
||||||
typeof core.damper === "function" &&
|
|
||||||
typeof core.telegram === "function"
|
|
||||||
) {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
initialization();
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
initialization();
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
function initialization() {
|
|
||||||
const timer_for_response = setTimeout(() => {
|
|
||||||
core.loading.setAttribute("disabled", true);
|
|
||||||
|
|
||||||
const p = document.createElement("p");
|
|
||||||
p.innerText = "Not authenticated";
|
|
||||||
|
|
||||||
core.footer.appendChild(p);
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
core.request(
|
|
||||||
"/session/connect/telegram",
|
|
||||||
"authentication=telegram&" + core.telegram.api.initData,
|
|
||||||
)
|
|
||||||
.then((json) => {
|
|
||||||
if (
|
|
||||||
json.errors !== null &&
|
|
||||||
typeof json.errors === "object" &&
|
|
||||||
json.errors.length > 0
|
|
||||||
) {
|
|
||||||
// Errors received
|
|
||||||
} else {
|
|
||||||
// Errors not received
|
|
||||||
|
|
||||||
if (json.connected === true) {
|
|
||||||
core.loading.setAttribute("disabled", true);
|
|
||||||
|
|
||||||
clearTimeout(timer_for_response);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
json.language !== null &&
|
|
||||||
typeof json.language === "string" &&
|
|
||||||
json.langiage.length === 2
|
|
||||||
) {
|
|
||||||
core.language = json.language;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
|
@ -1,35 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Import dependencies
|
|
||||||
import("/js/core.js").then(() =>
|
|
||||||
import("/js/damper.js").then(() => {
|
|
||||||
const dependencies = setInterval(() => {
|
|
||||||
if (
|
|
||||||
typeof core === "function" &&
|
|
||||||
typeof core.damper === "function"
|
|
||||||
) {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
initialization();
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
initialization();
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
function initialization() {
|
|
||||||
if (typeof core.cart === "undefined") {
|
|
||||||
// Not initialized
|
|
||||||
|
|
||||||
// Write to the core
|
|
||||||
core.cart = class cart {
|
|
||||||
/**
|
|
||||||
* Products in cart ["product/148181", "product/148181", "product/148181"...]
|
|
||||||
*/
|
|
||||||
static cart = [];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
|
@ -1,643 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Import dependencies
|
|
||||||
import("/js/core.js").then(() =>
|
|
||||||
import("/js/damper.js").then(() => {
|
|
||||||
import("/js/telegram.js").then(() => {
|
|
||||||
import("/js/hotline.js").then(() => {
|
|
||||||
const dependencies = setInterval(() => {
|
|
||||||
console.log(
|
|
||||||
typeof core,
|
|
||||||
typeof core.damper,
|
|
||||||
typeof core.telegram,
|
|
||||||
typeof core.hotline,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
typeof core === "function" &&
|
|
||||||
typeof core.damper === "function" &&
|
|
||||||
typeof core.telegram === "function" &&
|
|
||||||
typeof core.hotline === "function"
|
|
||||||
) {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
initialization();
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
initialization();
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
function initialization() {
|
|
||||||
if (typeof core.catalog === "undefined") {
|
|
||||||
// Not initialized
|
|
||||||
|
|
||||||
// Write to the core
|
|
||||||
core.catalog = class catalog {
|
|
||||||
/**
|
|
||||||
* Current position in hierarchy of the categories
|
|
||||||
*/
|
|
||||||
static categories = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registry of filters (instead of cookies)
|
|
||||||
*/
|
|
||||||
static filters = new Map([
|
|
||||||
['brand', null]
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a category (interface)
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} button Button of category <a>
|
|
||||||
* @param {bool} clean Clear search bar?
|
|
||||||
* @param {bool} force Ignore the damper?
|
|
||||||
*
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
static category(button, clean = true, force = false) {
|
|
||||||
// Initialize of the new category name
|
|
||||||
const category = button.getAttribute("data-category-name");
|
|
||||||
|
|
||||||
this._category(category, clean, force);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a category (damper)
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} button Button of category <a>
|
|
||||||
* @param {bool} clean Clear search bar?
|
|
||||||
* @param {bool} force Ignore the damper?
|
|
||||||
*
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
static _category = core.damper(
|
|
||||||
(...variables) => this.__category(...variables),
|
|
||||||
400,
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a category (system)
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} button Button of category <a>
|
|
||||||
* @param {bool} clean Clear search bar?
|
|
||||||
*
|
|
||||||
* @return {Promise} Request to the server
|
|
||||||
*/
|
|
||||||
static __category(category = "", clean = true) {
|
|
||||||
if (typeof category === "string") {
|
|
||||||
//
|
|
||||||
|
|
||||||
let urn;
|
|
||||||
if (category === "/" || category === "") urn = "/";
|
|
||||||
else {urn = this.categories.length > 0
|
|
||||||
? `/${this.categories.join("/")}/${category}`
|
|
||||||
: `/${category}`;}
|
|
||||||
|
|
||||||
return core.request(urn)
|
|
||||||
.then((json) => {
|
|
||||||
if (
|
|
||||||
json.errors !== null &&
|
|
||||||
typeof json.errors === "object" &&
|
|
||||||
json.errors.length > 0
|
|
||||||
) {
|
|
||||||
// Errors received
|
|
||||||
} else {
|
|
||||||
// Errors not received
|
|
||||||
|
|
||||||
if (clean) {
|
|
||||||
// Clearing the search bar
|
|
||||||
const search = core.main.querySelector(
|
|
||||||
'search[data-section="search"]>input',
|
|
||||||
);
|
|
||||||
if (search instanceof HTMLElement) search.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the category to position in the categories hierarchy
|
|
||||||
if (category !== "/" && category !== "") {
|
|
||||||
this.categories.push(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof json.title === "string" &&
|
|
||||||
json.title.length > 0
|
|
||||||
) {
|
|
||||||
// Received the page title
|
|
||||||
|
|
||||||
// Initialize a link to the categories list
|
|
||||||
const title = core.main.getElementsByTagName("h2")[0];
|
|
||||||
|
|
||||||
// Write the title
|
|
||||||
title.innerText = json.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof json.html.categories === "string" &&
|
|
||||||
json.html.categories.length > 0
|
|
||||||
) {
|
|
||||||
// Received categories (reinitialization of the categories)
|
|
||||||
|
|
||||||
const categories = core.main.querySelector(
|
|
||||||
'section[data-catalog-type="categories"]',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (categories instanceof HTMLElement) {
|
|
||||||
// Found list of categories
|
|
||||||
|
|
||||||
categories.outerHTML = json.html.categories;
|
|
||||||
} else {
|
|
||||||
// Not found list of categories
|
|
||||||
|
|
||||||
const element = document.createElement("section");
|
|
||||||
|
|
||||||
const search = core.main.querySelector(
|
|
||||||
'search[data-section="search"]',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (search instanceof HTMLElement) {
|
|
||||||
core.main.insertBefore(
|
|
||||||
element,
|
|
||||||
search.nextSibling,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.outerHTML = json.html.categories;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Not received categories (deinitialization of the categories)
|
|
||||||
|
|
||||||
const categories = core.main.querySelector(
|
|
||||||
'section[data-catalog-type="categories"',
|
|
||||||
);
|
|
||||||
if (categories instanceof HTMLElement) {
|
|
||||||
categories.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof json.html.products === "string" &&
|
|
||||||
json.html.products.length > 0
|
|
||||||
) {
|
|
||||||
// Received products (reinitialization of the products)
|
|
||||||
|
|
||||||
const products = core.main.querySelector(
|
|
||||||
'section[data-catalog-type="products"]',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (products instanceof HTMLElement) {
|
|
||||||
// Found list of products
|
|
||||||
|
|
||||||
products.outerHTML = json.html.products;
|
|
||||||
} else {
|
|
||||||
// Not found list of products
|
|
||||||
|
|
||||||
const element = document.createElement("section");
|
|
||||||
|
|
||||||
const categories = core.main.querySelector(
|
|
||||||
'section[data-catalog-type="categories"]',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (categories instanceof HTMLElement) {
|
|
||||||
core.main.insertBefore(
|
|
||||||
element,
|
|
||||||
categories.nextSibling,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.outerHTML = json.html.products;
|
|
||||||
} else {
|
|
||||||
const search = core.main.querySelector(
|
|
||||||
'search[data-section="search"]',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (search instanceof HTMLElement) {
|
|
||||||
core.main.insertBefore(
|
|
||||||
element,
|
|
||||||
search.nextSibling,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.outerHTML = json.html.products;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Not received products (deinitialization of the products)
|
|
||||||
|
|
||||||
const products = core.main.querySelector(
|
|
||||||
'section[data-catalog-type="products"',
|
|
||||||
);
|
|
||||||
if (products instanceof HTMLElement) {
|
|
||||||
products
|
|
||||||
.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a category (interface)
|
|
||||||
*
|
|
||||||
* @param {Event} event Event (keyup)
|
|
||||||
* @param {HTMLElement} element Search bar <input>
|
|
||||||
* @param {bool} force Ignore the damper?
|
|
||||||
*
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
static search(event, element, force = false) {
|
|
||||||
element.classList.remove("error");
|
|
||||||
|
|
||||||
if (element.innerText.length === 1) {
|
|
||||||
return;
|
|
||||||
} else if (event.keyCode === 13) {
|
|
||||||
// Button: "enter"
|
|
||||||
|
|
||||||
element.setAttribute("disabled", true);
|
|
||||||
|
|
||||||
this.__search(element);
|
|
||||||
} else {
|
|
||||||
// Button: any
|
|
||||||
|
|
||||||
this._search(element, force);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search in the catalog (damper)
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} button Button of category <a>
|
|
||||||
* @param {bool} clean Clear search bar?
|
|
||||||
* @param {bool} force Ignore the damper?
|
|
||||||
*
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
static _search = core.damper(
|
|
||||||
(...variables) => this.__search(...variables),
|
|
||||||
1400,
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search in the catalog (system)
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} element Search bar <input>
|
|
||||||
*
|
|
||||||
* @return {Promise} Request to the server
|
|
||||||
*
|
|
||||||
* @todo add animations of errors
|
|
||||||
*/
|
|
||||||
static __search(element) {
|
|
||||||
// Deinitialization of position in the categories hierarchy
|
|
||||||
this.categories = [];
|
|
||||||
|
|
||||||
return this.__category("/", false)
|
|
||||||
.then(function () {
|
|
||||||
core.request("/search", `text=${element.value}`)
|
|
||||||
.then((json) => {
|
|
||||||
element.removeAttribute("disabled");
|
|
||||||
element.focus();
|
|
||||||
|
|
||||||
if (
|
|
||||||
json.errors !== null &&
|
|
||||||
typeof json.errors === "object" &&
|
|
||||||
json.errors.length > 0
|
|
||||||
) {
|
|
||||||
// Errors received
|
|
||||||
|
|
||||||
element.classList.add("error");
|
|
||||||
} else {
|
|
||||||
// Errors not received
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof json.title === "string" &&
|
|
||||||
json.title.length > 0
|
|
||||||
) {
|
|
||||||
// Received the page title
|
|
||||||
|
|
||||||
// Initialize a link to the categories list
|
|
||||||
const title =
|
|
||||||
core.main.getElementsByTagName("h2")[0];
|
|
||||||
|
|
||||||
// Write the title
|
|
||||||
title.innerText = json.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deinitialization of the categories
|
|
||||||
const categories = core.main.querySelector(
|
|
||||||
'section[data-catalog-type="categories"]',
|
|
||||||
);
|
|
||||||
// if (categories instanceof HTMLElement) categories.remove();
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof json.html.products === "string" &&
|
|
||||||
json.html.products.length > 0
|
|
||||||
) {
|
|
||||||
// Received products (reinitialization of the products)
|
|
||||||
|
|
||||||
const products = core.main.querySelector(
|
|
||||||
'section[data-catalog-type="products"]',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (products instanceof HTMLElement) {
|
|
||||||
// Found list of products
|
|
||||||
|
|
||||||
products.outerHTML = json.html.products;
|
|
||||||
} else {
|
|
||||||
// Not found list of products
|
|
||||||
|
|
||||||
const element = document.createElement("section");
|
|
||||||
|
|
||||||
const categories = core.main.querySelector(
|
|
||||||
'section[data-catalog-type="categories"',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (categories instanceof HTMLElement) {
|
|
||||||
core.main.insertBefore(
|
|
||||||
element,
|
|
||||||
categories.nextSibling,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.outerHTML = json.html.products;
|
|
||||||
} else {
|
|
||||||
const search = core.main.querySelector(
|
|
||||||
'search[data-section="search"]',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (search instanceof HTMLElement) {
|
|
||||||
core.main.insertBefore(
|
|
||||||
element,
|
|
||||||
search.nextSibling,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.outerHTML = json.html.products;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Not received products (deinitialization of the products)
|
|
||||||
|
|
||||||
const products = core.main.querySelector(
|
|
||||||
'section[data-catalog-type="products"]',
|
|
||||||
);
|
|
||||||
if (products instanceof HTMLElement) {
|
|
||||||
products.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open product card (interface)
|
|
||||||
*
|
|
||||||
* @param {string} id Identifier of a product
|
|
||||||
* @param {bool} force Ignore the damper?
|
|
||||||
*
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
static product(id, force = false) {
|
|
||||||
this._product(id, force);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open product card (damper)
|
|
||||||
*
|
|
||||||
* @param {string} id Identifier of a product
|
|
||||||
* @param {bool} force Ignore the damper?
|
|
||||||
*
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
static _product = core.damper(
|
|
||||||
(...variables) => this.__product(...variables),
|
|
||||||
400,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open product card (system)
|
|
||||||
*
|
|
||||||
* @param {string} id Identifier of a product
|
|
||||||
*
|
|
||||||
* @return {Promise} Request to the server
|
|
||||||
*/
|
|
||||||
static __product(id) {
|
|
||||||
if (typeof id === "number") {
|
|
||||||
//
|
|
||||||
|
|
||||||
return core.request(`/product/${id}`)
|
|
||||||
.then((json) => {
|
|
||||||
if (
|
|
||||||
json.errors !== null &&
|
|
||||||
typeof json.errors === "object" &&
|
|
||||||
json.errors.length > 0
|
|
||||||
) {
|
|
||||||
// Errors received
|
|
||||||
} else {
|
|
||||||
// Errors not received
|
|
||||||
|
|
||||||
if (
|
|
||||||
json.product !== null &&
|
|
||||||
typeof json.product === "object"
|
|
||||||
) {
|
|
||||||
// Received data of the product
|
|
||||||
|
|
||||||
// Deinitializing of the old winow
|
|
||||||
const old = document.getElementById("window");
|
|
||||||
if (old instanceof HTMLElement) old.remove();
|
|
||||||
|
|
||||||
const wrap = document.createElement("section");
|
|
||||||
wrap.setAttribute("id", "window");
|
|
||||||
|
|
||||||
const card = document.createElement("div");
|
|
||||||
// card.classList.add("product", "card");
|
|
||||||
card.classList.add("card", "unselectable");
|
|
||||||
|
|
||||||
const h3 = document.createElement("h3");
|
|
||||||
h3.setAttribute("title", json.product.id);
|
|
||||||
|
|
||||||
const title = document.createElement("span");
|
|
||||||
title.classList.add("title");
|
|
||||||
title.innerText = json.product.title;
|
|
||||||
|
|
||||||
const brand = document.createElement("small");
|
|
||||||
brand.classList.add("brand");
|
|
||||||
brand.innerText = json.product.brand;
|
|
||||||
|
|
||||||
const images = document.createElement("div");
|
|
||||||
images.classList.add("images", "unselectable");
|
|
||||||
|
|
||||||
const button = core.telegram.api.isVisible;
|
|
||||||
|
|
||||||
const _open = (event) => {
|
|
||||||
if (event.target === from) {
|
|
||||||
if (typeof images.hotline === "object") {
|
|
||||||
if (images.hotline.moving) return;
|
|
||||||
images.hotline.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
images.classList.add("extend");
|
|
||||||
|
|
||||||
if (button) core.telegram.api.MainButton.hide();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
images.addEventListener("click", _close);
|
|
||||||
images.addEventListener("touch", _close);
|
|
||||||
}, 300);
|
|
||||||
images.removeEventListener("mouseup", _open);
|
|
||||||
images.removeEventListener("touchend", _open);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const _close = () => {
|
|
||||||
if (typeof images.hotline === "object") {
|
|
||||||
images.hotline.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
images.classList.remove("extend");
|
|
||||||
|
|
||||||
if (button) core.telegram.api.MainButton.show();
|
|
||||||
|
|
||||||
images.removeEventListener("click", _close);
|
|
||||||
images.removeEventListener("touch", _close);
|
|
||||||
images.addEventListener("mousedown", _start);
|
|
||||||
images.addEventListener("touchstart", _start);
|
|
||||||
};
|
|
||||||
|
|
||||||
const _start = (event) => {
|
|
||||||
if (
|
|
||||||
event.type === "touchstart" ||
|
|
||||||
event.button === 0
|
|
||||||
) {
|
|
||||||
images.removeEventListener("mousedown", _start);
|
|
||||||
images.removeEventListener("touchstart", _start);
|
|
||||||
images.addEventListener("mouseup", _open);
|
|
||||||
images.addEventListener("touchend", _open);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
images.addEventListener("mousedown", _start);
|
|
||||||
images.addEventListener("touchstart", _start);
|
|
||||||
|
|
||||||
for (const uri of json.product.images) {
|
|
||||||
const image = document.createElement("img");
|
|
||||||
image.setAttribute("src", uri);
|
|
||||||
image.setAttribute("ondragstart", "return false;");
|
|
||||||
|
|
||||||
images.append(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
const description = document.createElement("p");
|
|
||||||
description.classList.add("description");
|
|
||||||
description.innerText = json.product.description;
|
|
||||||
|
|
||||||
const compatibility = document.createElement("p");
|
|
||||||
compatibility.classList.add("compatibility");
|
|
||||||
compatibility.innerText = json.product.compatibility;
|
|
||||||
|
|
||||||
const footer = document.createElement("div");
|
|
||||||
footer.classList.add("footer");
|
|
||||||
footer.classList.add("footer");
|
|
||||||
|
|
||||||
const dimensions = document.createElement("small");
|
|
||||||
dimensions.classList.add("dimensions");
|
|
||||||
dimensions.innerText = json.product.dimensions.x +
|
|
||||||
"x" +
|
|
||||||
json.product.dimensions.y + "x" +
|
|
||||||
json.product.dimensions.z;
|
|
||||||
|
|
||||||
const weight = document.createElement("small");
|
|
||||||
weight.classList.add("weight");
|
|
||||||
weight.innerText = json.product.weight + "г";
|
|
||||||
|
|
||||||
const cost = document.createElement("p");
|
|
||||||
cost.classList.add("cost");
|
|
||||||
cost.innerText = json.product.cost + "р";
|
|
||||||
|
|
||||||
h3.append(title);
|
|
||||||
h3.append(brand);
|
|
||||||
card.append(h3);
|
|
||||||
card.append(images);
|
|
||||||
card.append(description);
|
|
||||||
card.append(compatibility);
|
|
||||||
footer.append(dimensions);
|
|
||||||
footer.append(weight);
|
|
||||||
footer.append(cost);
|
|
||||||
card.append(footer);
|
|
||||||
wrap.append(card);
|
|
||||||
core.main.append(wrap);
|
|
||||||
|
|
||||||
let width = 0;
|
|
||||||
let buffer;
|
|
||||||
[...images.children].forEach((child) =>
|
|
||||||
width += child.offsetWidth + (isNaN(
|
|
||||||
buffer = parseFloat(
|
|
||||||
getComputedStyle(child).marginRight,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
? 0
|
|
||||||
: buffer)
|
|
||||||
);
|
|
||||||
|
|
||||||
history.pushState(
|
|
||||||
{ product_card: json.product.id },
|
|
||||||
json.product.title,
|
|
||||||
);
|
|
||||||
|
|
||||||
// блокировка закрытия карточки
|
|
||||||
let from;
|
|
||||||
const _from = (event) => from = event.target;
|
|
||||||
wrap.addEventListener("mousedown", _from);
|
|
||||||
wrap.addEventListener("touchstart", _from);
|
|
||||||
|
|
||||||
const remove = () => {
|
|
||||||
wrap.remove();
|
|
||||||
wrap.removeEventListener("mousedown", _from);
|
|
||||||
wrap.removeEventListener("touchstart", _from);
|
|
||||||
document.removeEventListener("click", close);
|
|
||||||
document.removeEventListener("touch", close);
|
|
||||||
window.removeEventListener("popstate", remove);
|
|
||||||
};
|
|
||||||
|
|
||||||
const close = (event) => {
|
|
||||||
if (
|
|
||||||
from === wrap &&
|
|
||||||
!card.contains(event.target) &&
|
|
||||||
!!card &&
|
|
||||||
!!(card.offsetWidth ||
|
|
||||||
card.offsetHeight ||
|
|
||||||
card.getClientRects().length)
|
|
||||||
) {
|
|
||||||
remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
from = undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("click", close);
|
|
||||||
document.addEventListener("touch", close);
|
|
||||||
window.addEventListener("popstate", remove);
|
|
||||||
|
|
||||||
if (width > card.offsetWidth) {
|
|
||||||
images.hotline = new core.hotline(
|
|
||||||
json.product.id,
|
|
||||||
images,
|
|
||||||
);
|
|
||||||
images.hotline.step = -0.3;
|
|
||||||
images.hotline.wheel = true;
|
|
||||||
images.hotline.touch = true;
|
|
||||||
images.hotline.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
|
@ -1,52 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Initialize of the class in global namespace
|
|
||||||
const core = class core {
|
|
||||||
// Domain
|
|
||||||
static domain = window.location.hostname;
|
|
||||||
|
|
||||||
// Language
|
|
||||||
static language = "ru";
|
|
||||||
|
|
||||||
// Label for the "loding" element
|
|
||||||
static loading = document.getElementById("loading");
|
|
||||||
|
|
||||||
// Label for the <header> element
|
|
||||||
static header = document.body.getElementsByTagName("header")[0];
|
|
||||||
|
|
||||||
// Label for the <aside> element
|
|
||||||
static aside = document.body.getElementsByTagName("aside")[0];
|
|
||||||
|
|
||||||
// Label for the "menu" element
|
|
||||||
static menu = document.body.querySelector("section[data-section='menu']");
|
|
||||||
|
|
||||||
// Label for the <main> element
|
|
||||||
static main = document.body.getElementsByTagName("main")[0];
|
|
||||||
|
|
||||||
// Label for the <footer> element
|
|
||||||
static footer = document.body.getElementsByTagName("footer")[0];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request
|
|
||||||
*
|
|
||||||
* @param {string} address
|
|
||||||
* @param {string} body
|
|
||||||
* @param {string} method POST, GET...
|
|
||||||
* @param {object} headers
|
|
||||||
* @param {string} type Format of response (json, text...)
|
|
||||||
*
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
static async request(
|
|
||||||
address = "/",
|
|
||||||
body,
|
|
||||||
method = "POST",
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
type = "json",
|
|
||||||
) {
|
|
||||||
return await fetch(encodeURI(address), { method, headers, body })
|
|
||||||
.then((response) => response[type]());
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,71 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Import dependencies
|
|
||||||
import("/js/core.js").then(() => {
|
|
||||||
const dependencies = setInterval(() => {
|
|
||||||
if (typeof core === "function") {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
initialization();
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
initialization();
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
function initialization() {
|
|
||||||
if (typeof core.damper === "undefined") {
|
|
||||||
// Not initialized
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Damper
|
|
||||||
*
|
|
||||||
* @param {function} function Function to execute after damping
|
|
||||||
* @param {number} timeout Timer in milliseconds (ms)
|
|
||||||
* @param {number} force Argument number storing the status of enforcement execution (see @example)
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* $a = damper(
|
|
||||||
* async (
|
|
||||||
* a, // 0
|
|
||||||
* b, // 1
|
|
||||||
* c, // 2
|
|
||||||
* force = false, // 3
|
|
||||||
* d // 4
|
|
||||||
* ) => {
|
|
||||||
* // Body of function
|
|
||||||
* },
|
|
||||||
* 500,
|
|
||||||
* 3, // 3 -> "force" argument
|
|
||||||
* );
|
|
||||||
*
|
|
||||||
* $a('for a', 'for b', 'for c', true, 'for d'); // Force execute is enabled
|
|
||||||
*
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
core.damper = (func, timeout = 300, force) => {
|
|
||||||
// Initializing of the timer
|
|
||||||
let timer;
|
|
||||||
|
|
||||||
return (...args) => {
|
|
||||||
// Deinitializing of the timer
|
|
||||||
clearTimeout(timer);
|
|
||||||
|
|
||||||
if (typeof force === "number" && args[force]) {
|
|
||||||
// Force execution (ignoring the timer)
|
|
||||||
|
|
||||||
func.apply(this, args);
|
|
||||||
} else {
|
|
||||||
// Normal execution
|
|
||||||
|
|
||||||
// Execute the handled function (entry into recursion)
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
func.apply(this, args);
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,775 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Import dependencies
|
|
||||||
import("/js/core.js").then(() => {
|
|
||||||
const dependencies = setInterval(() => {
|
|
||||||
if (typeof core === "function") {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
initialization();
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
initialization();
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
function initialization() {
|
|
||||||
if (typeof core.hotline === "undefined") {
|
|
||||||
// Not initialized
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Бегущая строка
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* Простой, но мощный класс для создания бегущих строк. Поддерживает
|
|
||||||
* перемещение мышью и прокрутку колесом, полностью настраивается очень гибок
|
|
||||||
* для настроек в CSS и подразумевается, что отлично индексируется поисковыми роботами.
|
|
||||||
* Имеет свой препроцессор, благодаря которому можно создавать бегущие строки
|
|
||||||
* без программирования - с помощью HTML-аттрибутов, а так же возможность
|
|
||||||
* изменять параметры (data-hotline-* аттрибуты) на лету. Есть возможность вызывать
|
|
||||||
* события при выбранных действиях для того, чтобы пользователь имел возможность
|
|
||||||
* дорабатывать функционал без изучения и изменения моего кода
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* сonst hotline = new hotline();
|
|
||||||
* hotline.step = '-5';
|
|
||||||
* hotline.start();
|
|
||||||
*
|
|
||||||
* @todo
|
|
||||||
* 1. Бесконечный режим - элементы не удаляются если видны на экране (будут дубликаты).
|
|
||||||
* Сейчас при БЫСТРОМ прокручивании можно заметит как элементы "появляются" в начале и конце строки.
|
|
||||||
* 2. "gap" and "padding" in wrap should be removed! or added here to the calculations
|
|
||||||
*
|
|
||||||
* @copyright WTFPL
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
|
||||||
*/
|
|
||||||
core.hotline = class hotline {
|
|
||||||
// Идентификатор
|
|
||||||
#id = 0;
|
|
||||||
|
|
||||||
// Оболочка (instanceof HTMLElement)
|
|
||||||
#shell = document.getElementById("hotline");
|
|
||||||
|
|
||||||
// Инстанция горячей строки
|
|
||||||
#instance = null;
|
|
||||||
|
|
||||||
// Перемещение
|
|
||||||
#transfer = true;
|
|
||||||
|
|
||||||
// Движение
|
|
||||||
#move = true;
|
|
||||||
|
|
||||||
// Наблюдатель
|
|
||||||
#observer = null;
|
|
||||||
|
|
||||||
// Реестр запрещённых к изменению параметров
|
|
||||||
#block = new Set(["events"]);
|
|
||||||
|
|
||||||
// Status (null, active, inactive)
|
|
||||||
#status = null;
|
|
||||||
|
|
||||||
// Настраиваемые параметры
|
|
||||||
transfer = null;
|
|
||||||
move = null;
|
|
||||||
delay = 10;
|
|
||||||
step = 1;
|
|
||||||
hover = true;
|
|
||||||
movable = true;
|
|
||||||
sticky = false;
|
|
||||||
wheel = false;
|
|
||||||
delta = null;
|
|
||||||
vertical = false;
|
|
||||||
button = 0; // button for grabbing. 0 is main mouse button (left)
|
|
||||||
observe = false;
|
|
||||||
events = new Map([
|
|
||||||
["start", false],
|
|
||||||
["stop", false],
|
|
||||||
["move", false],
|
|
||||||
["move.block", false],
|
|
||||||
["move.unblock", false],
|
|
||||||
["offset", false],
|
|
||||||
["transfer.start", true],
|
|
||||||
["transfer.end", true],
|
|
||||||
["mousemove", false],
|
|
||||||
["touchmove", false],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Is hotline currently moving due to "onmousemove" or "ontouchmove"?
|
|
||||||
moving = false;
|
|
||||||
|
|
||||||
constructor(id, shell) {
|
|
||||||
// Запись идентификатора
|
|
||||||
if (typeof id === "string" || typeof id === "number") this.#id = id;
|
|
||||||
|
|
||||||
// Запись оболочки
|
|
||||||
if (shell instanceof HTMLElement) this.#shell = shell;
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
if (this.#instance === null) {
|
|
||||||
// Нет запущенной инстанции бегущей строки
|
|
||||||
|
|
||||||
// Инициализация ссылки на ядро
|
|
||||||
const _this = this;
|
|
||||||
|
|
||||||
// Запуск движения
|
|
||||||
this.#instance = setInterval(function () {
|
|
||||||
if (_this.#shell.childElementCount > 1) {
|
|
||||||
// Найдено содержимое бегущей строки (2 и более)
|
|
||||||
|
|
||||||
// Инициализация буфера для временных данных
|
|
||||||
let buffer;
|
|
||||||
|
|
||||||
// Инициализация данных первого элемента в строке
|
|
||||||
const first = {
|
|
||||||
element: (buffer = _this.#shell.firstElementChild),
|
|
||||||
coords: buffer.getBoundingClientRect(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (_this.vertical) {
|
|
||||||
// Вертикальная бегущая строка
|
|
||||||
|
|
||||||
// Инициализация сдвига у первого элемента (движение)
|
|
||||||
first.offset = isNaN(
|
|
||||||
buffer = parseFloat(first.element.style.marginTop),
|
|
||||||
)
|
|
||||||
? 0
|
|
||||||
: buffer;
|
|
||||||
|
|
||||||
// Инициализация отступа до второго элемента у первого элемента (разделение)
|
|
||||||
first.separator = isNaN(
|
|
||||||
buffer = parseFloat(
|
|
||||||
getComputedStyle(first.element).marginBottom,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
? 0
|
|
||||||
: buffer;
|
|
||||||
|
|
||||||
// Инициализация крайнего с конца ребра первого элемента в строке
|
|
||||||
first.end = first.coords.y + first.coords.height +
|
|
||||||
first.separator;
|
|
||||||
} else {
|
|
||||||
// Горизонтальная бегущая строка
|
|
||||||
|
|
||||||
// Инициализация отступа у первого элемента (движение)
|
|
||||||
first.offset = isNaN(
|
|
||||||
buffer = parseFloat(first.element.style.marginLeft),
|
|
||||||
)
|
|
||||||
? 0
|
|
||||||
: buffer;
|
|
||||||
|
|
||||||
// Инициализация отступа до второго элемента у первого элемента (разделение)
|
|
||||||
first.separator = isNaN(
|
|
||||||
buffer = parseFloat(
|
|
||||||
getComputedStyle(first.element).marginRight,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
? 0
|
|
||||||
: buffer;
|
|
||||||
|
|
||||||
// Инициализация крайнего с конца ребра первого элемента в строке
|
|
||||||
first.end = first.coords.x + first.coords.width +
|
|
||||||
first.separator;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(_this.vertical &&
|
|
||||||
Math.round(first.end) < _this.#shell.offsetTop) ||
|
|
||||||
(!_this.vertical &&
|
|
||||||
Math.round(first.end) < _this.#shell.offsetLeft)
|
|
||||||
) {
|
|
||||||
// Элемент (вместе с отступом до второго элемента) вышел из области видимости (строки)
|
|
||||||
|
|
||||||
if (
|
|
||||||
(_this.transfer === null && _this.#transfer) ||
|
|
||||||
_this.transfer === true
|
|
||||||
) {
|
|
||||||
// Перенос разрешен
|
|
||||||
|
|
||||||
if (_this.vertical) {
|
|
||||||
// Вертикальная бегущая строка
|
|
||||||
|
|
||||||
// Удаление отступов (движения)
|
|
||||||
first.element.style.marginTop = null;
|
|
||||||
} else {
|
|
||||||
// Горизонтальная бегущая строка
|
|
||||||
|
|
||||||
// Удаление отступов (движения)
|
|
||||||
first.element.style.marginLeft = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Копирование первого элемента в конец строки
|
|
||||||
_this.#shell.appendChild(first.element);
|
|
||||||
|
|
||||||
if (_this.events.get("transfer.end")) {
|
|
||||||
// Запрошен вызов события: "перемещение в конец"
|
|
||||||
|
|
||||||
// Вызов события: "перемещение в конец"
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent(`hotline.${_this.#id}.transfer.end`, {
|
|
||||||
detail: {
|
|
||||||
element: first.element,
|
|
||||||
offset: -(
|
|
||||||
(_this.vertical
|
|
||||||
? first.coords.height
|
|
||||||
: first.coords.width) + first.separator
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
(_this.vertical &&
|
|
||||||
Math.round(first.coords.y) > _this.#shell.offsetTop) ||
|
|
||||||
(!_this.vertical &&
|
|
||||||
Math.round(first.coords.x) > _this.#shell.offsetLeft)
|
|
||||||
) {
|
|
||||||
// Передняя (движущая) граница первого элемента вышла из области видимости
|
|
||||||
|
|
||||||
if (
|
|
||||||
(_this.transfer === null && _this.#transfer) ||
|
|
||||||
_this.transfer === true
|
|
||||||
) {
|
|
||||||
// Перенос разрешен
|
|
||||||
|
|
||||||
// Инициализация отступа у последнего элемента (разделение)
|
|
||||||
const separator = (buffer = isNaN(
|
|
||||||
buffer = parseFloat(
|
|
||||||
getComputedStyle(_this.#shell.lastElementChild)[
|
|
||||||
_this.vertical ? "marginBottom" : "marginRight"
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
? 0
|
|
||||||
: buffer) === 0
|
|
||||||
? first.separator
|
|
||||||
: buffer;
|
|
||||||
|
|
||||||
// Инициализация координат первого элемента в строке
|
|
||||||
const coords = _this.#shell.lastElementChild
|
|
||||||
.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (_this.vertical) {
|
|
||||||
// Вертикальная бегущая строка
|
|
||||||
|
|
||||||
// Удаление отступов (движения)
|
|
||||||
_this.#shell.lastElementChild.style.marginTop =
|
|
||||||
-coords.height - separator + "px";
|
|
||||||
} else {
|
|
||||||
// Горизонтальная бегущая строка
|
|
||||||
|
|
||||||
// Удаление отступов (движения)
|
|
||||||
_this.#shell.lastElementChild.style.marginLeft =
|
|
||||||
-coords.width - separator + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Копирование последнего элемента в начало строки
|
|
||||||
_this.#shell.insertBefore(
|
|
||||||
_this.#shell.lastElementChild,
|
|
||||||
first.element,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Удаление отступов у второго элемента в строке (движения)
|
|
||||||
_this.#shell.children[1].style[
|
|
||||||
_this.vertical ? "marginTop" : "marginLeft"
|
|
||||||
] = null;
|
|
||||||
|
|
||||||
if (_this.events.get("transfer.start")) {
|
|
||||||
// Запрошен вызов события: "перемещение в начало"
|
|
||||||
|
|
||||||
// Вызов события: "перемещение в начало"
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent(`hotline.${_this.#id}.transfer.start`, {
|
|
||||||
detail: {
|
|
||||||
element: _this.#shell.lastElementChild,
|
|
||||||
offset:
|
|
||||||
(_this.vertical ? coords.height : coords.width) +
|
|
||||||
separator,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Элемент в области видимости
|
|
||||||
|
|
||||||
if (
|
|
||||||
(_this.move === null && _this.#move) || _this.move === true
|
|
||||||
) {
|
|
||||||
// Движение разрешено
|
|
||||||
|
|
||||||
// Запись новых координат сдвига
|
|
||||||
const offset = first.offset + _this.step;
|
|
||||||
|
|
||||||
// Запись сдвига (движение)
|
|
||||||
_this.offset(offset);
|
|
||||||
|
|
||||||
if (_this.events.get("move")) {
|
|
||||||
// Запрошен вызов события: "движение"
|
|
||||||
|
|
||||||
// Вызов события: "движение"
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent(`hotline.${_this.#id}.move`, {
|
|
||||||
detail: {
|
|
||||||
from: first.offset,
|
|
||||||
to: offset,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, _this.delay);
|
|
||||||
|
|
||||||
if (this.hover) {
|
|
||||||
// Запрошена возможность останавливать бегущую строку
|
|
||||||
|
|
||||||
// Инициализация сдвига
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
// Инициализация слушателя события при перемещении элемента в бегущей строке
|
|
||||||
const listener = function (e) {
|
|
||||||
// Увеличение сдвига
|
|
||||||
offset += e.detail.offset ?? 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Объявление переменной в области видимости обработки остановки бегущей строки
|
|
||||||
let move;
|
|
||||||
|
|
||||||
// Инициализация обработчика наведения курсора (остановка движения)
|
|
||||||
this.#shell.onmouseover = function (e) {
|
|
||||||
// Курсор наведён на бегущую строку
|
|
||||||
|
|
||||||
// Блокировка движения
|
|
||||||
_this.#move = false;
|
|
||||||
|
|
||||||
if (_this.events.get("move.block")) {
|
|
||||||
// Запрошен вызов события: "блокировка движения"
|
|
||||||
|
|
||||||
// Вызов события: "блокировка движения"
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent(`hotline.${_this.#id}.move.block`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.movable) {
|
|
||||||
// Запрошена возможность двигать бегущую строку
|
|
||||||
|
|
||||||
_this.#shell.onmousedown =
|
|
||||||
_this.#shell.ontouchstart =
|
|
||||||
function (
|
|
||||||
start,
|
|
||||||
) {
|
|
||||||
// Handling a "mousedown" and a "touchstart" on hotline
|
|
||||||
|
|
||||||
if (
|
|
||||||
start.type === "touchstart" ||
|
|
||||||
start.button === _this.button
|
|
||||||
) {
|
|
||||||
const x = start.pageX || start.touches[0].pageX;
|
|
||||||
const y = start.pageY || start.touches[0].pageY;
|
|
||||||
|
|
||||||
// Блокировка движения
|
|
||||||
_this.#move = false;
|
|
||||||
|
|
||||||
if (_this.events.get("move.block")) {
|
|
||||||
// Запрошен вызов события: "блокировка движения"
|
|
||||||
|
|
||||||
// Вызов события: "блокировка движения"
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent(`hotline.${_this.#id}.move.block`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Инициализация слушателей события перемещения элемента в бегущей строке
|
|
||||||
document.addEventListener(
|
|
||||||
`hotline.${_this.#id}.transfer.start`,
|
|
||||||
listener,
|
|
||||||
);
|
|
||||||
document.addEventListener(
|
|
||||||
`hotline.${_this.#id}.transfer.end`,
|
|
||||||
listener,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Инициализация буфера для временных данных
|
|
||||||
let buffer;
|
|
||||||
|
|
||||||
// Инициализация данных первого элемента в строке
|
|
||||||
const first = {
|
|
||||||
offset: isNaN(
|
|
||||||
buffer = parseFloat(
|
|
||||||
_this.vertical
|
|
||||||
? _this.#shell.firstElementChild.style
|
|
||||||
.marginTop
|
|
||||||
: _this.#shell.firstElementChild.style
|
|
||||||
.marginLeft,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
? 0
|
|
||||||
: buffer,
|
|
||||||
};
|
|
||||||
|
|
||||||
move = (move) => {
|
|
||||||
// Обработка движения курсора
|
|
||||||
|
|
||||||
if (_this.#status === "active") {
|
|
||||||
// Запись статуса ручного перемещения
|
|
||||||
_this.moving = true;
|
|
||||||
|
|
||||||
const _x = move.pageX || move.touches[0].pageX;
|
|
||||||
const _y = move.pageY || move.touches[0].pageY;
|
|
||||||
|
|
||||||
if (_this.vertical) {
|
|
||||||
// Вертикальная бегущая строка
|
|
||||||
|
|
||||||
// Инициализация буфера местоположения
|
|
||||||
const from =
|
|
||||||
_this.#shell.firstElementChild.style.marginTop;
|
|
||||||
const to = _y - (y + offset - first.offset);
|
|
||||||
|
|
||||||
// Движение
|
|
||||||
_this.#shell.firstElementChild.style.marginTop =
|
|
||||||
to +
|
|
||||||
"px";
|
|
||||||
} else {
|
|
||||||
// Горизонтальная бегущая строка
|
|
||||||
|
|
||||||
// Инициализация буфера местоположения
|
|
||||||
const from =
|
|
||||||
_this.#shell.firstElementChild.style.marginLeft;
|
|
||||||
const to = _x - (x + offset - first.offset);
|
|
||||||
|
|
||||||
// Движение
|
|
||||||
_this.#shell.firstElementChild.style.marginLeft =
|
|
||||||
to +
|
|
||||||
"px";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_this.events.get(move.type)) {
|
|
||||||
// Запрошен вызов события: "перемещение" (мышью или касанием)
|
|
||||||
|
|
||||||
// Вызов события: "перемещение" (мышью или касанием)
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent(
|
|
||||||
`hotline.${_this.#id}.${move.type}`,
|
|
||||||
{
|
|
||||||
detail: { from, to },
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запись курсора
|
|
||||||
_this.#shell.style.cursor = "grabbing";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Запуск обработки движения
|
|
||||||
document.addEventListener("mousemove", move);
|
|
||||||
document.addEventListener("touchmove", move);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Перещапись событий браузера (чтобы не дёргалось)
|
|
||||||
_this.#shell.ondragstart = null;
|
|
||||||
|
|
||||||
_this.#shell.onmouseup = _this.#shell.ontouchend = function () {
|
|
||||||
// Курсор деактивирован
|
|
||||||
|
|
||||||
// Запись статуса ручного перемещения
|
|
||||||
_this.moving = false;
|
|
||||||
|
|
||||||
// Остановка обработки движения
|
|
||||||
document.removeEventListener("mousemove", move);
|
|
||||||
document.removeEventListener("touchmove", move);
|
|
||||||
|
|
||||||
// Сброс сдвига
|
|
||||||
offset = 0;
|
|
||||||
|
|
||||||
document.removeEventListener(
|
|
||||||
`hotline.${_this.#id}.transfer.start`,
|
|
||||||
listener,
|
|
||||||
);
|
|
||||||
document.removeEventListener(
|
|
||||||
`hotline.${_this.#id}.transfer.end`,
|
|
||||||
listener,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Разблокировка движения
|
|
||||||
_this.#move = true;
|
|
||||||
|
|
||||||
if (_this.events.get("move.unblock")) {
|
|
||||||
// Запрошен вызов события: "разблокировка движения"
|
|
||||||
|
|
||||||
// Вызов события: "разблокировка движения"
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent(`hotline.${_this.#id}.move.unblock`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Восстановление курсора
|
|
||||||
_this.#shell.style.cursor = null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Инициализация обработчика отведения курсора (остановка движения)
|
|
||||||
this.#shell.onmouseleave = function (onmouseleave) {
|
|
||||||
// Курсор отведён от бегущей строки
|
|
||||||
|
|
||||||
if (!_this.sticky) {
|
|
||||||
// Отключено прилипание
|
|
||||||
|
|
||||||
// Запись статуса ручного перемещения
|
|
||||||
_this.moving = false;
|
|
||||||
|
|
||||||
// Остановка обработки движения
|
|
||||||
document.removeEventListener("mousemove", move);
|
|
||||||
document.removeEventListener("touchmove", move);
|
|
||||||
|
|
||||||
document.removeEventListener(
|
|
||||||
`hotline.${_this.#id}.transfer.start`,
|
|
||||||
listener,
|
|
||||||
);
|
|
||||||
document.removeEventListener(
|
|
||||||
`hotline.${_this.#id}.transfer.end`,
|
|
||||||
listener,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Восстановление курсора
|
|
||||||
_this.#shell.style.cursor = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сброс сдвига
|
|
||||||
offset = 0;
|
|
||||||
|
|
||||||
// Разблокировка движения
|
|
||||||
_this.#move = true;
|
|
||||||
|
|
||||||
if (_this.events.get("move.unblock")) {
|
|
||||||
// Запрошен вызов события: "разблокировка движения"
|
|
||||||
|
|
||||||
// Вызов события: "разблокировка движения"
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent(`hotline.${_this.#id}.move.unblock`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.wheel) {
|
|
||||||
// Запрошена возможность прокручивать колесом мыши
|
|
||||||
|
|
||||||
// Инициализация обработчика наведения курсора (остановка движения)
|
|
||||||
this.#shell.onwheel = function (e) {
|
|
||||||
// Курсор наведён на бегущую
|
|
||||||
|
|
||||||
// Инициализация буфера для временных данных
|
|
||||||
let buffer;
|
|
||||||
|
|
||||||
// Перемещение
|
|
||||||
_this.offset(
|
|
||||||
(isNaN(
|
|
||||||
buffer = parseFloat(
|
|
||||||
_this.#shell.firstElementChild.style[
|
|
||||||
_this.vertical ? "marginTop" : "marginLeft"
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
? 0
|
|
||||||
: buffer) +
|
|
||||||
(_this.delta === null
|
|
||||||
? e.wheelDelta
|
|
||||||
: e.wheelDelta > 0
|
|
||||||
? _this.delta
|
|
||||||
: -_this.delta),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#status = "active";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.observe) {
|
|
||||||
// Запрошено наблюдение за изменениями аттрибутов элемента бегущей строки
|
|
||||||
|
|
||||||
if (this.#observer === null) {
|
|
||||||
// Отсутствует наблюдатель
|
|
||||||
|
|
||||||
// Инициализация ссылки на ядро
|
|
||||||
const _this = this;
|
|
||||||
|
|
||||||
// Инициализация наблюдателя
|
|
||||||
this.#observer = new MutationObserver(function (mutations) {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === "attributes") {
|
|
||||||
// Запись параметра в инстанцию бегущей строки
|
|
||||||
_this.configure(mutation.attributeName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Перезапуск бегущей строки
|
|
||||||
_this.restart();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Активация наблюдения
|
|
||||||
this.#observer.observe(this.#shell, {
|
|
||||||
attributes: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (this.#observer instanceof MutationObserver) {
|
|
||||||
// Запрошено отключение наблюдения
|
|
||||||
|
|
||||||
// Деактивация наблюдения
|
|
||||||
this.#observer.disconnect();
|
|
||||||
|
|
||||||
// Удаление наблюдателя
|
|
||||||
this.#observer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.events.get("start")) {
|
|
||||||
// Запрошен вызов события: "запуск"
|
|
||||||
|
|
||||||
// Вызов события: "запуск"
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent(`hotline.${this.#id}.start`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.#status = "inactive";
|
|
||||||
|
|
||||||
// Остановка бегущей строки
|
|
||||||
clearInterval(this.#instance);
|
|
||||||
|
|
||||||
// Удаление инстанции интервала
|
|
||||||
this.#instance = null;
|
|
||||||
|
|
||||||
if (this.events.get("stop")) {
|
|
||||||
// Запрошен вызов события: "остановка"
|
|
||||||
|
|
||||||
// Вызов события: "остановка"
|
|
||||||
document.dispatchEvent(new CustomEvent(`hotline.${this.#id}.stop`));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
restart() {
|
|
||||||
// Остановка бегущей строки
|
|
||||||
this.stop();
|
|
||||||
|
|
||||||
// Запуск бегущей строки
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
configure(attribute) {
|
|
||||||
// Инициализация названия параметра
|
|
||||||
const parameter =
|
|
||||||
(/^data-hotline-(\w+)$/.exec(attribute) ?? [, null])[1];
|
|
||||||
|
|
||||||
if (typeof parameter === "string") {
|
|
||||||
// Параметр найден
|
|
||||||
|
|
||||||
// Проверка на разрешение изменения
|
|
||||||
if (this.#block.has(parameter)) return;
|
|
||||||
|
|
||||||
// Инициализация значения параметра
|
|
||||||
const value = this.#shell.getAttribute(attribute);
|
|
||||||
|
|
||||||
if (typeof value !== undefined || typeof value !== null) {
|
|
||||||
// Найдено значение
|
|
||||||
|
|
||||||
// Инициализация буфера для временных данных
|
|
||||||
let buffer;
|
|
||||||
|
|
||||||
// Запись параметра
|
|
||||||
this[parameter] = isNaN(buffer = parseFloat(value))
|
|
||||||
? value === "true" ? true : value === "false" ? false : value
|
|
||||||
: buffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
offset(value) {
|
|
||||||
// Запись отступа
|
|
||||||
this.#shell.firstElementChild.style[
|
|
||||||
this.vertical ? "marginTop" : "marginLeft"
|
|
||||||
] = value + "px";
|
|
||||||
|
|
||||||
if (this.events.get("offset")) {
|
|
||||||
// Запрошен вызов события: "сдвиг"
|
|
||||||
|
|
||||||
// Вызов события: "сдвиг"
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent(`hotline.${this.#id}.offset`, {
|
|
||||||
detail: {
|
|
||||||
to: value,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
static preprocessing(event = false) {
|
|
||||||
// Инициализация счётчиков инстанций горячей строки
|
|
||||||
const success = new Set();
|
|
||||||
let error = 0;
|
|
||||||
|
|
||||||
for (
|
|
||||||
const element of document.querySelectorAll('*[data-hotline="true"]')
|
|
||||||
) {
|
|
||||||
// Перебор элементов для инициализации бегущих строк
|
|
||||||
|
|
||||||
if (typeof element.id === "string") {
|
|
||||||
// Найден идентификатор
|
|
||||||
|
|
||||||
// Инициализация инстанции бегущей строки
|
|
||||||
const hotline = new this(element.id, element);
|
|
||||||
|
|
||||||
for (const attribute of element.getAttributeNames()) {
|
|
||||||
// Перебор аттрибутов
|
|
||||||
|
|
||||||
// Запись параметра в инстанцию бегущей строки
|
|
||||||
hotline.configure(attribute);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запуск бегущей строки
|
|
||||||
hotline.start();
|
|
||||||
|
|
||||||
// Запись инстанции бегущей строки в элемент
|
|
||||||
element.hotline = hotline;
|
|
||||||
|
|
||||||
// Запись в счётчик успешных инициализаций
|
|
||||||
success.add(hotline);
|
|
||||||
} else ++error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
// Запрошен вызов события: "предварительная подготовка"
|
|
||||||
|
|
||||||
// Вызов события: "предварительная подготовка"
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent(`hotline.preprocessed`, {
|
|
||||||
detail: {
|
|
||||||
success,
|
|
||||||
error,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,45 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Import dependencies
|
|
||||||
import("/js/core.js").then(() =>
|
|
||||||
import("/js/damper.js").then(() => {
|
|
||||||
const dependencies = setInterval(() => {
|
|
||||||
if (
|
|
||||||
typeof core === "function" &&
|
|
||||||
typeof core.damper === "function"
|
|
||||||
) {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
initialization();
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
initialization();
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
function initialization() {
|
|
||||||
if (typeof core.session === "undefined") {
|
|
||||||
// Not initialized
|
|
||||||
|
|
||||||
// Write to the core
|
|
||||||
core.session = class session {
|
|
||||||
/**
|
|
||||||
* Current position in hierarchy of the categories
|
|
||||||
*/
|
|
||||||
static categories = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
static connect() {
|
|
||||||
core.request(
|
|
||||||
"/session/connect/telegram",
|
|
||||||
window.Telegram.WebApp.initData,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
|
@ -1,43 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// Import dependencies
|
|
||||||
import("/js/core.js").then(() =>
|
|
||||||
import("/js/damper.js").then(() => {
|
|
||||||
const dependencies = setInterval(() => {
|
|
||||||
if (
|
|
||||||
typeof core === "function" &&
|
|
||||||
typeof core.damper === "function"
|
|
||||||
) {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
initialization();
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
clearInterval(dependencies);
|
|
||||||
initialization();
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
function initialization() {
|
|
||||||
if (typeof core.telegram === "undefined") {
|
|
||||||
// Not initialized
|
|
||||||
|
|
||||||
// Write to the core
|
|
||||||
core.telegram = class telegram {
|
|
||||||
/**
|
|
||||||
* Telegram WebApp API
|
|
||||||
*
|
|
||||||
* @see {@link https://core.telegram.org/bots/webapps#initializing-mini-apps}
|
|
||||||
*/
|
|
||||||
static api = window.Telegram.WebApp;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* telegram.MainButton.text =
|
|
||||||
typeof core === "object" && core.language === "ru"
|
|
||||||
? "Корзина"
|
|
||||||
: "Cart";
|
|
||||||
telegram.MainButton.show(); */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
|
@ -1,88 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace mirzaev\arming_bot;
|
|
||||||
|
|
||||||
// Files of the project
|
|
||||||
use mirzaev\arming_bot\controllers\core as controller,
|
|
||||||
mirzaev\arming_bot\models\core as model,
|
|
||||||
mirzaev\arming_bot\models\telegram;
|
|
||||||
|
|
||||||
// Фреймворк Telegram
|
|
||||||
use Zanzara\Zanzara,
|
|
||||||
Zanzara\Context,
|
|
||||||
Zanzara\Config;
|
|
||||||
|
|
||||||
ini_set('error_reporting', E_ALL);
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
|
|
||||||
// Версия робота
|
|
||||||
define('ROBOT_VERSION', '1.0.0');
|
|
||||||
|
|
||||||
// Путь до настроек
|
|
||||||
define('SETTINGS', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'settings');
|
|
||||||
|
|
||||||
// Путь до хранилища
|
|
||||||
define('STORAGE', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'storage');
|
|
||||||
|
|
||||||
// Файл в формате xlsx с примером excel-документа для импорта каталога
|
|
||||||
define('CATALOG_EXAMPLE', STORAGE . DIRECTORY_SEPARATOR . 'example.xlsx');
|
|
||||||
|
|
||||||
// Файл в формате xlsx для импорта каталога
|
|
||||||
define('CATALOG_IMPORT', STORAGE . DIRECTORY_SEPARATOR . 'import.xlsx');
|
|
||||||
|
|
||||||
// Ключ чат-робота Telegram
|
|
||||||
define('KEY', require(SETTINGS . DIRECTORY_SEPARATOR . 'key.php'));
|
|
||||||
|
|
||||||
// Инициализация библиотек
|
|
||||||
require __DIR__ . DIRECTORY_SEPARATOR
|
|
||||||
. '..' . DIRECTORY_SEPARATOR
|
|
||||||
. '..' . DIRECTORY_SEPARATOR
|
|
||||||
. '..' . DIRECTORY_SEPARATOR
|
|
||||||
. '..' . DIRECTORY_SEPARATOR
|
|
||||||
. 'vendor' . DIRECTORY_SEPARATOR
|
|
||||||
. 'autoload.php';
|
|
||||||
|
|
||||||
// Инициализация ядра контроллеров MINIMAL
|
|
||||||
new controller(false);
|
|
||||||
|
|
||||||
// Инициализация ядра моделей MINIMAL
|
|
||||||
new model(true);
|
|
||||||
|
|
||||||
$config = new Config();
|
|
||||||
$config->setParseMode(Config::PARSE_MODE_MARKDOWN);
|
|
||||||
$config->useReactFileSystem(true);
|
|
||||||
|
|
||||||
$bot = new Zanzara(KEY, $config);
|
|
||||||
|
|
||||||
/* $bot->onUpdate(function (Context $ctx): void {
|
|
||||||
var_dump($ctx->getMessage()->getWebAppData());
|
|
||||||
var_dump($ctx->getEffectiveUser() );
|
|
||||||
}); */
|
|
||||||
|
|
||||||
$bot->onCommand('start', fn($ctx) => telegram::start($ctx));
|
|
||||||
$bot->onCommand('contacts', fn($ctx) => telegram::contacts($ctx));
|
|
||||||
$bot->onCommand('company', fn($ctx) => telegram::company($ctx));
|
|
||||||
$bot->onCommand('community', fn($ctx) => telegram::community($ctx));
|
|
||||||
$bot->onCommand('settings', fn($ctx) => telegram::settings($ctx));
|
|
||||||
|
|
||||||
$bot->onText('💬 Контакты', fn($ctx) => telegram::contacts($ctx));
|
|
||||||
$bot->onText('🏛️ О компании', fn($ctx) => telegram::company($ctx));
|
|
||||||
$bot->onText('🎯 Сообщество', fn($ctx) => telegram::community($ctx));
|
|
||||||
$bot->onText('⚙️ Настройки', fn($ctx) => telegram::settings($ctx));
|
|
||||||
|
|
||||||
$bot->onCbQueryData(['mail'], fn($ctx) => telegram::_mail($ctx));
|
|
||||||
$bot->onCbQueryData(['import_request'], fn($ctx) => telegram::import_request($ctx));
|
|
||||||
$bot->onCbQueryData(['tuning'], fn($ctx) => telegram::tuning($ctx));
|
|
||||||
$bot->onCbQueryData(['brands'], fn($ctx) => telegram::brands($ctx));
|
|
||||||
|
|
||||||
// Инициализация middleware с обработкой аккаунта
|
|
||||||
$bot->middleware([telegram::class, "account"]);
|
|
||||||
|
|
||||||
// Инициализация middleware с обработкой технических работ разных уровней
|
|
||||||
$bot->middleware([telegram::class, "suspension"]);
|
|
||||||
|
|
||||||
// Запуск чат-робота
|
|
||||||
$bot->run();
|
|
|
@ -1,160 +0,0 @@
|
||||||
@charset "UTF-8";
|
|
||||||
|
|
||||||
main>section[data-section="catalog"] {
|
|
||||||
width: var(--width);
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
gap: var(--gap, 5px);
|
|
||||||
/* justify-content: space-between; */
|
|
||||||
/* justify-content: space-evenly; */
|
|
||||||
/* background-color: var(--tg-theme-secondary-bg-color); */
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]:last-child {
|
|
||||||
margin-bottom: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>a.category[type="button"] {
|
|
||||||
height: 23px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
color: var(--tg-theme-button-text-color);
|
|
||||||
background-color: var(--tg-theme-button-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product {
|
|
||||||
/* --product-height: 200px; */
|
|
||||||
--product-height: 220px;
|
|
||||||
--title-font-size: 0.9rem;
|
|
||||||
--title-height: 1.5rem;
|
|
||||||
--button-height: 33px;
|
|
||||||
position: relative;
|
|
||||||
/* width: calc((100% - var(--gap) * 2) / 3); */
|
|
||||||
width: calc((100% - var(--gap)) / 2);
|
|
||||||
height: var(--product-height);
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
white-space: nowrap;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
overflow: clip;
|
|
||||||
backdrop-filter: brightness(0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product:hover {
|
|
||||||
/* flex-grow: 0.1; */
|
|
||||||
/* background-color: var(--tg-theme-secondary-bg-color); */
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product:hover>* {
|
|
||||||
transition: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product:not(:hover)>* {
|
|
||||||
transition: 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product>img:first-of-type {
|
|
||||||
z-index: -50;
|
|
||||||
position: absolute;
|
|
||||||
bottom: var(--button-height);
|
|
||||||
/* bottom: calc(var(--button-height) + var(--title-height)); */
|
|
||||||
width: 100%;
|
|
||||||
/* height: 100%; */
|
|
||||||
height: calc(var(--product-height) - var(--button-height));
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product>img:first-of-type+* {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product>a {
|
|
||||||
padding: 4px 8px 4px 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
word-break: break-all;
|
|
||||||
font-weight: bold;
|
|
||||||
backdrop-filter: brightness(0.4) contrast(1.2);
|
|
||||||
color: var(--text-light, var(--tg-theme-text-color));
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product>a.title {
|
|
||||||
padding: 0 8px 0 8px;
|
|
||||||
height: var(--title-height);
|
|
||||||
min-height: var(--title-height);
|
|
||||||
line-height: var(--title-height);
|
|
||||||
font-size: var(--title-font-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product:hover>a.title {
|
|
||||||
backdrop-filter: brightness(0.35) contrast(1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product>a+ :is(a, small) {
|
|
||||||
padding: 0px 8px 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product>small {
|
|
||||||
padding: 3px 8px 0 8px;
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-all;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
backdrop-filter: brightness(0.4) contrast(1.2);
|
|
||||||
color: var(--text-light, var(--tg-theme-text-color));
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product>small.description {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product:hover>small.description {
|
|
||||||
height: calc(var(--product-height) - var(--title-height) - var(--button-height) - 6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product:not(:hover)>small.description {
|
|
||||||
height: 0;
|
|
||||||
padding: 0 8px 0px 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product>*:has(+ button:last-of-type) {
|
|
||||||
--offset-before-button: 9px;
|
|
||||||
padding: 4px 8px 13px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"]>article.product>button:last-of-type {
|
|
||||||
height: var(--button-height);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--tg-theme-button-text-color);
|
|
||||||
background-color: var(--tg-theme-button-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="cart"] {
|
|
||||||
--diameter: 4rem;
|
|
||||||
z-index: 999;
|
|
||||||
right: 5vw;
|
|
||||||
bottom: 5vw;
|
|
||||||
position: fixed;
|
|
||||||
width: var(--diameter);
|
|
||||||
height: var(--diameter);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 100%;
|
|
||||||
background-color: var(--tg-theme-button-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="cart"]>i.icon.shopping.cart {
|
|
||||||
top: -1px;
|
|
||||||
color: var(--tg-theme-button-text-color);
|
|
||||||
}
|
|
|
@ -1,179 +0,0 @@
|
||||||
@charset "UTF-8";
|
|
||||||
|
|
||||||
main>section[data-section="catalog"] {
|
|
||||||
width: var(--width);
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
gap: var(--gap, 5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"] {
|
|
||||||
position: relative;
|
|
||||||
height: 23px;
|
|
||||||
padding: unset;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
color: var(--tg-theme-button-text-color);
|
|
||||||
background-color: var(--tg-theme-button-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="categories"]:last-child {
|
|
||||||
/* margin-bottom: unset; */
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:has(>img) {
|
|
||||||
height: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]>img {
|
|
||||||
position: absolute;
|
|
||||||
left: -5%;
|
|
||||||
top: -5%;
|
|
||||||
width: 110%;
|
|
||||||
height: 110%;
|
|
||||||
object-fit: cover;
|
|
||||||
filter: blur(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:hover>img {
|
|
||||||
filter: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]:has(>img)>p {
|
|
||||||
--padding: 0.7rem;
|
|
||||||
position: absolute;
|
|
||||||
left: var(--padding);
|
|
||||||
bottom: var(--padding);
|
|
||||||
right: var(--padding);
|
|
||||||
margin: unset;
|
|
||||||
width: min-content;
|
|
||||||
padding: var(--padding);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
background: var(--tg-theme-secondary-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"]>p {
|
|
||||||
z-index: 100;
|
|
||||||
padding: 8px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"] {
|
|
||||||
--column: calc((100% - var(--gap)) / 2);
|
|
||||||
width: var(--width);
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--gap);
|
|
||||||
grid-template-columns: repeat(2, var(--column));
|
|
||||||
grid-auto-flow: row dense;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--gap);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
overflow: clip;
|
|
||||||
backdrop-filter: brightness(0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover {
|
|
||||||
/* flex-grow: 0.1; */
|
|
||||||
/* background-color: var(--tg-theme-secondary-bg-color); */
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover>* {
|
|
||||||
transition: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:not(:hover)>* {
|
|
||||||
transition: 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a>img:first-of-type {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a>img:first-of-type+* {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a>p.title {
|
|
||||||
z-index: 50;
|
|
||||||
margin: unset;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: bold;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
hyphens: auto;
|
|
||||||
color: var(--tg-theme-text-color);
|
|
||||||
background-color: var(--tg-theme-secondary-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>button:last-of-type {
|
|
||||||
z-index: 100;
|
|
||||||
height: 33px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--tg-theme-button-text-color);
|
|
||||||
background-color: var(--tg-theme-button-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="cart"] {
|
|
||||||
--diameter: 4rem;
|
|
||||||
z-index: 999;
|
|
||||||
right: 5vw;
|
|
||||||
bottom: 5vw;
|
|
||||||
position: fixed;
|
|
||||||
width: var(--diameter);
|
|
||||||
height: var(--diameter);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 100%;
|
|
||||||
background-color: var(--tg-theme-button-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="cart"]>i.icon.shopping.cart {
|
|
||||||
top: -1px;
|
|
||||||
color: var(--tg-theme-button-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="filters"] {
|
|
||||||
--diameter: 4rem;
|
|
||||||
z-index: 999;
|
|
||||||
right: 5vw;
|
|
||||||
bottom: 5vw;
|
|
||||||
position: fixed;
|
|
||||||
width: var(--diameter);
|
|
||||||
height: var(--diameter);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 100%;
|
|
||||||
background-color: var(--tg-theme-button-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="filters"][data-filter="brand"] {}
|
|
|
@ -1,117 +0,0 @@
|
||||||
@charset "UTF-8";
|
|
||||||
|
|
||||||
main>section[data-section="catalog"] {
|
|
||||||
width: var(--width);
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
gap: var(--gap, 5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="categories"]>a.category[type="button"] {
|
|
||||||
height: 23px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
color: var(--tg-theme-button-text-color);
|
|
||||||
background-color: var(--tg-theme-button-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="categories"]:last-child {
|
|
||||||
/* margin-bottom: unset; */
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"] {
|
|
||||||
--column: calc((100% - var(--gap) * 2) / 3);
|
|
||||||
width: var(--width);
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--gap);
|
|
||||||
grid-template-columns: repeat(3, var(--column));
|
|
||||||
grid-auto-flow: row dense;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--gap);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
overflow: clip;
|
|
||||||
backdrop-filter: brightness(0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover {
|
|
||||||
/* flex-grow: 0.1; */
|
|
||||||
/* background-color: var(--tg-theme-secondary-bg-color); */
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:hover>* {
|
|
||||||
transition: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product:not(:hover)>* {
|
|
||||||
transition: 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>img:first-of-type {
|
|
||||||
z-index: -50;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 120px;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>img:first-of-type+* {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>a.title {
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: bold;
|
|
||||||
word-break: break-word;
|
|
||||||
hyphens: auto;
|
|
||||||
color: var(--tg-theme-text-color);
|
|
||||||
background-color: var(--tg-theme-secondary-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="catalog"][data-catalog-type="products"]>div.column>article.product>button:last-of-type {
|
|
||||||
height: 33px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--tg-theme-button-text-color);
|
|
||||||
background-color: var(--tg-theme-button-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="cart"] {
|
|
||||||
--diameter: 4rem;
|
|
||||||
z-index: 999;
|
|
||||||
right: 5vw;
|
|
||||||
bottom: 5vw;
|
|
||||||
position: fixed;
|
|
||||||
width: var(--diameter);
|
|
||||||
height: var(--diameter);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 100%;
|
|
||||||
background-color: var(--tg-theme-button-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section="cart"]>i.icon.shopping.cart {
|
|
||||||
top: -1px;
|
|
||||||
color: var(--tg-theme-button-text-color);
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
@charset "UTF-8";
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--text-light: #fafaff;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
text-decoration: none;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
font-family: "DejaVu";
|
|
||||||
color: var(--tg-theme-text-color);
|
|
||||||
transition: 0.1s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--tg-theme-link-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-x: clip;
|
|
||||||
background-color: var(--tg-theme-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
aside {}
|
|
||||||
|
|
||||||
header {}
|
|
||||||
|
|
||||||
main {
|
|
||||||
--offset-x: 2%;
|
|
||||||
padding: 0 var(--offset-x);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 26px;
|
|
||||||
transition: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>*[data-section] {
|
|
||||||
--gap: 16px;
|
|
||||||
--width: calc(100% - var(--gap) * 2);
|
|
||||||
width: var(--width);
|
|
||||||
}
|
|
||||||
|
|
||||||
main>section[data-section]>p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
main>search {
|
|
||||||
--gap: 16px;
|
|
||||||
--border-width: 1px;
|
|
||||||
width: var(--width);
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
border-radius: 1.375rem;
|
|
||||||
backdrop-filter: contrast(0.8);
|
|
||||||
border: 2px solid transparent;
|
|
||||||
overflow: clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
search:has(input:is(:focus, :active)) {
|
|
||||||
border-color: var(--tg-theme-accent-text-color);
|
|
||||||
transition: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
search>label {
|
|
||||||
margin-inline-start: 0.75rem;
|
|
||||||
width: 1.5rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
search>label>i.icon {
|
|
||||||
color: var(--tg-theme-subtitle-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
search:has(input:is(:focus, :active))>label>i.icon {
|
|
||||||
color: var(--tg-theme-accent-text-color);
|
|
||||||
transition: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
search>input {
|
|
||||||
width: 100%;
|
|
||||||
max-width: calc(100% - 3.25rem);
|
|
||||||
height: 2.5rem;
|
|
||||||
touch-action: manipulation;
|
|
||||||
padding: calc(.4375rem - var(--border-width)) calc(.625rem - var(--border-width)) calc(.5rem - var(--border-width)) calc(.75rem - var(--border-width));
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
search>input:disabled {
|
|
||||||
cursor: progress;
|
|
||||||
color: var(--tg-theme-subtitle-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
search:has(input:disabled) {
|
|
||||||
backdrop-filter: contrast(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(button, a[type="button"]) {
|
|
||||||
padding: 8px 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--tg-theme-button-text-color);
|
|
||||||
background-color: var(--tg-theme-button-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
height: 33px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a[type="button"] {
|
|
||||||
height: 23px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(button, a[type="button"]):is(:hover) {
|
|
||||||
filter: brightness(120%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(button, a[type="button"]):active {
|
|
||||||
filter: brightness(80%);
|
|
||||||
transition: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2 {
|
|
||||||
margin: 28px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {}
|
|
||||||
|
|
||||||
.unselectable {
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-khtml-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
*
|
|
||||||
!.gitignore
|
|
||||||
!*.sample
|
|
Binary file not shown.
|
@ -1,3 +0,0 @@
|
||||||
<section data-section="cart">
|
|
||||||
<i class="icon shopping cart"></i>
|
|
||||||
</section>
|
|
|
@ -1,12 +0,0 @@
|
||||||
{% if categories is not empty %}
|
|
||||||
<section class="unselectable" data-section="catalog" data-catalog-type="categories"
|
|
||||||
data-catalog-level="{{ level ?? 0 }}">
|
|
||||||
{% for category in categories %}
|
|
||||||
<a id="{{ category.getId() }}" class="category" type="button" onclick="return core.catalog.category(this);"
|
|
||||||
data-category-identifier="{{ category.identifier }}">
|
|
||||||
<img src="{{ category.images.0.storage }}" alt="{{ category.name[ account.language ?? settings.language ?? 'en' ] }}" ondrugstart="return false;">
|
|
||||||
<p>{{ category.name[ account.language ?? settings.language ?? 'en' ] }}</p>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{% if filters is not empty %}
|
|
||||||
<section data-section="filters" data-filter="brand">
|
|
||||||
{% for brand in filters.brands %}
|
|
||||||
<input class="menu" name="brand" type="radio" id="brand_{{ loop.index }}" checked>
|
|
||||||
<label for="brand_{{ loop.index }}" class="menu option">{{ brand }}</label>
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
|
@ -1,13 +0,0 @@
|
||||||
{% if products is not empty %}
|
|
||||||
<section class="unselectable" data-section="catalog" data-catalog-type="products" data-catalog-level="{{ level ?? 0 }}">
|
|
||||||
{% for product in products %}
|
|
||||||
<article id="{{ product.getId() }}" class="product unselectable">
|
|
||||||
<img src="/images/{{ product.getKey() }}/1.jpg" alt="{{ product.title.ru }}" ondrugstart="return false;" onclick="return core.catalog.product({{ product.getKey() }});">
|
|
||||||
<a class="title" title="{{ product.title.ru }}" onclick="return core.catalog.product({{ product.getKey() }});">{{ product.title.ru }}</a>
|
|
||||||
<small class="description" title="{{ product.description.ru }}" onclick="return core.catalog.product({{ product.getKey() }});">{{ product.description.ru | length > 160 ? product.description.ru | slice(0, 160) ~
|
|
||||||
'...' : product.description.ru }}</small>
|
|
||||||
<button title="Добавить в корзину" onclick="return core.catalog.cart.add({{ product.getKey() }})">{{ product.cost }}р</button>
|
|
||||||
</article>
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
|
@ -1,34 +0,0 @@
|
||||||
{% macro card(product) %}
|
|
||||||
{% set title = product.title.ru ~ ' ' ~ product.brand.ru ~ ' ' ~ product.dimensions.x ~ 'x' ~ product.dimensions.y ~ 'x'
|
|
||||||
~ product.dimensions.z ~ ' ' ~ product.weight ~ 'г' %}
|
|
||||||
<article id="{{ product.getId() }}" class="product unselectable">
|
|
||||||
<a onclick="core.catalog.product({{ product.getKey() }})">
|
|
||||||
<img src="{{ product.images.0.storage }}" alt="{{ product.title.ru }}" ondrugstart="return false;">
|
|
||||||
<p class="title" title="{{ product.title.ru }}">
|
|
||||||
{{ title | length > 45 ? title | slice(0, 45) ~ '...' : title }}
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
<button title="Добавить в корзину" onclick="catalog.cart.add({{ product.getKey() }})">
|
|
||||||
{{ product.cost }}р
|
|
||||||
</button>
|
|
||||||
</article>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% if products is not empty %}
|
|
||||||
<section class="unselectable" data-section="catalog" data-catalog-type="products" data-catalog-level="{{ level ?? 0 }}">
|
|
||||||
<div class="column">
|
|
||||||
{% for product in products %}
|
|
||||||
{% if loop.index % 2 == 0 %}
|
|
||||||
{{ _self.card(product) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
{% for product in products %}
|
|
||||||
{% if loop.index % 2 == 1 %}
|
|
||||||
{{ _self.card(product) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
|
@ -1,36 +0,0 @@
|
||||||
{% macro card(product) %}
|
|
||||||
<article id="{{ product.getId() }}" class="product unselectable">
|
|
||||||
<img src="/images/{{ product.getKey() }}/1.jpg" alt="{{ product.title.ru }}" ondrugstart="return false;"
|
|
||||||
onclick="return core.catalog.product({{ product.getKey() }});">
|
|
||||||
<a class="title" title="{{ product.title.ru }}" onclick="return core.catalog.product({{ product.getKey() }});">{{
|
|
||||||
product.title.ru | length > 35 ? product.title.ru | slice(0, 35) ~ '...' : product.title.ru }}</a>
|
|
||||||
<button title="Добавить в корзину" onclick="return core.catalog.cart.add({{ product.getKey() }})">{{ product.cost
|
|
||||||
}}р</button>
|
|
||||||
</article>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% if products is not empty %}
|
|
||||||
<section class="unselectable" data-section="catalog" data-catalog-type="products" data-catalog-level="{{ level ?? 0 }}">
|
|
||||||
<div class="column">
|
|
||||||
{% for product in products %}
|
|
||||||
{% if loop.index0 % 3 == 0 %}
|
|
||||||
{{ _self.card(product) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
{% for product in products %}
|
|
||||||
{% if loop.index0 % 3 == 1 %}
|
|
||||||
{{ _self.card(product) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
{% for product in products %}
|
|
||||||
{% if loop.index0 % 3 == 2 %}
|
|
||||||
{{ _self.card(product) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
|
@ -1,4 +0,0 @@
|
||||||
<search data-section="search">
|
|
||||||
<label><i class="icon search"></i></label>
|
|
||||||
<input placeholder="Поиск по каталогу" type="search" onkeyup="return core.catalog.search(event, this);" />
|
|
||||||
</search>
|
|
|
@ -1,25 +0,0 @@
|
||||||
{% extends "/themes/default/index.html" %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ parent() }}
|
|
||||||
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/catalog/2columns.css" />
|
|
||||||
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/search.css" />
|
|
||||||
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/shopping_cart.css" />
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
<h2 class="unselectable">Каталог</h2>
|
|
||||||
|
|
||||||
{% include "/themes/default/catalog/elements/search.html" %}
|
|
||||||
{% include "/themes/default/catalog/elements/categories.html" %}
|
|
||||||
{% include "/themes/default/catalog/elements/products/2columns.html" %}
|
|
||||||
<!-- {% include "/themes/default/catalog/elements/cart.html" %} -->
|
|
||||||
{% include "/themes/default/catalog/elements/filters.html" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ parent() }}
|
|
||||||
<script src="/js/catalog.js"></script>
|
|
||||||
<script src="/js/cart.js"></script>
|
|
||||||
<script src="/js/hotline.js"></script>
|
|
||||||
{% endblock %}
|
|
|
@ -1,10 +0,0 @@
|
||||||
{% block css %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<footer>
|
|
||||||
</footer>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,20 +0,0 @@
|
||||||
{% block title %}
|
|
||||||
<title>{% if head.title != empty %}{{head.title}}{% else %}ARMING{% endif %}</title>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block meta %}
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
{% for meta in head.metas %}
|
|
||||||
<meta {% for name, value in meta.attributes %}{{name}}="{{value}}" {% endfor %}>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
<!-- <link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/initialization.css" /> -->
|
|
||||||
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/main.css" />
|
|
||||||
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/loading_spinner.css" />
|
|
||||||
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/loading.css" />
|
|
||||||
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/window.css" />
|
|
||||||
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/fonts/dejavu.css" />
|
|
||||||
{% endblock %}
|
|
|
@ -1,10 +0,0 @@
|
||||||
{% block css %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<header>
|
|
||||||
</header>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,7 +0,0 @@
|
||||||
{% block js %}
|
|
||||||
<script src="https://telegram.org/js/telegram-web-app.js" defer></script>
|
|
||||||
<script src="/js/core.js" defer></script>
|
|
||||||
<script src="/js/damper.js"></script>
|
|
||||||
<script src="/js/telegram.js"></script>
|
|
||||||
<script src="/js/authentication.js"></script>
|
|
||||||
{% endblock %}
|
|
|
@ -1,14 +0,0 @@
|
||||||
{% extends "/themes/default/index.html" %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ parent() }}
|
|
||||||
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/catalog.css" />
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ parent() }}
|
|
||||||
<script src="/js/catalog.js" defer></script>
|
|
||||||
{% endblock %}
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\controllers;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\controllers\core;
|
||||||
|
|
||||||
|
// Framework for PHP
|
||||||
|
use mirzaev\minimal\http\enumerations\status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller of account
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\controllers
|
||||||
|
*
|
||||||
|
* @param array $errors Registry of errors
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Errors
|
||||||
|
*
|
||||||
|
* @var array $errors Registry of errors
|
||||||
|
*/
|
||||||
|
protected array $errors = [
|
||||||
|
'session' => [],
|
||||||
|
'account' => [],
|
||||||
|
'buffer' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write
|
||||||
|
*
|
||||||
|
* Write to the buffer
|
||||||
|
*
|
||||||
|
* @param mixed ...$parameters Parameters for writing to the buffer
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*
|
||||||
|
* @todo переделать под trait buffer
|
||||||
|
*/
|
||||||
|
public function write(mixed ...$parameters): null
|
||||||
|
{
|
||||||
|
if (!empty($parameters) && isset($this->account)) {
|
||||||
|
// Received parameters and initialized model with buffer trait
|
||||||
|
|
||||||
|
// Declaring the buffer of deserialized parameters
|
||||||
|
$deserialized = [];
|
||||||
|
|
||||||
|
foreach ($parameters as $name => $value) {
|
||||||
|
// Iterate over parameters
|
||||||
|
|
||||||
|
// Validation of the parameter value
|
||||||
|
if (mb_strlen(serialize($value)) > 4096) continue;
|
||||||
|
|
||||||
|
// Declaring the buffer of deserialized parameter
|
||||||
|
$parameter = null;
|
||||||
|
|
||||||
|
// Deserializing name to multidimensional array
|
||||||
|
foreach (array_reverse(explode('_', (string) $name)) as $key)
|
||||||
|
$parameter = [$key => $parameter ?? (is_string($value) && 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 document from ArangoDB
|
||||||
|
if (!empty($deserialized)) $this->account->write($deserialized, $this->errors['buffer']);
|
||||||
|
|
||||||
|
// Writing status of response
|
||||||
|
$this->response->status = status::created;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,375 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\controllers\api\acquirings;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\controllers\core,
|
||||||
|
mirzaev\huesos\models\order,
|
||||||
|
mirzaev\huesos\models\cart,
|
||||||
|
mirzaev\huesos\models\product,
|
||||||
|
mirzaev\huesos\models\telegram,
|
||||||
|
mirzaev\huesos\models\acquirings\robokassa as model,
|
||||||
|
mirzaev\huesos\models\enumerations\currency,
|
||||||
|
mirzaev\huesos\models\enumerations\language;
|
||||||
|
|
||||||
|
// Framework for PHP
|
||||||
|
use mirzaev\minimal\http\enumerations\content,
|
||||||
|
mirzaev\minimal\http\enumerations\status;
|
||||||
|
|
||||||
|
// Framework for ArangoDB
|
||||||
|
use mirzaev\arangodb\collection,
|
||||||
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
|
// Framework for Telegram
|
||||||
|
use Zanzara\Zanzara as zanzara,
|
||||||
|
Zanzara\Context as context,
|
||||||
|
Zanzara\Config as config,
|
||||||
|
Zanzara\Telegram\Type\Message as message;
|
||||||
|
|
||||||
|
// Event manager for PHP
|
||||||
|
use React\EventLoop\Loop as loop;
|
||||||
|
|
||||||
|
// Built-in libraries
|
||||||
|
use DateTime as datetime,
|
||||||
|
Exception as exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller of robokassa
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\controllers\api\acquirings
|
||||||
|
*
|
||||||
|
* @param array $errors Registry of errors
|
||||||
|
*
|
||||||
|
* @method null result()
|
||||||
|
* @method null success()
|
||||||
|
* @method null fail()
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
final class robokassa extends core
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Errors
|
||||||
|
*
|
||||||
|
* @var array $errors Registry of errors
|
||||||
|
*/
|
||||||
|
protected array $errors = [
|
||||||
|
'session' => [],
|
||||||
|
'account' => [],
|
||||||
|
'order' => [],
|
||||||
|
'robokassa' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result
|
||||||
|
*
|
||||||
|
* @param null
|
||||||
|
*/
|
||||||
|
public function result(
|
||||||
|
?string $OutSum = null,
|
||||||
|
?string $InvId = null,
|
||||||
|
?string $SignatureValue = null,
|
||||||
|
?string $PaymentMethod = null,
|
||||||
|
?string $IncSum = null,
|
||||||
|
?string $IncCurrLabel = null,
|
||||||
|
?string $IsTest = null,
|
||||||
|
?string $EMail = null,
|
||||||
|
?string $Fee = null,
|
||||||
|
string ...$other
|
||||||
|
): null {
|
||||||
|
// Searching for the order
|
||||||
|
$order = order::_read(
|
||||||
|
filter: 'd._key == @_key && d.paid != true',
|
||||||
|
sort: 'd.created DESC, d._key DESC',
|
||||||
|
amount: 1,
|
||||||
|
parameters: ['_key' => $InvId],
|
||||||
|
errors: $this->errors['robokassa']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($order instanceof order) {
|
||||||
|
// Initialized the order
|
||||||
|
|
||||||
|
if (model::result(identifier: (int) $InvId, cost: $OutSum, hash: $SignatureValue, test: (bool) $IsTest, errors: $this->errors['robokassa'])) {
|
||||||
|
// Verified the payment
|
||||||
|
|
||||||
|
// Initializing the cart
|
||||||
|
$cart = $order->cart();
|
||||||
|
|
||||||
|
if ($cart instanceof cart) {
|
||||||
|
// Initialized the cart
|
||||||
|
|
||||||
|
if ((float) ($cart->cost() + ($cart->buffer['delivery']['cost'] ?? 0)) === (float) $OutSum) {
|
||||||
|
// Full payment received
|
||||||
|
|
||||||
|
// Writing that the order being been paid
|
||||||
|
$order->paid = true;
|
||||||
|
/* $order->term = null; */
|
||||||
|
|
||||||
|
if (document::update($order->__document(), errors: $this->errors['order'])) {
|
||||||
|
// Writed into ArangoDB
|
||||||
|
|
||||||
|
// Initializing identifier of the order
|
||||||
|
$identifier = $order->__document()->getKey();
|
||||||
|
|
||||||
|
// Initializing the customer account
|
||||||
|
$customer = $order->account();
|
||||||
|
|
||||||
|
$config = new config();
|
||||||
|
$config->setParseMode(config::PARSE_MODE_MARKDOWN);
|
||||||
|
$config->useReactFileSystem(true);
|
||||||
|
/* $config->setLoop(loop::get()); */
|
||||||
|
|
||||||
|
$robot = new zanzara(TELEGRAM_KEY, $config);
|
||||||
|
|
||||||
|
// Sending the message
|
||||||
|
$robot->getTelegram()->sendMessage("✅ *Заказ \#$identifier оплачен*", ['chat_id' => $customer->identifier])
|
||||||
|
->then(function ($message) use ($robot, $cart, $order, $identifier, $customer) {
|
||||||
|
// Sended the message
|
||||||
|
|
||||||
|
// Initializing the chats for sending registry
|
||||||
|
$chats = [];
|
||||||
|
|
||||||
|
foreach ($this->settings->chats as $chat) {
|
||||||
|
// Iteration over chats
|
||||||
|
|
||||||
|
if ($chat['orders']) {
|
||||||
|
// Authorized to receive orders
|
||||||
|
|
||||||
|
// Writing into the chats for sending registry
|
||||||
|
$chats[] = $chat['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($chats) > 0) {
|
||||||
|
// Initialized chats
|
||||||
|
|
||||||
|
// Declaring the formatted list of products for message
|
||||||
|
$list = '';
|
||||||
|
|
||||||
|
// Declaring total cost of products
|
||||||
|
$cost = $cart->cost($list);
|
||||||
|
|
||||||
|
// Escaping the formatted list of products for the message
|
||||||
|
$list = telegram::unmarkdown($list);
|
||||||
|
|
||||||
|
// Initializing currency symbol
|
||||||
|
$symbol = ($account->currency ?? currency::rub)->symbol();
|
||||||
|
|
||||||
|
// Declaring delivery texts
|
||||||
|
$delivery_cost = $delivery_days = $delivery_address = '';
|
||||||
|
|
||||||
|
if (!empty($cart->buffer['delivery'])) {
|
||||||
|
// Initialized delibery data
|
||||||
|
|
||||||
|
// Initializing delivery cost for message
|
||||||
|
$delivery_cost = telegram::unmarkdown((string) $cart->buffer['delivery']['cost']);
|
||||||
|
|
||||||
|
// Initializing delivery days for message
|
||||||
|
$delivery_days = telegram::unmarkdown((string) $cart->buffer['delivery']['days']);
|
||||||
|
|
||||||
|
// Initializing delivery address for message
|
||||||
|
$delivery_address = telegram::unmarkdown($cart->buffer['delivery']['location']['name'] . ', ' . $cart->buffer['delivery']['street']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing receiver domain for the message
|
||||||
|
$domain = telegram::unmarkdown($customer->domain);
|
||||||
|
|
||||||
|
// Initializing receiver SIM for the message
|
||||||
|
$sim = telegram::unmarkdown((string) $customer->receiver['sim']);
|
||||||
|
|
||||||
|
// Initializing receiver name for the message
|
||||||
|
$name = telegram::unmarkdown($customer->receiver['name']);
|
||||||
|
|
||||||
|
// Initializing receiver address for the message
|
||||||
|
$address = telegram::unmarkdown($customer->receiver['address']);
|
||||||
|
|
||||||
|
// Initializing the message cost part
|
||||||
|
$part_cost = "*Стоимость:* $cost$symbol";
|
||||||
|
if (!empty($delivery_cost)) $part_cost .= " \+ $delivery_cost$symbol";
|
||||||
|
if (!empty($delivery_days)) $part_cost .= " \($delivery_days дней\)";
|
||||||
|
|
||||||
|
// Initializing the message delivery part
|
||||||
|
$part_delivery = '';
|
||||||
|
if (!empty($delivery_address)) $part_delivery .= "*Адрес доставки:* $delivery_address";
|
||||||
|
if (!empty($part_delivery)) $part_delivery = "\n$part_delivery";
|
||||||
|
|
||||||
|
// Initializing the message receiver part
|
||||||
|
$part_receiver = "*Получатель:* @$domain";
|
||||||
|
if (!empty($sim)) $part_receiver .= " $sim";
|
||||||
|
if (!empty($name)) $part_receiver .= "\n*$name*";
|
||||||
|
if (!empty($address)) $part_receiver .= "\n\n*Адрес:* $address";
|
||||||
|
|
||||||
|
// Initializing the message commentary part
|
||||||
|
$part_commentary = '';
|
||||||
|
if (!empty($order->commentary)) $part_commentary .= "\n\n*Комментарий:* " . $order->commentary;
|
||||||
|
|
||||||
|
// Sending messages
|
||||||
|
$robot->getTelegram()->sendBulkMessage(
|
||||||
|
$chats,
|
||||||
|
<<<TXT
|
||||||
|
📦 *Заказ* \#$identifier
|
||||||
|
|
||||||
|
$list
|
||||||
|
$part_cost$part_delivery
|
||||||
|
|
||||||
|
$part_receiver$part_commentary
|
||||||
|
TXT,
|
||||||
|
[
|
||||||
|
'reply_markup' => [
|
||||||
|
'inline_keyboard' => [
|
||||||
|
[
|
||||||
|
['text' => '✉️ Чат с покупателем', 'url' => 'https://t.me/' . $customer->domain]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'disable_notification' => true
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->write("OK$identifier")
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
} else throw new exception('Failed to update the order');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Success
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public function success(
|
||||||
|
?string $OutSum = null,
|
||||||
|
?string $InvId = null,
|
||||||
|
?string $SignatureValue = null,
|
||||||
|
?string $IsTest = null,
|
||||||
|
?string $Culture = null,
|
||||||
|
string ...$other
|
||||||
|
): null {
|
||||||
|
// Searching for the order
|
||||||
|
$order = order::_read(
|
||||||
|
filter: 'd._key == @_key',
|
||||||
|
sort: 'd.created DESC, d._key DESC',
|
||||||
|
amount: 1,
|
||||||
|
parameters: ['_key' => $InvId],
|
||||||
|
errors: $this->errors['robokassa']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($order instanceof order) {
|
||||||
|
// Initialized the order
|
||||||
|
|
||||||
|
if (model::success(identifier: (int) $InvId, cost: $OutSum, hash: $SignatureValue, test: (bool) $IsTest, errors: $this->errors['robokassa'])) {
|
||||||
|
// Verified the payment
|
||||||
|
|
||||||
|
if ($IsTest == 1) {
|
||||||
|
// Enabled the test mode
|
||||||
|
|
||||||
|
// Initializing a paragraph abount the test mode
|
||||||
|
$this->view->test = $this->language === language::ru ? 'Тестовый режим' : 'Test mode';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render page
|
||||||
|
$page = $this->view->render('api/acquirings/robokassa/success.html', [
|
||||||
|
'h2' => $this->language === language::ru ? 'Заказ #' . $order->getKey() . ' оплачен' : 'Order #' . $order->getKey() . ' paid',
|
||||||
|
'identifier' => $order->getKey(),
|
||||||
|
'closing' => [
|
||||||
|
'title' => $this->language === language::ru ? 'Закрытие окна' : 'Window closing',
|
||||||
|
'iterator' => 30
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->write($page)
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
// Deinitializing rendered page
|
||||||
|
unset($page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fail
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public function fail(
|
||||||
|
?string $OutSum = null,
|
||||||
|
?string $InvId = null,
|
||||||
|
?string $IsTest = null,
|
||||||
|
?string $Culture = null,
|
||||||
|
string ...$other
|
||||||
|
): null {
|
||||||
|
// Searching for the order
|
||||||
|
$order = order::_read(
|
||||||
|
filter: 'd._key == @_key',
|
||||||
|
sort: 'd.created DESC, d._key DESC',
|
||||||
|
amount: 1,
|
||||||
|
parameters: ['_key' => $InvId],
|
||||||
|
errors: $this->errors['robokassa']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($order instanceof order) {
|
||||||
|
// Initialized the order
|
||||||
|
|
||||||
|
if ($IsTest == 1) {
|
||||||
|
// Enabled the test mode
|
||||||
|
|
||||||
|
// Initializing a paragraph abount the test mode
|
||||||
|
$this->view->test = $this->language === language::ru ? 'Тестовый режим' : 'Test mode';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render page
|
||||||
|
$page = $this->view->render('api/acquirings/robokassa/fail.html', [
|
||||||
|
'h2' => $this->language === language::ru ? 'Заказ #' . $order->getKey() . ' не оплачен' : 'Order #' . $order->getKey() . ' not paid',
|
||||||
|
'identifier' => $order->getKey(),
|
||||||
|
'closing' => [
|
||||||
|
'title' => $this->language === language::ru ? 'Закрытие окна' : 'Window closing',
|
||||||
|
'iterator' => 30
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->write($page)
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
// Deinitializing rendered page
|
||||||
|
unset($page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,618 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\controllers;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\controllers\core,
|
||||||
|
mirzaev\huesos\models\cart as model,
|
||||||
|
mirzaev\huesos\models\product,
|
||||||
|
mirzaev\huesos\models\menu,
|
||||||
|
mirzaev\huesos\models\telegram,
|
||||||
|
mirzaev\huesos\models\enumerations\currency,
|
||||||
|
mirzaev\huesos\models\enumerations\language;
|
||||||
|
|
||||||
|
// Framework for PHP
|
||||||
|
use mirzaev\minimal\http\enumerations\content,
|
||||||
|
mirzaev\minimal\http\enumerations\status;
|
||||||
|
|
||||||
|
// Framework for Telegram
|
||||||
|
use Zanzara\Zanzara as zanzara,
|
||||||
|
Zanzara\Context as context,
|
||||||
|
Zanzara\Config as config,
|
||||||
|
Zanzara\Telegram\Type\Message as message;
|
||||||
|
|
||||||
|
// Event manager for PHP
|
||||||
|
use React\EventLoop\Loop as loop;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller of cart
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\controllers
|
||||||
|
*
|
||||||
|
* @param model|null $cart Instance of the cart
|
||||||
|
* @param array $errors Registry of errors
|
||||||
|
*
|
||||||
|
* @method null index() HTML-document with shopping cart and delivery settings
|
||||||
|
* @method null product(int|string|null $identifier, ?string $type, int|string|null $amount) Change status of the product in the cart
|
||||||
|
* @method null summary() Information about products in the cart
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Cart
|
||||||
|
*
|
||||||
|
* @var model|null $cart Instance of the cart
|
||||||
|
*/
|
||||||
|
protected readonly ?model $cart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Errors
|
||||||
|
*
|
||||||
|
* @var array $errors Registry of errors
|
||||||
|
*/
|
||||||
|
protected array $errors = [
|
||||||
|
'session' => [],
|
||||||
|
'account' => [],
|
||||||
|
'menu' => [],
|
||||||
|
'cart' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index
|
||||||
|
*
|
||||||
|
* HTML-document with shopping cart and delivery settings
|
||||||
|
*
|
||||||
|
* @param null
|
||||||
|
*/
|
||||||
|
public function index(): null
|
||||||
|
{
|
||||||
|
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']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Universalizing
|
||||||
|
if ($this->view->menu instanceof _document) $this->view->menu = [$this->view->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)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($this->view->cart['products'])) {
|
||||||
|
// Initialized products
|
||||||
|
|
||||||
|
// Declaring buffer of formatted products
|
||||||
|
$formatted = [];
|
||||||
|
|
||||||
|
foreach ($this->view->cart['products'] as $product) {
|
||||||
|
// Iterating over products
|
||||||
|
|
||||||
|
// Formatting products
|
||||||
|
for ($i = 0; $i < $product['amount']; ++$i) {
|
||||||
|
$formatted[] = [
|
||||||
|
'weight' => $product['document']['weight'] ?? 0,
|
||||||
|
...$product['document']['dimensions']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deinitializing unnecessary variables
|
||||||
|
unset($product);
|
||||||
|
|
||||||
|
// Initializing formatted products
|
||||||
|
$this->view->formatted = $formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_search('address', $this->settings->input['deliveries']['site'], true) !== false) {
|
||||||
|
// The deliveries input is enabled for the site
|
||||||
|
|
||||||
|
// Declaring the deliveries list
|
||||||
|
$deliveries = [];
|
||||||
|
|
||||||
|
foreach ($this->settings->deliveries as $key => $value) {
|
||||||
|
// Iterating over deliveries
|
||||||
|
|
||||||
|
if ($value['enabled']) {
|
||||||
|
// The delivery is enabled
|
||||||
|
|
||||||
|
// Writing into the deliveries list
|
||||||
|
$deliveries[$key] = ['label' => $value['label']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing the deliveries list into the templater variables
|
||||||
|
$this->view->deliveries = $deliveries;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($this->request->headers['accept'], content::json->value)) {
|
||||||
|
// Request for JSON response
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->json([
|
||||||
|
'main' => '',
|
||||||
|
'errors' => $this->errors
|
||||||
|
])
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
} else if (str_contains($this->request->headers['accept'], content::any->value)) {
|
||||||
|
// Request for any response
|
||||||
|
|
||||||
|
// Render page
|
||||||
|
$page = $this->view->render('cart/page.html', [
|
||||||
|
'h2' => $this->language === language::ru ? 'Корзина' : 'Cart' // @see https://git.mirzaev.sexy/mirzaev/huesos/issues/1
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->write($page)
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
// Deinitializing rendered page
|
||||||
|
unset($page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Product
|
||||||
|
*
|
||||||
|
* Change status of the product in the cart
|
||||||
|
*
|
||||||
|
* @param int|string|null $identifier Product identifier
|
||||||
|
* @param string|null $type Action type (toggle, write, delete, set)
|
||||||
|
* @param int|string|null $amount Amount of actions with the product (for write, delete and differently used by set)
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*
|
||||||
|
* @todo
|
||||||
|
* 1. Add a limit on adding products to the cart based on the number of products in stock
|
||||||
|
*/
|
||||||
|
public function product(
|
||||||
|
int|string|null $identifier = null,
|
||||||
|
?string $type = null,
|
||||||
|
int|string|null $amount = null
|
||||||
|
): null {
|
||||||
|
if (str_contains($this->request->headers['accept'], content::json->value)) {
|
||||||
|
// Request for JSON response
|
||||||
|
|
||||||
|
// Declaring buffer with amount of the product in the cart
|
||||||
|
$cart = 0;
|
||||||
|
|
||||||
|
// Validating @todo add throwing errors
|
||||||
|
if (isset($identifier) && preg_match('/[\d]+/', urldecode($identifier), $matches)) $identifier = (int) $matches[0];
|
||||||
|
else unset($identifier);
|
||||||
|
|
||||||
|
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 buffer with amount of the product in the cart
|
||||||
|
$cart = $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
|
||||||
|
if (isset($type) && preg_match('/[\w]+/', urldecode($type), $matches)) $type = $matches[0];
|
||||||
|
else unset($type);
|
||||||
|
|
||||||
|
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 ($cart > 0) {
|
||||||
|
// The cart has the product
|
||||||
|
|
||||||
|
// Deleting the product from the cart
|
||||||
|
$this->cart->delete(product: $product, amount: $cart, errors: $this->errors['cart']);
|
||||||
|
|
||||||
|
// Reinitializing the buffer with amount of the product in the cart
|
||||||
|
$cart = 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
|
||||||
|
$cart = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Received not the "toggle" command
|
||||||
|
|
||||||
|
// Validating @todo add throwing errors
|
||||||
|
if (isset($amount) && preg_match('/[\d]+/', urldecode($amount), $matches)) $amount = (int) $matches[0];
|
||||||
|
else unset($amount);
|
||||||
|
|
||||||
|
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 ($cart + $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
|
||||||
|
$cart += $amount;
|
||||||
|
}
|
||||||
|
} else if ($type === 'delete') {
|
||||||
|
// Decrease amount of the product in the cart
|
||||||
|
|
||||||
|
if ($cart - $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
|
||||||
|
$cart -= $amount;
|
||||||
|
}
|
||||||
|
} else if ($type === 'set') {
|
||||||
|
// Set amount of the product in the cart
|
||||||
|
|
||||||
|
if ($amount > -1 && $amount < 101) {
|
||||||
|
// Validated amount to setting
|
||||||
|
|
||||||
|
if ($amount > $cart) {
|
||||||
|
// Requested amount more than actual amount of the product in the cart
|
||||||
|
|
||||||
|
// Writing the product to the cart
|
||||||
|
$this->cart->write(product: $product, amount: $amount - $cart, 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: $cart - $amount, errors: $this->errors['cart']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinitializing the buffer with amount of the product in the cart
|
||||||
|
$cart = $amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->json([
|
||||||
|
'amount' => $cart, // $cart does not store a real value, but is calculated without a repeated request to ArangoDB
|
||||||
|
'errors' => $this->errors
|
||||||
|
])
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
// Deinitializing buffer with amount of the product in the cart
|
||||||
|
unset($cart);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summary
|
||||||
|
*
|
||||||
|
* Information about products in the cart
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public function summary(): null
|
||||||
|
{
|
||||||
|
if (str_contains($this->request->headers['accept'], content::json->value)) {
|
||||||
|
// Request for JSON response
|
||||||
|
|
||||||
|
// Initializing the cart
|
||||||
|
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
|
||||||
|
|
||||||
|
if ($this->cart instanceof model) {
|
||||||
|
// Initialized the cart
|
||||||
|
|
||||||
|
// Initializing products in the cart
|
||||||
|
$products = $this->cart->products(language: $this->language, currency: $this->currency);
|
||||||
|
|
||||||
|
if (!empty($products)) {
|
||||||
|
// Initialized products
|
||||||
|
|
||||||
|
// Initializing summary data of the cart
|
||||||
|
$summary = $this->cart?->summary(currency: $this->currency, errors: $this->errors['cart']);
|
||||||
|
|
||||||
|
// Declaring buffer of formatted products
|
||||||
|
$formatted = [];
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
// Iterating over products
|
||||||
|
|
||||||
|
// Formatting products
|
||||||
|
for ($i = 0; $i < $product['amount']; ++$i) {
|
||||||
|
$formatted[] = [
|
||||||
|
'weight' => $product['document']['weight'] ?? 0,
|
||||||
|
...$product['document']['dimensions']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deinitializing unnecessary variables
|
||||||
|
unset($product);
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->json([
|
||||||
|
'cost' => $summary['cost'] ?? 0,
|
||||||
|
'amount' => $summary['amount'] ?? 0,
|
||||||
|
'products' => $formatted ?? [],
|
||||||
|
'errors' => $this->errors
|
||||||
|
])
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
// Deinitializing summary data of the cart
|
||||||
|
unset($summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share
|
||||||
|
*
|
||||||
|
* Share the cart
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public function share(): null
|
||||||
|
{
|
||||||
|
if (str_contains($this->request->headers['accept'], content::json->value)) {
|
||||||
|
// Request for JSON response
|
||||||
|
|
||||||
|
// Initializing the cart
|
||||||
|
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
|
||||||
|
|
||||||
|
if ($this->cart instanceof model) {
|
||||||
|
// Initialized the cart
|
||||||
|
|
||||||
|
if ($share = $this->cart->share ?? $this->cart->share()) {
|
||||||
|
// The cart is available for sharing
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->json([
|
||||||
|
'share' => $share,
|
||||||
|
'errors' => $this->errors
|
||||||
|
])
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deinitializing unnecessary variables
|
||||||
|
unset($hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach
|
||||||
|
*
|
||||||
|
* Attach the cart
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*
|
||||||
|
* @todo кажется я сделал хуйню
|
||||||
|
*/
|
||||||
|
public function attach(?string $share = null): null
|
||||||
|
{
|
||||||
|
if (str_contains($this->request->headers['accept'], content::json->value)) {
|
||||||
|
// Request for JSON response
|
||||||
|
|
||||||
|
if ($this->account) {
|
||||||
|
// Initialized the account
|
||||||
|
|
||||||
|
$config = new config();
|
||||||
|
$config->setParseMode(config::PARSE_MODE_MARKDOWN);
|
||||||
|
$config->useReactFileSystem(true);
|
||||||
|
/* $config->setLoop(loop::get()); */
|
||||||
|
|
||||||
|
$robot = new zanzara(TELEGRAM_KEY, $config);
|
||||||
|
|
||||||
|
// Initializing cart
|
||||||
|
$cart = model::_read(
|
||||||
|
filter: 'd.share == @share',
|
||||||
|
sort: 'd.updated DESC, d.created DESC, d._key DESC',
|
||||||
|
amount: 1,
|
||||||
|
page: 1,
|
||||||
|
parameters: ['share' => $share]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Deinitializing unnecessary variables
|
||||||
|
unset($share);
|
||||||
|
|
||||||
|
if ($cart instanceof model) {
|
||||||
|
// Initialized the cart
|
||||||
|
|
||||||
|
// Unsharing the cart
|
||||||
|
$cart->unshare();
|
||||||
|
|
||||||
|
// Connecting the cart to the account
|
||||||
|
$edge = $this->account->connect($cart);
|
||||||
|
|
||||||
|
if (!empty($edge)) {
|
||||||
|
// Connected the cart to the account
|
||||||
|
|
||||||
|
// Initializing products in the cart
|
||||||
|
$products = $cart->products(language: $this->account->language ?? language::ru, currency: $this->account->currency ?? currency::rub);
|
||||||
|
|
||||||
|
if (!empty($products)) {
|
||||||
|
// Initialized products in the cart
|
||||||
|
|
||||||
|
// Declaring the formatted list of products for message
|
||||||
|
$list = '';
|
||||||
|
|
||||||
|
// Declaring total cost of products
|
||||||
|
$cost = $cart->cost($list);
|
||||||
|
|
||||||
|
// Escaping the formatted list of products for the message
|
||||||
|
$list = telegram::unmarkdown($list);
|
||||||
|
|
||||||
|
// Deinitializing unnecessary variables
|
||||||
|
unset($products, $product, $row);
|
||||||
|
|
||||||
|
// Initializing identifier of the cart
|
||||||
|
$identifier = $cart->getKey();
|
||||||
|
|
||||||
|
// Initializing currency symbol
|
||||||
|
$symbol = ($this->account->currency ?? currency::rub)->symbol();
|
||||||
|
|
||||||
|
// Declaring delivery texts
|
||||||
|
$delivery_cost = $delivery_days = $delivery_address = '';
|
||||||
|
|
||||||
|
if (!empty($cart->buffer['delivery'])) {
|
||||||
|
// Initialized delibery data
|
||||||
|
|
||||||
|
// Initializing delivery cost for message
|
||||||
|
$delivery_cost = telegram::unmarkdown((string) $cart->buffer['delivery']['cost']);
|
||||||
|
|
||||||
|
// Initializing delivery days for message
|
||||||
|
$delivery_days = telegram::unmarkdown((string) $cart->buffer['delivery']['days']);
|
||||||
|
|
||||||
|
// Initializing delivery address for message
|
||||||
|
$delivery_address = telegram::unmarkdown($cart->buffer['delivery']['location']['name'] . ', ' . $cart->buffer['delivery']['street']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing the message cost part
|
||||||
|
$part_cost = "*Стоимость:* $cost$symbol";
|
||||||
|
if (!empty($delivery_cost)) $part_cost .= " \+ $delivery_cost$symbol";
|
||||||
|
if (!empty($delivery_days)) $part_cost .= " \($delivery_days дней\)";
|
||||||
|
|
||||||
|
// Initializing the message delivery part
|
||||||
|
$part_delivery = '';
|
||||||
|
if (!empty($delivery_address)) $part_cost .= "*Адрес доставки:* $delivery_address";
|
||||||
|
if (!empty($part_delivery)) $part_delivery = "\n$part_delivery";
|
||||||
|
|
||||||
|
$robot->getTelegram()->sendMessage(
|
||||||
|
<<<TXT
|
||||||
|
🛒 *Добавлена корзина* \#$identifier
|
||||||
|
|
||||||
|
$list
|
||||||
|
$part_cost$part_delivery
|
||||||
|
TXT,
|
||||||
|
[
|
||||||
|
'chat_id' => $this->account->identifier,
|
||||||
|
'reply_markup' => [
|
||||||
|
'inline_keyboard' => [
|
||||||
|
[
|
||||||
|
['text' => '🚚 Оформить доставку', 'callback_data' => 'cart_delivery'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'disable_notification' => true
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deinitializing unnecessary variables
|
||||||
|
unset($cart, $list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deinitializing unnecessary variables
|
||||||
|
unset($cart);
|
||||||
|
|
||||||
|
$robot->getLoop()->run();
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->json([
|
||||||
|
'success' => true,
|
||||||
|
'errors' => $this->errors
|
||||||
|
])
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,482 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\controllers;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\controllers\core,
|
||||||
|
mirzaev\huesos\models\enumerations\language,
|
||||||
|
mirzaev\huesos\models\catalog as model,
|
||||||
|
mirzaev\huesos\models\entry,
|
||||||
|
mirzaev\huesos\models\category,
|
||||||
|
mirzaev\huesos\models\product,
|
||||||
|
mirzaev\huesos\models\cart,
|
||||||
|
mirzaev\huesos\models\menu;
|
||||||
|
|
||||||
|
// Framework for PHP
|
||||||
|
use mirzaev\minimal\http\enumerations\content,
|
||||||
|
mirzaev\minimal\http\enumerations\protocol,
|
||||||
|
mirzaev\minimal\http\request;
|
||||||
|
|
||||||
|
// Library for ArangoDB
|
||||||
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller of catalog
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\controllers
|
||||||
|
*
|
||||||
|
* @param cart|null $cart Instance of the cart
|
||||||
|
* @param array $errors Registry of errors
|
||||||
|
*
|
||||||
|
* @method null index(?string $product, ?string $category, ?string $brand, ?string $sort, ?string $text) Catalog
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Cart
|
||||||
|
*
|
||||||
|
* @var model|null $cart Instance of the cart
|
||||||
|
*/
|
||||||
|
protected readonly ?cart $cart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Errors
|
||||||
|
*
|
||||||
|
* @var array $errors Registry of errors
|
||||||
|
*/
|
||||||
|
protected array $errors = [
|
||||||
|
'session' => [],
|
||||||
|
'account' => [],
|
||||||
|
'menu' => [],
|
||||||
|
'catalog' => [],
|
||||||
|
'cart' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catalog
|
||||||
|
*
|
||||||
|
* Browsing the catalog and receiving product data at the same time
|
||||||
|
*
|
||||||
|
* Receiving product data is necessary so that you can simultaneously open the product card at the desired location when opening the page
|
||||||
|
*
|
||||||
|
* @param string|null $product Product identifier (&product=1)
|
||||||
|
* @param string|null $category Category identifier (&category=1)
|
||||||
|
* @param string|null $brand Brand name (&brand=mirzaev)
|
||||||
|
* @param string|null $sort Sort typ (&sort=cost)
|
||||||
|
* @param string|null $text Search text (&text=zaloopa)
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public function index(
|
||||||
|
?string $product = null,
|
||||||
|
?string $category = null,
|
||||||
|
?string $brand = null,
|
||||||
|
?string $sort = null,
|
||||||
|
?string $text = null
|
||||||
|
): null {
|
||||||
|
// Initializing the cart
|
||||||
|
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
|
||||||
|
|
||||||
|
// Initializing summary data of the cart
|
||||||
|
$summary = $this->cart?->summary(currency: $this->currency, errors: $this->errors['cart']);
|
||||||
|
|
||||||
|
// Initializing the cart data
|
||||||
|
$this->view->cart = [
|
||||||
|
'products' => $this->cart?->products(language: $this->language, currency: $this->currency),
|
||||||
|
'summary' => $summary
|
||||||
|
];
|
||||||
|
|
||||||
|
// Validating received product identifier
|
||||||
|
if (isset($product) && preg_match('/[\d]+/', $product, $matches)) $product = (int) $matches[0];
|
||||||
|
else unset($product);
|
||||||
|
|
||||||
|
if (isset($product)) {
|
||||||
|
// Received and validated product identifier
|
||||||
|
|
||||||
|
// 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: '{_id: d._id, 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: {"200": d.images[*].storage["200"], "800": d.images[*].storage["800"]}}',
|
||||||
|
language: $this->language,
|
||||||
|
currency: $this->currency,
|
||||||
|
parameters: ['identifier' => $product],
|
||||||
|
errors: $this->errors['catalog']
|
||||||
|
);
|
||||||
|
|
||||||
|
// This is only for generate product card @todo need to move that in backed templated
|
||||||
|
if (!empty($this->view->product)) {
|
||||||
|
// Initialized the product
|
||||||
|
|
||||||
|
// Writing data about being in the cart
|
||||||
|
$this->view->product = [
|
||||||
|
'cart' => [
|
||||||
|
'amount' => $this->view->cart['products'][$this->view->product->getId()]['amount'] ?? 0,
|
||||||
|
'text' => [
|
||||||
|
'cart' => $this->language === language::ru ? 'Корзина' : 'Cart',
|
||||||
|
'add' => $this->language === language::ru ? 'Добавить в корзину' : 'Add to the cart',
|
||||||
|
'added' => $this->language === language::ru ? 'Добавлено в корзину' : 'Added to the cart'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
] + $this->view->product->getAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intializing buffer of query parameters
|
||||||
|
$parameters = [];
|
||||||
|
|
||||||
|
// Initializing buffer of filters query (AQL)
|
||||||
|
$_filters = 'd.deleted != true && d.hidden != true';
|
||||||
|
|
||||||
|
// Validating received brand name
|
||||||
|
if (isset($brand) && preg_match('/[\w\s]+/u', urldecode($brand), $matches)) $brand = $matches[0];
|
||||||
|
else unset($brand);
|
||||||
|
|
||||||
|
if (isset($brand)) {
|
||||||
|
// Received and validated filter by brand
|
||||||
|
|
||||||
|
// Writing to the buffer of filters query (AQL)
|
||||||
|
$_filters .= ' && d.brand.@language == @brand';
|
||||||
|
|
||||||
|
// Writing to the buffer of query parameters
|
||||||
|
$parameters['brand'] = $brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing to the session buffer
|
||||||
|
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: ['catalog_filters_brand' => $brand ?? null]));
|
||||||
|
|
||||||
|
// Writing to the account buffer
|
||||||
|
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: ['catalog_filters_brand' => $brand ?? null]));
|
||||||
|
|
||||||
|
// Writing to the current implementator of the buffer
|
||||||
|
$this->session->buffer = ['catalog' => ['filters' => ['brand' => $brand ?? null]]] + ($this->session->buffer ?? []);
|
||||||
|
|
||||||
|
// Initialize buffer of filters query (AQL)
|
||||||
|
$_sort = 'd.position ASC, d.name ASC, d.created DESC';
|
||||||
|
|
||||||
|
// Validating received sort
|
||||||
|
if (isset($sort) && preg_match('/[\w\s]+/u', $sort, $matches)) $sort = $matches[0];
|
||||||
|
else unset($sort);
|
||||||
|
|
||||||
|
if (isset($sort)) {
|
||||||
|
// Received and validated sort
|
||||||
|
|
||||||
|
// Write to the buffer of sort query (AQL)
|
||||||
|
$_sort = "d.@sort DESC, $_sort";
|
||||||
|
|
||||||
|
// Write to the buffer of query parameters
|
||||||
|
$parameters['sort'] = $sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing to the session buffer
|
||||||
|
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: ['catalog_sort' => $sort ?? null]));
|
||||||
|
|
||||||
|
// Writing to the account buffer
|
||||||
|
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: ['catalog_sort' => $sort ?? null]));
|
||||||
|
|
||||||
|
// Validating
|
||||||
|
if (isset($category) && preg_match('/[\d]+/', $category, $matches)) $category = (int) $matches[0];
|
||||||
|
else unset($category);
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
// Searching for entries that are descendants of $category
|
||||||
|
$search = model::search(
|
||||||
|
document: $category,
|
||||||
|
amount: 200,
|
||||||
|
depth: 100,
|
||||||
|
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']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($this->settings->catalog['categories']['structure']) && $this->settings->catalog['categories']['structure'] === 'pages') {
|
||||||
|
// Pages
|
||||||
|
|
||||||
|
// Write to the buffer of global variables of view templater
|
||||||
|
$this->view->categories = $search['categories'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to the buffer of global variables of view templater
|
||||||
|
$this->view->products = $search['products'];
|
||||||
|
|
||||||
|
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($category)) {
|
||||||
|
// Not received identifier of the category
|
||||||
|
|
||||||
|
if (isset($this->settings->catalog['categories']['structure']) && $this->settings->catalog['categories']['structure'] === 'pages') {
|
||||||
|
// Pages
|
||||||
|
|
||||||
|
// Searching for root ascendants categories
|
||||||
|
$this->view->categories = entry::ascendants(
|
||||||
|
descendant: new category,
|
||||||
|
return: 'DISTINCT MERGE(d, { name: d.name.@language})',
|
||||||
|
parameters: ['language' => $this->language->name],
|
||||||
|
errors: $this->errors['catalog']
|
||||||
|
) ?? null;
|
||||||
|
} else if (isset($this->settings->catalog['categories']['structure']) && $this->settings->catalog['categories']['structure'] === 'lists') {
|
||||||
|
// Lists
|
||||||
|
|
||||||
|
// Initializing the list of all categories
|
||||||
|
$categories = [];
|
||||||
|
|
||||||
|
// Initializing the structure of the list of all categories
|
||||||
|
$structure = [];
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
entry::ascendants(
|
||||||
|
descendant: new category,
|
||||||
|
return: 'DISTINCT MERGE(d, { name: d.name.@language})',
|
||||||
|
parameters: ['language' => $this->language->name],
|
||||||
|
errors: $this->errors['catalog']
|
||||||
|
) ?? null as $document
|
||||||
|
) {
|
||||||
|
// Iterating over found root ascendants categories
|
||||||
|
|
||||||
|
// Generating the list (entering into recursion)
|
||||||
|
static::list(
|
||||||
|
document: $document,
|
||||||
|
language: $this->language,
|
||||||
|
categories: $categories,
|
||||||
|
structure: $structure,
|
||||||
|
errors: $this->errors['catalog']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing the list of all categories into the templater variable
|
||||||
|
$this->view->categories = $categories;
|
||||||
|
|
||||||
|
// Writing the structure of the list of all categories into the templater variable
|
||||||
|
$this->view->structure = $structure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validating @todo add throwing errors
|
||||||
|
if (isset($text) && preg_match('/[\w\s]+/u', urldecode($text), $matches) && mb_strlen($matches[0]) > 2) $text = $matches[0];
|
||||||
|
else unset($text);
|
||||||
|
|
||||||
|
// Writing to the session buffer
|
||||||
|
$this->core->request(new request('PATCH', '/session/write', protocol::http_3, parameters: ['catalog_search_text' => $text ?? null]));
|
||||||
|
|
||||||
|
// Writing to the account buffer
|
||||||
|
$this->core->request(new request('PATCH', '/account/write', protocol::http_3, parameters: ['catalog_search_text' => $text ?? 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']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Universalizing
|
||||||
|
if ($this->view->menu instanceof _document) $this->view->menu = [$this->view->menu];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($this->request->headers['accept'] ?? [], content::json->value)) {
|
||||||
|
// Request for JSON response
|
||||||
|
|
||||||
|
// Initializing the response body buffer
|
||||||
|
$body = [
|
||||||
|
'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 body buffer
|
||||||
|
$body['categories'] = $this->view->render('catalog/elements/categories.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->view->product)) {
|
||||||
|
// Initialized product
|
||||||
|
|
||||||
|
// Writing data of the product to the response body buffer @todo GENERATE THIS ON THE SERVER
|
||||||
|
$body['product'] = $this->view->product;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->view->products)) {
|
||||||
|
// Initialized products
|
||||||
|
|
||||||
|
// Render HTML-code of products and write to the response body buffer
|
||||||
|
$body['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 body buffer
|
||||||
|
$body['filters'] = $this->view->render('catalog/elements/filters.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->json($body + ['errors' => $this->errors])
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
// Deinitializing the response body buffer
|
||||||
|
unset($body);
|
||||||
|
} else if (str_contains($this->request->headers['accept'], content::any->value)) {
|
||||||
|
// Request for any response
|
||||||
|
|
||||||
|
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 ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render page
|
||||||
|
$page = $this->view->render('catalog/page.html', [
|
||||||
|
'h2' => $this->language === language::ru ? 'Каталог' : 'Catalog' // @see https://git.mirzaev.sexy/mirzaev/huesos/issues/1
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->write($page)
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
// Deinitializing rendered page
|
||||||
|
unset($page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function list(_document $document, language $language, array &$categories = [], array &$structure = [], array &$errors = []): void
|
||||||
|
{
|
||||||
|
// Initializing the object
|
||||||
|
$category = new category;
|
||||||
|
|
||||||
|
if (method_exists($category, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Writing the instance of product document from ArangoDB to the implement object
|
||||||
|
$category->__document($document);
|
||||||
|
|
||||||
|
// Writing the category into the list of categories
|
||||||
|
$categories[$category->getId()] = $category;
|
||||||
|
|
||||||
|
// Writing the category into the structure of the list categories
|
||||||
|
$structure[$category->getId()] = [];
|
||||||
|
|
||||||
|
// Searching for entries that are descendants of $category
|
||||||
|
$search = model::search(
|
||||||
|
document: $category,
|
||||||
|
amount: 200,
|
||||||
|
categories_merge: 'name: v.name.@language',
|
||||||
|
parameters: ['language' => $language->name],
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
if (count($search['categories']) > 0) {
|
||||||
|
// Found descendants categories
|
||||||
|
|
||||||
|
foreach ($search['categories'] as $document) {
|
||||||
|
// Iterating over descendants categories
|
||||||
|
|
||||||
|
// Entering into descendant level of the recursion for the list generation
|
||||||
|
static::list(
|
||||||
|
document: $document,
|
||||||
|
language: $language,
|
||||||
|
categories: $categories,
|
||||||
|
structure: $structure[$category->getId()],
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,49 +2,104 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\controllers;
|
namespace mirzaev\huesos\controllers;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\arming_bot\views\templater,
|
use mirzaev\huesos\views\templater,
|
||||||
mirzaev\arming_bot\models\core as models,
|
mirzaev\huesos\models\core as models,
|
||||||
mirzaev\arming_bot\models\account,
|
mirzaev\huesos\models\account,
|
||||||
mirzaev\arming_bot\models\session,
|
mirzaev\huesos\models\session,
|
||||||
mirzaev\arming_bot\models\settings,
|
mirzaev\huesos\models\settings,
|
||||||
mirzaev\arming_bot\models\suspension;
|
mirzaev\huesos\models\cart,
|
||||||
|
mirzaev\huesos\models\suspension,
|
||||||
|
mirzaev\huesos\models\enumerations\language,
|
||||||
|
mirzaev\huesos\models\enumerations\currency;
|
||||||
|
|
||||||
// Framework for PHP
|
// Framework for PHP
|
||||||
use mirzaev\minimal\controller;
|
use mirzaev\minimal\core as minimal,
|
||||||
|
mirzaev\minimal\controller,
|
||||||
|
mirzaev\minimal\http\response,
|
||||||
|
mirzaev\minimal\http\enumerations\status;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core of controllers
|
* Core of controllers
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\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
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
class core extends controller
|
class core extends controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Postfix for name of controllers files
|
* Settings
|
||||||
|
*
|
||||||
|
* @var settings $settings Instance of the settings
|
||||||
*/
|
*/
|
||||||
final public const string POSTFIX = '';
|
protected readonly settings $settings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instance of the settings
|
* Session
|
||||||
*/
|
*
|
||||||
public static settings $settings;
|
* @var session|null $session Instance of the session
|
||||||
|
|
||||||
/**
|
|
||||||
* Instance of a session
|
|
||||||
*/
|
*/
|
||||||
protected readonly session $session;
|
protected readonly session $session;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instance of an account
|
* Account
|
||||||
|
*
|
||||||
|
* @var account|null $account Instance of the account
|
||||||
*/
|
*/
|
||||||
protected readonly ?account $account;
|
protected readonly ?account $account;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry of errors
|
* Cart
|
||||||
|
*
|
||||||
|
* @var cart|null $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
|
||||||
*/
|
*/
|
||||||
protected array $errors = [
|
protected array $errors = [
|
||||||
'session' => [],
|
'session' => [],
|
||||||
|
@ -52,25 +107,30 @@ class core extends controller
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor of an instance
|
* Constructor
|
||||||
*
|
*
|
||||||
|
* @param minimal $core Initialize a controller?
|
||||||
* @param bool $initialize Initialize a controller?
|
* @param bool $initialize Initialize a controller?
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
|
*
|
||||||
|
* @todo
|
||||||
|
* 1. settings account и session не имеют проверок на возврат null
|
||||||
|
* 2. TRANSIT EVERYTHING TO MIDDLEWARES
|
||||||
*/
|
*/
|
||||||
public function __construct(bool $initialize = true)
|
public function __construct(minimal $core, bool $initialize = true)
|
||||||
{
|
{
|
||||||
// Blocking requests from CloudFlare (better to write this blocking into nginx config file)
|
// 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;
|
if (isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] === 'nginx-ssl early hints') return status::bruh->label;
|
||||||
|
|
||||||
// For the extends system
|
// For the extends system
|
||||||
parent::__construct($initialize);
|
parent::__construct(core: $core);
|
||||||
|
|
||||||
if ($initialize) {
|
if ($initialize) {
|
||||||
// Initializing is requested
|
// Initializing is requested
|
||||||
|
|
||||||
// Initializing of models core (connect to ArangoDB...)
|
// Initializing of models core (connect to ArangoDB...)
|
||||||
new models();
|
new models(true);
|
||||||
|
|
||||||
// Initializing of the date until which the session will be active
|
// Initializing of the date until which the session will be active
|
||||||
$expires = strtotime('+1 week');
|
$expires = strtotime('+1 week');
|
||||||
|
@ -83,9 +143,7 @@ class core extends controller
|
||||||
|
|
||||||
// Handle a problems with initializing a session
|
// Handle a problems with initializing a session
|
||||||
if (!empty($this->errors['session'])) exit(1);
|
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)
|
// Hash of the session is changed (implies that the session has expired and recreated)
|
||||||
|
|
||||||
// Write a new hash of the session to cookies
|
// Write a new hash of the session to cookies
|
||||||
|
@ -100,16 +158,29 @@ class core extends controller
|
||||||
'samesite' => 'strict'
|
'samesite' => 'strict'
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} */
|
}
|
||||||
|
|
||||||
|
// Initializing registry of account errors
|
||||||
|
$this->errors['account'];
|
||||||
|
|
||||||
// Initializing of the account
|
// Initializing of the account
|
||||||
$this->account = $this->session->account($this->errors['account']);
|
$this->account = $this->session->account($this->errors['account']);
|
||||||
|
|
||||||
// Initializing of the settings
|
// Initializing of the settings
|
||||||
self::$settings = settings::active();
|
$this->settings = settings::active(create: SETTINGS_PROJECT);
|
||||||
|
|
||||||
|
// Initializing of the language
|
||||||
|
$this->language = $this->account?->language ?? $this->session?->buffer['language'] ?? $this->settings?->language ?? language::en;
|
||||||
|
|
||||||
|
// Initializing of the currency
|
||||||
|
$this->currency = $this->account?->currency ?? $this->session?->buffer['currency'] ?? $this->settings?->currency ?? currency::usd;
|
||||||
|
|
||||||
// Initializing of preprocessor of views
|
// Initializing of preprocessor of views
|
||||||
$this->view = new templater($this->session, $this->account);
|
$this->view = new templater(
|
||||||
|
session: $this->session,
|
||||||
|
account: $this->account,
|
||||||
|
settings: $this->settings
|
||||||
|
);
|
||||||
|
|
||||||
// @todo перенести в middleware
|
// @todo перенести в middleware
|
||||||
|
|
||||||
|
@ -142,20 +213,20 @@ class core extends controller
|
||||||
suspension:
|
suspension:
|
||||||
|
|
||||||
// Write title of the page to templater global variables
|
// Write title of the page to templater global variables
|
||||||
$this->view->title = match ($account?->language ?? self::$settings?->language) {
|
$this->view->title = match ($this->language) {
|
||||||
'ru' => 'Приостановлено',
|
language::en => 'Suspended',
|
||||||
'en' => 'Suspended',
|
language::ru => 'Приостановлено',
|
||||||
default => 'Suspended'
|
default => 'Suspended'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write description of the suspension to templater global variables
|
// Write description of the suspension to templater global variables
|
||||||
$this->view->description = $suspension->description[$account?->language ?? self::$settings?->language] ?? array_values($suspension->description)[0];
|
$this->view->description = $suspension->description[$this->language] ?? array_values($suspension->description)[0];
|
||||||
|
|
||||||
// Write message of remaining time of the suspension to templater global variables
|
// Write message of remaining time of the suspension to templater global variables
|
||||||
$this->view->remain = [
|
$this->view->remain = [
|
||||||
'title' => match ($account?->language ?? self::$settings?->language) {
|
'title' => match ($this->language) {
|
||||||
'ru' => 'Осталось времени: ',
|
language::en => 'Time remaining: ',
|
||||||
'en' => 'Time remaining: ',
|
language::ru => 'Осталось времени: ',
|
||||||
default => 'Time remaining: '
|
default => 'Time remaining: '
|
||||||
},
|
},
|
||||||
'value' => $suspension?->message()
|
'value' => $suspension?->message()
|
||||||
|
@ -170,21 +241,4 @@ 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})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\controllers;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\controllers\core,
|
||||||
|
mirzaev\huesos\models\menu;
|
||||||
|
|
||||||
|
// Framework for PHP
|
||||||
|
use mirzaev\minimal\http\enumerations\content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller of pages
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\controllers
|
||||||
|
* @param array $errors Registry of errors
|
||||||
|
*
|
||||||
|
* @method null offer() Public offer
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Errors
|
||||||
|
*
|
||||||
|
* @var array $errors Registry of errors
|
||||||
|
*/
|
||||||
|
protected array $errors = [
|
||||||
|
'session' => [],
|
||||||
|
'account' => [],
|
||||||
|
'menu' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public offer
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public function offer(): null
|
||||||
|
{
|
||||||
|
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']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Universalizing
|
||||||
|
if ($this->view->menu instanceof _document) $this->view->menu = [$this->view->menu];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($this->request->headers['accept'], content::any->value)) {
|
||||||
|
// Request for any response
|
||||||
|
|
||||||
|
// Render page
|
||||||
|
$page = $this->view->render('offer/page.html');
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->write($page)
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
// Deinitializing rendered page
|
||||||
|
unset($page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,480 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\controllers;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\controllers\core,
|
||||||
|
mirzaev\huesos\models\cart as model,
|
||||||
|
mirzaev\huesos\models\product,
|
||||||
|
mirzaev\huesos\models\menu,
|
||||||
|
mirzaev\huesos\models\enumerations\language;
|
||||||
|
|
||||||
|
// Framework for PHP
|
||||||
|
use mirzaev\minimal\http\enumerations\content,
|
||||||
|
mirzaev\minimal\http\enumerations\status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller of cart
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\controllers
|
||||||
|
*
|
||||||
|
* @param model|null $cart Instance of the cart
|
||||||
|
* @param array $errors Registry of errors
|
||||||
|
*
|
||||||
|
* @method null index() HTML-document with shopping cart and delivery settings
|
||||||
|
* @method null product(int|string|null $identifier, ?string $type, int|string|null $amount) Change status of the product in the cart
|
||||||
|
* @method null summary() Information about products in the cart
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Cart
|
||||||
|
*
|
||||||
|
* @var model|null $cart Instance of the cart
|
||||||
|
*/
|
||||||
|
protected readonly ?model $cart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Errors
|
||||||
|
*
|
||||||
|
* @var array $errors Registry of errors
|
||||||
|
*/
|
||||||
|
protected array $errors = [
|
||||||
|
'session' => [],
|
||||||
|
'account' => [],
|
||||||
|
'menu' => [],
|
||||||
|
'cart' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index
|
||||||
|
*
|
||||||
|
* HTML-document with shopping cart and delivery settings
|
||||||
|
*
|
||||||
|
* @param null
|
||||||
|
*/
|
||||||
|
public function index(): null
|
||||||
|
{
|
||||||
|
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']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Universalizing
|
||||||
|
if ($this->view->menu instanceof _document) $this->view->menu = [$this->view->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 (str_contains($this->request->headers['accept'], content::json->value)) {
|
||||||
|
// Request for JSON response
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->json([
|
||||||
|
'main' => '',
|
||||||
|
'errors' => $this->errors
|
||||||
|
])
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
} else if (str_contains($this->request->headers['accept'], content::any->value)) {
|
||||||
|
// Request for any response
|
||||||
|
|
||||||
|
// Render page
|
||||||
|
$page = $this->view->render('cart/page.html', [
|
||||||
|
'h2' => $this->language === language::ru ? 'Корзина' : 'Cart' // @see https://git.mirzaev.sexy/mirzaev/huesos/issues/1
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->write($page)
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
// Deinitializing rendered page
|
||||||
|
unset($page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Product
|
||||||
|
*
|
||||||
|
* Change status of the product in the cart
|
||||||
|
*
|
||||||
|
* @param int|string|null $identifier Product identifier
|
||||||
|
* @param string|null $type Action type (toggle, write, delete, set)
|
||||||
|
* @param int|string|null $amount Amount of actions with the product (for write, delete and differently used by set)
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*
|
||||||
|
* @todo
|
||||||
|
* 1. Add a limit on adding products to the cart based on the number of products in stock
|
||||||
|
*/
|
||||||
|
public function product(
|
||||||
|
int|string|null $identifier = null,
|
||||||
|
?string $type = null,
|
||||||
|
int|string|null $amount = null
|
||||||
|
): null {
|
||||||
|
if (str_contains($this->request->headers['accept'], content::json->value)) {
|
||||||
|
// Request for JSON response
|
||||||
|
|
||||||
|
// Declaring buffer with amount of the product in the cart
|
||||||
|
$cart = 0;
|
||||||
|
|
||||||
|
// Validating @todo add throwing errors
|
||||||
|
if (isset($identifier) && preg_match('/[\d]+/', urldecode($identifier), $matches)) $identifier = (int) $matches[0];
|
||||||
|
else unset($identifier);
|
||||||
|
|
||||||
|
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 buffer with amount of the product in the cart
|
||||||
|
$cart = $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
|
||||||
|
if (isset($type) && preg_match('/[\w]+/', urldecode($type), $matches)) $type = $matches[0];
|
||||||
|
else unset($type);
|
||||||
|
|
||||||
|
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 ($cart > 0) {
|
||||||
|
// The cart has the product
|
||||||
|
|
||||||
|
// Deleting the product from the cart
|
||||||
|
$this->cart->delete(product: $product, amount: $cart, errors: $this->errors['cart']);
|
||||||
|
|
||||||
|
// Reinitializing the buffer with amount of the product in the cart
|
||||||
|
$cart = 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
|
||||||
|
$cart = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Received not the "toggle" command
|
||||||
|
|
||||||
|
// Validating @todo add throwing errors
|
||||||
|
if (isset($amount) && preg_match('/[\d]+/', urldecode($amount), $matches)) $amount = (int) $matches[0];
|
||||||
|
else unset($amount);
|
||||||
|
|
||||||
|
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 ($cart + $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
|
||||||
|
$cart += $amount;
|
||||||
|
}
|
||||||
|
} else if ($type === 'delete') {
|
||||||
|
// Decrease amount of the product in the cart
|
||||||
|
|
||||||
|
if ($cart - $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
|
||||||
|
$cart -= $amount;
|
||||||
|
}
|
||||||
|
} else if ($type === 'set') {
|
||||||
|
// Set amount of the product in the cart
|
||||||
|
|
||||||
|
if ($amount > -1 && $amount < 101) {
|
||||||
|
// Validated amount to setting
|
||||||
|
|
||||||
|
if ($amount > $cart) {
|
||||||
|
// Requested amount more than actual amount of the product in the cart
|
||||||
|
|
||||||
|
// Writing the product to the cart
|
||||||
|
$this->cart->write(product: $product, amount: $amount - $cart, 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: $cart - $amount, errors: $this->errors['cart']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinitializing the buffer with amount of the product in the cart
|
||||||
|
$cart = $amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->json([
|
||||||
|
'amount' => $cart, // $cart does not store a real value, but is calculated without a repeated request to ArangoDB
|
||||||
|
'errors' => $this->errors
|
||||||
|
])
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
// Deinitializing buffer with amount of the product in the cart
|
||||||
|
unset($cart);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summary
|
||||||
|
*
|
||||||
|
* Information about products in the cart
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public function summary(): null
|
||||||
|
{
|
||||||
|
if (str_contains($this->request->headers['accept'], content::json->value)) {
|
||||||
|
// Request for JSON response
|
||||||
|
|
||||||
|
// 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']);
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->json([
|
||||||
|
'cost' => $summary['cost'] ?? 0,
|
||||||
|
'amount' => $summary['amount'] ?? 0,
|
||||||
|
'errors' => $this->errors
|
||||||
|
])
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
// Deinitializing summary data of the cart
|
||||||
|
unset($summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share
|
||||||
|
*
|
||||||
|
* Share the cart
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public function share(): null
|
||||||
|
{
|
||||||
|
if (str_contains($this->request->headers['accept'], content::json->value)) {
|
||||||
|
// Request for JSON response
|
||||||
|
|
||||||
|
// Initializing the cart
|
||||||
|
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
|
||||||
|
|
||||||
|
if ($this->cart instanceof model) {
|
||||||
|
// Initialized the cart
|
||||||
|
|
||||||
|
if ($share = $this->cart->share ?? $this->cart->share()) {
|
||||||
|
// The cart is available for sharing
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->json([
|
||||||
|
'share' => $share,
|
||||||
|
'errors' => $this->errors
|
||||||
|
])
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deinitializing unnecessary variables
|
||||||
|
unset($hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pay
|
||||||
|
*
|
||||||
|
* Pay for the cart
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public function pay(): null
|
||||||
|
{
|
||||||
|
if (str_contains($this->request->headers['accept'], content::json->value)) {
|
||||||
|
// Request for JSON response
|
||||||
|
|
||||||
|
// Initializing the cart
|
||||||
|
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
|
||||||
|
|
||||||
|
if ($this->cart instanceof model) {
|
||||||
|
// Initialized the cart
|
||||||
|
|
||||||
|
if ($share = $this->cart->share ?? $this->cart->share()) {
|
||||||
|
// The cart is available for sharing
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->json([
|
||||||
|
'share' => $share,
|
||||||
|
'errors' => $this->errors
|
||||||
|
])
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deinitializing unnecessary variables
|
||||||
|
unset($hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Robokassa
|
||||||
|
*
|
||||||
|
* HTML-document with robokassa iframe
|
||||||
|
*
|
||||||
|
* @param null
|
||||||
|
*
|
||||||
|
* @todo THIS MUST BE A PAYMENT OF ORDER IN THE FUTURE, NOT CART
|
||||||
|
*/
|
||||||
|
public function robokassa(): null
|
||||||
|
{
|
||||||
|
// Initializing the cart
|
||||||
|
$this->cart ??= $this->account?->cart() ?? $this->session?->cart();
|
||||||
|
|
||||||
|
// Initializing the cart data
|
||||||
|
$this->view->cart = $this->cart;
|
||||||
|
$this->view->summary = $this->cart?->summary(currency: $this->currency);
|
||||||
|
|
||||||
|
if (str_contains($this->request->headers['accept'], content::any->value)) {
|
||||||
|
// Request for any response
|
||||||
|
|
||||||
|
// Render page
|
||||||
|
$page = $this->view->render('iframes/robokassa.html');
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->write($page)
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
|
||||||
|
// Deinitializing rendered page
|
||||||
|
unset($page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,238 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\controllers;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\controllers\core,
|
||||||
|
mirzaev\huesos\models\account;
|
||||||
|
|
||||||
|
// Framework for PHP
|
||||||
|
use mirzaev\minimal\http\enumerations\content,
|
||||||
|
mirzaev\minimal\http\enumerations\status;
|
||||||
|
|
||||||
|
// Framework for ArangoDB
|
||||||
|
use mirzaev\arangodb\document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller of session
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\controllers
|
||||||
|
*
|
||||||
|
* @param array $errors Registry of errors
|
||||||
|
*
|
||||||
|
* @method null write(string ...$parameters) Write to the session buffer
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Errors
|
||||||
|
*
|
||||||
|
* @var array $errors Registry of errors
|
||||||
|
*/
|
||||||
|
protected array $errors = [
|
||||||
|
'session' => [],
|
||||||
|
'account' => [],
|
||||||
|
'buffer' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect session to the telegram account
|
||||||
|
*
|
||||||
|
* @see https://core.telegram.org/bots/webapps#initializing-mini-apps
|
||||||
|
* @see https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app
|
||||||
|
* @see https://core.telegram.org/bots/webapps#webappinitdata
|
||||||
|
*
|
||||||
|
* @param ?string $user JSON
|
||||||
|
* @param ?string $chat_instance
|
||||||
|
* @param ?string $chat_type
|
||||||
|
* @param ?string $auth_date
|
||||||
|
* @param ?string $hash
|
||||||
|
* @param ?string $query_id
|
||||||
|
* @param ?string $signature
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public function telegram(
|
||||||
|
?string $user = null,
|
||||||
|
?string $chat_instance = null,
|
||||||
|
?string $chat_type = null,
|
||||||
|
?string $auth_date = null,
|
||||||
|
?string $hash = null,
|
||||||
|
?string $query_id = null,
|
||||||
|
?string $signature = null
|
||||||
|
): null {
|
||||||
|
if (str_contains($this->request->headers['accept'], content::json->value)) {
|
||||||
|
// Request for JSON response
|
||||||
|
|
||||||
|
// Declaring variables in the correct scope
|
||||||
|
$identifier = $domain = $language = null;
|
||||||
|
|
||||||
|
// Initializing data of the account
|
||||||
|
$data = json_decode($user);
|
||||||
|
|
||||||
|
// Initializing avatar of the account
|
||||||
|
$avatar = $data->photo_url;
|
||||||
|
|
||||||
|
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 (isset($user, $auth_date, $hash)) {
|
||||||
|
// Received required parameters
|
||||||
|
|
||||||
|
$buffer = ['user' => $user];
|
||||||
|
|
||||||
|
if (isset($query_id)) $buffer += ['query_id' => $query_id];
|
||||||
|
if (isset($signature)) $buffer += ['signature' => $signature];
|
||||||
|
if (isset($chat_instance)) $buffer += ['chat_instance' => $chat_instance];
|
||||||
|
if (isset($chat_type)) $buffer += ['chat_type' => $chat_type];
|
||||||
|
|
||||||
|
$buffer += ['auth_date' => $auth_date];
|
||||||
|
|
||||||
|
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 . 'telegram.php'), 'WebAppData', true);
|
||||||
|
$_hash = bin2hex(hash_hmac('sha256', implode(PHP_EOL, $prepared), $key, true));
|
||||||
|
|
||||||
|
if (hash_equals($hash, $_hash)) {
|
||||||
|
// Data confirmed (according to telegram documentation)
|
||||||
|
|
||||||
|
if (time() - $auth_date < 86400) {
|
||||||
|
// Authorization date less than 1 day ago
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
'chat' => [
|
||||||
|
'type' => $chat_type,
|
||||||
|
'instance' => $chat_instance
|
||||||
|
]
|
||||||
|
],
|
||||||
|
$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 avatar of the account
|
||||||
|
$avatar = $data->photo_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sending response
|
||||||
|
$this->response
|
||||||
|
->start()
|
||||||
|
->clean()
|
||||||
|
->sse()
|
||||||
|
->json([
|
||||||
|
'connected' => (bool) $connected,
|
||||||
|
'identifier' => $identifier ?? null,
|
||||||
|
'domain' => $domain ?? null,
|
||||||
|
'avatar' => $avatar ?? null,
|
||||||
|
'language' => $language?->name ?? null,
|
||||||
|
'errors' => $this->errors
|
||||||
|
])
|
||||||
|
->validate($this->request)
|
||||||
|
?->body()
|
||||||
|
->end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success/fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write
|
||||||
|
*
|
||||||
|
* Write to the buffer
|
||||||
|
*
|
||||||
|
* @param mixed ...$parameters Parameters for writing to the buffer
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*
|
||||||
|
* @todo переделать под trait buffer
|
||||||
|
*/
|
||||||
|
public function write(mixed ...$parameters): null
|
||||||
|
{
|
||||||
|
if (!empty($parameters) && isset($this->session)) {
|
||||||
|
// Received parameters and initialized model with buffer trait
|
||||||
|
|
||||||
|
// Declaring the buffer of deserialized parameters
|
||||||
|
$deserialized = [];
|
||||||
|
|
||||||
|
foreach ($parameters as $name => $value) {
|
||||||
|
// Iterate over parameters
|
||||||
|
|
||||||
|
// Validation of the parameter value
|
||||||
|
if (mb_strlen(serialize($value)) > 4096) continue;
|
||||||
|
|
||||||
|
// Declaring the buffer of deserialized parameter
|
||||||
|
$parameter = null;
|
||||||
|
|
||||||
|
// Deserializing name to multidimensional array
|
||||||
|
foreach (array_reverse(explode('_', (string) $name)) as $key)
|
||||||
|
$parameter = [$key => $parameter ?? (is_string($value) && 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 document from ArangoDB
|
||||||
|
if (!empty($deserialized)) $this->session->write($deserialized, $this->errors['buffer']);
|
||||||
|
|
||||||
|
// Writing status of response
|
||||||
|
$this->response->status = status::created;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\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;
|
||||||
|
|
||||||
|
// Framework for ArangoDB
|
||||||
|
use mirzaev\arangodb\collection,
|
||||||
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
|
// Framework for Telegram
|
||||||
|
use Zanzara\Telegram\Type\User as telegram;
|
||||||
|
|
||||||
|
// Library for ArangoDB
|
||||||
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
|
// Built-in libraries
|
||||||
|
use exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model of account
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\models
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
final class account extends core implements document_interface, collection_interface
|
||||||
|
{
|
||||||
|
use status, document_trait, buffer, cart {
|
||||||
|
buffer::write as write;
|
||||||
|
cart::initialize as cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the collection in ArangoDB
|
||||||
|
*/
|
||||||
|
final public const string COLLECTION = 'account';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize
|
||||||
|
*
|
||||||
|
* @param int $identifier Identifier of the account
|
||||||
|
* @param telegram|array|null $registration Данные для регистрация, если аккаунт не найден
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return static|null Объект аккаунта, если найден
|
||||||
|
*/
|
||||||
|
public static function initialize(int $identifier, telegram|array|null $registration = null, array &$errors = []): static|null
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
|
// Initialized the collection
|
||||||
|
|
||||||
|
// Initializing the account
|
||||||
|
$result = collection::execute(
|
||||||
|
<<<'AQL'
|
||||||
|
FOR d IN @@collection
|
||||||
|
FILTER d.identifier == @identifier
|
||||||
|
RETURN d
|
||||||
|
AQL,
|
||||||
|
[
|
||||||
|
'@collection' => static::COLLECTION,
|
||||||
|
'identifier' => $identifier
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result instanceof _document) {
|
||||||
|
// Initialized the account
|
||||||
|
|
||||||
|
// Initializing the object
|
||||||
|
$account = new static;
|
||||||
|
|
||||||
|
if (method_exists($account, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Implementinf parameters
|
||||||
|
if (isset($result->language)) $result->language = language::{$result->language};
|
||||||
|
if (isset($result->currency)) $result->currency = currency::{$result->currency};
|
||||||
|
|
||||||
|
// Writing the instance of account document from ArangoDB to the implement object
|
||||||
|
$account->__document($result);
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $account;
|
||||||
|
} else throw new exception('Class ' . static::class . ' does not implement a document from ArangoDB');
|
||||||
|
} else if ($registration) {
|
||||||
|
// Not found the account and registration is requested
|
||||||
|
|
||||||
|
// Creating account
|
||||||
|
$_id = document::write(
|
||||||
|
static::COLLECTION,
|
||||||
|
(is_array($registration)
|
||||||
|
? $registration :
|
||||||
|
[
|
||||||
|
'identifier' => $registration->getId(),
|
||||||
|
'name' => [
|
||||||
|
'first' => $registration->getFirstName(),
|
||||||
|
'last' => $registration->getLastName()
|
||||||
|
],
|
||||||
|
'domain' => $registration->getUsername(),
|
||||||
|
'robot' => $registration->isBot(),
|
||||||
|
'menus' => [
|
||||||
|
'attachments' => $registration->getAddedToAttachmentMenu()
|
||||||
|
],
|
||||||
|
'messages' => true,
|
||||||
|
'groups' => [
|
||||||
|
'join' => $registration->getCanJoinGroups(),
|
||||||
|
'messages' => $registration->getCanReadAllGroupMessages()
|
||||||
|
],
|
||||||
|
'premium' => $registration->isPremium(),
|
||||||
|
'language' => language::{$registration->getLanguageCode() ?? 'en'}?->name ?? 'en',
|
||||||
|
'queries' => [
|
||||||
|
'inline' => $registration->getSupportsInlineQueries()
|
||||||
|
]
|
||||||
|
]) + [
|
||||||
|
'banned' => false,
|
||||||
|
'tester' => false,
|
||||||
|
'developer' => false,
|
||||||
|
'access' => [
|
||||||
|
'settings' => false
|
||||||
|
],
|
||||||
|
'version' => ROBOT_VERSION,
|
||||||
|
'active' => true
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($_id) {
|
||||||
|
// Created account
|
||||||
|
|
||||||
|
// Initializing of the account (without registration request)
|
||||||
|
return static::initialize($identifier, errors: $errors);
|
||||||
|
} else throw new exception('Failed to register account');
|
||||||
|
} else throw new exception('Failed to find account');
|
||||||
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,260 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\models\acquirings;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\models\core,
|
||||||
|
mirzaev\huesos\models\settings,
|
||||||
|
mirzaev\huesos\models\cart,
|
||||||
|
mirzaev\huesos\models\order,
|
||||||
|
mirzaev\huesos\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,
|
||||||
|
DomainException as exception_domain,
|
||||||
|
RuntimeException as exception_runtime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model of robokassa
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\models\acquirings
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
final class robokassa extends core
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Link
|
||||||
|
*
|
||||||
|
* Generate the link to pay for the order
|
||||||
|
*
|
||||||
|
* @param order $order The order
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return string|null The link to pay for the order, if generated
|
||||||
|
*/
|
||||||
|
public static function link(order $order, array &$errors = []): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Initializing the settings
|
||||||
|
$settings = settings::active();
|
||||||
|
|
||||||
|
if ($settings instanceof settings) {
|
||||||
|
// Initialized the settings
|
||||||
|
|
||||||
|
if ($settings->acquirings['robokassa']['enabled']) {
|
||||||
|
// Enabled the robokassa acquiring
|
||||||
|
|
||||||
|
// Declaring the robokassa settings
|
||||||
|
$robokassa = null;
|
||||||
|
|
||||||
|
// Initializing the test mode register
|
||||||
|
$test = 0;
|
||||||
|
|
||||||
|
if ($settings->acquirings['robokassa']['mode'] === 'work') {
|
||||||
|
// Work mode
|
||||||
|
|
||||||
|
// Initializing the robokassa settings
|
||||||
|
$robokassa = require(SETTINGS . DIRECTORY_SEPARATOR . 'acquirings' . DIRECTORY_SEPARATOR . 'robokassa' . DIRECTORY_SEPARATOR . 'work.php');
|
||||||
|
} else if ($settings->acquirings['robokassa']['mode'] === 'test') {
|
||||||
|
// Test mode
|
||||||
|
|
||||||
|
// Reinitializing the test mode register
|
||||||
|
$test = 1;
|
||||||
|
|
||||||
|
// Initializing the robokassa settings
|
||||||
|
$robokassa = require(SETTINGS . DIRECTORY_SEPARATOR . 'acquirings' . DIRECTORY_SEPARATOR . 'robokassa' . DIRECTORY_SEPARATOR . 'test.php');
|
||||||
|
} else {
|
||||||
|
// Failed to initialized the mode
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
throw new exception_domain('Failed to initialize the robokassa mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing the cart
|
||||||
|
$cart = $order->cart();
|
||||||
|
|
||||||
|
if ($cart instanceof cart) {
|
||||||
|
// Initialized the cart
|
||||||
|
|
||||||
|
// Initializing robokassa parameters
|
||||||
|
$login = $robokassa['login'];
|
||||||
|
$password = $robokassa['passwords'][0];
|
||||||
|
|
||||||
|
// Initializing the order parameters
|
||||||
|
$identifier = $order->getKey();
|
||||||
|
|
||||||
|
// Initializing the invoice parameters
|
||||||
|
$description = "Оплата заказа";
|
||||||
|
$cost = $cart->cost() + ($cart->buffer['delivery']['cost'] ?? 0);
|
||||||
|
|
||||||
|
// Generatings the MD5 hash
|
||||||
|
$md5 = md5("$login:$cost:$identifier:$password");
|
||||||
|
/*
|
||||||
|
// Writing the MD5 hash into the order implementator
|
||||||
|
$order->acquirings = ['robokassa' => ['hash' => $md5] + ($order->acquirings['robokassa'] ?? [])] + ($order->acquirings ?? []);
|
||||||
|
|
||||||
|
if (document::update($order->__document(), errors: $errors)) {
|
||||||
|
// Writed to ArangoDB */
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return "https://auth.robokassa.ru/Merchant/Index.aspx?MerchantLogin=$login&OutSum=$cost&InvId=$identifier&Description=$description&SignatureValue=$md5&IsTest=$test";
|
||||||
|
/* } else throw new exception('Failed to write the robokassa hash into the order'); */
|
||||||
|
} else {
|
||||||
|
// Not initialized the cart
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
throw new exception_runtime('Failed to initialize the cart');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify (result)
|
||||||
|
*
|
||||||
|
* Generate hash by arguments and verify for result with the hash
|
||||||
|
*
|
||||||
|
* @param int $identifier Identifier of the order
|
||||||
|
* @param string $cost Cost of the order (can be with two zeros, like 1000.00)
|
||||||
|
* @param string $hash Hash of the order
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return string|null The link to pay for the order, if generated
|
||||||
|
*/
|
||||||
|
public static function result(int $identifier, string $cost, string $hash, bool $test = false, array &$errors = []): ?bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Initializing the settings
|
||||||
|
$settings = settings::active();
|
||||||
|
|
||||||
|
if ($settings instanceof settings) {
|
||||||
|
// Initialized the settings
|
||||||
|
|
||||||
|
if ($settings->acquirings['robokassa']['enabled']) {
|
||||||
|
// Enabled the robokassa acquiring
|
||||||
|
|
||||||
|
// Declaring the robokassa settings
|
||||||
|
$robokassa = null;
|
||||||
|
|
||||||
|
if ($test) {
|
||||||
|
// Test mode
|
||||||
|
|
||||||
|
// Initializing the robokassa settings
|
||||||
|
$robokassa = require(SETTINGS . DIRECTORY_SEPARATOR . 'acquirings' . DIRECTORY_SEPARATOR . 'robokassa' . DIRECTORY_SEPARATOR . 'test.php');
|
||||||
|
} else {
|
||||||
|
// Work mode
|
||||||
|
|
||||||
|
// Initializing the robokassa settings
|
||||||
|
$robokassa = require(SETTINGS . DIRECTORY_SEPARATOR . 'acquirings' . DIRECTORY_SEPARATOR . 'robokassa' . DIRECTORY_SEPARATOR . 'work.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing robokassa parameters
|
||||||
|
$password = $robokassa['passwords'][1];
|
||||||
|
|
||||||
|
// Generatings the MD5 hash
|
||||||
|
$md5 = strtoupper(md5("$cost:$identifier:$password"));
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $md5 === $hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify (success)
|
||||||
|
*
|
||||||
|
* Generate hash by arguments and verify for success with the hash
|
||||||
|
*
|
||||||
|
* @param int $identifier Identifier of the order
|
||||||
|
* @param string $cost Cost of the order (can be with two zeros, like 1000.00)
|
||||||
|
* @param string $hash Hash of the order
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return string|null The link to pay for the order, if generated
|
||||||
|
*/
|
||||||
|
public static function success(int $identifier, string $cost, string $hash, bool $test = false, array &$errors = []): ?bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Initializing the settings
|
||||||
|
$settings = settings::active();
|
||||||
|
|
||||||
|
if ($settings instanceof settings) {
|
||||||
|
// Initialized the settings
|
||||||
|
|
||||||
|
if ($settings->acquirings['robokassa']['enabled']) {
|
||||||
|
// Enabled the robokassa acquiring
|
||||||
|
|
||||||
|
// Declaring the robokassa settings
|
||||||
|
$robokassa = null;
|
||||||
|
|
||||||
|
if ($test) {
|
||||||
|
// Test mode
|
||||||
|
|
||||||
|
// Initializing the robokassa settings
|
||||||
|
$robokassa = require(SETTINGS . DIRECTORY_SEPARATOR . 'acquirings' . DIRECTORY_SEPARATOR . 'robokassa' . DIRECTORY_SEPARATOR . 'test.php');
|
||||||
|
} else {
|
||||||
|
// Work mode
|
||||||
|
|
||||||
|
// Initializing the robokassa settings
|
||||||
|
$robokassa = require(SETTINGS . DIRECTORY_SEPARATOR . 'acquirings' . DIRECTORY_SEPARATOR . 'robokassa' . DIRECTORY_SEPARATOR . 'work.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing robokassa parameters
|
||||||
|
$password = $robokassa['passwords'][0];
|
||||||
|
|
||||||
|
// Generatings the MD5 hash
|
||||||
|
$md5 = md5("$cost:$identifier:$password");
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $md5 === $hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,689 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\models;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\models\core,
|
||||||
|
mirzaev\huesos\models\reservation,
|
||||||
|
mirzaev\huesos\models\traits\buffer,
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Framework for ArangoDB
|
||||||
|
use mirzaev\arangodb\collection,
|
||||||
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
|
// Library for ArangoDB
|
||||||
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
|
// Built-in libraries
|
||||||
|
use DateTime as datetime,
|
||||||
|
Exception as exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model of cart
|
||||||
|
*
|
||||||
|
* @uses reservation
|
||||||
|
* @package mirzaev\huesos\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, buffer {
|
||||||
|
buffer::write as buffer_write;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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->name . ' collection: ' . product::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . reservation::TYPE->name . ' collection: ' . reservation::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' 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->name . ' collection: ' . product::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . reservation::TYPE->name . ' collection: ' . reservation::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' 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->name . ' collection: ' . product::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . reservation::TYPE->name . ' collection: ' . reservation::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' 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 into 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->name . ' collection: ' . product::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . reservation::TYPE->name . ' collection: ' . reservation::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' 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->name . ' collection: ' . product::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . reservation::TYPE->name . ' collection: ' . reservation::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' 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()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Share
|
||||||
|
*
|
||||||
|
* Generate hash for sharing the cart
|
||||||
|
*
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return string|false Hash for sharing, if generated and writed to ArangoDB
|
||||||
|
*/
|
||||||
|
public function share(array &$errors = []): string|false
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
|
// Initialized collection
|
||||||
|
|
||||||
|
// Generating hash and writing to the cart implement instance
|
||||||
|
$this->share = sodium_bin2hex(sodium_crypto_generichash($this->getId()));
|
||||||
|
|
||||||
|
if (document::update($this->__document(), errors: $errors)) {
|
||||||
|
// Writed to ArangoDB
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $this->share;
|
||||||
|
} else throw new exception('Failed to write confirmed cart to ArangoDB');
|
||||||
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Writing to the registry of errors
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Unshare
|
||||||
|
*
|
||||||
|
* Deleting hash for sharing the cart
|
||||||
|
*
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return bool Is the cart unshared?
|
||||||
|
*/
|
||||||
|
public function unshare(array &$errors = []): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
|
// Initialized collection
|
||||||
|
|
||||||
|
// Deleting hash and writing to the cart implement instance
|
||||||
|
$this->share = null;
|
||||||
|
|
||||||
|
if (document::update($this->__document(), errors: $errors)) {
|
||||||
|
// Writed to ArangoDB
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return true;
|
||||||
|
} else throw new exception('Failed to write confirmed cart to ArangoDB');
|
||||||
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Writing to the registry of errors
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account
|
||||||
|
*
|
||||||
|
* Search for the connected account
|
||||||
|
*
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return account|null The connected account, if found
|
||||||
|
*/
|
||||||
|
public function account(array &$errors = []): ?account
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($result = collection::execute(
|
||||||
|
<<<'AQL'
|
||||||
|
FOR v IN 1..1 OUTBOUND @cart GRAPH users
|
||||||
|
FILTER PARSE_IDENTIFIER(v._id).collection == @account
|
||||||
|
LIMIT 1
|
||||||
|
return v
|
||||||
|
AQL,
|
||||||
|
[
|
||||||
|
'cart' => $this->document->getId(),
|
||||||
|
'account' => account::COLLECTION
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
)) {
|
||||||
|
// Found the instance of the account connected to the account
|
||||||
|
|
||||||
|
// Initializing the object
|
||||||
|
$account = new account;
|
||||||
|
|
||||||
|
if (method_exists($account, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Implementinf parameters
|
||||||
|
if (isset($result->language)) $result->language = language::{$result->language};
|
||||||
|
if (isset($result->currency)) $result->currency = currency::{$result->currency};
|
||||||
|
|
||||||
|
// Writing the instance of the account document from ArangoDB to the implement object
|
||||||
|
$account->__document($result);
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $account;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cost
|
||||||
|
*
|
||||||
|
* Generate the cart products cost
|
||||||
|
*
|
||||||
|
* @param string &$list List of the calculated products
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return float|int|null The cart products cost, if generated
|
||||||
|
*/
|
||||||
|
public function cost(string &$list = '', array &$errors = []): float|int|null
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Initializing the account
|
||||||
|
$account = $this->account($errors);
|
||||||
|
|
||||||
|
if ($account instanceof account) {
|
||||||
|
// Initialized the account
|
||||||
|
|
||||||
|
// Initializing products in the cart
|
||||||
|
$products = $this->products(language: $account->language ?? language::ru, currency: $account->currency ?? currency::rub);
|
||||||
|
|
||||||
|
if (!empty($products)) {
|
||||||
|
// Initialized products in the cart
|
||||||
|
|
||||||
|
// Declaring total cost of products
|
||||||
|
$cost = 0;
|
||||||
|
|
||||||
|
// Initializing iterator of rows
|
||||||
|
$row = 0;
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
// Iterating over products
|
||||||
|
|
||||||
|
// Generating formatted list of products for message
|
||||||
|
$list .= ++$row . '. ' . $product['document']['name'] . ' (' . $product['amount'] . 'шт)' . "\n";
|
||||||
|
|
||||||
|
// Generating total cost of products
|
||||||
|
$cost += $product['document']['cost'] * $product['amount'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $cost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Order
|
||||||
|
*
|
||||||
|
* Create the order and connect to the cart and to the account
|
||||||
|
* Make the cart ordered (the account will create a new cart)
|
||||||
|
*
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return order|null The order, if created
|
||||||
|
*
|
||||||
|
* @todo Handling errors
|
||||||
|
*/
|
||||||
|
public function order(array &$errors = []): ?order
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
|
if (collection::initialize(order::COLLECTION, order::TYPE, errors: $errors)) {
|
||||||
|
if (collection::initialize(reservation::COLLECTION, reservation::TYPE, errors: $errors)) {
|
||||||
|
if (collection::initialize(account::COLLECTION, account::TYPE, errors: $errors)) {
|
||||||
|
// Initialized collections
|
||||||
|
|
||||||
|
// Searching for the order
|
||||||
|
$existed = collection::execute(
|
||||||
|
<<<'AQL'
|
||||||
|
FOR v IN 1..1 OUTBOUND @cart GRAPH users
|
||||||
|
FILTER PARSE_IDENTIFIER(v._id).collection == @order
|
||||||
|
LIMIT 1
|
||||||
|
return v
|
||||||
|
AQL,
|
||||||
|
[
|
||||||
|
'cart' => $this->document->getId(),
|
||||||
|
'order' => order::COLLECTION
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($existed) {
|
||||||
|
// The order has already been created
|
||||||
|
|
||||||
|
// Initializing the object
|
||||||
|
$order = new order;
|
||||||
|
|
||||||
|
if (method_exists($order, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Writing the instance of the order document from ArangoDB to the implement object
|
||||||
|
$order->__document($existed);
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $order;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The order has not been created
|
||||||
|
|
||||||
|
// Initializing a new order
|
||||||
|
$_id = document::write(
|
||||||
|
order::COLLECTION,
|
||||||
|
[
|
||||||
|
'active' => true,
|
||||||
|
/* 'term' => (int) new datetime('+15 minutes')->ormat('U') */
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result = collection::execute(
|
||||||
|
<<<'AQL'
|
||||||
|
FOR d IN @@collection
|
||||||
|
FILTER d._id == @_id && d.active == true
|
||||||
|
RETURN d
|
||||||
|
AQL,
|
||||||
|
[
|
||||||
|
'@collection' => order::COLLECTION,
|
||||||
|
'_id' => $_id
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
)) {
|
||||||
|
// Found the instance of just created the new order
|
||||||
|
|
||||||
|
// Writing the ordered status for the cart
|
||||||
|
$this->document->ordered = true;
|
||||||
|
|
||||||
|
if (document::update($this->__document(), errors: $errors)) {
|
||||||
|
// Writed into ArangoDB
|
||||||
|
|
||||||
|
// Initializing the object
|
||||||
|
$order = new order;
|
||||||
|
|
||||||
|
if (method_exists($order, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Writing the instance of the order document from ArangoDB to the implement object
|
||||||
|
$order->__document($result);
|
||||||
|
|
||||||
|
// Connecting the cart to the order
|
||||||
|
$connected = $order->connect($this, $errors);
|
||||||
|
|
||||||
|
if ($connected) {
|
||||||
|
// Connected the cart with the order
|
||||||
|
|
||||||
|
if ($result = collection::execute(
|
||||||
|
<<<'AQL'
|
||||||
|
FOR v IN 1..1 OUTBOUND @cart GRAPH users
|
||||||
|
FILTER PARSE_IDENTIFIER(v._id).collection == @account
|
||||||
|
LIMIT 1
|
||||||
|
return v
|
||||||
|
AQL,
|
||||||
|
[
|
||||||
|
'cart' => $this->document->getId(),
|
||||||
|
'account' => account::COLLECTION
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
)) {
|
||||||
|
// Found the instance of the account connected to the cart
|
||||||
|
|
||||||
|
// Initializing the object
|
||||||
|
$account = new account;
|
||||||
|
|
||||||
|
if (method_exists($account, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Writing the instance of the account document from ArangoDB to the implement object
|
||||||
|
$account->__document($result);
|
||||||
|
|
||||||
|
// Connecting the account with the order
|
||||||
|
$connected = $account->connect($order, $errors);
|
||||||
|
|
||||||
|
if ($connected) {
|
||||||
|
// Connected the account with the order
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else throw new exception('Class ' . order::class . ' does not implement a document from ArangoDB');
|
||||||
|
} else throw new exception('Failed to write the ordered status for the cart');
|
||||||
|
} else throw new exception('Failed to create or find just created ' . static::class);
|
||||||
|
}
|
||||||
|
} else throw new exception('Failed to initialize ' . account::TYPE->name . ' collection: ' . account::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . reservation::TYPE->name . ' collection: ' . reservation::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . order::TYPE->name . ' collection: ' . order::COLLECTION);
|
||||||
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,743 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\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;
|
||||||
|
|
||||||
|
// Framework for ArangoDB
|
||||||
|
use mirzaev\arangodb\collection,
|
||||||
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
|
// Framework for Excel
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
final class catalog extends core
|
||||||
|
{
|
||||||
|
use yandex, files {
|
||||||
|
yandex::download as 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect parameter from all products
|
||||||
|
*
|
||||||
|
* @param string $documment Path to the EXCEL-document
|
||||||
|
* @param int &$categories_loaded Counter of loaded categories
|
||||||
|
* @param int &$categories_created Counter of created categories
|
||||||
|
* @param int &$categories_updated Counter of updated categories
|
||||||
|
* @param int &$categories_deleted Counter of deleted categories
|
||||||
|
* @param int &$categories_new Counter of new categories
|
||||||
|
* @param int &$categories_old Counter of old categories
|
||||||
|
* @param int &$products_loaded Counter of loaded products
|
||||||
|
* @param int &$products_created Counter of created products
|
||||||
|
* @param int &$products_updated Counter of updated products
|
||||||
|
* @param int &$products_deleted Counter of deleted products
|
||||||
|
* @param int &$products_new Counter of new products
|
||||||
|
* @param int &$products_old Counter of old products
|
||||||
|
* @param language $language Language
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @todo
|
||||||
|
* 1. Сначала создать все категории и затем снова по циклу пройтись уже создавать entry между ними
|
||||||
|
* 2. Сжимать изображения
|
||||||
|
*/
|
||||||
|
public static function import(
|
||||||
|
string $document,
|
||||||
|
int &$categories_loaded = 0,
|
||||||
|
int &$categories_created = 0,
|
||||||
|
int &$categories_updated = 0,
|
||||||
|
int &$categories_deleted = 0,
|
||||||
|
int &$categories_old = 0,
|
||||||
|
int &$categories_new = 0,
|
||||||
|
int &$products_loaded = 0,
|
||||||
|
int &$products_created = 0,
|
||||||
|
int &$products_updated = 0,
|
||||||
|
int &$products_deleted = 0,
|
||||||
|
int &$products_old = 0,
|
||||||
|
int &$products_new = 0,
|
||||||
|
language $language = language::en,
|
||||||
|
currency $currency = currency::usd,
|
||||||
|
array &$errors = []
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
// Initializing the spreadsheet
|
||||||
|
$spreadsheet = excel::open($document);
|
||||||
|
|
||||||
|
// Inititalizing worksheets
|
||||||
|
$categories = $spreadsheet->getSheet('Категории');
|
||||||
|
$products = $spreadsheet->getSheet('Товары');
|
||||||
|
|
||||||
|
// Counting old documents
|
||||||
|
$categories_old = collection::count(category::COLLECTION, errors: $errors);
|
||||||
|
$products_old = collection::count(product::COLLECTION, errors: $errors);
|
||||||
|
|
||||||
|
// Initializing the buffer of handler categories and products
|
||||||
|
$handled = [
|
||||||
|
'categories' => [],
|
||||||
|
'products' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
$categories->nextRow(
|
||||||
|
[
|
||||||
|
'A' => 'identifier',
|
||||||
|
'B' => 'name',
|
||||||
|
'C' => 'category',
|
||||||
|
'D' => 'images',
|
||||||
|
'E' => 'position'
|
||||||
|
],
|
||||||
|
excel::KEYS_FIRST_ROW
|
||||||
|
) as $number => $row
|
||||||
|
) {
|
||||||
|
// Iterate over categories
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!empty($row['identifier']) && is_int($row['identifier']) && $row['identifier'] > 0 && !empty($row['name'])) {
|
||||||
|
// Required cells are filled in
|
||||||
|
|
||||||
|
// Incrementing the counter of loaded categories
|
||||||
|
++$categories_loaded;
|
||||||
|
|
||||||
|
// Declaring the variable with the status that a new category has been created
|
||||||
|
$created = false;
|
||||||
|
|
||||||
|
// Declaring the variable with the category
|
||||||
|
$category = null;
|
||||||
|
|
||||||
|
// Initializing the category
|
||||||
|
$category = category::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $row['identifier']], errors: $errors);
|
||||||
|
|
||||||
|
if ($category instanceof category) {
|
||||||
|
// Initialized the category
|
||||||
|
|
||||||
|
// Initializing name of the category
|
||||||
|
if (empty($category->name) || empty($category->name[$language->name]) || $category->name[$language->name] !== $row['name'])
|
||||||
|
$category->name = [$language->name => $row['name']] + ($category->name ?? []);
|
||||||
|
|
||||||
|
// Initializing position of the category
|
||||||
|
if (empty($category->position) || $category->position === $row['position'])
|
||||||
|
$category->position = $row['position'];
|
||||||
|
} else {
|
||||||
|
// Not initialized the category
|
||||||
|
|
||||||
|
// Creating the category
|
||||||
|
$_id = $created = category::write(
|
||||||
|
identifier: (int) $row['identifier'],
|
||||||
|
name: [$language->name => $row['name']],
|
||||||
|
position: (int) $row['position'] ?? null,
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initializing the category
|
||||||
|
$category = category::_read(
|
||||||
|
filter: 'd._id == @_id',
|
||||||
|
parameters: ['_id' => $_id],
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Incrementing the counter of created categories
|
||||||
|
if ($created) ++$categories_created;
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($category instanceof category) {
|
||||||
|
// Initialized the category
|
||||||
|
|
||||||
|
if (!empty($row['category'])) {
|
||||||
|
// Received the ascendant category
|
||||||
|
|
||||||
|
// Initializing the ascendant category
|
||||||
|
$ascendant = category::_read(
|
||||||
|
filter: 'd.identifier == @identifier',
|
||||||
|
parameters: ['identifier' => (int) $row['category']],
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($ascendant instanceof category) {
|
||||||
|
// Found the ascendant category
|
||||||
|
|
||||||
|
// Deleting entries of the category in ArangoDB
|
||||||
|
entry::banish($category, $errors);
|
||||||
|
|
||||||
|
// Writing the category as an entry to the ascendant category in ArangoDB
|
||||||
|
entry::write($category, $ascendant, $errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($row['images'])) {
|
||||||
|
// Received images
|
||||||
|
|
||||||
|
// Initializing new images of the category
|
||||||
|
$images = static::folder(
|
||||||
|
uri: explode(' ', mb_trim($row['images']))[0],
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reinitialize images? (true, if no images found or their amount does not match)
|
||||||
|
/* $reinitialize = !$category->images || count($category->images) !== count($images); */
|
||||||
|
$reinitialize = true;
|
||||||
|
|
||||||
|
// Checking the identity of existing images with new images (if reinitialization of images has not yet been requested)
|
||||||
|
/* if (!$reinitialize) foreach ($category->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break; */
|
||||||
|
|
||||||
|
if ($reinitialize) {
|
||||||
|
// Requested reinitialization of images
|
||||||
|
|
||||||
|
// Initializing the buffer of images
|
||||||
|
$buffer = [];
|
||||||
|
|
||||||
|
foreach (is_array($images) ? $images : [] as $image) {
|
||||||
|
// Iterating over new images
|
||||||
|
|
||||||
|
// Initializing identifier of the image
|
||||||
|
$identifier = preg_replace('/\.\w+$/', '', $image->name);
|
||||||
|
|
||||||
|
// Initializing path to directory of images in storage
|
||||||
|
$directory = DIRECTORY_SEPARATOR . 'categories' . DIRECTORY_SEPARATOR . $row['identifier'] . DIRECTORY_SEPARATOR . $identifier;
|
||||||
|
|
||||||
|
// Initializing URL of the image in storage
|
||||||
|
$url = STORAGE . $directory;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing images of the category
|
||||||
|
$category->images = $buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing in ArangoDB
|
||||||
|
$updated = document::update($category->__document(), errors: $errors);
|
||||||
|
|
||||||
|
// Incrementing the counter of updated categories
|
||||||
|
if ($updated && !$created) ++$categories_updated;
|
||||||
|
} else throw new exception('Failed to initialize category: ' . $row['name'] . " ($number)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing to the registry of handled categories and products
|
||||||
|
$handled['categories'][] = $row['identifier'];
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Writing to the registry of errors
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
$products->nextRow(
|
||||||
|
[
|
||||||
|
'A' => 'identifier',
|
||||||
|
'B' => 'name',
|
||||||
|
'C' => 'category',
|
||||||
|
'D' => 'description',
|
||||||
|
'E' => 'cost',
|
||||||
|
'F' => 'weight',
|
||||||
|
'G' => 'x',
|
||||||
|
'H' => 'y',
|
||||||
|
'I' => 'z',
|
||||||
|
'J' => 'brand',
|
||||||
|
'K' => 'compatibility',
|
||||||
|
'L' => 'images',
|
||||||
|
'M' => 'position'
|
||||||
|
],
|
||||||
|
excel::KEYS_FIRST_ROW
|
||||||
|
) as $number => $row
|
||||||
|
) {
|
||||||
|
// Iterate over products
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!empty($row['identifier']) && is_int($row['identifier']) && $row['identifier'] > 0 && !empty($row['name'])) {
|
||||||
|
// Required cells are filled in
|
||||||
|
|
||||||
|
// Incrementing the counter of loaded products
|
||||||
|
++$products_loaded;
|
||||||
|
|
||||||
|
// Declaring the variable with the status that a new product has been created
|
||||||
|
$created = false;
|
||||||
|
|
||||||
|
// Declaring the variable with the product
|
||||||
|
$product = null;
|
||||||
|
|
||||||
|
// Initializing the product
|
||||||
|
$product = product::_read('d.identifier == @identifier', parameters: ['identifier' => (int) $row['identifier']], errors: $errors);
|
||||||
|
|
||||||
|
if ($product instanceof product) {
|
||||||
|
// Initialized the product
|
||||||
|
|
||||||
|
// Initializing name of the product
|
||||||
|
if (empty($product->name) || empty($product->name[$language->name]) || $product->name[$language->name] !== $row['name'])
|
||||||
|
$product->name = [$language->name => $row['name']] + ($product->name ?? []);
|
||||||
|
|
||||||
|
// Initializing description of the product
|
||||||
|
if (empty($product->description) || empty($product->description[$language->name]) || $product->description[$language->name] !== $row['description'])
|
||||||
|
$product->description = [$language->name => $row['description']] + ($product->description ?? []);
|
||||||
|
|
||||||
|
// Initializing brand of the product
|
||||||
|
if (empty($product->brand) || empty($product->brand[$language->name]) || $product->brand[$language->name] !== $row['brand'])
|
||||||
|
$product->brand = [$language->name => $row['brand']] + ($product->brand ?? []);
|
||||||
|
|
||||||
|
// Initializing compatibility of the product
|
||||||
|
if (empty($product->compatibility) || empty($product->brand[$language->name]) || $product->compatibility[$language->name] !== $row['compatibility'])
|
||||||
|
$product->compatibility = [$language->name => $row['compatibility']] + ($product->compatibility ?? []);
|
||||||
|
|
||||||
|
// Initializing position of the product
|
||||||
|
if (empty($product->position) || $product->position !== $row['position'])
|
||||||
|
$product->position = isset($row['position']) ? (int) $row['position'] : 0;
|
||||||
|
} 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,
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initializing the product
|
||||||
|
$product = $created = product::_read('d._id == @_id', parameters: ['_id' => $_id], errors: $errors);
|
||||||
|
|
||||||
|
// Incrementing the counter of created products
|
||||||
|
if ($created) ++$products_created;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($product instanceof product) {
|
||||||
|
// Initialized the product
|
||||||
|
|
||||||
|
if (!empty($row['category'])) {
|
||||||
|
// Received the category
|
||||||
|
|
||||||
|
// Initializing the category
|
||||||
|
$category = category::_read(
|
||||||
|
filter: sprintf('d.identifier == %u', (int) $row['category']),
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($category instanceof category) {
|
||||||
|
// Found the ascendant category
|
||||||
|
|
||||||
|
// Deleting entries of the product in ArangoDB
|
||||||
|
entry::banish($product, $errors);
|
||||||
|
|
||||||
|
// Writing the product as an entry to the ascendant category in ArangoDB
|
||||||
|
entry::write($product, $category, $errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($row['images'])) {
|
||||||
|
// Received images
|
||||||
|
|
||||||
|
// Initializing new images of the product
|
||||||
|
$images = static::folder(
|
||||||
|
uri: explode(' ', mb_trim($row['images']))[0],
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reinitialize images? (true, if no images found or their amount does not match)
|
||||||
|
/* $reinitialize = !$product->images || count($product->images) !== count($images); */
|
||||||
|
$reinitialize = true;
|
||||||
|
|
||||||
|
// Checking the identity of existing images with new images (if reinitialization of images has not yet been requested)
|
||||||
|
/* if (!$reinitialize) foreach ($product->images as $key => $image) if ($reinitialize = $image['source'] !== $images[$key]) break; */
|
||||||
|
|
||||||
|
if ($reinitialize) {
|
||||||
|
// Requested reinitialization of images
|
||||||
|
|
||||||
|
// Initializing the buffer of images
|
||||||
|
$buffer = [];
|
||||||
|
|
||||||
|
foreach (is_array($images) ? $images : [] as $image) {
|
||||||
|
// Iterating over new images
|
||||||
|
|
||||||
|
// Initializing identifier of the image
|
||||||
|
$identifier = preg_replace('/\.\w+$/', '', $image->name);
|
||||||
|
|
||||||
|
// Initializing path to directory of images in storage
|
||||||
|
$directory = DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $row['identifier'] . DIRECTORY_SEPARATOR . $identifier;
|
||||||
|
|
||||||
|
// Initializing URL of the image in storage
|
||||||
|
$url = STORAGE . $directory;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing images of the product
|
||||||
|
$product->images = $buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing in ArangoDB
|
||||||
|
$updated = document::update($product->__document(), errors: $errors);
|
||||||
|
|
||||||
|
// Incrementing the counter of updated products
|
||||||
|
if ($updated && !$created) ++$products_updated;
|
||||||
|
} else throw new exception('Failed to initialize product: ' . $row['name'] . " ($number)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing to the registry of handled categories and products
|
||||||
|
$handled['products'][] = $row['identifier'];
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Writing to the registry of errors
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleting old categories
|
||||||
|
foreach (
|
||||||
|
category::_read(
|
||||||
|
/* filter: sprintf('%s - d.updated > 3600', time()), */
|
||||||
|
sort: 'd.updated DESC',
|
||||||
|
amount: 100000,
|
||||||
|
errors: $errors
|
||||||
|
) ?? [] as $document
|
||||||
|
) {
|
||||||
|
// Iterating over categories
|
||||||
|
|
||||||
|
// Initializing the category
|
||||||
|
$category = new category(document: $document);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$category instanceof category
|
||||||
|
&& array_search($category->identifier, $handled['categories']) === false
|
||||||
|
) {
|
||||||
|
// Not found identifier of the category 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
|
||||||
|
);
|
||||||
|
|
||||||
|
// Deleting entries of the category in ArangoDB
|
||||||
|
entry::banish(
|
||||||
|
document: $category,
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Deleting the category in ArangoDB
|
||||||
|
document::delete(
|
||||||
|
document: $category->__document(),
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Incrementing the counter of deleted categories
|
||||||
|
++$categories_deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleting old products
|
||||||
|
foreach (
|
||||||
|
product::_read(
|
||||||
|
/* filter: sprintf('%s - d.updated > 3600', time()), */
|
||||||
|
sort: 'd.updated DESC',
|
||||||
|
amount: 100000,
|
||||||
|
errors: $errors
|
||||||
|
) ?? [] as $document
|
||||||
|
) {
|
||||||
|
// Iterating over products
|
||||||
|
|
||||||
|
// Initializing the category
|
||||||
|
$product = new product(document: $document);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$product instanceof product
|
||||||
|
&& array_search($product->identifier, $handled['products']) === false
|
||||||
|
) {
|
||||||
|
// Not found identifier of the product in the buffer of handled categories and products
|
||||||
|
|
||||||
|
// Deleting images of the product from storage
|
||||||
|
static::delete(
|
||||||
|
directory: STORAGE . DIRECTORY_SEPARATOR . 'products' . DIRECTORY_SEPARATOR . $product->identifier,
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Deleting entries of the product in ArangoDB
|
||||||
|
entry::banish(
|
||||||
|
document: $product,
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Deleting the product in ArangoDB
|
||||||
|
document::delete(
|
||||||
|
document: $product->__document(),
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Incrementing the counter of deleted products
|
||||||
|
++$products_deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Counting new documents
|
||||||
|
$categories_new = collection::count(collection: category::COLLECTION, errors: $errors);
|
||||||
|
$products_new = collection::count(collection: product::COLLECTION, errors: $errors);
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Writing to the registry of errors
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,13 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models;
|
namespace mirzaev\huesos\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\arming_bot\models\core,
|
use mirzaev\huesos\models\core,
|
||||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
mirzaev\huesos\models\traits\document as document_trait,
|
||||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
mirzaev\huesos\models\interfaces\document as document_interface,
|
||||||
|
mirzaev\huesos\models\interfaces\collection as collection_interface;
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\collection,
|
use mirzaev\arangodb\collection,
|
||||||
|
@ -20,14 +21,14 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Model of category
|
* Model of category
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\models
|
* @package mirzaev\huesos\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class category extends core implements arangodb_document_interface
|
final class category extends core implements document_interface, collection_interface
|
||||||
{
|
{
|
||||||
use arangodb_document_trait;
|
use document_trait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
||||||
|
@ -68,7 +69,7 @@ final class category extends core implements arangodb_document_interface
|
||||||
],
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
|
@ -2,12 +2,13 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models;
|
namespace mirzaev\huesos\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\arming_bot\models\core,
|
use mirzaev\huesos\models\core,
|
||||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
mirzaev\huesos\models\traits\document as document_trait,
|
||||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
mirzaev\huesos\models\interfaces\document as document_interface,
|
||||||
|
mirzaev\huesos\models\interfaces\collection as collection_interface;
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\enumerations\collection\type;
|
use mirzaev\arangodb\enumerations\collection\type;
|
||||||
|
@ -15,14 +16,14 @@ use mirzaev\arangodb\enumerations\collection\type;
|
||||||
/**
|
/**
|
||||||
* Model of connect
|
* Model of connect
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\models
|
* @package mirzaev\huesos\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class connect extends core implements arangodb_document_interface
|
final class connect extends core implements document_interface, collection_interface
|
||||||
{
|
{
|
||||||
use arangodb_document_trait;
|
use document_trait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
|
@ -2,18 +2,17 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models;
|
namespace mirzaev\huesos\models;
|
||||||
|
|
||||||
// Framework for PHP
|
// Framework for PHP
|
||||||
use mirzaev\minimal\model;
|
use mirzaev\minimal\model;
|
||||||
|
|
||||||
|
// Library for ArangoDB
|
||||||
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\connection as arangodb,
|
use mirzaev\arangodb\connection as arangodb,
|
||||||
mirzaev\arangodb\collection,
|
mirzaev\arangodb\collection;
|
||||||
mirzaev\arangodb\enumerations\collection\type;
|
|
||||||
|
|
||||||
// Libraries for ArangoDB
|
|
||||||
use ArangoDBClient\Document as _document;
|
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
use exception;
|
use exception;
|
||||||
|
@ -21,18 +20,13 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Core of models
|
* Core of models
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\models
|
* @package mirzaev\huesos\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
class core extends model
|
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
|
* Path to the file with settings of connecting to the ArangoDB
|
||||||
*/
|
*/
|
||||||
|
@ -40,28 +34,20 @@ class core extends model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instance of the session of ArangoDB
|
* Instance of the session of ArangoDB
|
||||||
|
*
|
||||||
|
* @todo ПЕРЕДЕЛАТЬ В php 8.4
|
||||||
*/
|
*/
|
||||||
protected static arangodb $arangodb;
|
protected static arangodb $arangodb;
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the collection in ArangoDB
|
|
||||||
*/
|
|
||||||
public const string COLLECTION = 'THIS_COLLECTION_SHOULD_NOT_EXIST';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of the collection in ArangoDB
|
|
||||||
*/
|
|
||||||
public const type TYPE = type::document;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor of an instance
|
* Constructor of an instance
|
||||||
*
|
*
|
||||||
* @param bool $initialize Initialize a model?
|
* @param bool $initialize Initialize ...?
|
||||||
* @param ?arangodb $arangodb Instance of a session of ArangoDB
|
* @param ?arangodb $arangodb Instance of a session of ArangoDB
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(bool $initialize = true, ?arangodb $arangodb = null)
|
public function __construct(bool $initialize = false, ?arangodb $arangodb = null)
|
||||||
{
|
{
|
||||||
// For the extends system
|
// For the extends system
|
||||||
parent::__construct($initialize);
|
parent::__construct($initialize);
|
||||||
|
@ -69,17 +55,8 @@ class core extends model
|
||||||
if ($initialize) {
|
if ($initialize) {
|
||||||
// Initializing is requested
|
// Initializing is requested
|
||||||
|
|
||||||
if (isset($arangodb)) {
|
|
||||||
// Recieved an instance of a session of ArangoDB
|
|
||||||
|
|
||||||
// Writing an instance of a session of ArangoDB to the property
|
// Writing an instance of a session of ArangoDB to the property
|
||||||
$this->__set('arangodb', $arangodb);
|
self::$arangodb = $arangodb ?? new arangodb(require static::ARANGODB);
|
||||||
} else {
|
|
||||||
// Not recieved an instance of a session of ArangoDB
|
|
||||||
|
|
||||||
// Initializing of an instance of a session of ArangoDB
|
|
||||||
$this->__get('arangodb');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +81,7 @@ class core extends model
|
||||||
string $return = 'd',
|
string $return = 'd',
|
||||||
array $parameters = [],
|
array $parameters = [],
|
||||||
array &$errors = []
|
array &$errors = []
|
||||||
): _document|array|null {
|
): _document|static|array|null {
|
||||||
try {
|
try {
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE)) {
|
if (collection::initialize(static::COLLECTION, static::TYPE)) {
|
||||||
// Initialized the collection
|
// Initialized the collection
|
||||||
|
@ -117,23 +94,40 @@ class core extends model
|
||||||
%s
|
%s
|
||||||
%s
|
%s
|
||||||
LIMIT @offset, @amount
|
LIMIT @offset, @amount
|
||||||
RETURN @return
|
RETURN %s
|
||||||
AQL,
|
AQL,
|
||||||
empty($filter) ? '' : "FILTER $filter",
|
empty($filter) ? '' : "FILTER $filter",
|
||||||
empty($sort) ? '' : "SORT $sort",
|
empty($sort) ? '' : "SORT $sort",
|
||||||
|
empty($return) ? 'd' : $return
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
'@collection' => static::COLLECTION,
|
'@collection' => static::COLLECTION,
|
||||||
'offset' => --$page <= 0 ? 0 : $page * $amount,
|
'offset' => --$page <= 0 ? 0 : $page * $amount,
|
||||||
'amount' => $amount,
|
'amount' => $amount
|
||||||
'return' => $return
|
|
||||||
] + $parameters,
|
] + $parameters,
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($amount === 1 && $result instanceof _document) {
|
||||||
|
// Received only 1 document and @todo rebuild
|
||||||
|
|
||||||
|
// Initializing the object
|
||||||
|
$object = new static;
|
||||||
|
|
||||||
|
if (method_exists($object, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Writing the instance of document from ArangoDB to the implement object
|
||||||
|
$object->__document($result);
|
||||||
|
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return is_array($result) ? $result : [$result];
|
return $object;
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $result;
|
||||||
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to registry of errors
|
// Writing to registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -160,26 +154,9 @@ class core extends model
|
||||||
{
|
{
|
||||||
match ($name) {
|
match ($name) {
|
||||||
'arangodb' => (function () use ($value) {
|
'arangodb' => (function () use ($value) {
|
||||||
if ($this->__isset('arangodb')) {
|
if (isset(static::$arangodb)) throw new exception('Forbidden to reinitialize the session of ArangoDB ($this::$arangodb)', 500);
|
||||||
// Is alredy initialized
|
else if ($value instanceof arangodb) self::$arangodb = $value;
|
||||||
|
else throw new exception('Session of connection to ArangoDB ($this::$arangodb) is need to be mirzaev\arangodb\connection', 500);
|
||||||
// Exit (fail)
|
|
||||||
throw new exception('Forbidden to reinitialize the session of ArangoDB ($this::$arangodb)', 500);
|
|
||||||
} else {
|
|
||||||
// Is not already initialized
|
|
||||||
|
|
||||||
if ($value instanceof arangodb) {
|
|
||||||
// Recieved an appropriate value
|
|
||||||
|
|
||||||
// Writing the property and exit (success)
|
|
||||||
self::$arangodb = $value;
|
|
||||||
} else {
|
|
||||||
// Recieved an inappropriate value
|
|
||||||
|
|
||||||
// Exit (fail)
|
|
||||||
throw new exception('Session of ArangoDB ($this::$arangodb) is need to be mirzaev\arangodb\connection', 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})(),
|
})(),
|
||||||
default => parent::__set($name, $value)
|
default => parent::__set($name, $value)
|
||||||
};
|
};
|
||||||
|
@ -195,22 +172,6 @@ class core extends model
|
||||||
public function __get(string $name): mixed
|
public function __get(string $name): mixed
|
||||||
{
|
{
|
||||||
return match ($name) {
|
return match ($name) {
|
||||||
'arangodb' => (function () {
|
|
||||||
try {
|
|
||||||
if (!$this->__isset('arangodb')) {
|
|
||||||
// Is not initialized
|
|
||||||
|
|
||||||
// Initializing of a default value from settings
|
|
||||||
$this->__set('arangodb', new arangodb(require static::ARANGODB));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit (success)
|
|
||||||
return self::$arangodb;
|
|
||||||
} catch (exception) {
|
|
||||||
// Exit (fail)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
default => parent::__get($name)
|
default => parent::__get($name)
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\models\deliveries;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
CdekSDK2\Dto\Tariff as _tariff,
|
||||||
|
CdekSDK2\BaseTypes\Tariff as tariff,
|
||||||
|
CdekSDK2\BaseTypes\Tarifflist as tariffs,
|
||||||
|
CdekSDK2\Constraints\Currencies as currencies,
|
||||||
|
CdekSDK2\BaseTypes\Location as location,
|
||||||
|
CdekSDK2\BaseTypes\Package as package;
|
||||||
|
|
||||||
|
// Built-in libraries
|
||||||
|
use exception,
|
||||||
|
DateTime as datetime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model of CDEK
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\models\deliveries
|
||||||
|
*
|
||||||
|
* @method cities|null location(string $name, array &$errors) Search for CDEK location by name
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Location
|
||||||
|
*
|
||||||
|
* Search for CDEK location by name (different villages, towns and farms may have the same names)
|
||||||
|
*
|
||||||
|
* @see https://api-docs.cdek.ru/182405028.html
|
||||||
|
* @see https://github.com/TTATPuOT/cdek-sdk2.0/blob/master/docs/index.md#получение-cписка-городов
|
||||||
|
*
|
||||||
|
* @param string $name Location name
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return _cities|null Locations, if found
|
||||||
|
*/
|
||||||
|
public static function location(string $name, array &$errors = []): ?_cities
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (!empty(CDEK)) {
|
||||||
|
// Initialized CDEK account data
|
||||||
|
|
||||||
|
// Initializing HTTP-client
|
||||||
|
$client = new client(new guzzle, CDEK['account'], CDEK['secret']);
|
||||||
|
|
||||||
|
// Request
|
||||||
|
$response = $client->cities()->getFiltered(['country_codes' => 'RU', 'city' => $name]);
|
||||||
|
|
||||||
|
if ($response->isOk()) {
|
||||||
|
// Received response
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $client->formatResponseList($response, _cities::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate
|
||||||
|
*
|
||||||
|
* Calculate delivery from warehouse to recipient according to tariff
|
||||||
|
*
|
||||||
|
* @see https://api-docs.cdek.ru/63345430.html
|
||||||
|
* @see https://github.com/TTATPuOT/cdek-sdk2.0/blob/master/docs/index.md#калькулятор-расчет-по-коду-тарифа
|
||||||
|
*
|
||||||
|
* @param int $from_location Location code (sender)
|
||||||
|
* @param string $from_street Street (sender)
|
||||||
|
* @param int $to_location Location code (receiver)
|
||||||
|
* @param string $to_street Street (receiver)
|
||||||
|
* @param string $tariff Tariff (identifier)
|
||||||
|
* @param array $products Products that will be sent [weight, x, y, z]
|
||||||
|
* @param datetime $date Date of sending
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return _tariff|null Calculating data, if calculated
|
||||||
|
*/
|
||||||
|
public static function calculate(
|
||||||
|
int $from_location,
|
||||||
|
string $from_street,
|
||||||
|
int $to_location,
|
||||||
|
string $to_street,
|
||||||
|
int $tariff,
|
||||||
|
array $products,
|
||||||
|
datetime $date,
|
||||||
|
array &$errors = []
|
||||||
|
): ?_tariff {
|
||||||
|
try {
|
||||||
|
if (!empty(CDEK)) {
|
||||||
|
// Initialized CDEK account data
|
||||||
|
|
||||||
|
// Initializing HTTP-client
|
||||||
|
$client = new client(new guzzle, CDEK['account'], CDEK['secret']);
|
||||||
|
|
||||||
|
// Initializing buffer of packages
|
||||||
|
$packages = [];
|
||||||
|
|
||||||
|
// Initializing packages from products
|
||||||
|
foreach ($products as $product)
|
||||||
|
$packages[] = package::create([
|
||||||
|
'weight' => $product['weight'],
|
||||||
|
'width' => $product['x'],
|
||||||
|
'height' => $product['y'],
|
||||||
|
'length' => $product['z']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Deinitializing unnecessary variables
|
||||||
|
unset($product);
|
||||||
|
|
||||||
|
if (!empty($packages)) {
|
||||||
|
// Initialized packages
|
||||||
|
|
||||||
|
// Request
|
||||||
|
$response = $client
|
||||||
|
->calculator()
|
||||||
|
->add(
|
||||||
|
tariff::create([
|
||||||
|
'type' => tariffs::TYPE_ECOMMERCE,
|
||||||
|
'currency' => currencies::RUBLE, // @todo globalize this
|
||||||
|
/* 'date' => $date->format(datetime::ISO8601_EXPANDED), */
|
||||||
|
'date' => $date->format(datetime::ISO8601),
|
||||||
|
'lang' => tariffs::LANG_RUS, // @todo globalize this
|
||||||
|
'tariff_code' => $tariff,
|
||||||
|
'from_location' => location::create([
|
||||||
|
'code' => $from_location,
|
||||||
|
'address' => $from_street,
|
||||||
|
'country_code' => 'RU' // @todo globalize this
|
||||||
|
]),
|
||||||
|
'to_location' => location::create([
|
||||||
|
'code' => $to_location,
|
||||||
|
'address' => $to_street,
|
||||||
|
'country_code' => 'RU' // @todo globalize this
|
||||||
|
]),
|
||||||
|
'packages' => $packages
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($response->hasErrors()) {
|
||||||
|
// Receied response with errors
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response->isOk()) {
|
||||||
|
// Received response
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $client->formatBaseResponse($response, _tariff::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deinitializing unnecessary variables
|
||||||
|
unset($client, $packages);
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\models;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\models\core,
|
||||||
|
mirzaev\huesos\models\enumerations\delivery as company;
|
||||||
|
|
||||||
|
// Built-in libraries
|
||||||
|
use exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model of settings
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\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 delivery extends core
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Ready
|
||||||
|
*
|
||||||
|
* @param company $company Delivery company
|
||||||
|
* @param array $parameters Delivery company parameters
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return bool|null Is delivery data ready for creating order?
|
||||||
|
*/
|
||||||
|
public static function ready(company $company, array $parameters, array &$errors = []): ?bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (!empty($parameters)) {
|
||||||
|
// Initialized delivery company data
|
||||||
|
|
||||||
|
if (
|
||||||
|
!empty($parameters['type'])
|
||||||
|
&& !empty($parameters['tariff'])
|
||||||
|
&& (!empty($parameters['location']['name']) || (!empty($parameters['longitude']) && !empty($parameters['latitude'])))
|
||||||
|
&& !empty($parameters['street'])
|
||||||
|
) {
|
||||||
|
// Initialized required parameters: company, type, tariff, location (or longitude with latitude), street
|
||||||
|
|
||||||
|
if ($parameters['type'] === 'door') {
|
||||||
|
// Delivery to the door
|
||||||
|
|
||||||
|
if (!empty($parameters['apartament'])) {
|
||||||
|
// Required parameters initialized: apartament
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Not initialized required parameters
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if ($parameters['type'] === 'office') {
|
||||||
|
// Delivery to the office
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Not identified delivery type
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not initialized required parameters
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Writing to the registry of errors
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,13 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models;
|
namespace mirzaev\huesos\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\arming_bot\models\core,
|
use mirzaev\huesos\models\core,
|
||||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
mirzaev\huesos\models\traits\document as document_trait,
|
||||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
mirzaev\huesos\models\interfaces\document as document_interface,
|
||||||
|
mirzaev\huesos\models\interfaces\collection as collection_interface;
|
||||||
|
|
||||||
// Library for ArangoDB
|
// Library for ArangoDB
|
||||||
use ArangoDBClient\Document as _document;
|
use ArangoDBClient\Document as _document;
|
||||||
|
@ -23,14 +24,14 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Model of entry
|
* Model of entry
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\models
|
* @package mirzaev\huesos\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class entry extends core implements arangodb_document_interface
|
final class entry extends core implements document_interface, collection_interface
|
||||||
{
|
{
|
||||||
use arangodb_document_trait;
|
use document_trait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
||||||
|
@ -72,9 +73,9 @@ final class entry extends core implements arangodb_document_interface
|
||||||
],
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . $to::TYPE . ' collection: ' . $to::COLLECTION);
|
} else throw new exception('Failed to initialize ' . $to::TYPE->name . ' collection: ' . $to::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . $from::TYPE . ' collection: ' . $from::COLLECTION);
|
} else throw new exception('Failed to initialize ' . $from::TYPE->name . ' collection: ' . $from::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -90,40 +91,54 @@ final class entry extends core implements arangodb_document_interface
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find ascendants
|
* Ascendants
|
||||||
*
|
*
|
||||||
* Find ascendants that are not descendants for anyone
|
* Search for ascendants that are not descendants for anyone
|
||||||
*
|
*
|
||||||
* @param category|product $descendant Descendant document
|
* @param category|product $descendant Descendant document
|
||||||
|
* @param string|null $return Return (AQL)
|
||||||
|
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
|
||||||
* @param array &$errors Registry of errors
|
* @param array &$errors Registry of errors
|
||||||
*
|
*
|
||||||
* @return array|null Ascendants that are not descendants for anyone, if found
|
* @return array|null Ascendants that are not descendants for anyone, if found
|
||||||
*/
|
*/
|
||||||
public static function ascendants(
|
public static function ascendants(
|
||||||
category|product $descendant,
|
category|product $descendant,
|
||||||
|
?string $return = 'DISTINCT ascendant',
|
||||||
|
array $parameters = [],
|
||||||
array &$errors = []
|
array &$errors = []
|
||||||
): ?array {
|
): ?array {
|
||||||
try {
|
try {
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
// Initialized the collection
|
// Initialized the collection
|
||||||
if ($ascendants = collection::execute(
|
|
||||||
|
// Search for ascendants
|
||||||
|
if ($result = collection::execute(
|
||||||
|
sprintf(
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR d IN @@collection
|
let from = (
|
||||||
FOR ascendant IN OUTBOUND d @@edge
|
FOR e IN @@edge
|
||||||
RETURN DISTINCT ascendant
|
RETURN DISTINCT e._from
|
||||||
|
)
|
||||||
|
|
||||||
|
FOR d in @@collection
|
||||||
|
FILTER !POSITION(from, d._id)
|
||||||
|
RETURN %s
|
||||||
AQL,
|
AQL,
|
||||||
|
empty($return) ? 'DISTINCT d' : $return
|
||||||
|
),
|
||||||
[
|
[
|
||||||
'@collection' => $descendant::COLLECTION,
|
'@collection' => $descendant::COLLECTION,
|
||||||
'@edge' => static::COLLECTION
|
'@edge' => static::COLLECTION
|
||||||
],
|
] + $parameters,
|
||||||
errors: $errors
|
errors: $errors
|
||||||
)) {
|
)) {
|
||||||
// Found ascendants
|
// Found ascendants
|
||||||
|
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return is_array($ascendants) ? $ascendants : [$ascendants];
|
return is_array($result) ? $result : [$result];
|
||||||
} else return [];
|
} else return [];
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -179,9 +194,9 @@ final class entry extends core implements arangodb_document_interface
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return is_array($entry) ? $entry[0] : $entry;
|
return is_array($entry) ? $entry[0] : $entry;
|
||||||
} else return null;
|
} else return null;
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . $to::TYPE . ' collection: ' . $to::COLLECTION);
|
} else throw new exception('Failed to initialize ' . $to::TYPE->name . ' collection: ' . $to::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . $from::TYPE . ' collection: ' . $from::COLLECTION);
|
} else throw new exception('Failed to initialize ' . $from::TYPE->name . ' collection: ' . $from::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -208,8 +223,12 @@ final class entry extends core implements arangodb_document_interface
|
||||||
* @param category|product $document Ascendant document
|
* @param category|product $document Ascendant document
|
||||||
* @param string|null $filter Expression for filtering (AQL)
|
* @param string|null $filter Expression for filtering (AQL)
|
||||||
* @param string|null $sort Expression for sorting (AQL)
|
* @param string|null $sort Expression for sorting (AQL)
|
||||||
* @param int $page Страница
|
* @param int $depth Amount of nodes for traversal search (subcategories/products inside subcategories...)
|
||||||
* @param int $amount Количество товаров на странице
|
* @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
|
* @param array &$errors Registry of errors
|
||||||
*
|
*
|
||||||
* @return array Массив с найденными вхождениями (может быть пустым)
|
* @return array Массив с найденными вхождениями (может быть пустым)
|
||||||
|
@ -218,8 +237,12 @@ final class entry extends core implements arangodb_document_interface
|
||||||
category|product $document,
|
category|product $document,
|
||||||
?string $filter = 'v.deleted != true && v.hidden != true',
|
?string $filter = 'v.deleted != true && v.hidden != true',
|
||||||
?string $sort = 'v.position ASC, v.created DESC',
|
?string $sort = 'v.position ASC, v.created DESC',
|
||||||
|
int $depth = 1,
|
||||||
int $page = 1,
|
int $page = 1,
|
||||||
int $amount = 100,
|
int $amount = 100,
|
||||||
|
?string $categories_merge = null,
|
||||||
|
?string $products_merge = null,
|
||||||
|
array $parameters = [],
|
||||||
array &$errors = []
|
array &$errors = []
|
||||||
): array {
|
): array {
|
||||||
try {
|
try {
|
||||||
|
@ -227,34 +250,34 @@ final class entry extends core implements arangodb_document_interface
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
// Initialized collections
|
// Initialized collections
|
||||||
|
|
||||||
if ($documents = collection::execute(
|
// Execute and exit (success)
|
||||||
|
return is_array($result = collection::execute(
|
||||||
sprintf(
|
sprintf(
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR v IN 1..1 INBOUND @document GRAPH @graph
|
FOR v IN 1..%u INBOUND @document GRAPH @graph
|
||||||
%s
|
%s
|
||||||
%s
|
%s
|
||||||
LIMIT @offset, @amount
|
LIMIT @offset, @amount
|
||||||
LET _type = (FOR v2 IN INBOUND v._id GRAPH @graph RETURN v2)[0] ? "category" : "product"
|
RETURN DISTINCT IS_SAME_COLLECTION(@category, v._id) ? MERGE(v, {_type: @category%s}) : MERGE(v, {_type: @product%s})
|
||||||
RETURN MERGE(v, {_type})
|
|
||||||
AQL,
|
AQL,
|
||||||
|
$depth,
|
||||||
empty($filter) ? '' : "FILTER $filter",
|
empty($filter) ? '' : "FILTER $filter",
|
||||||
empty($sort) ? '' : "SORT $sort",
|
empty($sort) ? '' : "SORT $sort",
|
||||||
|
empty($categories_merge) ? '' : ", $categories_merge",
|
||||||
|
empty($products_merge) ? '' : ", $products_merge"
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
'grapth' => 'catalog',
|
'category' => category::COLLECTION,
|
||||||
|
'product' => product::COLLECTION,
|
||||||
|
'graph' => 'catalog',
|
||||||
'document' => $document->getId(),
|
'document' => $document->getId(),
|
||||||
'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
|
'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
|
||||||
'amount' => $amount
|
'amount' => $amount
|
||||||
],
|
] + $parameters,
|
||||||
errors: $errors
|
errors: $errors
|
||||||
)) {
|
)) ? $result : [$result];
|
||||||
// Fount entries
|
} 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);
|
||||||
// Возврат (успех)
|
|
||||||
return is_array($documents) ? $documents : [$documents];
|
|
||||||
} else return [];
|
|
||||||
} 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) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -300,8 +323,8 @@ final class entry extends core implements arangodb_document_interface
|
||||||
],
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::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 ' . $document::TYPE->name . ' collection: ' . $document::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\models\enumerations;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\models\enumerations\language;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types of currencies by ISO 4217 standart
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\models\enumerations
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
enum currency
|
||||||
|
{
|
||||||
|
case usd;
|
||||||
|
case rub;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label
|
||||||
|
*
|
||||||
|
* Initialize label of the currency
|
||||||
|
*
|
||||||
|
* @param language|null $language Language into which to translate
|
||||||
|
*
|
||||||
|
* @return string Translated label of the currency
|
||||||
|
*
|
||||||
|
* @todo
|
||||||
|
* 1. More currencies
|
||||||
|
* 2. Cases???
|
||||||
|
*/
|
||||||
|
public function label(?language $language = language::en): string
|
||||||
|
{
|
||||||
|
// Exit (success)
|
||||||
|
return match ($this) {
|
||||||
|
currency::usd => match ($language) {
|
||||||
|
language::en => 'Dollar',
|
||||||
|
language::ru => 'Доллар'
|
||||||
|
},
|
||||||
|
currency::rub => match ($language) {
|
||||||
|
language::en => 'Ruble',
|
||||||
|
language::ru => 'Рубль'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Symbol
|
||||||
|
*
|
||||||
|
* Initialize symbol of the currency
|
||||||
|
*
|
||||||
|
* @return string Symbol of the currency
|
||||||
|
*
|
||||||
|
* @todo
|
||||||
|
* 1. More currencies
|
||||||
|
*/
|
||||||
|
public function symbol(): string
|
||||||
|
{
|
||||||
|
// Exit (success)
|
||||||
|
return match ($this) {
|
||||||
|
currency::usd => '$',
|
||||||
|
currency::rub => '₽'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\models\enumerations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types of deliveries
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\models\enumerations
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
enum delivery: string
|
||||||
|
{
|
||||||
|
case cdek = 'CDEK';
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\models\enumerations;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\models\enumerations\language;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types of destination
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\models\enumerations
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
enum destination
|
||||||
|
{
|
||||||
|
case office;
|
||||||
|
case door;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label
|
||||||
|
*
|
||||||
|
* Initialize label of the destination
|
||||||
|
*
|
||||||
|
* @param language|null $language Language into which to translate
|
||||||
|
*
|
||||||
|
* @return string Translated label of the destination
|
||||||
|
*/
|
||||||
|
public function label(?language $language = language::en): string
|
||||||
|
{
|
||||||
|
// Exit (success)
|
||||||
|
return match ($this) {
|
||||||
|
destination::office => match ($language) {
|
||||||
|
language::en => 'Office',
|
||||||
|
language::ru => 'Пункт выдачи'
|
||||||
|
},
|
||||||
|
destination::door => match ($language) {
|
||||||
|
language::en => 'Door',
|
||||||
|
language::ru => 'До двери'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\models\enumerations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types of languages by ISO 639-1 standart
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\models\enumerations
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
enum language
|
||||||
|
{
|
||||||
|
case en;
|
||||||
|
case ru;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label
|
||||||
|
*
|
||||||
|
* Initialize label of the language
|
||||||
|
*
|
||||||
|
* @param language|null $language Language into which to translate
|
||||||
|
*
|
||||||
|
* @return string Translated label of the language
|
||||||
|
*
|
||||||
|
* @todo
|
||||||
|
* 1. More languages
|
||||||
|
* 2. Cases???
|
||||||
|
*/
|
||||||
|
public function label(?language $language = language::en): string
|
||||||
|
{
|
||||||
|
// Exit (success)
|
||||||
|
return match ($this) {
|
||||||
|
language::en => match ($language) {
|
||||||
|
language::en => 'English',
|
||||||
|
language::ru => 'Английский'
|
||||||
|
},
|
||||||
|
language::ru => match ($language) {
|
||||||
|
language::en => 'Russian',
|
||||||
|
language::ru => 'Русский'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\models\enumerations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types of session verification
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\models\enumerations
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
enum session
|
||||||
|
{
|
||||||
|
case hash_only;
|
||||||
|
case hash_else_address;
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\models\interfaces;
|
||||||
|
|
||||||
|
// Framework for ArangoDB
|
||||||
|
use mirzaev\arangodb\enumerations\collection\type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for implementing a collection from ArangoDB
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\models\traits
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
interface collection
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Name of the collection in ArangoDB
|
||||||
|
*/
|
||||||
|
public const string COLLECTION = 'THIS_COLLECTION_SHOULD_NOT_EXIST';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of the collection in ArangoDB
|
||||||
|
*/
|
||||||
|
public const type TYPE = type::document;
|
||||||
|
}
|
|
@ -2,20 +2,19 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models\interfaces;
|
namespace mirzaev\huesos\models\interfaces;
|
||||||
|
|
||||||
// Library для ArangoDB
|
// Library для ArangoDB
|
||||||
use ArangoDBClient\Document as _document;
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
// Framework for ArangoDB
|
|
||||||
use mirzaev\arangodb\connection as arangodb;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for implementing a document instance from ArangoDB
|
* Interface for implementing a document instance from ArangoDB
|
||||||
*
|
*
|
||||||
* @param _document $document An instance of the ArangoDB document from ArangoDB (protected readonly)
|
* @param _document $document An instance of the ArangoDB document from ArangoDB (protected readonly)
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\models\traits
|
* @package mirzaev\huesos\models\traits
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
interface document
|
interface document
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model of menu
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\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 menu extends core implements document_interface, collection_interface
|
||||||
|
{
|
||||||
|
use document_trait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the collection in ArangoDB
|
||||||
|
*/
|
||||||
|
final public const string COLLECTION = 'menu';
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\models;
|
||||||
|
|
||||||
|
// Files of the project
|
||||||
|
use mirzaev\huesos\models\core,
|
||||||
|
mirzaev\huesos\models\cart,
|
||||||
|
mirzaev\huesos\models\account,
|
||||||
|
mirzaev\huesos\models\reservation,
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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 order
|
||||||
|
*
|
||||||
|
* @uses !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
* @package mirzaev\huesos\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 order extends core implements document_interface, collection_interface
|
||||||
|
{
|
||||||
|
use document_trait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the collection in ArangoDB
|
||||||
|
*/
|
||||||
|
final public const string COLLECTION = 'order';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cart
|
||||||
|
*
|
||||||
|
* Search for the connected cart
|
||||||
|
*
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return cart|null The connected cart, if found
|
||||||
|
*/
|
||||||
|
public function cart(array &$errors = []): ?cart
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($result = collection::execute(
|
||||||
|
<<<'AQL'
|
||||||
|
FOR v IN 1..1 INBOUND @order GRAPH users
|
||||||
|
FILTER PARSE_IDENTIFIER(v._id).collection == @cart
|
||||||
|
LIMIT 1
|
||||||
|
return v
|
||||||
|
AQL,
|
||||||
|
[
|
||||||
|
'order' => $this->document->getId(),
|
||||||
|
'cart' => cart::COLLECTION
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
)) {
|
||||||
|
// Found the instance of the cart connected to the order
|
||||||
|
|
||||||
|
// Initializing the object
|
||||||
|
$cart = new cart;
|
||||||
|
|
||||||
|
if (method_exists($cart, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Writing the instance of the cart document from ArangoDB to the implement object
|
||||||
|
$cart->__document($result);
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $cart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account
|
||||||
|
*
|
||||||
|
* Search for the connected account
|
||||||
|
*
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return account|null The connected account, if found
|
||||||
|
*/
|
||||||
|
public function account(array &$errors = []): ?account
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($result = collection::execute(
|
||||||
|
<<<'AQL'
|
||||||
|
FOR v IN 1..1 OUTBOUND @order GRAPH users
|
||||||
|
FILTER PARSE_IDENTIFIER(v._id).collection == @account
|
||||||
|
LIMIT 1
|
||||||
|
return v
|
||||||
|
AQL,
|
||||||
|
[
|
||||||
|
'order' => $this->document->getId(),
|
||||||
|
'account' => account::COLLECTION
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
)) {
|
||||||
|
// Found the instance of the account connected to the account
|
||||||
|
|
||||||
|
// Initializing the object
|
||||||
|
$account = new account;
|
||||||
|
|
||||||
|
if (method_exists($account, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Implementinf parameters
|
||||||
|
if (isset($result->language)) $result->language = language::{$result->language};
|
||||||
|
if (isset($result->currency)) $result->currency = currency::{$result->currency};
|
||||||
|
|
||||||
|
// Writing the instance of the account document from ArangoDB to the implement object
|
||||||
|
$account->__document($result);
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $account;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,30 +2,37 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models;
|
namespace mirzaev\huesos\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\arming_bot\models\core,
|
use mirzaev\huesos\models\core,
|
||||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait;
|
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;
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\collection,
|
use mirzaev\arangodb\collection,
|
||||||
mirzaev\arangodb\document;
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
|
// Library for ArangoDB
|
||||||
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
use exception;
|
use exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model of a product
|
* Model of a product
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\models
|
* @package mirzaev\huesos\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class product extends core
|
final class product extends core implements document_interface, collection_interface
|
||||||
{
|
{
|
||||||
use arangodb_document_trait;
|
use document_trait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
||||||
|
@ -41,6 +48,8 @@ final class product extends core
|
||||||
* @param float $cost Cost
|
* @param float $cost Cost
|
||||||
* @param float $weight Weight
|
* @param float $weight Weight
|
||||||
* @param array $dimensions Dimensions ['x' => 0.0, 'y' => 0.0, 'z' => 0.0]
|
* @param array $dimensions Dimensions ['x' => 0.0, 'y' => 0.0, 'z' => 0.0]
|
||||||
|
* @param array|null $brand Brand [['en' => value], ['ru' => значение]]
|
||||||
|
* @param array|null $compatibility Compatibility [['en' => value], ['ru' => значение]]
|
||||||
* @param array $images Images (first will be thumbnail)
|
* @param array $images Images (first will be thumbnail)
|
||||||
* @param int|null $position Position for sorting in the catalog (ASC)
|
* @param int|null $position Position for sorting in the catalog (ASC)
|
||||||
* @param array $data Data
|
* @param array $data Data
|
||||||
|
@ -55,9 +64,11 @@ final class product extends core
|
||||||
int $identifier,
|
int $identifier,
|
||||||
array $name = [['en' => 'ERROR']],
|
array $name = [['en' => 'ERROR']],
|
||||||
?array $description = [['en' => 'ERROR']],
|
?array $description = [['en' => 'ERROR']],
|
||||||
float $cost = 0,
|
array $cost = [['usd' => 0]],
|
||||||
float $weight = 0,
|
float $weight = 0,
|
||||||
array $dimensions = ['x' => 0, 'y' => 0, 'z' => 0],
|
array $dimensions = ['x' => 0, 'y' => 0, 'z' => 0],
|
||||||
|
?array $brand = [['en' => 'ERROR']],
|
||||||
|
?array $compatibility = [['en' => 'ERROR']],
|
||||||
array $images = [],
|
array $images = [],
|
||||||
?int $position = null,
|
?int $position = null,
|
||||||
array $data = [],
|
array $data = [],
|
||||||
|
@ -81,13 +92,15 @@ final class product extends core
|
||||||
'y' => $dimensions['y'] ?? 0,
|
'y' => $dimensions['y'] ?? 0,
|
||||||
'z' => $dimensions['z'] ?? 0,
|
'z' => $dimensions['z'] ?? 0,
|
||||||
],
|
],
|
||||||
|
'brand' => $brand,
|
||||||
|
'compatibility' => $compatibility,
|
||||||
'images' => $images,
|
'images' => $images,
|
||||||
'position' => $position,
|
'position' => $position,
|
||||||
'version' => ROBOT_VERSION
|
'version' => ROBOT_VERSION
|
||||||
] + $data,
|
] + $data,
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -111,10 +124,14 @@ final class product extends core
|
||||||
* @param int $page Page
|
* @param int $page Page
|
||||||
* @param int $amount Amount per page
|
* @param int $amount Amount per page
|
||||||
* @param string|null $return Return (AQL)
|
* @param string|null $return Return (AQL)
|
||||||
* @param string $language Language code (en, ru...)
|
* @param language|null $language Language
|
||||||
|
* @param currency|null $currency Currency
|
||||||
|
* @param array $parameters Binded parameters for placeholders ['placeholder' => parameter]
|
||||||
* @param array &$errors Registry of errors
|
* @param array &$errors Registry of errors
|
||||||
*
|
*
|
||||||
* @return array Массив с найденными товарами (может быть пустым)
|
* @return array|static Found products or instance of the product from ArangoDB (can be empty)
|
||||||
|
*
|
||||||
|
* @todo убрать language и currency
|
||||||
*/
|
*/
|
||||||
public static function read(
|
public static function read(
|
||||||
?string $search = null,
|
?string $search = null,
|
||||||
|
@ -122,24 +139,39 @@ final class product extends core
|
||||||
?string $sort = 'd.position ASC, d.created DESC',
|
?string $sort = 'd.position ASC, d.created DESC',
|
||||||
int $page = 1,
|
int $page = 1,
|
||||||
int $amount = 100,
|
int $amount = 100,
|
||||||
?string $return = 'd',
|
?string $return = 'DISTINCT d',
|
||||||
string $language = 'en',
|
?language $language = null,
|
||||||
|
?currency $currency = null,
|
||||||
|
array $parameters = [],
|
||||||
array &$errors = []
|
array &$errors = []
|
||||||
): array {
|
): array|static {
|
||||||
try {
|
try {
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
// Initialized the collection
|
// Initialized the collection
|
||||||
|
|
||||||
// Initializing the query
|
// Initializing of the language parameter
|
||||||
$aql = <<<'AQL'
|
if ($language instanceof language) $parameters['language'] = $language->name;
|
||||||
FOR d IN @@collection
|
|
||||||
AQL;
|
|
||||||
|
|
||||||
if ($search) {
|
// Initializing of the currency parameter
|
||||||
// Requested search
|
if ($currency instanceof currency) $parameters['currency'] = $currency->name;
|
||||||
|
|
||||||
// Writing to the query
|
// Initializing parameters for search
|
||||||
$aql .= <<<'AQL'
|
if ($search) $parameters += [
|
||||||
|
'search' => $search,
|
||||||
|
'analyzer' => 'text_' . $language->name ?? language::en->name
|
||||||
|
];
|
||||||
|
|
||||||
|
// Search for products
|
||||||
|
$result = collection::execute(
|
||||||
|
sprintf(
|
||||||
|
<<<'AQL'
|
||||||
|
FOR d IN @@collection %s
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
LIMIT @offset, @amount
|
||||||
|
RETURN %s
|
||||||
|
AQL,
|
||||||
|
empty($search) ? '' : <<<'AQL'
|
||||||
SEARCH
|
SEARCH
|
||||||
LEVENSHTEIN_MATCH(
|
LEVENSHTEIN_MATCH(
|
||||||
d.name.@language,
|
d.name.@language,
|
||||||
|
@ -147,56 +179,53 @@ final class product extends core
|
||||||
1,
|
1,
|
||||||
false
|
false
|
||||||
) OR
|
) OR
|
||||||
levenshtein_match(
|
LEVENSHTEIN_MATCH(
|
||||||
d.description.@language,
|
d.description.@language,
|
||||||
tokens(@search, @analyzer)[0],
|
TOKENS(@search, @analyzer)[0],
|
||||||
1,
|
1,
|
||||||
false
|
false
|
||||||
) OR
|
) OR
|
||||||
levenshtein_match(
|
LEVENSHTEIN_MATCH(
|
||||||
d.compatibility.@language,
|
d.compatibility.@language,
|
||||||
tokens(@search, @analyzer)[0],
|
TOKENS(@search, @analyzer)[0],
|
||||||
1,
|
1,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
AQL;
|
|
||||||
|
|
||||||
// Adding sorting
|
|
||||||
if ($sort) $sort = "BM25(d) DESC, $sort";
|
|
||||||
else $sort = "BM25(d) DESC";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reading products
|
|
||||||
$documents = collection::execute(
|
|
||||||
sprintf(
|
|
||||||
$aql . <<<'AQL'
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
LIMIT @offset, @amount
|
|
||||||
RETURN $s
|
|
||||||
AQL,
|
AQL,
|
||||||
empty($filter) ? '' : "FILTER $filter",
|
empty($filter) ? '' : "FILTER $filter",
|
||||||
empty($sort) ? '' : "SORT $sort",
|
empty($search) ? (empty($sort) ? '' : "SORT $sort") : (empty($sort) ? "SORT BM25(d) DESC" : "SORT BM25(d) DESC, $sort"),
|
||||||
empty($return) ? 'd' : $return
|
empty($return) ? 'DISTINCT d' : $return
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
'@collection' => $search ? static::COLLECTION . 's_search' : static::COLLECTION,
|
'@collection' => empty($search) ? static::COLLECTION : static::COLLECTION . 's_search',
|
||||||
'search' => $search,
|
|
||||||
'language' => $language,
|
|
||||||
'analyzer' => "text_$language",
|
|
||||||
'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
|
'offset' => --$page <= 0 ? $page = 0 : $page * $amount,
|
||||||
'amount' => $amount,
|
'amount' => $amount,
|
||||||
],
|
] + $parameters,
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($documents) {
|
if ($amount === 1 && $result instanceof _document) {
|
||||||
|
// Found the product @todo need to rebuild this
|
||||||
|
|
||||||
|
// Initializing the object
|
||||||
|
$product = new static;
|
||||||
|
|
||||||
|
if (method_exists($product, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Writing the instance of product document from ArangoDB to the implement object
|
||||||
|
$product->__document($result);
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $product;
|
||||||
|
} else throw new exception('Class ' . static::class . ' does not implement a document from ArangoDB');
|
||||||
|
} else if (!empty($result)) {
|
||||||
// Found products
|
// Found products
|
||||||
|
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return is_array($documents) ? $documents : [$documents];
|
return is_array($result) ? $result : [$result];
|
||||||
} else return [];
|
}
|
||||||
} else throw new exception('Failed to initialize ' . static::COLLECTION . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -214,36 +243,45 @@ final class product extends core
|
||||||
/**
|
/**
|
||||||
* Collect parameter from all products
|
* Collect parameter from all products
|
||||||
*
|
*
|
||||||
* @param string $name Name of the parameter (AQL path)
|
* @param string $return Return (AQL path)
|
||||||
|
* @param array $products Array with products system identifiers ["_id", "_id", "_id"...]
|
||||||
* @param array &$errors Registry of errors
|
* @param array &$errors Registry of errors
|
||||||
*
|
*
|
||||||
* @return array Array with found unique parameter values from all products (can be empty)
|
* @return array Array with found unique parameter values from all products (can be empty)
|
||||||
*/
|
*/
|
||||||
public static function collect(
|
public static function collect(
|
||||||
string $name = 'd._key',
|
string $return = 'd._key',
|
||||||
|
array $products = [],
|
||||||
|
language $language = language::en,
|
||||||
|
array $parameters = [],
|
||||||
array &$errors = []
|
array &$errors = []
|
||||||
): array {
|
): array {
|
||||||
try {
|
try {
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
// Initialized the collection
|
// Initialized the collection
|
||||||
|
|
||||||
if ($values = collection::execute(
|
if ($result = collection::execute(
|
||||||
|
sprintf(
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR d IN @@collecton
|
FOR d IN @@collection
|
||||||
RETURN DISTINCT @parameter
|
%s
|
||||||
|
RETURN DISTINCT %s
|
||||||
AQL,
|
AQL,
|
||||||
|
empty($products) ? '' : 'FILTER POSITION(["' . implode('", "', $products) . '"], d._id)',
|
||||||
|
empty($return) ? 'd._key' : $return
|
||||||
|
),
|
||||||
[
|
[
|
||||||
'@collection' => static::COLLECTION,
|
'@collection' => static::COLLECTION,
|
||||||
'parameter' => $name
|
'language' => $language->name,
|
||||||
],
|
] + $parameters,
|
||||||
errors: $errors
|
errors: $errors
|
||||||
)) {
|
)) {
|
||||||
// Found parameters
|
// Found parameters
|
||||||
|
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return $values;
|
return is_array($result) ? $result : [$result];
|
||||||
} else return [];
|
} else return [];
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\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;
|
||||||
|
|
||||||
|
// Framework for ArangoDB
|
||||||
|
use mirzaev\arangodb\enumerations\collection\type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model of reservtion
|
||||||
|
*
|
||||||
|
* @used-by cart
|
||||||
|
* @package mirzaev\huesos\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 reservation extends core implements document_interface, collection_interface
|
||||||
|
{
|
||||||
|
use document_trait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the collection in ArangoDB
|
||||||
|
*/
|
||||||
|
final public const string COLLECTION = 'reservation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of the collection in ArangoDB
|
||||||
|
*/
|
||||||
|
public const type TYPE = type::edge;
|
||||||
|
}
|
|
@ -2,21 +2,26 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models;
|
namespace mirzaev\huesos\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\arming_bot\models\account,
|
use mirzaev\huesos\models\account,
|
||||||
mirzaev\arming_bot\models\connect,
|
mirzaev\huesos\models\connect,
|
||||||
mirzaev\arming_bot\models\enumerations\session as verification,
|
mirzaev\huesos\models\enumerations\session as verification,
|
||||||
mirzaev\arming_bot\models\traits\status,
|
mirzaev\huesos\models\traits\status,
|
||||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
mirzaev\huesos\models\traits\buffer,
|
||||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
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;
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\collection,
|
use mirzaev\arangodb\collection,
|
||||||
mirzaev\arangodb\document;
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
// Library для ArangoDB
|
// Library for ArangoDB
|
||||||
use ArangoDBClient\Document as _document;
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
|
@ -25,14 +30,17 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Model of a session
|
* Model of a session
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\models
|
* @package mirzaev\huesos\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class session extends core implements arangodb_document_interface
|
final class session extends core implements document_interface, collection_interface
|
||||||
{
|
{
|
||||||
use status, arangodb_document_trait;
|
use status, document_trait, buffer, cart {
|
||||||
|
buffer::write as write;
|
||||||
|
cart::initialize as cart;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
||||||
|
@ -42,18 +50,18 @@ final class session extends core implements arangodb_document_interface
|
||||||
/**
|
/**
|
||||||
* Type of sessions verification
|
* Type of sessions verification
|
||||||
*/
|
*/
|
||||||
public const verification VERIFICATION = verification::hash_else_address;
|
final public const verification VERIFICATION = verification::hash_else_address;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor of instance
|
* Constructor
|
||||||
*
|
*
|
||||||
* Initialize of a session and write them to the $this->document property
|
* Initialize session and write into the $this->document property
|
||||||
*
|
*
|
||||||
* @param ?string $hash Hash of the session in ArangoDB
|
* @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 ?int $expires Date of expiring of the session (used for creating a new session)
|
||||||
* @param array &$errors Registry of errors
|
* @param array &$errors Registry of errors
|
||||||
*
|
*
|
||||||
* @return static
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
|
public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
|
||||||
{
|
{
|
||||||
|
@ -106,14 +114,14 @@ final class session extends core implements arangodb_document_interface
|
||||||
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
|
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
|
||||||
|
|
||||||
if (document::update($session, errors: $errors)) {
|
if (document::update($session, errors: $errors)) {
|
||||||
// Update is writed to ArangoDB
|
// Writed into ArangoDB
|
||||||
|
|
||||||
// Writing instance of the session document from ArangoDB to the property of the implementing object
|
// Writing instance of the session document from ArangoDB to the property of the implementing object
|
||||||
$this->__document($session);
|
$this->__document($session);
|
||||||
} else throw new exception('Failed to write the session data');
|
} 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 create or find just created session');
|
||||||
}
|
}
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -141,79 +149,45 @@ final class session extends core implements arangodb_document_interface
|
||||||
// Initialized collections
|
// Initialized collections
|
||||||
|
|
||||||
// Search for connected account
|
// Search for connected account
|
||||||
$document = collection::execute(
|
$result = collection::execute(
|
||||||
<<<AQL
|
<<<AQL
|
||||||
FOR v IN INBOUND @session GRAPH sessions
|
FOR v IN INBOUND @session GRAPH @graph
|
||||||
|
FILTER IS_SAME_COLLECTION(@collection, v._id)
|
||||||
SORT v.created DESC
|
SORT v.created DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
RETURN v
|
RETURN v
|
||||||
AQL,
|
AQL,
|
||||||
[
|
[
|
||||||
|
'graph' => 'users',
|
||||||
|
'collection' => account::COLLECTION,
|
||||||
'session' => $this->getId()
|
'session' => $this->getId()
|
||||||
],
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($document instanceof _document) {
|
if ($result instanceof _document) {
|
||||||
// Found connected account
|
// Found the account
|
||||||
|
|
||||||
// Initializing the implement object of the instance of sesson document from ArangoDB
|
// Initializing the object
|
||||||
$account = new account;
|
$account = new account;
|
||||||
|
|
||||||
// Writing the instance of session document from ArangoDB to the implement object
|
if (method_exists($account, '__document')) {
|
||||||
$account->__document($document);
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Abstractioning of parameters
|
||||||
|
if (isset($result->language)) $result->language = language::{$result->language};
|
||||||
|
if (isset($result->currency)) $result->currency = currency::{$result->currency};
|
||||||
|
|
||||||
|
// Writing the instance of account document from ArangoDB to the implement object
|
||||||
|
$account->__document($result);
|
||||||
|
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return $account;
|
return $account;
|
||||||
|
} else throw new exception('Class ' . account::class . ' does not implement a document from ArangoDB');
|
||||||
} else return null;
|
} else return null;
|
||||||
} else throw new exception('Failed to initialize ' . account::TYPE . ' collection: ' . account::COLLECTION);
|
} else throw new exception('Failed to initialize ' . account::TYPE->name . ' collection: ' . account::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . connect::TYPE . ' collection: ' . connect::COLLECTION);
|
} else throw new exception('Failed to initialize ' . connect::TYPE->name . ' collection: ' . connect::COLLECTION);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
|
||||||
// Writing to the registry of errors
|
|
||||||
$errors[] = [
|
|
||||||
'text' => $e->getMessage(),
|
|
||||||
'file' => $e->getFile(),
|
|
||||||
'line' => $e->getLine(),
|
|
||||||
'stack' => $e->getTrace()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit (fail)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect account to session
|
|
||||||
*
|
|
||||||
* @param account $account Account
|
|
||||||
* @param array &$errors Registry of errors
|
|
||||||
*
|
|
||||||
* @return string|null The identifier of the created edge of the "connect" collection, if created
|
|
||||||
*/
|
|
||||||
public function connect(account $account, array &$errors = []): ?string
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
|
||||||
if (collection::initialize(connect::COLLECTION, connect::TYPE, errors: $errors)) {
|
|
||||||
if (collection::initialize(account::COLLECTION, account::TYPE, errors: $errors)) {
|
|
||||||
// Collections initialized
|
|
||||||
|
|
||||||
// The instance of the session document from ArangoDB is initialized?
|
|
||||||
isset($this->document) || throw new exception('The instance of the sessoin document from ArangoDB is not initialized');
|
|
||||||
|
|
||||||
// Writing document and exit (success)
|
|
||||||
return document::write(
|
|
||||||
connect::COLLECTION,
|
|
||||||
[
|
|
||||||
'_from' => $account->getId(),
|
|
||||||
'_to' => $this->document->getId()
|
|
||||||
],
|
|
||||||
errors: $errors
|
|
||||||
);
|
|
||||||
} else throw new exception('Failed to initialize ' . account::TYPE . ' collection: ' . account::COLLECTION);
|
|
||||||
} else throw new exception('Failed to initialize ' . connect::TYPE . ' collection: ' . connect::COLLECTION);
|
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -248,7 +222,7 @@ final class session extends core implements arangodb_document_interface
|
||||||
return collection::execute(
|
return collection::execute(
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR d IN @@collection
|
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
|
RETURN d
|
||||||
AQL,
|
AQL,
|
||||||
[
|
[
|
||||||
|
@ -258,7 +232,7 @@ final class session extends core implements arangodb_document_interface
|
||||||
],
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -294,6 +268,8 @@ final class session extends core implements arangodb_document_interface
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR d IN @@collection
|
FOR d IN @@collection
|
||||||
FILTER d.address == @address && d.expires > @time && d.active == true
|
FILTER d.address == @address && d.expires > @time && d.active == true
|
||||||
|
SORT d.updated DESC
|
||||||
|
LIMIT 1
|
||||||
RETURN d
|
RETURN d
|
||||||
AQL,
|
AQL,
|
||||||
[
|
[
|
||||||
|
@ -303,7 +279,7 @@ final class session extends core implements arangodb_document_interface
|
||||||
],
|
],
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -317,44 +293,4 @@ final class session extends core implements arangodb_document_interface
|
||||||
// Exit (fail)
|
// Exit (fail)
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Write to buffer of the session
|
|
||||||
*
|
|
||||||
* @param array $data Data for merging
|
|
||||||
* @param array &$errors Registry of errors
|
|
||||||
*
|
|
||||||
* @return bool Is data has written into the session document from ArangoDB?
|
|
||||||
*/
|
|
||||||
public function write(array $data, array &$errors = []): bool
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
|
||||||
// Initialized the collection
|
|
||||||
|
|
||||||
// The instance of the session document from ArangoDB is initialized?
|
|
||||||
isset($this->document) || throw new exception('The instance of the sessoin document from ArangoDB is not initialized');
|
|
||||||
|
|
||||||
// Writing data into buffer of the instance of the session document from ArangoDB
|
|
||||||
$this->document->buffer = array_replace_recursive(
|
|
||||||
$this->document->buffer ?? [],
|
|
||||||
[$_SERVER['INTERFACE'] => array_replace_recursive($this->document->buffer[$_SERVER['INTERFACE']] ?? [], $data)]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Writing to ArangoDB and exit (success)
|
|
||||||
return document::update($this->document, errors: $errors);
|
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
|
||||||
} catch (exception $e) {
|
|
||||||
// Writing to the registry of errors
|
|
||||||
$errors[] = [
|
|
||||||
'text' => $e->getMessage(),
|
|
||||||
'file' => $e->getFile(),
|
|
||||||
'line' => $e->getLine(),
|
|
||||||
'stack' => $e->getTrace()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit (fail)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -2,18 +2,21 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models;
|
namespace mirzaev\huesos\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\arming_bot\models\core,
|
use mirzaev\huesos\models\core,
|
||||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
mirzaev\huesos\models\traits\document as document_trait,
|
||||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
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;
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\collection,
|
use mirzaev\arangodb\collection,
|
||||||
mirzaev\arangodb\document;
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
// Library для ArangoDB
|
// Library for ArangoDB
|
||||||
use ArangoDBClient\Document as _document;
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
|
@ -22,14 +25,14 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Model of settings
|
* Model of settings
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\models
|
* @package mirzaev\huesos\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class settings extends core implements arangodb_document_interface
|
final class settings extends core implements document_interface, collection_interface
|
||||||
{
|
{
|
||||||
use arangodb_document_trait;
|
use document_trait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
||||||
|
@ -51,7 +54,7 @@ final class settings extends core implements arangodb_document_interface
|
||||||
// Initialized the collection
|
// Initialized the collection
|
||||||
|
|
||||||
// Search for active settings
|
// Search for active settings
|
||||||
$document = collection::execute(
|
$result = collection::execute(
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR d IN @@collection
|
FOR d IN @@collection
|
||||||
FILTER d.status == 'active'
|
FILTER d.status == 'active'
|
||||||
|
@ -65,17 +68,25 @@ final class settings extends core implements arangodb_document_interface
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($document instanceof _document) {
|
if ($result instanceof _document) {
|
||||||
// Found active settings
|
// Found active settings
|
||||||
|
|
||||||
// Initializing the implement object of the instance of settings document from ArangoDB
|
// Initializing the object
|
||||||
$settings = new static;
|
$settings = new static;
|
||||||
|
|
||||||
|
if (method_exists($settings, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Abstractioning of parameters
|
||||||
|
if (isset($result->language)) $result->language = language::{$result->language};
|
||||||
|
if (isset($result->currency)) $result->currency = currency::{$result->currency};
|
||||||
|
|
||||||
// Writing the instance of settings document from ArangoDB to the implement object
|
// Writing the instance of settings document from ArangoDB to the implement object
|
||||||
$settings->__document($document);
|
$settings->__document($result);
|
||||||
|
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return $settings;
|
return $settings;
|
||||||
|
} else throw new exception('Class ' . static::class . ' does not implement a document from ArangoDB');
|
||||||
} else if ($create) {
|
} else if ($create) {
|
||||||
// Not found active settings and requested their creating
|
// Not found active settings and requested their creating
|
||||||
|
|
||||||
|
@ -85,7 +96,7 @@ final class settings extends core implements arangodb_document_interface
|
||||||
// Re-search (without creating) and exit (success || fail)
|
// Re-search (without creating) and exit (success || fail)
|
||||||
return static::active(errors: $errors);
|
return static::active(errors: $errors);
|
||||||
} else throw new exception('Active settings not found');
|
} else throw new exception('Active settings not found');
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models;
|
namespace mirzaev\huesos\models;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\arming_bot\models\core,
|
use mirzaev\huesos\models\core,
|
||||||
mirzaev\arming_bot\controllers\core as controller,
|
mirzaev\huesos\models\traits\document as document_trait,
|
||||||
mirzaev\arming_bot\models\settings,
|
mirzaev\huesos\models\interfaces\document as document_interface,
|
||||||
mirzaev\arming_bot\models\traits\document as arangodb_document_trait,
|
mirzaev\huesos\models\interfaces\collection as collection_interface,
|
||||||
mirzaev\arming_bot\models\interfaces\document as arangodb_document_interface;
|
mirzaev\huesos\models\enumerations\language;
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\collection,
|
use mirzaev\arangodb\collection,
|
||||||
|
@ -25,14 +25,14 @@ use exception,
|
||||||
/**
|
/**
|
||||||
* Model of a suspension
|
* Model of a suspension
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\models
|
* @package mirzaev\huesos\models
|
||||||
*
|
*
|
||||||
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
final class suspension extends core implements arangodb_document_interface
|
final class suspension extends core implements document_interface, collection_interface
|
||||||
{
|
{
|
||||||
use arangodb_document_trait;
|
use document_trait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the collection in ArangoDB
|
* Name of the collection in ArangoDB
|
||||||
|
@ -53,7 +53,7 @@ final class suspension extends core implements arangodb_document_interface
|
||||||
// Initialized the collection
|
// Initialized the collection
|
||||||
|
|
||||||
// Search for active suspension
|
// Search for active suspension
|
||||||
$document = collection::execute(
|
$result = collection::execute(
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR d IN @@collection
|
FOR d IN @@collection
|
||||||
FILTER d.end > @time
|
FILTER d.end > @time
|
||||||
|
@ -68,19 +68,23 @@ final class suspension extends core implements arangodb_document_interface
|
||||||
errors: $errors
|
errors: $errors
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($document instanceof _document) {
|
if ($result instanceof _document) {
|
||||||
// Found active suspension
|
// Found active settings
|
||||||
|
|
||||||
// Initializing the implement object of the instance of suspension document from ArangoDB
|
// Initializing the object
|
||||||
$suspension = new static;
|
$suspension = new static;
|
||||||
|
|
||||||
|
if (method_exists($suspension, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
// Writing the instance of suspension document from ArangoDB to the implement object
|
// Writing the instance of suspension document from ArangoDB to the implement object
|
||||||
$suspension->__document($document);
|
$suspension->__document($result);
|
||||||
|
|
||||||
// Exit (success)
|
// Exit (success)
|
||||||
return $suspension;
|
return $suspension;
|
||||||
|
} else throw new exception('Class ' . static::class . ' does not implement a document from ArangoDB');
|
||||||
} else return null;
|
} else return null;
|
||||||
} else throw new exception('Failed to initialize ' . static::TYPE . ' collection: ' . static::COLLECTION);
|
} else throw new exception('Failed to initialize ' . static::TYPE->name . ' collection: ' . static::COLLECTION);
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Writing to the registry of errors
|
// Writing to the registry of errors
|
||||||
$errors[] = [
|
$errors[] = [
|
||||||
|
@ -98,17 +102,14 @@ final class suspension extends core implements arangodb_document_interface
|
||||||
/**
|
/**
|
||||||
* Generate message about remaining time
|
* Generate message about remaining time
|
||||||
*
|
*
|
||||||
* @param string|null $language Language of the generated text (otherwise used from settings.language)
|
* @param language|null $language Language of the generated text (otherwise used from settings.language)
|
||||||
* @param array &$errors Registry of errors
|
* @param array &$errors Registry of errors
|
||||||
*
|
*
|
||||||
* @return string|null Text: "? days, ? hours and ? minutes"
|
* @return string|null Text: "? days, ? hours and ? minutes"
|
||||||
*/
|
*/
|
||||||
public function message(?string $language = null, array &$errors = []): ?string
|
public function message(?language $language = language::en, array &$errors = []): ?string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// Initializing default value
|
|
||||||
$language ??= controller::$settings?->language ?? 'en';
|
|
||||||
|
|
||||||
// Initializing the time until the suspension ends
|
// Initializing the time until the suspension ends
|
||||||
$difference = date_diff(new datetime('@' . $this->document->end), new datetime());
|
$difference = date_diff(new datetime('@' . $this->document->end), new datetime());
|
||||||
|
|
||||||
|
@ -118,54 +119,54 @@ final class suspension extends core implements arangodb_document_interface
|
||||||
$difference->d,
|
$difference->d,
|
||||||
match ($difference->d > 20 ? $difference->d % 10 : $difference->d % 100) {
|
match ($difference->d > 20 ? $difference->d % 10 : $difference->d % 100) {
|
||||||
1 => match ($language) {
|
1 => match ($language) {
|
||||||
'ru' => 'день',
|
language::en => 'day',
|
||||||
'en' => 'day',
|
language::ru => 'день',
|
||||||
default => 'day'
|
default => 'day'
|
||||||
},
|
},
|
||||||
2, 3, 4 => match ($language) {
|
2, 3, 4 => match ($language) {
|
||||||
'ru' => 'дня',
|
language::en => 'days',
|
||||||
'en' => 'days',
|
language::ru => 'дня',
|
||||||
default => 'days'
|
default => 'days'
|
||||||
},
|
},
|
||||||
default => match ($language) {
|
default => match ($language) {
|
||||||
'ru' => 'дней',
|
language::en => 'days',
|
||||||
'en' => 'days',
|
language::ru => 'дней',
|
||||||
default => 'days'
|
default => 'days'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
$difference->h,
|
$difference->h,
|
||||||
match ($difference->h > 20 ? $difference->h % 10 : $difference->h % 100) {
|
match ($difference->h > 20 ? $difference->h % 10 : $difference->h % 100) {
|
||||||
1 => match ($language) {
|
1 => match ($language) {
|
||||||
'ru' => 'час',
|
language::en => 'hours',
|
||||||
'en' => 'hours',
|
language::ru => 'час',
|
||||||
default => 'hour'
|
default => 'hour'
|
||||||
},
|
},
|
||||||
2, 3, 4 => match ($language) {
|
2, 3, 4 => match ($language) {
|
||||||
'ru' => 'часа',
|
language::en => 'hours',
|
||||||
'en' => 'hours',
|
language::ru => 'часа',
|
||||||
default => 'hours'
|
default => 'hours'
|
||||||
},
|
},
|
||||||
default => match ($language) {
|
default => match ($language) {
|
||||||
'ru' => 'часов',
|
language::en => 'hours',
|
||||||
'en' => 'hours',
|
language::ru => 'часов',
|
||||||
default => 'hours'
|
default => 'hours'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
$difference->i,
|
$difference->i,
|
||||||
match ($difference->i > 20 ? $difference->i % 10 : $difference->i % 100) {
|
match ($difference->i > 20 ? $difference->i % 10 : $difference->i % 100) {
|
||||||
1 => match ($language) {
|
1 => match ($language) {
|
||||||
'ru' => 'минута',
|
language::en => 'minute',
|
||||||
'en' => 'minute',
|
language::ru => 'минута',
|
||||||
default => 'minute'
|
default => 'minute'
|
||||||
},
|
},
|
||||||
2, 3, 4 => match ($language) {
|
2, 3, 4 => match ($language) {
|
||||||
'ru' => 'минуты',
|
language::en => 'minutes',
|
||||||
'en' => 'minutes',
|
language::ru => 'минуты',
|
||||||
default => 'minutes'
|
default => 'minutes'
|
||||||
},
|
},
|
||||||
default => match ($language) {
|
default => match ($language) {
|
||||||
'ru' => 'минут',
|
language::en => 'minutes',
|
||||||
'en' => 'minutes',
|
language::ru => 'минут',
|
||||||
default => 'minutes'
|
default => 'minutes'
|
||||||
}
|
}
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\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;
|
||||||
|
|
||||||
|
// Library for ArangoDB
|
||||||
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
|
// Framework for ArangoDB
|
||||||
|
use mirzaev\arangodb\collection,
|
||||||
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
|
// Built-in libraries
|
||||||
|
use exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buffer
|
||||||
|
*
|
||||||
|
* Storage of data in the document from ArangoDB
|
||||||
|
*
|
||||||
|
* @param static COLLECTION Name of the collection in ArangoDB
|
||||||
|
* @param static TYPE Type of the collection in ArangoDB
|
||||||
|
*
|
||||||
|
* @uses collection_interface
|
||||||
|
* @package mirzaev\huesos\models\traits
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
trait buffer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Write to buffer of the document
|
||||||
|
*
|
||||||
|
* @param array $data Data for writing (merge)
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return bool Is data has written into the document from ArangoDB?
|
||||||
|
*/
|
||||||
|
public function write(array $data, array &$errors = []): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
// Writing data into buffer of the instance of the document from ArangoDB
|
||||||
|
$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');
|
||||||
|
|
||||||
|
// 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; */
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\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;
|
||||||
|
|
||||||
|
// Library для ArangoDB
|
||||||
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
|
// Framework for ArangoDB
|
||||||
|
use mirzaev\arangodb\collection,
|
||||||
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
|
// Built-in libraries
|
||||||
|
use exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cart
|
||||||
|
*
|
||||||
|
* Cart for a document from ArangoDB
|
||||||
|
*
|
||||||
|
* @uses collection_interface
|
||||||
|
* @uses document_interface
|
||||||
|
* @package mirzaev\huesos\models\traits
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
trait cart
|
||||||
|
{
|
||||||
|
use document_trait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cart
|
||||||
|
*
|
||||||
|
* Search for a connected cart
|
||||||
|
*
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return model|null An object implements the instance of the cart document from ArangoDB, if found
|
||||||
|
*/
|
||||||
|
public function initialize(array &$errors = []): ?model
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
|
if (collection::initialize(connect::COLLECTION, connect::TYPE, errors: $errors)) {
|
||||||
|
if (collection::initialize(model::COLLECTION, model::TYPE, errors: $errors)) {
|
||||||
|
// Initialized collections
|
||||||
|
|
||||||
|
// Search for connected cart
|
||||||
|
$result = collection::execute(
|
||||||
|
<<<AQL
|
||||||
|
FOR v IN 1..1 INBOUND @document GRAPH @graph
|
||||||
|
FILTER IS_SAME_COLLECTION(@cart, v._id) && v.active == true && v.ordered != true
|
||||||
|
SORT v.updated DESC, v.created DESC
|
||||||
|
LIMIT 1
|
||||||
|
RETURN v
|
||||||
|
AQL,
|
||||||
|
[
|
||||||
|
'cart' => model::COLLECTION,
|
||||||
|
'graph' => 'users',
|
||||||
|
'document' => $this->document->getId()
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result instanceof _document) {
|
||||||
|
// Found the cart
|
||||||
|
|
||||||
|
// Initializing the object
|
||||||
|
$cart = new model;
|
||||||
|
|
||||||
|
if (method_exists($cart, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Writing the instance of cart document from ArangoDB to the implement object
|
||||||
|
$cart->__document($result);
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $cart;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not found the cart
|
||||||
|
|
||||||
|
// Initializing a new cart and write they into ArangoDB
|
||||||
|
$_id = document::write(
|
||||||
|
model::COLLECTION,
|
||||||
|
[
|
||||||
|
'active' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result = collection::execute(
|
||||||
|
<<<'AQL'
|
||||||
|
FOR d IN @@collection
|
||||||
|
FILTER d._id == @_id && d.active == true
|
||||||
|
RETURN d
|
||||||
|
AQL,
|
||||||
|
[
|
||||||
|
'@collection' => model::COLLECTION,
|
||||||
|
'_id' => $_id
|
||||||
|
],
|
||||||
|
errors: $errors
|
||||||
|
)) {
|
||||||
|
// Found the instance of just created new cart
|
||||||
|
|
||||||
|
// Initializing the object
|
||||||
|
$cart = new model;
|
||||||
|
|
||||||
|
if (method_exists($cart, '__document')) {
|
||||||
|
// Object can implement a document from ArangoDB
|
||||||
|
|
||||||
|
// Writing the instance of cart document from ArangoDB to the implement object
|
||||||
|
$cart->__document($result);
|
||||||
|
|
||||||
|
// Connecting the cart to the document
|
||||||
|
$connected = $this->connect($cart, $errors);
|
||||||
|
|
||||||
|
if ($connected) {
|
||||||
|
// The cart has been connected to the document
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return $cart;
|
||||||
|
} else throw new exception('Failed to connect the ' . model::class . ' to the ' . static::class);
|
||||||
|
} 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);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,16 +2,20 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models\traits;
|
namespace mirzaev\huesos\models\traits;
|
||||||
|
|
||||||
// Files of the project
|
// Files of the project
|
||||||
use mirzaev\arming_bot\models\core;
|
use mirzaev\huesos\models\interfaces\document as document_interface,
|
||||||
|
mirzaev\huesos\models\interfaces\collection as collection_interface,
|
||||||
|
mirzaev\huesos\models\connect;
|
||||||
|
|
||||||
// Library для ArangoDB
|
// Library для ArangoDB
|
||||||
use ArangoDBClient\Document as _document;
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
// Framework for ArangoDB
|
// Framework for ArangoDB
|
||||||
use mirzaev\arangodb\connection as arangodb;
|
use mirzaev\arangodb\connection as arangodb,
|
||||||
|
mirzaev\arangodb\document as framework_document,
|
||||||
|
mirzaev\arangodb\collection;
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
use exception;
|
use exception;
|
||||||
|
@ -21,7 +25,10 @@ use exception;
|
||||||
*
|
*
|
||||||
* @var protected readonly _document|null $document An instance of the ArangoDB document
|
* @var protected readonly _document|null $document An instance of the ArangoDB document
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\models\traits
|
* @uses document_interface
|
||||||
|
* @package mirzaev\huesos\models\traits
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
trait document
|
trait document
|
||||||
|
@ -49,7 +56,7 @@ trait document
|
||||||
parent::__construct($initialize, $arangodb);
|
parent::__construct($initialize, $arangodb);
|
||||||
|
|
||||||
// Writing to the property
|
// Writing to the property
|
||||||
if ($document instanceof _document) $this->document = $document;
|
if ($document instanceof _document) $this->__document($document);
|
||||||
else if ($document === null) throw new exception('Failed to initialize an instance of the document from ArangoDB');
|
else if ($document === null) throw new exception('Failed to initialize an instance of the document from ArangoDB');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +76,85 @@ trait document
|
||||||
return $this->document ?? null;
|
return $this->document ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
public function connect(collection_interface $document, array &$errors = []): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (collection::initialize(static::COLLECTION, static::TYPE, errors: $errors)) {
|
||||||
|
if (collection::initialize(connect::COLLECTION, connect::TYPE, errors: $errors)) {
|
||||||
|
if (collection::initialize($document::COLLECTION, $document::TYPE, errors: $errors)) {
|
||||||
|
// Initialized collections
|
||||||
|
|
||||||
|
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,
|
||||||
|
[
|
||||||
|
'@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);
|
||||||
|
} 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
|
||||||
*
|
*
|
||||||
|
@ -98,7 +184,6 @@ trait document
|
||||||
{
|
{
|
||||||
// Read a property from an instance of the ArangoDB document and exit (success)
|
// Read a property from an instance of the ArangoDB document and exit (success)
|
||||||
return match ($name) {
|
return match ($name) {
|
||||||
'arangodb' => core::$arangodb,
|
|
||||||
default => $this->document->{$name}
|
default => $this->document->{$name}
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models\traits;
|
namespace mirzaev\huesos\models\traits;
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
use exception;
|
use exception;
|
||||||
|
@ -10,7 +10,9 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Trait for initialization of files handlers
|
* Trait for initialization of files handlers
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\models\traits
|
* @package mirzaev\huesos\models\traits
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
trait files
|
trait files
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\arming_bot\models\traits;
|
namespace mirzaev\huesos\models\traits;
|
||||||
|
|
||||||
// Built-in libraries
|
// Built-in libraries
|
||||||
use exception;
|
use exception;
|
||||||
|
@ -10,7 +10,9 @@ use exception;
|
||||||
/**
|
/**
|
||||||
* Trait for initialization of a status
|
* Trait for initialization of a status
|
||||||
*
|
*
|
||||||
* @package mirzaev\arming_bot\models\traits
|
* @package mirzaev\huesos\models\traits
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
*/
|
*/
|
||||||
trait status
|
trait status
|
|
@ -0,0 +1,152 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos\models\traits\yandex;
|
||||||
|
|
||||||
|
// Framework for PHP
|
||||||
|
use mirzaev\minimal\http\enumerations\content;
|
||||||
|
|
||||||
|
// Built-in libraries
|
||||||
|
use exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trait for "Yandex Disk"
|
||||||
|
*
|
||||||
|
* @package mirzaev\huesos\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 the file from "Yandex Disk"
|
||||||
|
*
|
||||||
|
* @param string $uri URI of the file from "Yandex Disk"
|
||||||
|
* @param string $destination Destination to write the file
|
||||||
|
* @param string|int $name Name for the file
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return array|false The [destination, name, content] array of the downloaded file, if the file was downloaded
|
||||||
|
*/
|
||||||
|
private static function download(
|
||||||
|
string $uri,
|
||||||
|
string $destination,
|
||||||
|
string|int $name,
|
||||||
|
array &$errors = []
|
||||||
|
): array|false {
|
||||||
|
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
|
||||||
|
$session = curl_init($url);
|
||||||
|
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_exec($session);
|
||||||
|
$code = curl_getinfo($session, CURLINFO_RESPONSE_CODE);
|
||||||
|
curl_close($session);
|
||||||
|
|
||||||
|
if ($code === 200) {
|
||||||
|
// The file is available for download
|
||||||
|
|
||||||
|
// Initializing URI of the file
|
||||||
|
$uri = json_decode(file_get_contents($url))?->href;
|
||||||
|
|
||||||
|
// Downloading the file
|
||||||
|
$session = curl_init($uri);
|
||||||
|
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($session, CURLOPT_AUTOREFERER, true);
|
||||||
|
curl_setopt($session, CURLOPT_FOLLOWLOCATION, true);
|
||||||
|
curl_exec($session);
|
||||||
|
$file = curl_exec($session);
|
||||||
|
$code = curl_getinfo($session, CURLINFO_RESPONSE_CODE);
|
||||||
|
preg_match('/^\w+\/\w+/', curl_getinfo($session, CURLINFO_CONTENT_TYPE), $matches);
|
||||||
|
$type = content::from($matches[0]);
|
||||||
|
curl_close($session);
|
||||||
|
|
||||||
|
if ($code === 200 && $file) {
|
||||||
|
// The file is downloaded
|
||||||
|
|
||||||
|
if ($type instanceof content) {
|
||||||
|
// Initialized content-type header
|
||||||
|
|
||||||
|
if (file_put_contents($destination . DIRECTORY_SEPARATOR . "$name." . $type->extension(), $file) > 0) {
|
||||||
|
// Downloaded the file
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return ['destination' => $destination, 'name' => $name, 'content' => $type];
|
||||||
|
}
|
||||||
|
} else throw new exception("Failed to initialize content-type header");
|
||||||
|
} else throw new exception("Failed to download the file by link: $uri");
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize list of files inside the folder from "Yandex Disk"
|
||||||
|
*
|
||||||
|
* @param string $uri URI of the folder from "Yandex Disk"
|
||||||
|
* @param array &$errors Registry of errors
|
||||||
|
*
|
||||||
|
* @return array|false JSON objects list of files in the folder
|
||||||
|
*/
|
||||||
|
private static function list(string $uri, array &$errors = []): array|false
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (!empty($uri)) {
|
||||||
|
// Not empty URI
|
||||||
|
|
||||||
|
// Initializing URL of the file
|
||||||
|
$url = "https://cloud-api.yandex.net/v1/disk/public/resources?public_key=$uri";
|
||||||
|
|
||||||
|
// Checking if the folder is available
|
||||||
|
$session = curl_init($url);
|
||||||
|
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_exec($session);
|
||||||
|
$code = curl_getinfo($session, CURLINFO_RESPONSE_CODE);
|
||||||
|
curl_close($session);
|
||||||
|
|
||||||
|
if ($code === 200) {
|
||||||
|
// The folder is available
|
||||||
|
|
||||||
|
// Downloading the list of files in the folder
|
||||||
|
$files = json_decode(file_get_contents($url));
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return isset($files?->_embedded?->items) ? $files->_embedded->items : [$files];
|
||||||
|
} else throw new exception("Failed to download list of files inside the folder by link: $uri");
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,348 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
define('SETTINGS', realpath('..' . DIRECTORY_SEPARATOR . 'settings'));
|
||||||
|
define('CDEK', require(SETTINGS . DIRECTORY_SEPARATOR . 'deliveries' . DIRECTORY_SEPARATOR . 'cdek.php'));
|
||||||
|
|
||||||
|
$service = new service(
|
||||||
|
/**
|
||||||
|
* Вставьте свой аккаунт\идентификатор для интеграции
|
||||||
|
* Put your account for integration here
|
||||||
|
*/
|
||||||
|
CDEK['account'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вставьте свой пароль для интеграции
|
||||||
|
* Put your password for integration here
|
||||||
|
*/
|
||||||
|
CDEK['secret']
|
||||||
|
);
|
||||||
|
|
||||||
|
$service->process($_GET, file_get_contents('php://input'));
|
||||||
|
|
||||||
|
class service
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Auth login
|
||||||
|
*/
|
||||||
|
private $login;
|
||||||
|
/**
|
||||||
|
* @var string Auth pwd
|
||||||
|
*/
|
||||||
|
private $secret;
|
||||||
|
/**
|
||||||
|
* @var string Base Url for API 2.0 Production
|
||||||
|
*/
|
||||||
|
private $baseUrl;
|
||||||
|
/**
|
||||||
|
* @var string Auth Token
|
||||||
|
*/
|
||||||
|
private $authToken;
|
||||||
|
/**
|
||||||
|
* @var array Data From Request
|
||||||
|
*/
|
||||||
|
private $requestData;
|
||||||
|
/** @var array Request metrics */
|
||||||
|
private $metrics;
|
||||||
|
|
||||||
|
public function __construct($login, $secret, $baseUrl = 'https://api.cdek.ru/v2')
|
||||||
|
{
|
||||||
|
$this->login = $login;
|
||||||
|
$this->secret = $secret;
|
||||||
|
$this->baseUrl = $baseUrl;
|
||||||
|
$this->metrics = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process($requestData, $body)
|
||||||
|
{
|
||||||
|
$time = $this->startMetrics();
|
||||||
|
|
||||||
|
$this->requestData = array_merge($requestData, json_decode($body, true) ?: array());
|
||||||
|
|
||||||
|
if (!isset($this->requestData['action'])) {
|
||||||
|
$this->sendValidationError('Action is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getAuthToken();
|
||||||
|
|
||||||
|
switch ($this->requestData['action']) {
|
||||||
|
case 'offices':
|
||||||
|
$this->sendResponse($this->getOffices(), $time);
|
||||||
|
break;
|
||||||
|
case 'calculate':
|
||||||
|
$this->sendResponse($this->calculate(), $time);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->sendValidationError('Unknown action');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendValidationError($message)
|
||||||
|
{
|
||||||
|
$this->http_response_code(400);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('X-Service-Version: 3.11.1');
|
||||||
|
echo json_encode(array('message' => $message));
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function http_response_code($code)
|
||||||
|
{
|
||||||
|
switch ($code) {
|
||||||
|
case 100:
|
||||||
|
$text = 'Continue';
|
||||||
|
break;
|
||||||
|
case 101:
|
||||||
|
$text = 'Switching Protocols';
|
||||||
|
break;
|
||||||
|
case 200:
|
||||||
|
$text = 'OK';
|
||||||
|
break;
|
||||||
|
case 201:
|
||||||
|
$text = 'Created';
|
||||||
|
break;
|
||||||
|
case 202:
|
||||||
|
$text = 'Accepted';
|
||||||
|
break;
|
||||||
|
case 203:
|
||||||
|
$text = 'Non-Authoritative Information';
|
||||||
|
break;
|
||||||
|
case 204:
|
||||||
|
$text = 'No Content';
|
||||||
|
break;
|
||||||
|
case 205:
|
||||||
|
$text = 'Reset Content';
|
||||||
|
break;
|
||||||
|
case 206:
|
||||||
|
$text = 'Partial Content';
|
||||||
|
break;
|
||||||
|
case 300:
|
||||||
|
$text = 'Multiple Choices';
|
||||||
|
break;
|
||||||
|
case 301:
|
||||||
|
$text = 'Moved Permanently';
|
||||||
|
break;
|
||||||
|
case 302:
|
||||||
|
$text = 'Moved Temporarily';
|
||||||
|
break;
|
||||||
|
case 303:
|
||||||
|
$text = 'See Other';
|
||||||
|
break;
|
||||||
|
case 304:
|
||||||
|
$text = 'Not Modified';
|
||||||
|
break;
|
||||||
|
case 305:
|
||||||
|
$text = 'Use Proxy';
|
||||||
|
break;
|
||||||
|
case 400:
|
||||||
|
$text = 'Bad Request';
|
||||||
|
break;
|
||||||
|
case 401:
|
||||||
|
$text = 'Unauthorized';
|
||||||
|
break;
|
||||||
|
case 402:
|
||||||
|
$text = 'Payment Required';
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
$text = 'Forbidden';
|
||||||
|
break;
|
||||||
|
case 404:
|
||||||
|
$text = 'Not Found';
|
||||||
|
break;
|
||||||
|
case 405:
|
||||||
|
$text = 'Method Not Allowed';
|
||||||
|
break;
|
||||||
|
case 406:
|
||||||
|
$text = 'Not Acceptable';
|
||||||
|
break;
|
||||||
|
case 407:
|
||||||
|
$text = 'Proxy Authentication Required';
|
||||||
|
break;
|
||||||
|
case 408:
|
||||||
|
$text = 'Request Time-out';
|
||||||
|
break;
|
||||||
|
case 409:
|
||||||
|
$text = 'Conflict';
|
||||||
|
break;
|
||||||
|
case 410:
|
||||||
|
$text = 'Gone';
|
||||||
|
break;
|
||||||
|
case 411:
|
||||||
|
$text = 'Length Required';
|
||||||
|
break;
|
||||||
|
case 412:
|
||||||
|
$text = 'Precondition Failed';
|
||||||
|
break;
|
||||||
|
case 413:
|
||||||
|
$text = 'Request Entity Too Large';
|
||||||
|
break;
|
||||||
|
case 414:
|
||||||
|
$text = 'Request-URI Too Large';
|
||||||
|
break;
|
||||||
|
case 415:
|
||||||
|
$text = 'Unsupported Media Type';
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
$text = 'Internal Server Error';
|
||||||
|
break;
|
||||||
|
case 501:
|
||||||
|
$text = 'Not Implemented';
|
||||||
|
break;
|
||||||
|
case 502:
|
||||||
|
$text = 'Bad Gateway';
|
||||||
|
break;
|
||||||
|
case 503:
|
||||||
|
$text = 'Service Unavailable';
|
||||||
|
break;
|
||||||
|
case 504:
|
||||||
|
$text = 'Gateway Time-out';
|
||||||
|
break;
|
||||||
|
case 505:
|
||||||
|
$text = 'HTTP Version not supported';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
exit('Unknown http status code "' . htmlentities($code) . '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
$protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
|
||||||
|
header($protocol . ' ' . $code . ' ' . $text);
|
||||||
|
$GLOBALS['http_response_code'] = $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAuthToken()
|
||||||
|
{
|
||||||
|
$time = $this->startMetrics();
|
||||||
|
|
||||||
|
$token = $this->httpRequest('oauth/token', array(
|
||||||
|
'grant_type' => 'client_credentials',
|
||||||
|
'client_id' => $this->login,
|
||||||
|
'client_secret' => $this->secret,
|
||||||
|
), true);
|
||||||
|
|
||||||
|
$this->endMetrics('auth', 'Server Auth Time', $time);
|
||||||
|
|
||||||
|
$result = json_decode($token['result'], true);
|
||||||
|
|
||||||
|
if (!isset($result['access_token'])) {
|
||||||
|
throw new RuntimeException('Server not authorized to CDEK API');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->authToken = $result['access_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function startMetrics()
|
||||||
|
{
|
||||||
|
return function_exists('hrtime') ? hrtime(true) : microtime(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function httpRequest($method, $data, $useFormData = false, $useJson = false)
|
||||||
|
{
|
||||||
|
$ch = curl_init("$this->baseUrl/$method");
|
||||||
|
|
||||||
|
$headers = array(
|
||||||
|
'Accept: application/json',
|
||||||
|
'X-App-Name: widget_pvz',
|
||||||
|
'X-App-Version: 3.11.1'
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->authToken) {
|
||||||
|
$headers[] = "Authorization: Bearer $this->authToken";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($useFormData) {
|
||||||
|
curl_setopt_array($ch, array(
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $data,
|
||||||
|
));
|
||||||
|
} elseif ($useJson) {
|
||||||
|
$headers[] = 'Content-Type: application/json';
|
||||||
|
curl_setopt_array($ch, array(
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => json_encode($data),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
curl_setopt($ch, CURLOPT_URL, "$this->baseUrl/$method?" . http_build_query($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt_array($ch, array(
|
||||||
|
CURLOPT_USERAGENT => 'widget/3.11.1',
|
||||||
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HEADER => true,
|
||||||
|
));
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||||
|
$headers = substr($response, 0, $headerSize);
|
||||||
|
$result = substr($response, $headerSize);
|
||||||
|
|
||||||
|
$addedHeaders = $this->getHeaderValue($headers);
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
throw new RuntimeException(curl_error($ch), curl_errno($ch));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array('result' => $result, 'addedHeaders' => $addedHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getHeaderValue($headers)
|
||||||
|
{
|
||||||
|
$headerLines = explode("\r\n", $headers);
|
||||||
|
return array_filter($headerLines, static function ($line) {
|
||||||
|
return !empty($line) && stripos($line, 'X-') !== false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function endMetrics($metricName, $metricDescription, $start)
|
||||||
|
{
|
||||||
|
$this->metrics[] = array(
|
||||||
|
'name' => $metricName,
|
||||||
|
'description' => $metricDescription,
|
||||||
|
'time' => round(
|
||||||
|
function_exists('hrtime') ? (hrtime(true) - $start) / 1e+6 : (microtime(true) - $start) * 1000,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendResponse($data, $start)
|
||||||
|
{
|
||||||
|
$this->http_response_code(200);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('X-Service-Version: 3.11.1');
|
||||||
|
if (!empty($data['addedHeaders'])) {
|
||||||
|
foreach ($data['addedHeaders'] as $header) {
|
||||||
|
header($header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->endMetrics('total', 'Total Time', $start);
|
||||||
|
|
||||||
|
if (!empty($this->metrics)) {
|
||||||
|
header('Server-Timing: ' . array_reduce($this->metrics, function ($c, $i) {
|
||||||
|
return $c . $i['name'] . ';desc="' . $i['description'] . '";dur=' . $i['time'] . ',';
|
||||||
|
}, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $data['result'];
|
||||||
|
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getOffices()
|
||||||
|
{
|
||||||
|
$time = $this->startMetrics();
|
||||||
|
$result = $this->httpRequest('deliverypoints', $this->requestData);
|
||||||
|
|
||||||
|
$this->endMetrics('office', 'Offices Request', $time);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function calculate()
|
||||||
|
{
|
||||||
|
$time = $this->startMetrics();
|
||||||
|
$result = $this->httpRequest('calculator/tarifflist', $this->requestData, false, true);
|
||||||
|
|
||||||
|
$this->endMetrics('calc', 'Calculate Request', $time);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\huesos;
|
||||||
|
|
||||||
|
// Framework for PHP
|
||||||
|
use mirzaev\minimal\core,
|
||||||
|
mirzaev\minimal\route;
|
||||||
|
|
||||||
|
// Enabling debugging
|
||||||
|
/* 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('SETTINGS_PROJECT', require(SETTINGS . DIRECTORY_SEPARATOR . 'project.php'));
|
||||||
|
define('INDEX', __DIR__);
|
||||||
|
define('ROOT', INDEX . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR);
|
||||||
|
define('THEME', 'default');
|
||||||
|
|
||||||
|
define('CDEK', require(SETTINGS . DIRECTORY_SEPARATOR . 'deliveries' . DIRECTORY_SEPARATOR . 'cdek.php'));
|
||||||
|
|
||||||
|
define('TELEGRAM_KEY', require(SETTINGS . DIRECTORY_SEPARATOR . 'telegram.php'));
|
||||||
|
|
||||||
|
// Initialize dependencies
|
||||||
|
require ROOT . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
|
||||||
|
|
||||||
|
// Initializing core
|
||||||
|
$core = new core(namespace: __NAMESPACE__);
|
||||||
|
|
||||||
|
// Initializing routes
|
||||||
|
$core->router
|
||||||
|
->write('/', new route('catalog', 'index', 'catalog'), 'GET')
|
||||||
|
->write('/offer', new route('index', 'offer'), 'GET')
|
||||||
|
->write('/cart', new route('cart', 'index', 'cart'), 'GET')
|
||||||
|
->write('/cart/product', new route('cart', 'product', 'cart'), 'PATCH')
|
||||||
|
->write('/cart/summary', new route('cart', 'summary', 'cart'), 'GET')
|
||||||
|
->write('/cart/share', new route('cart', 'share', 'cart'), 'POST')
|
||||||
|
->write('/cart/attach', new route('cart', 'attach', 'cart'), 'POST')
|
||||||
|
->write('/order/robokassa', new route('cart', 'robokassa', 'cart'), 'GET')
|
||||||
|
->write('/api/robokassa/result', new route('api\acquirings\robokassa', 'result'), 'POST')
|
||||||
|
->write('/robokassa/success', new route('api\acquirings\robokassa', 'success'), 'GET')
|
||||||
|
->write('/robokassa/fail', new route('api\acquirings\robokassa', 'fail'), 'GET')
|
||||||
|
->write('/account/write', new route('account', 'write', 'account'), 'PATCH')
|
||||||
|
->write('/session/write', new route('session', 'write', 'session'), 'PATCH')
|
||||||
|
->write('/session/connect/telegram', new route('session', 'telegram', 'session'), 'PUT')
|
||||||
|
->write('/delivery/write', new route('delivery', 'write', 'delivery'), 'PATCH')
|
||||||
|
->write('/delivery/calculate', new route('delivery', 'calculate', 'delivery'), 'GET');
|
||||||
|
|
||||||
|
// Handling request
|
||||||
|
$core->start();
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Initializing the closing element
|
||||||
|
const closing = document.getElementById("closing");
|
||||||
|
console.log(closing);
|
||||||
|
// Initializing the closing iterator
|
||||||
|
let iterator =
|
||||||
|
parseInt(closing.style.getPropertyValue("--iterator").replaceAll("'", "")) ||
|
||||||
|
0;
|
||||||
|
console.log(iterator);
|
||||||
|
// Initializing the closing inteval
|
||||||
|
const interval = setInterval(function () {
|
||||||
|
if (iterator-- <= 0) {
|
||||||
|
// Deinitializing the closing inteval
|
||||||
|
clearInterval(interval);
|
||||||
|
|
||||||
|
core.modules.connect("telegram").then(() => {
|
||||||
|
// Imported the telegram module
|
||||||
|
|
||||||
|
// Closing the window
|
||||||
|
core.telegram.api.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing the iterator into the closing element
|
||||||
|
closing.style.setProperty("--iterator", "'" + iterator + "'");
|
||||||
|
}, 1000);
|
|
@ -0,0 +1,403 @@
|
||||||
|
/**
|
||||||
|
* @name Core
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Core of the project
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
class core {
|
||||||
|
// Domain
|
||||||
|
static domain = window.location.hostname;
|
||||||
|
|
||||||
|
// Language
|
||||||
|
static language = "ru";
|
||||||
|
|
||||||
|
// Window
|
||||||
|
static window;
|
||||||
|
|
||||||
|
// Account
|
||||||
|
static account;
|
||||||
|
|
||||||
|
// The "loading" element
|
||||||
|
static loading = document.getElementById("loading");
|
||||||
|
|
||||||
|
// The <header> element
|
||||||
|
static header = document.body.getElementsByTagName("header")[0];
|
||||||
|
|
||||||
|
// The <aside> element
|
||||||
|
static aside = document.body.getElementsByTagName("aside")[0];
|
||||||
|
|
||||||
|
// The <menu> element
|
||||||
|
static menu = document.body.getElementsByTagName("menu")[0];
|
||||||
|
|
||||||
|
// The <main> element
|
||||||
|
static main = document.body.getElementsByTagName("main")[0];
|
||||||
|
|
||||||
|
// The <footer> element
|
||||||
|
static footer = document.body.getElementsByTagName("footer")[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request
|
||||||
|
*
|
||||||
|
* @param {string} uri
|
||||||
|
* @param {string} body
|
||||||
|
* @param {string} method POST, GET...
|
||||||
|
* @param {object} headers
|
||||||
|
* @param {string} type Format of response (json, text...)
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
static async request(
|
||||||
|
uri = "/",
|
||||||
|
body,
|
||||||
|
method = "GET",
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
type = "json",
|
||||||
|
) {
|
||||||
|
return await fetch(encodeURI(uri), { method, headers, body })
|
||||||
|
.then((response) => type === null || response[type]());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Modules
|
||||||
|
*
|
||||||
|
* @method connect(modules) Connect modules
|
||||||
|
*
|
||||||
|
* @return {Array} List of initialized modules
|
||||||
|
*/
|
||||||
|
static modules() {
|
||||||
|
return Object.keys(this).filter((module) =>
|
||||||
|
this[module]?.type === "module"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Buffer
|
||||||
|
*/
|
||||||
|
static buffer = class buffer {
|
||||||
|
/**
|
||||||
|
* @name Write to buffers
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Write to buffers (interface)
|
||||||
|
*
|
||||||
|
* @param {string} name Name of the parameter
|
||||||
|
* @param {string} value Value of the parameter (it can be JSON)
|
||||||
|
*
|
||||||
|
* @return {bool} Execution completed with an error?
|
||||||
|
*/
|
||||||
|
static write(name, value) {
|
||||||
|
core.modules.connect("damper").then(
|
||||||
|
() => {
|
||||||
|
// Imported the damper module
|
||||||
|
|
||||||
|
// Execute under damper
|
||||||
|
this.write.damper(name, value);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// Not imported the damper module
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
this.write.system(name, value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сгенерировать окно выбора действия
|
||||||
|
*
|
||||||
|
* @param {string} title Верхний колонтинул
|
||||||
|
* @param {string} text Основное содержимое окна
|
||||||
|
* @param {string} left Содержимое левой кнопки
|
||||||
|
* @param {object} left_css Перечисление CSS-классов левой кнопки (массив)
|
||||||
|
* @param {function} left_click Действие после нажатия на левую кнопку
|
||||||
|
* @param {string} right Содержимое правой кнопки
|
||||||
|
* @param {object} right_css Перечисление CSS-классов правой кнопки (массив)
|
||||||
|
* @param {function} right_click Действие после нажатия на правую кнопку
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
/* static choose = core.damper(
|
||||||
|
(
|
||||||
|
title = "Выбор действия",
|
||||||
|
text = "",
|
||||||
|
left = "Да",
|
||||||
|
left_css = ["grass"],
|
||||||
|
left_click = () => {},
|
||||||
|
right = "Нет",
|
||||||
|
right_css = ["clay"],
|
||||||
|
right_click = () => {},
|
||||||
|
) => {
|
||||||
|
// Инициализация оболочки всплывающего окна
|
||||||
|
this.popup_body.wrap = document.createElement("div");
|
||||||
|
this.popup_body.wrap.setAttribute("id", "popup");
|
||||||
|
|
||||||
|
// Инициализация всплывающего окна
|
||||||
|
const popup = document.createElement("section");
|
||||||
|
popup.classList.add("list", "small");
|
||||||
|
|
||||||
|
// Инициализация заголовка всплывающего окна
|
||||||
|
const title_h3 = document.createElement("h3");
|
||||||
|
title_h3.classList.add("unselectable");
|
||||||
|
title_h3.innerText = title;
|
||||||
|
|
||||||
|
// Инициализация оболочки с основной информацией
|
||||||
|
const main = document.createElement("section");
|
||||||
|
main.classList.add("main");
|
||||||
|
|
||||||
|
// Инициализация колонки
|
||||||
|
const column = document.createElement("div");
|
||||||
|
column.classList.add("column");
|
||||||
|
|
||||||
|
// Инициализация текста
|
||||||
|
const text_p = document.createElement("p");
|
||||||
|
text_p.innerText = text;
|
||||||
|
|
||||||
|
// Инициализация строки
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.classList.add("row", "buttons");
|
||||||
|
|
||||||
|
// Инициализация левой кнопки
|
||||||
|
const left_button = document.createElement("button");
|
||||||
|
left_button.classList.add(...left_css);
|
||||||
|
left_button.innerText = left;
|
||||||
|
left_button.addEventListener("click", left_click);
|
||||||
|
|
||||||
|
// Инициализация правой кнопки
|
||||||
|
const right_button = document.createElement("button");
|
||||||
|
right_button.classList.add(...right_css);
|
||||||
|
right_button.innerText = right;
|
||||||
|
right_button.addEventListener("click", right_click);
|
||||||
|
|
||||||
|
// Инициализация окна с ошибками
|
||||||
|
this.popup_body.errors = document.createElement("section");
|
||||||
|
this.popup_body.errors.classList.add(
|
||||||
|
"errors",
|
||||||
|
"window",
|
||||||
|
"list",
|
||||||
|
"calculated",
|
||||||
|
"hidden",
|
||||||
|
);
|
||||||
|
this.popup_body.errors.setAttribute("data-errors", true);
|
||||||
|
|
||||||
|
// Инициализация элемента-тела (оболочки) окна с ошибками
|
||||||
|
const errors = document.createElement("section");
|
||||||
|
errors.classList.add("body");
|
||||||
|
|
||||||
|
// Инициализация элемента-списка ошибок
|
||||||
|
const dl = document.createElement("dl");
|
||||||
|
|
||||||
|
// Инициализация активного всплывающего окна
|
||||||
|
const old = document.getElementById("popup");
|
||||||
|
|
||||||
|
if (old instanceof HTMLElement) {
|
||||||
|
// Найдено активное окно
|
||||||
|
|
||||||
|
// Деинициализация быстрых действий по кнопкам
|
||||||
|
document.removeEventListener("keydown", this.buttons);
|
||||||
|
|
||||||
|
// Сброс блокировки
|
||||||
|
this.freeze = false;
|
||||||
|
|
||||||
|
// Удаление активного окна
|
||||||
|
old.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запись в документ
|
||||||
|
popup.appendChild(title_h3);
|
||||||
|
|
||||||
|
column.appendChild(text_p);
|
||||||
|
|
||||||
|
row.appendChild(left_button);
|
||||||
|
row.appendChild(right_button);
|
||||||
|
column.appendChild(row);
|
||||||
|
|
||||||
|
main.appendChild(column);
|
||||||
|
popup.appendChild(main);
|
||||||
|
|
||||||
|
this.popup_body.wrap.appendChild(popup);
|
||||||
|
document.body.appendChild(this.popup_body.wrap);
|
||||||
|
|
||||||
|
errors.appendChild(dl);
|
||||||
|
this.popup_body.errors.appendChild(errors);
|
||||||
|
this.popup_body.wrap.appendChild(this.popup_body.errors);
|
||||||
|
|
||||||
|
// Инициализация ширины окна с ошибками
|
||||||
|
this.popup_body.errors.style.setProperty(
|
||||||
|
"--calculated-width",
|
||||||
|
popup.offsetWidth + "px",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Инициализация переменных для окна с ошибками (12 - это значение gap из div#popup)
|
||||||
|
function top(errors) {
|
||||||
|
errors.style.setProperty("transition", "0s");
|
||||||
|
errors.style.setProperty(
|
||||||
|
"--top",
|
||||||
|
popup.offsetTop + popup.offsetHeight + 12 + "px",
|
||||||
|
);
|
||||||
|
setTimeout(() => errors.style.removeProperty("transition"), 100);
|
||||||
|
}
|
||||||
|
top(this.popup_body.errors);
|
||||||
|
const resize = new ResizeObserver(() => top(this.popup_body.errors));
|
||||||
|
resize.observe(this.popup_body.wrap);
|
||||||
|
|
||||||
|
// Инициализация функции закрытия всплывающего окна
|
||||||
|
const click = () => {
|
||||||
|
// Блокировка
|
||||||
|
if (this.freeze) return;
|
||||||
|
|
||||||
|
// Удаление всплывающего окна
|
||||||
|
this.popup_body.wrap.remove();
|
||||||
|
|
||||||
|
// Удаление статуса активной строки
|
||||||
|
row.removeAttribute("data-selected");
|
||||||
|
|
||||||
|
// Деинициализация быстрых действий по кнопкам
|
||||||
|
document.removeEventListener("keydown", this.buttons);
|
||||||
|
|
||||||
|
// Сброс блокировки
|
||||||
|
this.freeze = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Инициализация функции добавления функции закрытия всплывающего окна
|
||||||
|
const enable = () =>
|
||||||
|
this.popup_body.wrap.addEventListener("click", click);
|
||||||
|
|
||||||
|
// Инициализация функции удаления функции закрытия всплывающего окна
|
||||||
|
const disable = () =>
|
||||||
|
this.popup_body.wrap.removeEventListener("click", click);
|
||||||
|
|
||||||
|
// Первичная активация функции удаления всплывающего окна
|
||||||
|
enable();
|
||||||
|
|
||||||
|
// Добавление функции удаления всплывающего окна по событиям
|
||||||
|
popup.addEventListener("mouseenter", disable);
|
||||||
|
popup.addEventListener("mouseleave", enable);
|
||||||
|
|
||||||
|
// Добавление функции удаления всплывающего окна по кнопкам
|
||||||
|
left_button.addEventListener("click", click);
|
||||||
|
right_button.addEventListener("click", click);
|
||||||
|
|
||||||
|
// Фокусировка
|
||||||
|
right_button.focus();
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
); */
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
core.modules,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @name Connect modules
|
||||||
|
*
|
||||||
|
* @param {Array|string} modules Names of modules or name of the module
|
||||||
|
*
|
||||||
|
* @return {Prommise}
|
||||||
|
*/
|
||||||
|
async connect(modules) {
|
||||||
|
// Normalisation required argiments
|
||||||
|
if (typeof modules === "string") modules = [modules];
|
||||||
|
|
||||||
|
if (modules instanceof Array) {
|
||||||
|
// Received and validated required arguments
|
||||||
|
|
||||||
|
// Initializing the registry of loaded modules
|
||||||
|
const loaded = [];
|
||||||
|
|
||||||
|
for (const module of modules) {
|
||||||
|
// Iterating over modules
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
core.modules.connect("damper").then(() => {
|
||||||
|
// Imported the damper module
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
core.buffer.write,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @name Write to buffers
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Write to buffers (damper)
|
||||||
|
*
|
||||||
|
* @param {string} name Name of the parameter
|
||||||
|
* @param {string} value Value of the parameter (it can be JSON)
|
||||||
|
* @param {bool} force Ignore the damper? (false)
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
damper: core.damper(
|
||||||
|
(...variables) => core.buffer.write.system(...variables),
|
||||||
|
300,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Write to buffers
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Write to buffers (system)
|
||||||
|
*
|
||||||
|
* @param {string} name Name of the parameter
|
||||||
|
* @param {string} value Value of the parameter (it can be JSON)
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
Object.assign(
|
||||||
|
core.buffer.write,
|
||||||
|
{
|
||||||
|
system(
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
resolve = () => {},
|
||||||
|
reject = () => {},
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
core.modules.connect("session").then(() => {
|
||||||
|
// Imported the session module
|
||||||
|
|
||||||
|
// Write to the session buffer
|
||||||
|
core.session.buffer?.write(name, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
core.modules.connect("account").then(() => {
|
||||||
|
// Imported the account module
|
||||||
|
|
||||||
|
// Write to the account buffer
|
||||||
|
core.account.buffer?.write(name, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
resolve();
|
||||||
|
} catch (e) {
|
||||||
|
// Exit (fail)
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
|
@ -0,0 +1,272 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Account
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Implements actions with accounts
|
||||||
|
*
|
||||||
|
* @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 account {
|
||||||
|
/**
|
||||||
|
* @name Type of the program
|
||||||
|
*/
|
||||||
|
static type = "module";
|
||||||
|
|
||||||
|
// Wrap of indicator of the account
|
||||||
|
static wrap = document.getElementById("account");
|
||||||
|
|
||||||
|
// Indicator of the account
|
||||||
|
static indicator = this.wrap?.getElementsByTagName("i")[0] ?? null;
|
||||||
|
|
||||||
|
// Description of the account
|
||||||
|
static description = this.wrap?.getElementsByTagName("small")[0] ?? null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
static authentication() {
|
||||||
|
core.loading.removeAttribute("disabled");
|
||||||
|
|
||||||
|
const timer_for_response = setTimeout(() => {
|
||||||
|
core.loading.setAttribute("disabled", true);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
core.modules.connect("telegram").then(() => {
|
||||||
|
// Imported the telegram module
|
||||||
|
|
||||||
|
if (core.telegram.api.initData.length > 0) {
|
||||||
|
core
|
||||||
|
.request(
|
||||||
|
"/session/connect/telegram",
|
||||||
|
core.telegram.api.initData,
|
||||||
|
"PUT",
|
||||||
|
)
|
||||||
|
.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)
|
||||||
|
|
||||||
|
if (json.connected === true) {
|
||||||
|
// Deactivating the loading screen
|
||||||
|
core.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
json.language !== null &&
|
||||||
|
typeof json.language === "string" &&
|
||||||
|
json.language.length === 2
|
||||||
|
) {
|
||||||
|
core.language = json.language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Buffer
|
||||||
|
*/
|
||||||
|
static buffer = class buffer {
|
||||||
|
/**
|
||||||
|
* @name Write
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Write to the account buffer (interface)
|
||||||
|
*
|
||||||
|
* @param {string} name Name of the parameter
|
||||||
|
* @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?
|
||||||
|
*/
|
||||||
|
static write = (name, value, force = false) => {
|
||||||
|
core.modules.connect("damper").then(
|
||||||
|
() => {
|
||||||
|
// Imported the damper module
|
||||||
|
|
||||||
|
// Execute under damper
|
||||||
|
this.write.damper(name, value, force);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// Not imported the damper module
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
this.write.system(name, value, force);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
core.modules.connect("damper").then(() => {
|
||||||
|
// Imported the damper module
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
account.buffer.write,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @name Write (damper)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Write to the account buffer
|
||||||
|
*
|
||||||
|
* @memberof account.buffer.write
|
||||||
|
*
|
||||||
|
* @param {string} name Name of the parameter
|
||||||
|
* @param {string} value Value of the parameter (it can be JSON)
|
||||||
|
* @param {bool} force Ignore the damper? (false)
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
damper: core.damper(
|
||||||
|
(...variables) => account.buffer.write.system(...variables),
|
||||||
|
300,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
account.buffer.write,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @name Write (system)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Write to the account buffer
|
||||||
|
*
|
||||||
|
* @memberof account.buffer.write
|
||||||
|
*
|
||||||
|
* @param {string} name Name of the parameter
|
||||||
|
* @param {string} value Value of the parameter (it can be JSON)
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async system(
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
resolve = () => {},
|
||||||
|
reject = () => {},
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
typeof name === "string" &&
|
||||||
|
(typeof value === "string" || typeof value === "number")
|
||||||
|
) {
|
||||||
|
// Received and validated required arguments
|
||||||
|
|
||||||
|
// Sending request to the server
|
||||||
|
return await core.request(
|
||||||
|
"/account/write",
|
||||||
|
`${name}=${value}`,
|
||||||
|
"PATCH",
|
||||||
|
)
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => reject(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Exit (fail)
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Connecting to the core
|
||||||
|
if (!core.account) core.account = account;
|
|
@ -0,0 +1,959 @@
|
||||||
|
"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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Toggle (interface)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Toggle the product in the cart
|
||||||
|
*
|
||||||
|
* @param {HTMLButtonElement|HTMLInputElement|null} 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} Did the execution complete without errors?
|
||||||
|
*/
|
||||||
|
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 true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Write (interface)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Write the product into the cart
|
||||||
|
*
|
||||||
|
* @param {HTMLButtonElement|HTMLInputElement|null} 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} Did the execution complete without errors?
|
||||||
|
*/
|
||||||
|
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 true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Delete (interface)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Delete the product from the cart
|
||||||
|
*
|
||||||
|
* @param {HTMLButtonElement|HTMLInputElement|null} 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} Did the execution complete without errors?
|
||||||
|
*/
|
||||||
|
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 true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Set (interface)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Set amount of the product in the cart
|
||||||
|
*
|
||||||
|
* @param {HTMLButtonElement|HTMLInputElement|null} 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} Did the execution complete without errors?
|
||||||
|
*/
|
||||||
|
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 true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Product (interface)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Handle the product in the cart
|
||||||
|
*
|
||||||
|
* @param {HTMLButtonElement|HTMLInputElement|null} 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} Did the execution complete without errors?
|
||||||
|
*/
|
||||||
|
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 true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Summary (interface)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Initialize summary of products the cart
|
||||||
|
*
|
||||||
|
* @param {HTMLButtonElement} button Button <button>
|
||||||
|
*
|
||||||
|
* @return {bool} Did the execution complete without errors?
|
||||||
|
*/
|
||||||
|
static summary(button) {
|
||||||
|
// Disabling button
|
||||||
|
button?.setAttribute("disabled", true);
|
||||||
|
|
||||||
|
// Initializing timer of enabling the delivery company <input> (radio) element
|
||||||
|
const enabling = setTimeout(() => {
|
||||||
|
// Enabling button
|
||||||
|
button?.removeAttribute("disabled");
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Resolved
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Render the result of the resolved request
|
||||||
|
*
|
||||||
|
* @param {object} json
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
const resolved = (json) => {
|
||||||
|
if (json) {
|
||||||
|
// Received JSON-response
|
||||||
|
|
||||||
|
// Deinitializing timer of enabling the delivery company <input> (radio) element
|
||||||
|
clearTimeout(enabling);
|
||||||
|
|
||||||
|
// Enabling button
|
||||||
|
button?.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Rejected
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Render the result of the rejected request
|
||||||
|
*
|
||||||
|
* @param {object} json
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
const rejected = (json) => {
|
||||||
|
if (json) {
|
||||||
|
// Received JSON-response
|
||||||
|
|
||||||
|
// Deinitializing timer of enabling the delivery company <input> (radio) element
|
||||||
|
clearTimeout(enabling);
|
||||||
|
|
||||||
|
// Enabling button
|
||||||
|
button?.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
core.modules.connect("damper").then(
|
||||||
|
() => {
|
||||||
|
// Imported the damper module
|
||||||
|
|
||||||
|
// Execute under damper
|
||||||
|
this.summary.damper()
|
||||||
|
.then(resolved, rejected);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// Not imported the damper module
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
this.summary.system()
|
||||||
|
.then(resolved, rejected);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Share (interface)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Generate sharing hash of the cart and sent ot to the chat-robot
|
||||||
|
*
|
||||||
|
* @param {HTMLButtonElement} button Button <button>
|
||||||
|
*
|
||||||
|
* @return {bool} Did the execution complete without errors?
|
||||||
|
*/
|
||||||
|
static async share(button) {
|
||||||
|
return await core.modules.connect("telegram").then(
|
||||||
|
() => {
|
||||||
|
// Imported the telegram module
|
||||||
|
|
||||||
|
// Disabling button
|
||||||
|
button?.setAttribute("disabled", true);
|
||||||
|
|
||||||
|
// Initializing timer of enabling the delivery company <input> (radio) element
|
||||||
|
const enabling = setTimeout(() => {
|
||||||
|
// Enabling button
|
||||||
|
button?.removeAttribute("disabled");
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Resolved
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Render the result of the resolved request
|
||||||
|
*
|
||||||
|
* @param {object} json
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
const resolved = (json) => {
|
||||||
|
if (json) {
|
||||||
|
// Received JSON-response
|
||||||
|
|
||||||
|
// Deinitializing timer of enabling the delivery company <input> (radio) element
|
||||||
|
clearTimeout(enabling);
|
||||||
|
|
||||||
|
// Enabling button
|
||||||
|
button?.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Rejected
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Render the result of the rejected request
|
||||||
|
*
|
||||||
|
* @param {object} json
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
const rejected = (json) => {
|
||||||
|
if (json) {
|
||||||
|
// Received JSON-response
|
||||||
|
|
||||||
|
// Deinitializing timer of enabling the delivery company <input> (radio) element
|
||||||
|
clearTimeout(enabling);
|
||||||
|
|
||||||
|
// Enabling button
|
||||||
|
button?.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
core.modules.connect("damper").then(
|
||||||
|
() => {
|
||||||
|
// Imported the damper module
|
||||||
|
|
||||||
|
// Execute under damper
|
||||||
|
this.share.damper()
|
||||||
|
.then(resolved, rejected);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// Not imported the damper module
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
this.share.system()
|
||||||
|
.then(resolved, rejected);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
// Exit (fail)
|
||||||
|
() => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
core.modules.connect("damper").then(() => {
|
||||||
|
// Imported the damper module
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
cart.product,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @name Product (damper)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Handle the product in the cart
|
||||||
|
*
|
||||||
|
* @memberof cart.product
|
||||||
|
*
|
||||||
|
* @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.system),
|
||||||
|
300,
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
cart.summary,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @name Summary (damper)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Initialize summary of products the cart
|
||||||
|
*
|
||||||
|
* @memberof cart.summary
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
damper: core.damper(
|
||||||
|
(...variables) => cart.summary.system(...variables),
|
||||||
|
300,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
cart.share,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @name Share (damper)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Generate sharing hash of the cart and sent ot to the chat-robot
|
||||||
|
*
|
||||||
|
* @memberof cart.share
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
damper: core.damper(
|
||||||
|
(...variables) => cart.share.system(...variables),
|
||||||
|
400,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
cart.product,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @name Product (system)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Handle the product in the cart
|
||||||
|
*
|
||||||
|
* @memberof cart.product
|
||||||
|
*
|
||||||
|
* @param {HTMLButtonElement|HTMLInputElement|null} 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 (product instanceof HTMLElement) {
|
||||||
|
// Validated required arguments
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(type === "toggle" &&
|
||||||
|
amount === null ||
|
||||||
|
typeof amount === "undefined") ||
|
||||||
|
(type === "set" &&
|
||||||
|
amount === 0 ||
|
||||||
|
amount === 100) ||
|
||||||
|
typeof amount === "number" &&
|
||||||
|
amount > 0 &&
|
||||||
|
amount < 100
|
||||||
|
) {
|
||||||
|
// Validated 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,
|
||||||
|
"PATCH",
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
(json) => {
|
||||||
|
if (json) {
|
||||||
|
// Received a JSON-response
|
||||||
|
|
||||||
|
if (
|
||||||
|
json.errors !== null &&
|
||||||
|
typeof json.errors === "object" &&
|
||||||
|
json.errors.length > 0
|
||||||
|
) {
|
||||||
|
// Fail (received errors)
|
||||||
|
|
||||||
|
core.modules.connect("telegram").then(() => {
|
||||||
|
// Imported the telegram module
|
||||||
|
|
||||||
|
core.telegram.api.HapticFeedback
|
||||||
|
.notificationOccurred("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
reject(json);
|
||||||
|
} else {
|
||||||
|
// Success (not received errors)
|
||||||
|
|
||||||
|
core.modules.connect("telegram").then(() => {
|
||||||
|
// Imported the telegram module
|
||||||
|
|
||||||
|
core.telegram.api.HapticFeedback
|
||||||
|
.notificationOccurred("success");
|
||||||
|
});
|
||||||
|
|
||||||
|
core.modules.connect("delivery").then(
|
||||||
|
() => {
|
||||||
|
// Imported the damper module
|
||||||
|
|
||||||
|
// Calculating delivery
|
||||||
|
core.delivery.calculate();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
cart.summary,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @name Summary (system)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Initialize summary of products the cart
|
||||||
|
*
|
||||||
|
* @memberof cart.summary
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async system(
|
||||||
|
resolve = () => {},
|
||||||
|
reject = () => {},
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Request
|
||||||
|
return await core.request("/cart/summary", undefined, "GET")
|
||||||
|
.then((json) => {
|
||||||
|
if (json) {
|
||||||
|
// Received a JSON-response
|
||||||
|
|
||||||
|
if (
|
||||||
|
json.errors !== null &&
|
||||||
|
typeof json.errors === "object" &&
|
||||||
|
json.errors.length > 0
|
||||||
|
) {
|
||||||
|
// Fail (received errors)
|
||||||
|
|
||||||
|
core.modules.connect("telegram").then(() => {
|
||||||
|
// Imported the telegram module
|
||||||
|
|
||||||
|
core.telegram.api.HapticFeedback.notificationOccurred(
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
reject(json);
|
||||||
|
} else {
|
||||||
|
// Success (not received errors)
|
||||||
|
|
||||||
|
if (json.amount > 0) {
|
||||||
|
// The cart has products
|
||||||
|
|
||||||
|
// Writing the CSS variable of the document element
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
"--cart-amount",
|
||||||
|
'"' + json.amount + '"',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// The cart has no products
|
||||||
|
|
||||||
|
// Writing the CSS variable of the document element
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
"--cart-amount",
|
||||||
|
"unset",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializing the summary amount <span> element
|
||||||
|
const amount = document.getElementById("amount");
|
||||||
|
|
||||||
|
if (amount instanceof HTMLElement) {
|
||||||
|
// Initialized the summary amount element
|
||||||
|
|
||||||
|
// Writing summary 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
|
||||||
|
|
||||||
|
core.modules.connect("telegram").then(() => {
|
||||||
|
// Imported the telegram module
|
||||||
|
|
||||||
|
core.telegram.api.HapticFeedback.notificationOccurred(
|
||||||
|
"success",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Writing summary cost into the summary cost element
|
||||||
|
cost.innerText = json.cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatching event
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent("core.cart.summary.received", {
|
||||||
|
detail: {
|
||||||
|
json,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
resolve(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
reject(json);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Exit (fail)
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
cart.share,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @name Share (system)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Generate sharing hash of the cart and sent ot to the chat-robot
|
||||||
|
*
|
||||||
|
* @memberof cart.share
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async system(
|
||||||
|
resolve = () => {},
|
||||||
|
reject = () => {},
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Request
|
||||||
|
return await core.request("/cart/share", undefined, "POST")
|
||||||
|
.then(async (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)
|
||||||
|
|
||||||
|
if (json.share) {
|
||||||
|
// Received sharing hash
|
||||||
|
|
||||||
|
// Sending the request to the chat-robot
|
||||||
|
const sended = core.telegram.api.sendData(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "cart_share",
|
||||||
|
hash: json.share,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sended === undefined) {
|
||||||
|
// Failed to send the request
|
||||||
|
|
||||||
|
if (core.account.identifier > 0) {
|
||||||
|
// Initialized the account identifier
|
||||||
|
|
||||||
|
// Request
|
||||||
|
return await core.request(
|
||||||
|
"/cart/attach",
|
||||||
|
'share=' + json.share,
|
||||||
|
"POST",
|
||||||
|
).then((json) => {
|
||||||
|
if (json) {
|
||||||
|
// Received a JSON-response
|
||||||
|
|
||||||
|
if (json.success) {
|
||||||
|
// Received the success status
|
||||||
|
|
||||||
|
core.modules.connect("telegram").then(() => {
|
||||||
|
// Imported the telegram module
|
||||||
|
|
||||||
|
// Closing the Telegram Mini App
|
||||||
|
core.telegram.api.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* if (json.robokassa) {
|
||||||
|
// Received data for the Robokassa acquiring
|
||||||
|
|
||||||
|
// Initializing iframe
|
||||||
|
Robokassa.StartPayment({
|
||||||
|
MerchantLogin: json.robokassa.identifier,
|
||||||
|
OutSum: json.robokassa.cost,
|
||||||
|
InvId: json.robokassa.cart,
|
||||||
|
Description: json.robokassa.description,
|
||||||
|
Culture: json.robokassa.language,
|
||||||
|
Encoding: "utf-8",
|
||||||
|
Settings: JSON.stringify({
|
||||||
|
PaymentMethods: ["BankCard", "SBP"],
|
||||||
|
Mode: "modal",
|
||||||
|
}),
|
||||||
|
SignatureValue: json.robokassa.hash,
|
||||||
|
});
|
||||||
|
} */
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
resolve(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
reject(json);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} 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
|
@ -0,0 +1,305 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Connection
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Implements actions with websocket connection
|
||||||
|
*
|
||||||
|
* @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 connection {
|
||||||
|
/**
|
||||||
|
* @name Type of the program
|
||||||
|
*/
|
||||||
|
static type = "module";
|
||||||
|
|
||||||
|
// Wrap of indicator of the connection
|
||||||
|
static wrap = document.getElementById("connection");
|
||||||
|
|
||||||
|
// Indicator of the connection
|
||||||
|
static indicator = this.wrap?.getElementsByTagName("i")[0];
|
||||||
|
|
||||||
|
// Description of the connection
|
||||||
|
static description = this.wrap?.getElementsByTagName("small")[0];
|
||||||
|
|
||||||
|
// Statuc of the connection
|
||||||
|
static connected = false;
|
||||||
|
|
||||||
|
// Duration of the disconnected status
|
||||||
|
static timeout = 0;
|
||||||
|
|
||||||
|
// Instance of the time counter in disconnected status
|
||||||
|
static counter;
|
||||||
|
|
||||||
|
// Socket address (xn--e1ajlli это сокет)
|
||||||
|
static socket = "wss://arming.dev.mirzaev.sexy:9502";
|
||||||
|
|
||||||
|
// Instance of connection to the socket
|
||||||
|
static session;
|
||||||
|
|
||||||
|
// Iterval for reconnect
|
||||||
|
static interval;
|
||||||
|
|
||||||
|
// Attempts to connect (when connection.readyState === 0)
|
||||||
|
static attempts = 0;
|
||||||
|
|
||||||
|
// Interval for block
|
||||||
|
static block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize status of the connection to socket
|
||||||
|
*
|
||||||
|
* @param {bool} connected Connected?
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
static status(connected = false) {
|
||||||
|
if (this.indicator instanceof HTMLElement) {
|
||||||
|
// Initialized the indicator
|
||||||
|
|
||||||
|
this.connected = connected;
|
||||||
|
|
||||||
|
if (this.connected) {
|
||||||
|
// Connected
|
||||||
|
|
||||||
|
this.wrap.setAttribute("title", "Connected");
|
||||||
|
|
||||||
|
this.indicator.classList.remove("disconnected");
|
||||||
|
this.indicator.classList.add("connected");
|
||||||
|
|
||||||
|
clearInterval(this.counter);
|
||||||
|
this.description.innerText = "";
|
||||||
|
this.counter = undefined;
|
||||||
|
this.timeout = 0;
|
||||||
|
} else {
|
||||||
|
// Disconnected
|
||||||
|
|
||||||
|
this.wrap.setAttribute("title", "Disconnected");
|
||||||
|
|
||||||
|
this.indicator.classList.remove("connected");
|
||||||
|
this.indicator.classList.add("disconnected");
|
||||||
|
|
||||||
|
if (typeof this.counter === "undefined") {
|
||||||
|
this.counter = setInterval(() => {
|
||||||
|
this.timeout += 0.01;
|
||||||
|
this.description.innerText = this.timeout.toFixed(2);
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to the socket
|
||||||
|
*
|
||||||
|
* @param {bool|number} interval Connection check interval (ms)
|
||||||
|
* @param {function} preprocessing Will be executed every cycle
|
||||||
|
* @param {function} onmessage New message
|
||||||
|
* @param {function} onopen Connection opened
|
||||||
|
* @param {function} onclose Connection closed
|
||||||
|
* @param {function} onerror An error has occurred
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
static connect(
|
||||||
|
interval = false,
|
||||||
|
preprocessing,
|
||||||
|
onmessage,
|
||||||
|
onopen,
|
||||||
|
onclose,
|
||||||
|
onerror,
|
||||||
|
) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (typeof interval === "number" && interval > 0) {
|
||||||
|
// Connect with automatic reconnect
|
||||||
|
|
||||||
|
if (typeof this.interval === "undefined") {
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
preprocessing();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(this.session instanceof WebSocket) ||
|
||||||
|
(this.session.readyState === 3 ||
|
||||||
|
this.session.readyState === 4) ||
|
||||||
|
(this.session.readyState === 0 &&
|
||||||
|
++this.attempts > 10)
|
||||||
|
) {
|
||||||
|
this.attempts = 0;
|
||||||
|
|
||||||
|
if (this.session instanceof WebSocket) {
|
||||||
|
this.session.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.session = new WebSocket(this.socket);
|
||||||
|
this.session.addEventListener("message", (e) => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(e.data);
|
||||||
|
|
||||||
|
if (json.type === "registration") {
|
||||||
|
// Подключение сокета к сессии
|
||||||
|
|
||||||
|
fetch("/socket/registration", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: `key=${json.key}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (_e) {}
|
||||||
|
});
|
||||||
|
this.session.addEventListener("message", onmessage);
|
||||||
|
this.session.addEventListener("open", onopen);
|
||||||
|
this.session.addEventListener("close", onclose);
|
||||||
|
this.session.addEventListener("error", onerror);
|
||||||
|
|
||||||
|
resolve(this.session);
|
||||||
|
} else resolve(this.session);
|
||||||
|
}, interval);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Connect without reconnecting
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(this.session instanceof WebSocket) ||
|
||||||
|
(this.session.readyState === 3 ||
|
||||||
|
this.session.readyState === 4)
|
||||||
|
) {
|
||||||
|
if (this.session instanceof WebSocket) {
|
||||||
|
this.session.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.session = new WebSocket(this.socket);
|
||||||
|
this.session.addEventListener("message", onmessage);
|
||||||
|
this.session.addEventListener("open", onopen);
|
||||||
|
this.session.addEventListener("close", onclose);
|
||||||
|
this.session.addEventListener("error", onerror);
|
||||||
|
|
||||||
|
resolve(this.session);
|
||||||
|
} else resolve(this.session);
|
||||||
|
}
|
||||||
|
} catch (_e) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core is connected to the socket?
|
||||||
|
*
|
||||||
|
* @return {bool}
|
||||||
|
*/
|
||||||
|
static connected() {
|
||||||
|
return this.session instanceof WebSocket &&
|
||||||
|
this.session.readyState === 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.connect(
|
||||||
|
3000,
|
||||||
|
() =>
|
||||||
|
connection.status(
|
||||||
|
connection.session instanceof WebSocket &&
|
||||||
|
connection.session.readyState === 1,
|
||||||
|
),
|
||||||
|
(e) => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(e.data);
|
||||||
|
|
||||||
|
if (json.target === "task") {
|
||||||
|
// Заявка
|
||||||
|
|
||||||
|
// Инициализация строки
|
||||||
|
const row = document.getElementById(json._key);
|
||||||
|
|
||||||
|
if (row instanceof HTMLElement) {
|
||||||
|
// Инициализирована строка
|
||||||
|
|
||||||
|
if (json.type === "blocked") {
|
||||||
|
// Заблокирована заявка
|
||||||
|
|
||||||
|
// Запись статуса: "заблокирована"
|
||||||
|
row.setAttribute("data-blocked", json.account._key);
|
||||||
|
row.setAttribute(
|
||||||
|
"title",
|
||||||
|
"Редактирует: " + json.account.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Удалить блокировку (60000 === 1 минута) (в базе данных стоит expires 1 минута тоже)
|
||||||
|
setTimeout(() => {
|
||||||
|
// Удаление статуса: "заблокирована"
|
||||||
|
row.removeAttribute("data-blocked");
|
||||||
|
row.removeAttribute("title");
|
||||||
|
|
||||||
|
// Обновление строки
|
||||||
|
tasks.row(row);
|
||||||
|
}, 60000);
|
||||||
|
} else if (json.type === "unblocked") {
|
||||||
|
// Разблокирована заявка
|
||||||
|
|
||||||
|
// Удаление статуса: "заблокирована"
|
||||||
|
row.removeAttribute("data-blocked");
|
||||||
|
row.removeAttribute("title");
|
||||||
|
|
||||||
|
// Обновление строки
|
||||||
|
tasks.row(row);
|
||||||
|
} else if (json.type === "updated") {
|
||||||
|
// Обновлена заявка
|
||||||
|
|
||||||
|
// Обновление строки
|
||||||
|
tasks.row(row);
|
||||||
|
} else if (json.type === "deleted") {
|
||||||
|
// Удалена заявка
|
||||||
|
|
||||||
|
// Удаление строки
|
||||||
|
row.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_e) {}
|
||||||
|
// Инициализация идентифиатора
|
||||||
|
//const id = row.getAttribute("id");
|
||||||
|
|
||||||
|
// Инициализация количества непрочитанных сообщений
|
||||||
|
//const messages = row.lastElementChild.innerText;
|
||||||
|
|
||||||
|
// Инициализация статуса активной строки
|
||||||
|
//const selected = row.getAttribute("data-selected");
|
||||||
|
|
||||||
|
// Реинициализация строки
|
||||||
|
//row.outerHTML = data.rows;
|
||||||
|
|
||||||
|
// Реинициализация перезаписанной строки
|
||||||
|
//row = document.getElementById(id);
|
||||||
|
|
||||||
|
// Копирование статуса активной строки
|
||||||
|
//if (
|
||||||
|
// typeof selected === "string" &&
|
||||||
|
// selected === "true" &&
|
||||||
|
// document.body.contains(document.getElementById("popup"))
|
||||||
|
//) {
|
||||||
|
// row.setAttribute("data-selected", "true");
|
||||||
|
//}
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
//connection.status(
|
||||||
|
// connection instanceof WebSocket &&
|
||||||
|
// connection.readyState === 1,
|
||||||
|
//)
|
||||||
|
//console.log("Connected to WebSocket!");
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
//connection.status(
|
||||||
|
// connection instanceof WebSocket &&
|
||||||
|
// connection.readyState === 1,
|
||||||
|
//)
|
||||||
|
//console.log("Connection closed");
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
//console.log("Error happens");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Connecting to the core
|
||||||
|
if (!core.connection) core.connection = connection;
|
|
@ -0,0 +1,82 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Damper
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Execute multiple "function" calls in a "timeout" amount of time just once
|
||||||
|
*
|
||||||
|
* @param {function} function Function to execute after damping
|
||||||
|
* @param {number} timeout Timer in milliseconds (ms)
|
||||||
|
* @param {number} force Argument number storing the status of enforcement execution (see @example)
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* a = damper(
|
||||||
|
* async (
|
||||||
|
* a, // 0
|
||||||
|
* b, // 1
|
||||||
|
* c, // 2
|
||||||
|
* force = false, // 3
|
||||||
|
* d, // 4
|
||||||
|
* resolve,
|
||||||
|
* reject
|
||||||
|
* ) => {
|
||||||
|
* // Body of the function
|
||||||
|
*
|
||||||
|
* resolve();
|
||||||
|
* },
|
||||||
|
* 500,
|
||||||
|
* 3, // 3 -> "force" argument
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* a('for a', 'for b', 'for c', true, 'for d'); // Force execute is enabled
|
||||||
|
*
|
||||||
|
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
export default function damper(func, timeout = 300, force) {
|
||||||
|
// Declaring of the timer for executing the function
|
||||||
|
let timer;
|
||||||
|
|
||||||
|
return ((...args) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Deinitializing of the timer
|
||||||
|
clearTimeout(timer);
|
||||||
|
|
||||||
|
if (typeof force === "number" && args[force]) {
|
||||||
|
// Requested execution with ignoring the timer
|
||||||
|
|
||||||
|
// Deleting the force argument
|
||||||
|
if (typeof force === "number") args = [
|
||||||
|
...args.splice(0, force),
|
||||||
|
...args.splice(force + 1)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Writing promise handlers into the arguments variable
|
||||||
|
args.push(resolve, reject);
|
||||||
|
|
||||||
|
// Executing the function
|
||||||
|
func.apply(this, args);
|
||||||
|
} else {
|
||||||
|
// Normal execution
|
||||||
|
|
||||||
|
// Deleting the force argument
|
||||||
|
if (typeof force === "number") args = [
|
||||||
|
...args.splice(0, force),
|
||||||
|
...args.splice(force + 1)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Writing promise handlers into the arguments variable
|
||||||
|
args.push(resolve, reject);
|
||||||
|
|
||||||
|
// Resetting the timer and executing the function when the timer expires
|
||||||
|
timer = setTimeout(() => func.apply(this, args), timeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connecting to the core
|
||||||
|
if (!core.damper) core.damper = damper;
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue