v1.
This commit is contained in:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
*.*~$
|
||||
^#*
|
||||
^.#*
|
||||
*~$
|
||||
api.credentials*
|
||||
evo/products/*
|
||||
evo/groups/*
|
||||
evo/stores/*
|
||||
vk/categories/*
|
||||
vk/albums/*
|
||||
vk/products/*
|
||||
run/test.log
|
||||
vk/whitelist
|
||||
logs/
|
||||
passwords.txt
|
||||
BIN
5393364294319597854.png
Normal file
BIN
5393364294319597854.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
20
CHANGELOG.md
Normal file
20
CHANGELOG.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.6.0] - 2025-06-15
|
||||
|
||||
### Added
|
||||
- Initial changelog implementation
|
||||
- Version tracking system
|
||||
|
||||
### Changed
|
||||
- Minor version bump from 1.5.2 to 1.6.0
|
||||
|
||||
## [1.5.2] - Previous Release
|
||||
|
||||
### Notes
|
||||
- Historical version before changelog implementation
|
||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --update \
|
||||
curl \
|
||||
git \
|
||||
openrc \
|
||||
bash \
|
||||
jq \
|
||||
yq
|
||||
|
||||
RUN mkdir -p /var/www/
|
||||
RUN git config --system --add safe.directory '*'
|
||||
|
||||
COPY ./cronjobs /etc/cron.d/cronjobs
|
||||
RUN chmod 0644 /etc/cron.d/cronjobs
|
||||
RUN /usr/bin/crontab /etc/cron.d/cronjobs
|
||||
|
||||
WORKDIR /var/www/
|
||||
COPY ./ /var/www/
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["/usr/sbin/crond", "-f", "-l", "8"]
|
||||
@@ -1,2 +1,3 @@
|
||||
# evo-sync
|
||||
|
||||
evo-sync is a command-line synchronization tool that fetches product, group, and store data from the Evo platform and syncs it with VK (VKontakte).
|
||||
|
||||
27
evo/examples/products.json
Normal file
27
evo/examples/products.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"uuid": "0291602f-8de3-4df6-90d3-78917b51b6b5",
|
||||
"group": true,
|
||||
"hasVariants": null,
|
||||
"type": null,
|
||||
"name": "Чай с добавками",
|
||||
"code": null,
|
||||
"barCodes": null,
|
||||
"price": null,
|
||||
"costPrice": null,
|
||||
"quantity": null,
|
||||
"measureName": null,
|
||||
"tax": null,
|
||||
"allowToSell": null,
|
||||
"description": null,
|
||||
"articleNumber": null,
|
||||
"parentUuid": null,
|
||||
"alcoCodes": null,
|
||||
"alcoholByVolume": null,
|
||||
"alcoholProductKindCode": null,
|
||||
"tareVolume": null,
|
||||
"classificationCode": null,
|
||||
"allowPartialSell": null,
|
||||
"quantityInPackage": null,
|
||||
"isExcisable": null,
|
||||
"isAgeLimited": null
|
||||
}
|
||||
34
run/evo/clear.sh
Executable file
34
run/evo/clear.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script cleans all EVO-related directories
|
||||
# It removes all files from products, groups and stores directories
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
# Load constants and functions
|
||||
source $SCRIPT_DIR/constants.sh
|
||||
source $SCRIPT_DIR/functions.sh
|
||||
|
||||
# Initialize logging system
|
||||
setup_logging
|
||||
|
||||
# Ensure we clean up properly even if script is interrupted
|
||||
trap 'cleanup 1' HUP INT QUIT TERM
|
||||
|
||||
# Begin cleanup process
|
||||
echo "$(timestamp) [INFO] Starting EVO cleanup process" >> "$LOG_FILE"
|
||||
|
||||
# Clean EVO content directories
|
||||
echo "$(timestamp) [INFO] Cleaning EVO directories" >> "$LOG_FILE"
|
||||
# Remove product files
|
||||
echo "$(timestamp) [INFO] Removing product files" >> "$LOG_FILE"
|
||||
echo "rm -f $EVO_PRODUCTS_PATH/*" && rm -rf $EVO_PRODUCTS_PATH/*
|
||||
# Remove group files
|
||||
echo "$(timestamp) [INFO] Removing group files" >> "$LOG_FILE"
|
||||
echo "rm -f $EVO_GROUPS_PATH/*" && rm -rf $EVO_GROUPS_PATH/*
|
||||
# Remove store files
|
||||
echo "$(timestamp) [INFO] Removing store files" >> "$LOG_FILE"
|
||||
echo "rm -rf $EVO_STORES_PATH/*" && rm -rf $EVO_STORES_PATH/*
|
||||
|
||||
echo "$(timestamp) [INFO] EVO cleanup completed successfully" >> "$LOG_FILE"
|
||||
cleanup 0
|
||||
21
run/evo/constants.sh
Executable file
21
run/evo/constants.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ROOT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
ROOT_DIR+='/../..'
|
||||
|
||||
EVO_STORE_PERIOD_HOURS=2
|
||||
EVO_STORE_PERIOD_MINUTES=$((EVO_STORE_PERIOD_HOURS * 60))
|
||||
EVO_PRODUCTS_PATH="$ROOT_DIR/evo/products"
|
||||
EVO_GROUPS_PATH="$ROOT_DIR/evo/groups"
|
||||
EVO_STORES_PATH="$ROOT_DIR/evo/stores"
|
||||
EVO_EXAMPLE_PATH="$ROOT_DIR/evo/examples"
|
||||
EVO_RESPONSE_FILE_NAME_FORMAT="%(%Y-%m-%d_%H:%M:%S)T"
|
||||
EVO_RESPONSE_FILE_NAME_EXT="json"
|
||||
EVO_API_HOST="https://api.evotor.ru"
|
||||
EVO_API_TOKEN=`cat "$ROOT_DIR/evo/api.credentials.token"`
|
||||
EVO_API_STORE_ID=`cat "$ROOT_DIR/evo/api.credentials.store_id"`
|
||||
EVO_API_ACCEPT=`cat "$ROOT_DIR/evo/api.credentials.accept"`
|
||||
EVO_API_CONTENT_TYPE=`cat "$ROOT_DIR/evo/api.credentials.content_type"`
|
||||
EVO_API_GET_PRODUCTS="$EVO_API_HOST/stores/$EVO_API_STORE_ID/products"
|
||||
EVO_API_GET_GROUPS="$EVO_API_HOST/stores/$EVO_API_STORE_ID/product-groups"
|
||||
EVO_API_GET_STORES="$EVO_API_HOST/stores"
|
||||
45
run/evo/functions.sh
Executable file
45
run/evo/functions.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Setup timestamp function
|
||||
timestamp() {
|
||||
date "+%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
|
||||
# Setup process cleanup
|
||||
cleanup() {
|
||||
# Kill all child processes of this script
|
||||
pkill -P $$
|
||||
exit $1
|
||||
}
|
||||
|
||||
# Function to handle API requests and save response
|
||||
handle_request() {
|
||||
local request_type=$1
|
||||
local api_endpoint=$2
|
||||
local path=$3
|
||||
local fileName=$4
|
||||
|
||||
echo "$(timestamp) [REQUEST] Getting $request_type" >> "$LOG_FILE"
|
||||
response=$(curl -s -w "%{http_code}" -H "Accept: $EVO_API_ACCEPT" \
|
||||
-H "Content-Type: $EVO_API_CONTENT_TYPE" \
|
||||
-H "Authorization: $EVO_API_TOKEN" \
|
||||
-X GET \
|
||||
$api_endpoint)
|
||||
http_code=${response: -3}
|
||||
response_body=${response:0:-3}
|
||||
if [ "$http_code" = "200" ]; then
|
||||
touch "$path/$fileName"
|
||||
echo "$response_body" > "$path/$fileName"
|
||||
fi
|
||||
echo "$(timestamp) [RESPONSE] code=$http_code" >> "$LOG_FILE"
|
||||
|
||||
# Delete old files (files older than configured period)
|
||||
find $path ! -type d -mmin +$EVO_STORE_PERIOD_MINUTES -delete
|
||||
}
|
||||
|
||||
# Setup logging function
|
||||
setup_logging() {
|
||||
LOG_DIR="$ROOT_DIR/logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
LOG_FILE="$LOG_DIR/$(date +%Y%m%d).log"
|
||||
}
|
||||
21
run/evo/get-groups.sh
Executable file
21
run/evo/get-groups.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
source $SCRIPT_DIR/constants.sh
|
||||
source $SCRIPT_DIR/functions.sh
|
||||
|
||||
# Setup logging
|
||||
setup_logging
|
||||
|
||||
# Trap signals to ensure proper cleanup
|
||||
trap 'cleanup 1' HUP INT QUIT TERM
|
||||
|
||||
path=$EVO_GROUPS_PATH/$EVO_API_STORE_ID
|
||||
printf -v fileName "$EVO_RESPONSE_FILE_NAME_FORMAT.$EVO_RESPONSE_FILE_NAME_EXT" -1
|
||||
mkdir -p $path
|
||||
|
||||
# Handle request for groups
|
||||
handle_request "groups" "$EVO_API_GET_GROUPS" "$path" "$fileName"
|
||||
|
||||
# Use the cleanup function instead of directly exiting
|
||||
cleanup 0
|
||||
21
run/evo/get-products.sh
Executable file
21
run/evo/get-products.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
source $SCRIPT_DIR/constants.sh
|
||||
source $SCRIPT_DIR/functions.sh
|
||||
|
||||
# Setup logging
|
||||
setup_logging
|
||||
|
||||
# Trap signals to ensure proper cleanup
|
||||
trap 'cleanup 1' HUP INT QUIT TERM
|
||||
|
||||
path=$EVO_PRODUCTS_PATH/$EVO_API_STORE_ID
|
||||
printf -v fileName "$EVO_RESPONSE_FILE_NAME_FORMAT.$EVO_RESPONSE_FILE_NAME_EXT" -1
|
||||
mkdir -p $path
|
||||
|
||||
# Handle request for products
|
||||
handle_request "products" "$EVO_API_GET_PRODUCTS" "$path" "$fileName"
|
||||
|
||||
# Use the cleanup function instead of directly exiting
|
||||
cleanup 0
|
||||
21
run/evo/get-stores.sh
Executable file
21
run/evo/get-stores.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
source $SCRIPT_DIR/constants.sh
|
||||
source $SCRIPT_DIR/functions.sh
|
||||
|
||||
# Setup logging
|
||||
setup_logging
|
||||
|
||||
# Trap signals to ensure proper cleanup
|
||||
trap 'cleanup 1' HUP INT QUIT TERM
|
||||
|
||||
path=$EVO_STORES_PATH/$EVO_API_STORE_ID
|
||||
printf -v fileName "$EVO_RESPONSE_FILE_NAME_FORMAT.$EVO_RESPONSE_FILE_NAME_EXT" -1
|
||||
mkdir -p $path
|
||||
|
||||
# Handle request for stores
|
||||
handle_request "stores" "$EVO_API_GET_STORES" "$path" "$fileName"
|
||||
|
||||
# Use the cleanup function instead of directly exiting
|
||||
cleanup 0
|
||||
13
run/run.sh
Executable file
13
run/run.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
$SCRIPT_DIR/evo/get-groups.sh
|
||||
$SCRIPT_DIR/evo/get-products.sh
|
||||
$SCRIPT_DIR/vk/get-albums.sh
|
||||
$SCRIPT_DIR/vk/send-albums.sh
|
||||
$SCRIPT_DIR/vk/get-albums.sh
|
||||
$SCRIPT_DIR/vk/get-products.sh
|
||||
$SCRIPT_DIR/vk/send-products.sh
|
||||
$SCRIPT_DIR/vk/get-products.sh
|
||||
$SCRIPT_DIR/vk/delete-products.sh
|
||||
5
run/test.sh
Executable file
5
run/test.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
echo "Test [OK]." >> $SCRIPT_DIR/test.res
|
||||
37
run/vk/clear.sh
Executable file
37
run/vk/clear.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script cleans all VK-related directories and logs
|
||||
# It removes all files from categories, albums, products and logs directories
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
# Load constants and functions
|
||||
source $SCRIPT_DIR/constants.sh
|
||||
source $SCRIPT_DIR/functions.sh
|
||||
|
||||
# Initialize logging system
|
||||
setup_logging
|
||||
|
||||
# Ensure we clean up properly even if script is interrupted
|
||||
trap 'cleanup 1' HUP INT QUIT TERM
|
||||
|
||||
# Begin cleanup process
|
||||
echo "$(timestamp) [INFO] Starting cleanup process" >> "$LOG_FILE"
|
||||
|
||||
# Clean VK content directories
|
||||
echo "$(timestamp) [INFO] Cleaning VK directories" >> "$LOG_FILE"
|
||||
# Remove category files
|
||||
echo "$(timestamp) [INFO] Removing category files" >> "$LOG_FILE"
|
||||
echo "rm -f $VK_CATEGORIES_PATH/*" && rm -rf $VK_CATEGORIES_PATH/*
|
||||
# Remove album files
|
||||
echo "$(timestamp) [INFO] Removing album files" >> "$LOG_FILE"
|
||||
echo "rm -f $VK_ALBUMS_PATH/*" && rm -rf $VK_ALBUMS_PATH/*
|
||||
# Remove product files
|
||||
echo "$(timestamp) [INFO] Removing product files" >> "$LOG_FILE"
|
||||
echo "rm -f $VK_PRODUCTS_PATH/*" && rm -rf $VK_PRODUCTS_PATH/*
|
||||
# Clean logs directory
|
||||
echo "$(timestamp) [INFO] Cleaning logs directory" >> "$LOG_FILE"
|
||||
echo "rm -f $LOG_DIR/*" && rm -rf $LOG_DIR/*
|
||||
|
||||
echo "$(timestamp) [INFO] Cleanup completed successfully" >> "$LOG_FILE"
|
||||
cleanup 0
|
||||
30
run/vk/constants.sh
Executable file
30
run/vk/constants.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ROOT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
ROOT_DIR+='/../..'
|
||||
|
||||
VK_STORE_PERIOD_MINUTES=120
|
||||
VK_API_CONTENT_TYPE_MULTIPART="multipart/form-data"
|
||||
VK_API_HOST="https://api.vk.ru/method"
|
||||
VK_API_USER_TOKEN=`cat "$ROOT_DIR/vk/api.credentials.user_token"`
|
||||
VK_API_VERSION="5.199"
|
||||
VK_API_ADD_PRODUCT="$VK_API_HOST/market.add?v=$VK_API_VERSION"
|
||||
VK_API_EDIT_PRODUCT="$VK_API_HOST/market.edit?v=$VK_API_VERSION"
|
||||
VK_API_ADD_PRODUCT_TO_ALBUM="$VK_API_HOST/market.addToAlbum?v=$VK_API_VERSION"
|
||||
VK_API_ADD_ALBUM="$VK_API_HOST/market.addAlbum?v=$VK_API_VERSION"
|
||||
VK_API_GET_CATEGORIES="$VK_API_HOST/market.getCategories?v=$VK_API_VERSION"
|
||||
VK_API_GET_ALBUMS="$VK_API_HOST/market.getAlbums?v=$VK_API_VERSION"
|
||||
VK_API_GET_PRODUCTS="$VK_API_HOST/market.get?v=$VK_API_VERSION"
|
||||
VK_API_GROUP_ID=`cat "$ROOT_DIR/vk/api.credentials.group_id"`
|
||||
VK_API_PARAM_OWNER_ID="-$VK_API_GROUP_ID"
|
||||
VK_API_DELETE_PRODUCT="$VK_API_HOST/market.delete?v=$VK_API_VERSION"
|
||||
VK_CATEGORIES_PATH="$ROOT_DIR/vk/categories"
|
||||
VK_ALBUMS_PATH="$ROOT_DIR/vk/albums"
|
||||
VK_PRODUCTS_PATH="$ROOT_DIR/vk/products"
|
||||
VK_RESPONSE_FILE_NAME_FORMAT="%(%Y-%m-%d_%H:%M:%S)T"
|
||||
VK_RESPONSE_FILE_NAME_EXT="json"
|
||||
VK_API_CATEGORY_ID="40932"
|
||||
VK_STOCK_AMOUNT=1000
|
||||
VK_API_GET_PHOTO_UPLOAD_URL="$VK_API_HOST/market.getProductPhotoUploadServer?v=$VK_API_VERSION"
|
||||
VK_API_PHOTO_PATH="$ROOT_DIR/5393364294319597854.png"
|
||||
VK_API_UPLOAD_PHOTO_URL="$VK_API_HOST/market.saveProductPhoto"
|
||||
92
run/vk/delete-products.sh
Executable file
92
run/vk/delete-products.sh
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
source $SCRIPT_DIR/constants.sh
|
||||
source $ROOT_DIR/run/evo/constants.sh
|
||||
source $SCRIPT_DIR/functions.sh
|
||||
|
||||
# Setup logging
|
||||
setup_logging
|
||||
|
||||
# Trap signals to ensure proper cleanup
|
||||
trap 'cleanup 1' HUP INT QUIT TERM
|
||||
|
||||
# Delete products from vk which were deleted from evo.
|
||||
# loop across vk products.
|
||||
|
||||
vkPath=$VK_PRODUCTS_PATH/$VK_API_GROUP_ID
|
||||
vkFileName=`ls $vkPath -Art | tail -1`
|
||||
vkFilePath=$vkPath/$vkFileName
|
||||
|
||||
evoPath=$EVO_PRODUCTS_PATH/$EVO_API_STORE_ID
|
||||
evoFileName=`ls $evoPath -Art | tail -1`
|
||||
evoFilePath=$evoPath/$evoFileName
|
||||
|
||||
hasEvoItems=`yq '. | has("items")' $evoFilePath`
|
||||
if ! $hasEvoItems; then
|
||||
cleanup 1
|
||||
fi
|
||||
|
||||
# Build associative array of EVO product names (transformed)
|
||||
declare -A evoNames
|
||||
while IFS= read -r line; do
|
||||
if [[ -n "$line" ]]; then
|
||||
item_name=$(echo "$line" | yq -r '.name')
|
||||
if [[ -n "$item_name" && "$item_name" != "null" ]]; then
|
||||
transformed_name=$(echo "$item_name" | xargs)
|
||||
transformed_name="${transformed_name//;/,}"
|
||||
evoNames["$transformed_name"]=1
|
||||
fi
|
||||
fi
|
||||
done < <(yq -o=j -I=0 '.items[]' $evoFilePath)
|
||||
|
||||
# Build associative array of VK items by name (transformed)
|
||||
declare -A vkItemsByName
|
||||
readarray vkItems < <(yq -o=j -I=0 '.response.items[]' $vkFilePath )
|
||||
for vkItem in "${vkItems[@]}"; do
|
||||
vkItemName=$(echo $vkItem | yq .title | xargs)
|
||||
vkItemName="${vkItemName//;/,}"
|
||||
vkItemId=$(echo $vkItem | yq .id)
|
||||
# Store VK item IDs by name (append to comma-separated list)
|
||||
if [[ -n "${vkItemsByName[$vkItemName]}" ]]; then
|
||||
vkItemsByName[$vkItemName]="${vkItemsByName[$vkItemName]},$vkItemId"
|
||||
else
|
||||
vkItemsByName[$vkItemName]="$vkItemId"
|
||||
fi
|
||||
done
|
||||
|
||||
# For each VK name, check if it exists in EVO
|
||||
for vkName in "${!vkItemsByName[@]}"; do
|
||||
IFS=',' read -ra ids <<< "${vkItemsByName[$vkName]}"
|
||||
if [[ -n "${evoNames[$vkName]}" ]]; then
|
||||
# If multiple VK items for this name, keep the oldest (first), delete the rest
|
||||
if (( ${#ids[@]} > 1 )); then
|
||||
for ((i=1; i<${#ids[@]}; i++)); do
|
||||
vkItemId="${ids[$i]}"
|
||||
echo "$(timestamp) [REQUEST] Deleting duplicate product: $vkName (id=$vkItemId)" >> "$LOG_FILE"
|
||||
response=$(curl -s -w "%{http_code}" -H "Authorization: Bearer $VK_API_USER_TOKEN" \
|
||||
-F owner_id=$VK_API_PARAM_OWNER_ID \
|
||||
-F item_id=$vkItemId \
|
||||
$VK_API_DELETE_PRODUCT)
|
||||
http_code=${response: -3}
|
||||
response_body=${response:0:-3}
|
||||
echo "$(timestamp) [RESPONSE] code=$http_code body=$response_body" >> "$LOG_FILE"
|
||||
done
|
||||
fi
|
||||
else
|
||||
# If VK name not in EVO, delete all VK items for this name
|
||||
for vkItemId in "${ids[@]}"; do
|
||||
echo "$(timestamp) [REQUEST] Deleting product not in EVO: $vkName (id=$vkItemId)" >> "$LOG_FILE"
|
||||
response=$(curl -s -w "%{http_code}" -H "Authorization: Bearer $VK_API_USER_TOKEN" \
|
||||
-F owner_id=$VK_API_PARAM_OWNER_ID \
|
||||
-F item_id=$vkItemId \
|
||||
$VK_API_DELETE_PRODUCT)
|
||||
http_code=${response: -3}
|
||||
response_body=${response:0:-3}
|
||||
echo "$(timestamp) [RESPONSE] code=$http_code body=$response_body" >> "$LOG_FILE"
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
# Use the cleanup function instead of directly exiting
|
||||
cleanup 0
|
||||
52
run/vk/functions.sh
Executable file
52
run/vk/functions.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Setup logging
|
||||
function setup_logging() {
|
||||
LOG_DIR="$ROOT_DIR/logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
SCRIPT_NAME=$(basename "${BASH_SOURCE[0]}" .sh)
|
||||
LOG_FILE="$LOG_DIR/$(date +%Y%m%d).log"
|
||||
}
|
||||
|
||||
function timestamp() {
|
||||
date "+%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
|
||||
# Function to handle cleanup and exit
|
||||
function cleanup() {
|
||||
# Kill all child processes of this script
|
||||
pkill -P $$
|
||||
exit $1
|
||||
}
|
||||
|
||||
# Function to handle requests and responses
|
||||
function handle_vk_request() {
|
||||
local request_name=$1
|
||||
local request_url=$2
|
||||
local path=$3
|
||||
local fileName=$4
|
||||
local additional_params=$5
|
||||
local debug_response=${6:-true}
|
||||
|
||||
echo "$(timestamp) [REQUEST] Getting $request_name" >> "$LOG_FILE"
|
||||
response=$(curl -s -w "%{http_code}" -H "Authorization: Bearer $VK_API_USER_TOKEN" \
|
||||
-X GET \
|
||||
"$request_url$additional_params")
|
||||
http_code=${response: -3}
|
||||
response_body=${response:0:-3}
|
||||
if [ "$http_code" = "200" ]; then
|
||||
if ! echo "$response_body" | jq -e 'has("error")' > /dev/null; then
|
||||
touch "$path/$fileName"
|
||||
echo "$response_body" > "$path/$fileName"
|
||||
else
|
||||
error_msg=$(echo "$response_body" | jq -r '.error.error_msg')
|
||||
echo "$(timestamp) [ERROR] $error_msg" >> "$LOG_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$debug_response" = true ]; then
|
||||
echo "$(timestamp) [RESPONSE] code=$http_code body=$response_body" >> "$LOG_FILE"
|
||||
else
|
||||
echo "$(timestamp) [RESPONSE] code=$http_code" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
24
run/vk/get-albums.sh
Executable file
24
run/vk/get-albums.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
source $SCRIPT_DIR/constants.sh
|
||||
source $SCRIPT_DIR/functions.sh
|
||||
|
||||
# Setup logging
|
||||
setup_logging
|
||||
|
||||
# Trap signals to ensure proper cleanup
|
||||
trap 'cleanup 1' HUP INT QUIT TERM
|
||||
|
||||
path=$VK_ALBUMS_PATH/$VK_API_GROUP_ID
|
||||
mkdir -p $path
|
||||
printf -v fileName "$VK_RESPONSE_FILE_NAME_FORMAT.$VK_RESPONSE_FILE_NAME_EXT" -1
|
||||
|
||||
# Handle request for albums
|
||||
handle_vk_request "albums list" "$VK_API_GET_ALBUMS" "$path" "$fileName" "&owner_id=$VK_API_PARAM_OWNER_ID" false
|
||||
|
||||
# Clean up old files
|
||||
find $path ! -type d -mmin +$VK_STORE_PERIOD_MINUTES -delete
|
||||
|
||||
# Use the cleanup function instead of directly exiting
|
||||
cleanup 0
|
||||
24
run/vk/get-categories.sh
Executable file
24
run/vk/get-categories.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
source $SCRIPT_DIR/constants.sh
|
||||
source $SCRIPT_DIR/functions.sh
|
||||
|
||||
# Setup logging
|
||||
setup_logging
|
||||
|
||||
# Trap signals to ensure proper cleanup
|
||||
trap 'cleanup 1' HUP INT QUIT TERM
|
||||
|
||||
path=$VK_CATEGORIES_PATH/$VK_API_GROUP_ID
|
||||
mkdir -p $path
|
||||
printf -v fileName "$VK_RESPONSE_FILE_NAME_FORMAT.$VK_RESPONSE_FILE_NAME_EXT" -1
|
||||
|
||||
# Handle request for categories
|
||||
handle_vk_request "categories list" "$VK_API_GET_CATEGORIES" "$path" "$fileName" "" false
|
||||
|
||||
# Clean up old files
|
||||
find $path ! -type d -mmin +$VK_STORE_PERIOD_MINUTES -delete
|
||||
|
||||
# Use the cleanup function instead of directly exiting
|
||||
cleanup 0
|
||||
24
run/vk/get-products.sh
Executable file
24
run/vk/get-products.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
source $SCRIPT_DIR/constants.sh
|
||||
source $SCRIPT_DIR/functions.sh
|
||||
|
||||
# Setup logging
|
||||
setup_logging
|
||||
|
||||
# Trap signals to ensure proper cleanup
|
||||
trap 'cleanup 1' HUP INT QUIT TERM
|
||||
|
||||
path=$VK_PRODUCTS_PATH/$VK_API_GROUP_ID
|
||||
mkdir -p $path
|
||||
printf -v fileName "$VK_RESPONSE_FILE_NAME_FORMAT.$VK_RESPONSE_FILE_NAME_EXT" -1
|
||||
|
||||
# Handle request for products
|
||||
handle_vk_request "products list" "$VK_API_GET_PRODUCTS" "$path" "$fileName" "&owner_id=$VK_API_PARAM_OWNER_ID&extended=1&with_disabled=1&count=200" false
|
||||
|
||||
# Clean up old files
|
||||
find $path ! -type d -mmin +$VK_STORE_PERIOD_MINUTES -delete
|
||||
|
||||
# Use the cleanup function instead of directly exiting
|
||||
cleanup 0
|
||||
47
run/vk/send-albums.sh
Executable file
47
run/vk/send-albums.sh
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
source $SCRIPT_DIR/constants.sh
|
||||
source $ROOT_DIR/run/evo/constants.sh
|
||||
source $SCRIPT_DIR/functions.sh
|
||||
|
||||
# Setup logging
|
||||
setup_logging
|
||||
|
||||
# Trap signals to ensure proper cleanup
|
||||
trap 'cleanup 1' HUP INT QUIT TERM
|
||||
|
||||
evoPath=$EVO_GROUPS_PATH/$EVO_API_STORE_ID
|
||||
evoFileName=`ls $evoPath -Art | tail -1`
|
||||
evoFilePath=$evoPath/$evoFileName
|
||||
vkPath=$VK_ALBUMS_PATH/$VK_API_GROUP_ID
|
||||
vkFileName=`ls $vkPath -Art | tail -1`
|
||||
vkFilePath=$vkPath/$vkFileName
|
||||
|
||||
# Load whitelist
|
||||
readarray -t whitelist < "$ROOT_DIR/vk/whitelist"
|
||||
|
||||
# Filter items by whitelist
|
||||
readarray items < <(yq -o=j -I=0 ".items[]" $evoFilePath)
|
||||
|
||||
for item in "${items[@]}"; do
|
||||
evoTitle=`echo $item | yq .name`
|
||||
# Check if group is whitelisted
|
||||
if [[ ! " ${whitelist[@]} " =~ " ${evoTitle} " ]]; then
|
||||
continue
|
||||
fi
|
||||
found=`evoTitle="$evoTitle" yq '.response.items[] | select(.title==strenv(evoTitle))' $vkFilePath`
|
||||
if [[ ! -n "$found" ]]; then
|
||||
echo "$(timestamp) [REQUEST] Creating album: $evoTitle" >> "$LOG_FILE"
|
||||
response=$(curl -s -w "%{http_code}" -H "Authorization: Bearer $VK_API_USER_TOKEN" \
|
||||
-F owner_id=$VK_API_PARAM_OWNER_ID \
|
||||
-F title="$evoTitle" \
|
||||
$VK_API_ADD_ALBUM)
|
||||
http_code=${response: -3}
|
||||
response_body=${response:0:-3}
|
||||
echo "$(timestamp) [RESPONSE] code=$http_code body=$response_body" >> "$LOG_FILE"
|
||||
fi
|
||||
done
|
||||
|
||||
# Use the cleanup function instead of directly exiting
|
||||
cleanup 0
|
||||
257
run/vk/send-products.sh
Executable file
257
run/vk/send-products.sh
Executable file
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Rename to better reflect purpose - only applied to weight measures (г, г., грамм, etc)
|
||||
WEIGHT_PRICE_MULTIPLIER=${WEIGHT_PRICE_MULTIPLIER:-10}
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
source $SCRIPT_DIR/constants.sh
|
||||
source $ROOT_DIR/run/evo/constants.sh
|
||||
source $SCRIPT_DIR/functions.sh
|
||||
|
||||
# Setup logging
|
||||
setup_logging
|
||||
|
||||
# Trap signals to ensure proper cleanup
|
||||
trap 'cleanup 1' HUP INT QUIT TERM
|
||||
|
||||
# Function to check if a measure is a weight measure
|
||||
is_weight_measure() {
|
||||
local measure="$1"
|
||||
# Convert to lowercase for case-insensitive comparison
|
||||
local measure_lower=$(echo "$measure" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Check for various weight measure formats
|
||||
if [[ "$measure_lower" == "г" ||
|
||||
"$measure_lower" == "г." ||
|
||||
"$measure_lower" == "грамм" ||
|
||||
"$measure_lower" == "граммов" ||
|
||||
"$measure_lower" == "гр" ||
|
||||
"$measure_lower" == "гр." ]]; then
|
||||
return 0 # True - it is a weight measure
|
||||
else
|
||||
return 1 # False - it is not a weight measure
|
||||
fi
|
||||
}
|
||||
|
||||
function uploadPhoto() {
|
||||
getUploadPhotoResp=$(curl -s -w "%{http_code}" -H "Authorization: Bearer $VK_API_USER_TOKEN" \
|
||||
"$VK_API_GET_PHOTO_UPLOAD_URL&group_id=$VK_API_GROUP_ID")
|
||||
http_code=${getUploadPhotoResp: -3}
|
||||
response_body=${getUploadPhotoResp:0:-3}
|
||||
echo "$(timestamp) [RESPONSE] code=$http_code body=$response_body" >> "$LOG_FILE"
|
||||
|
||||
# Check if the first request was successful
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "$(timestamp) [ERROR] Failed to get photo upload URL" >> "$LOG_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
uploadPhotoUrl=`echo $response_body | yq -pj '.response.upload_url'`
|
||||
uploadPhotoObj=`curl -s -X POST --header "Content-Type: $VK_API_CONTENT_TYPE_MULTIPART" $uploadPhotoUrl -F "file=@$VK_API_PHOTO_PATH"`
|
||||
uploadPhotoResp=$(curl -s -w "%{http_code}" -H "Authorization: Bearer $VK_API_USER_TOKEN" \
|
||||
-F upload_response=$uploadPhotoObj \
|
||||
-F v=$VK_API_VERSION \
|
||||
$VK_API_UPLOAD_PHOTO_URL)
|
||||
http_code=${uploadPhotoResp: -3}
|
||||
response_body=${uploadPhotoResp:0:-3}
|
||||
echo "$(timestamp) [RESPONSE] code=$http_code body=$response_body" >> "$LOG_FILE"
|
||||
|
||||
# Check if the second request was successful
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "$(timestamp) [ERROR] Failed to upload photo" >> "$LOG_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract and return the photo ID - update to use the correct JSON path
|
||||
local photoId=$(echo "$response_body" | yq -pj '.response.photo_id')
|
||||
echo "$(timestamp) [INFO] Uploaded photo with ID: $photoId" >> "$LOG_FILE"
|
||||
echo $photoId
|
||||
}
|
||||
|
||||
evoPath=$EVO_PRODUCTS_PATH/$EVO_API_STORE_ID
|
||||
evoFileName=`ls $evoPath -Art | tail -1`
|
||||
evoFilePath=$evoPath/$evoFileName
|
||||
evoGroupsPath=$EVO_GROUPS_PATH/$EVO_API_STORE_ID
|
||||
evoGroupsFileName=`ls $evoGroupsPath -Art | tail -1`
|
||||
evoGroupsFilePath=$evoGroupsPath/$evoGroupsFileName
|
||||
vkPath=$VK_PRODUCTS_PATH/$VK_API_GROUP_ID
|
||||
vkFileName=`ls $vkPath -Art | tail -1`
|
||||
vkFilePath=$vkPath/$vkFileName
|
||||
vkAlbumPath=$VK_ALBUMS_PATH/$VK_API_GROUP_ID
|
||||
vkAlbumFileName=`ls $vkAlbumPath -Art | tail -1`
|
||||
vkAlbumFilePath=$vkAlbumPath/$vkAlbumFileName
|
||||
|
||||
# Load whitelist
|
||||
readarray -t whitelist < "$ROOT_DIR/vk/whitelist"
|
||||
|
||||
readarray items < <(yq -o=j -I=0 '.items[]' $evoFilePath )
|
||||
for item in "${items[@]}"; do
|
||||
evoTitle=`echo $item | yq .name`
|
||||
evoGroupId=`echo $item | yq .parent_id`
|
||||
evoGroupName=`evoGroupId="$evoGroupId" yq -r '.items[] | select(.id==strenv(evoGroupId)) | .name' $evoGroupsFilePath`
|
||||
# Check if group is whitelisted
|
||||
if [[ ! " ${whitelist[@]} " =~ " ${evoGroupName} " ]]; then
|
||||
continue
|
||||
fi
|
||||
vkAlbumId=`albumName="$evoGroupName" yq '.response.items[] | select(.title==strenv(albumName)) | .id' $vkAlbumFilePath`
|
||||
name=`echo $item | yq .name | xargs`
|
||||
|
||||
# Replace semicolons with commas for VK API compatibility
|
||||
name_for_vk="${name//;/,}"
|
||||
|
||||
measure=`echo $item | yq .measure_name`
|
||||
|
||||
# Calculate price based on measure type
|
||||
base_price=$(echo $item | yq .price)
|
||||
if is_weight_measure "$measure"; then
|
||||
# Apply multiplier for weight measures
|
||||
price=$((base_price * WEIGHT_PRICE_MULTIPLIER))
|
||||
price_info="${WEIGHT_PRICE_MULTIPLIER}$measure"
|
||||
else
|
||||
# No multiplier for non-weight measures
|
||||
price=$base_price
|
||||
price_info="$measure"
|
||||
fi
|
||||
|
||||
quantity=`echo $item | yq .quantity`
|
||||
allow_to_sell=`echo $item | yq .allow_to_sell`
|
||||
|
||||
# Set stock amount based on allow_to_sell flag
|
||||
if [[ "$allow_to_sell" = "true" ]]; then
|
||||
stock_amount=$VK_STOCK_AMOUNT
|
||||
else
|
||||
stock_amount=0
|
||||
fi
|
||||
|
||||
desc="$name (цена за ${price_info}.)
|
||||
|
||||
"
|
||||
description=$(echo $item | yq '.description // ""')
|
||||
if [[ -n "$description" ]]; then
|
||||
desc+="$description"
|
||||
fi
|
||||
article=`echo $item | yq .article_number`
|
||||
found=0
|
||||
readarray vkItems < <(yq -o=j -I=0 '.response.items[]' $vkFilePath )
|
||||
for vkItem in "${vkItems[@]}"; do
|
||||
vkTitle=`echo $vkItem | yq .title`
|
||||
vkTitleTrimmed="$(echo "$vkTitle" | xargs)"
|
||||
evoTitleTrimmed="$(echo "$evoTitle" | xargs)"
|
||||
|
||||
# For comparison, transform EVO title the same way (replace ; with ,)
|
||||
evoTitleForVk="${evoTitleTrimmed//;/,}"
|
||||
|
||||
if [[ "$vkTitleTrimmed" = "$evoTitleForVk" ]]; then
|
||||
found=1
|
||||
originalName=$(echo "$vkItem" | yq .title | xargs)
|
||||
originalDesc=$(echo "$vkItem" | yq .description | xargs)
|
||||
originalPrice=$(echo "$vkItem" | yq .price.amount)
|
||||
originalPrice=$((originalPrice / 100))
|
||||
originalArticle=$(echo "$vkItem" | yq .sku | xargs)
|
||||
originalStockAmount=$(echo "$vkItem" | yq .stock_amount)
|
||||
|
||||
# Debug output to log file
|
||||
echo "$(timestamp) [DEBUG] name='$name' originalName='$originalName'" >> "$LOG_FILE"
|
||||
echo "$(timestamp) [DEBUG] desc='$desc' originalDesc='$originalDesc'" >> "$LOG_FILE"
|
||||
echo "$(timestamp) [DEBUG] price='$price' originalPrice='$originalPrice'" >> "$LOG_FILE"
|
||||
echo "$(timestamp) [DEBUG] stock_amount='$stock_amount' allow_to_sell='$allow_to_sell'" >> "$LOG_FILE"
|
||||
|
||||
# Normalize descriptions more carefully to maintain proper spacing
|
||||
# Replace newlines with spaces first, then normalize spaces and trim
|
||||
normalized_desc=$(echo "$desc" | sed 's/\n/ /g' | tr -s ' ' | xargs)
|
||||
normalized_orig_desc=$(echo "$originalDesc" | sed 's/\n/ /g' | tr -s ' ' | xargs)
|
||||
|
||||
# Apply semicolon-to-comma transformation for consistent comparison
|
||||
normalized_desc="${normalized_desc//;/,}"
|
||||
normalized_orig_desc="${normalized_orig_desc//;/,}"
|
||||
|
||||
# Check for changes
|
||||
if [[ "$(echo "$name_for_vk" | xargs)" != "$originalName" ]]; then
|
||||
name_changed="true"
|
||||
else
|
||||
name_changed="false"
|
||||
fi
|
||||
|
||||
if [[ "$price" != "$originalPrice" ]]; then
|
||||
price_changed="true"
|
||||
else
|
||||
price_changed="false"
|
||||
fi
|
||||
|
||||
if [[ "$normalized_desc" != "$normalized_orig_desc" ]]; then
|
||||
desc_changed="true"
|
||||
else
|
||||
desc_changed="false"
|
||||
fi
|
||||
|
||||
if [[ "$stock_amount" != "$originalStockAmount" ]]; then
|
||||
stock_changed="true"
|
||||
else
|
||||
stock_changed="false"
|
||||
fi
|
||||
|
||||
# Check if update is needed
|
||||
if [[ "$name_changed" == "true" || "$price_changed" == "true" || "$desc_changed" == "true" || "$stock_changed" == "true" ]]; then
|
||||
productId=$(echo "$vkItem" | yq .id)
|
||||
echo "$(timestamp) [REQUEST] Updating product: $name_for_vk (name_changed=$name_changed, price_changed=$price_changed, desc_changed=$desc_changed, stock_changed=$stock_changed)" >> "$LOG_FILE"
|
||||
response=$(curl -s -w "%{http_code}" -H "Authorization: Bearer $VK_API_USER_TOKEN" \
|
||||
-F owner_id="$VK_API_PARAM_OWNER_ID" \
|
||||
-F item_id=$productId \
|
||||
-F name="$name_for_vk" \
|
||||
-F description="$normalized_desc" \
|
||||
-F category_id=$VK_API_CATEGORY_ID \
|
||||
-F price="$price" \
|
||||
-F stock_amount="$stock_amount" \
|
||||
$VK_API_EDIT_PRODUCT)
|
||||
http_code=${response: -3}
|
||||
response_body=${response:0:-3}
|
||||
echo "$(timestamp) [RESPONSE] code=$http_code body=$response_body" >> "$LOG_FILE"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [[ "$found" = 0 ]] && [[ "$allow_to_sell" = "true" ]] ; then
|
||||
photoId=$(uploadPhoto)
|
||||
|
||||
# Check if photo upload was successful
|
||||
if [[ -z "$photoId" || "$photoId" == "null" ]]; then
|
||||
echo "$(timestamp) [ERROR] Failed to get valid photo ID, skipping product: $name" >> "$LOG_FILE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$(timestamp) [REQUEST] Creating product: $name_for_vk" >> "$LOG_FILE"
|
||||
response=$(curl -s -w "%{http_code}" -H "Authorization: Bearer $VK_API_USER_TOKEN" \
|
||||
-F owner_id=$VK_API_PARAM_OWNER_ID \
|
||||
-F name="$name_for_vk" \
|
||||
-F description="$desc" \
|
||||
-F category_id=$VK_API_CATEGORY_ID \
|
||||
-F price=$price \
|
||||
-F main_photo_id=$photoId \
|
||||
-F stock_amount="$stock_amount" \
|
||||
$VK_API_ADD_PRODUCT)
|
||||
http_code=${response: -3}
|
||||
response_body=${response:0:-3}
|
||||
echo "$(timestamp) [RESPONSE] code=$http_code body=$response_body" >> "$LOG_FILE"
|
||||
|
||||
# Check if product creation was successful
|
||||
if [[ "$http_code" != "200" ]] || [[ $(echo $response_body | grep -c "error") -gt 0 ]]; then
|
||||
echo "$(timestamp) [ERROR] Failed to create product: $name" >> "$LOG_FILE"
|
||||
continue
|
||||
fi
|
||||
|
||||
productId=$(echo $response_body | yq .response.market_item_id)
|
||||
|
||||
# Only proceed if we got a valid product ID
|
||||
if [[ -n "$productId" && "$productId" != "null" ]]; then
|
||||
resp=$(curl -s -w "%{http_code}" -H "Authorization: Bearer $VK_API_USER_TOKEN" \
|
||||
"$VK_API_ADD_PRODUCT_TO_ALBUM&owner_id=$VK_API_PARAM_OWNER_ID&item_ids=$productId&album_ids=$vkAlbumId")
|
||||
http_code=${resp: -3}
|
||||
response_body=${resp:0:-3}
|
||||
echo "$(timestamp) [RESPONSE] code=$http_code body=$response_body" >> "$LOG_FILE"
|
||||
else
|
||||
echo "$(timestamp) [ERROR] Invalid product ID, skipping album addition for: $name" >> "$LOG_FILE"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Use the cleanup function instead of directly exiting
|
||||
cleanup 0
|
||||
10
vk/whitelist-old
Normal file
10
vk/whitelist-old
Normal file
@@ -0,0 +1,10 @@
|
||||
Белый
|
||||
Желтый
|
||||
Зеленый
|
||||
Красный
|
||||
Улуны светлые
|
||||
Улуны тёмные
|
||||
Чай с добавками
|
||||
Чёрный
|
||||
Шупуэр
|
||||
Шэнпуэр
|
||||
Reference in New Issue
Block a user