diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cece179 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/5393364294319597854.png b/5393364294319597854.png new file mode 100644 index 0000000..82ddaef Binary files /dev/null and b/5393364294319597854.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..806185d --- /dev/null +++ b/CHANGELOG.md @@ -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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..26b6a1f --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index 453b95d..23f40f6 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/cronjobs b/cronjobs new file mode 100644 index 0000000..9e01049 --- /dev/null +++ b/cronjobs @@ -0,0 +1 @@ +0 * * * * /var/www/run/run.sh diff --git a/evo/examples/products.json b/evo/examples/products.json new file mode 100644 index 0000000..ae868b5 --- /dev/null +++ b/evo/examples/products.json @@ -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 +} diff --git a/run/evo/clear.sh b/run/evo/clear.sh new file mode 100755 index 0000000..a446206 --- /dev/null +++ b/run/evo/clear.sh @@ -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 diff --git a/run/evo/constants.sh b/run/evo/constants.sh new file mode 100755 index 0000000..13580ec --- /dev/null +++ b/run/evo/constants.sh @@ -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" diff --git a/run/evo/functions.sh b/run/evo/functions.sh new file mode 100755 index 0000000..3f2e218 --- /dev/null +++ b/run/evo/functions.sh @@ -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" +} diff --git a/run/evo/get-groups.sh b/run/evo/get-groups.sh new file mode 100755 index 0000000..c53e9af --- /dev/null +++ b/run/evo/get-groups.sh @@ -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 diff --git a/run/evo/get-products.sh b/run/evo/get-products.sh new file mode 100755 index 0000000..532446b --- /dev/null +++ b/run/evo/get-products.sh @@ -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 diff --git a/run/evo/get-stores.sh b/run/evo/get-stores.sh new file mode 100755 index 0000000..c72d347 --- /dev/null +++ b/run/evo/get-stores.sh @@ -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 diff --git a/run/run.sh b/run/run.sh new file mode 100755 index 0000000..7678036 --- /dev/null +++ b/run/run.sh @@ -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 diff --git a/run/test.sh b/run/test.sh new file mode 100755 index 0000000..93a9663 --- /dev/null +++ b/run/test.sh @@ -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 diff --git a/run/vk/clear.sh b/run/vk/clear.sh new file mode 100755 index 0000000..0b73ce7 --- /dev/null +++ b/run/vk/clear.sh @@ -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 \ No newline at end of file diff --git a/run/vk/constants.sh b/run/vk/constants.sh new file mode 100755 index 0000000..9049cbe --- /dev/null +++ b/run/vk/constants.sh @@ -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" diff --git a/run/vk/delete-products.sh b/run/vk/delete-products.sh new file mode 100755 index 0000000..03ebf90 --- /dev/null +++ b/run/vk/delete-products.sh @@ -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 diff --git a/run/vk/functions.sh b/run/vk/functions.sh new file mode 100755 index 0000000..0189514 --- /dev/null +++ b/run/vk/functions.sh @@ -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 +} diff --git a/run/vk/get-albums.sh b/run/vk/get-albums.sh new file mode 100755 index 0000000..26ba1a5 --- /dev/null +++ b/run/vk/get-albums.sh @@ -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 diff --git a/run/vk/get-categories.sh b/run/vk/get-categories.sh new file mode 100755 index 0000000..e6917bf --- /dev/null +++ b/run/vk/get-categories.sh @@ -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 diff --git a/run/vk/get-products.sh b/run/vk/get-products.sh new file mode 100755 index 0000000..cf8dceb --- /dev/null +++ b/run/vk/get-products.sh @@ -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 diff --git a/run/vk/send-albums.sh b/run/vk/send-albums.sh new file mode 100755 index 0000000..7771284 --- /dev/null +++ b/run/vk/send-albums.sh @@ -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 diff --git a/run/vk/send-products.sh b/run/vk/send-products.sh new file mode 100755 index 0000000..f89ea56 --- /dev/null +++ b/run/vk/send-products.sh @@ -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 diff --git a/version b/version new file mode 100644 index 0000000..0a182f2 --- /dev/null +++ b/version @@ -0,0 +1 @@ +1.7.2 \ No newline at end of file diff --git a/vk/whitelist-old b/vk/whitelist-old new file mode 100644 index 0000000..f463ff2 --- /dev/null +++ b/vk/whitelist-old @@ -0,0 +1,10 @@ +Белый +Желтый +Зеленый +Красный +Улуны светлые +Улуны тёмные +Чай с добавками +Чёрный +Шупуэр +Шэнпуэр \ No newline at end of file