From 20b32a58562b655405c24076614af64818ac68b6 Mon Sep 17 00:00:00 2001 From: Samiya Date: Tue, 5 Mar 2024 17:12:36 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E4=BB=93=E5=BA=93=E6=89=98=E7=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 12 +++- Dockerfile-Alpine | 9 ++- docker-compose.yaml | 10 +-- mercari_buyer_bot.py | 108 ---------------------------- mercari_get_buyer.py | 95 +++++++++++++++++++++++++ start.sh | 166 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 281 insertions(+), 119 deletions(-) delete mode 100644 mercari_buyer_bot.py create mode 100644 mercari_get_buyer.py create mode 100644 start.sh diff --git a/Dockerfile b/Dockerfile index 473c054..f4ae830 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,12 @@ FROM python:3.10-slim AS runner COPY --from=builder /etc/localtime /etc/localtime COPY --from=builder /etc/timezone /etc/timezone +# 安装运行时依赖 +RUN apt-get update && \ + apt-get install -y --no-install-recommends jq curl && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + # 设置工作目录 WORKDIR /root/VintageVigil @@ -39,6 +45,9 @@ COPY --from=builder /root/VintageVigil/dependencies /usr/local/lib/python3.10/si # 从构建阶段复制项目文件 COPY --from=builder /root/VintageVigil . +# 确保启动脚本具有执行权限 +RUN chmod +x /root/VintageVigil/start.sh + # 设置容器启动时执行的命令(根据你的需要选择一个) # 自动监控 # CMD ["bash", "./run_checker.sh"] @@ -47,4 +56,5 @@ COPY --from=builder /root/VintageVigil . # CMD ["sh", "-c", "python ./main.py > /dev/null 2>&1"] # 在控制台输出日志 -CMD ["python", "./main.py"] \ No newline at end of file +# CMD ["python", "./main.py"] +CMD ["sh", "-c", "./start.sh"] \ No newline at end of file diff --git a/Dockerfile-Alpine b/Dockerfile-Alpine index 0a586c1..e7a0428 100644 --- a/Dockerfile-Alpine +++ b/Dockerfile-Alpine @@ -35,6 +35,9 @@ FROM python:3.10-alpine AS runner COPY --from=builder /etc/localtime /etc/localtime COPY --from=builder /etc/timezone /etc/timezone +# 安装运行时依赖 +RUN apk add --no-cache jq curl + # 设置工作目录 WORKDIR /root/VintageVigil @@ -44,5 +47,9 @@ COPY --from=builder /root/VintageVigil/dependencies /usr/local/lib/python3.10/si # 从构建阶段复制项目文件 COPY --from=builder /root/VintageVigil . +# 确保启动脚本具有执行权限 +RUN chmod +x /root/VintageVigil/start.sh + # 设置容器启动时执行的命令 -CMD ["python", "./main.py"] \ No newline at end of file +# CMD ["python", "./main.py"] +CMD ["sh", "-c", "./start.sh"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 5ffca95..66a548e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -12,8 +12,6 @@ services: stdin_open: true tty: true command: /data -A - networks: - - vintagevigil # 爬虫 VintageVigil: image: samiya777/vintagevigil:latest @@ -26,10 +24,4 @@ services: max-file: "3" volumes: - /root/VintageUser/.env:/root/VintageVigil/.env # 环境变量 - - /root/VintageUser/lx:/root/VintageVigil/user/lx # 配置文件 - networks: - - vintagevigil - -networks: - vintagevigil: - driver: bridge \ No newline at end of file + - /root/VintageUser/lx:/root/VintageVigil/user/lx # 配置文件 \ No newline at end of file diff --git a/mercari_buyer_bot.py b/mercari_buyer_bot.py deleted file mode 100644 index bec1ff3..0000000 --- a/mercari_buyer_bot.py +++ /dev/null @@ -1,108 +0,0 @@ -import re -import telebot -import requests -from uuid import uuid4, UUID -import ecdsa -from datetime import datetime - -import random -from time import time -from typing import Dict, Optional - -from ecdsa import SigningKey -from jose import jws -from jose.backends.ecdsa_backend import ECDSAECKey -from jose.constants import ALGORITHMS - -API_TOKEN = "5652509870:AAG93c-5qeSeBN-hYUPy0cWywX5RvAXm6cQ" -bot = telebot.TeleBot(API_TOKEN) - - -def generate_dpop( - url: str, - method: str, - key: SigningKey, - extra_payload: Optional[Dict[str, str]] = None, -) -> str: - payload = { - "iat": int(time()), - "jti": str(UUID(int=random.getrandbits(128))), - "htu": url, - "htm": method, - **extra_payload, - } - - ec_key = ECDSAECKey(key, ALGORITHMS.ES256) - headers = { - "typ": "dpop+jwt", - "alg": "ES256", - "jwk": {k: ec_key.to_dict()[k] for k in ["crv", "kty", "x", "y"]}, - } - - return jws.sign(payload, key, headers, ALGORITHMS.ES256) - - -def create_headers_dpop(method, url): - dpop = generate_dpop( - url=url, - method=method, - key=ecdsa.SigningKey.generate(curve=ecdsa.NIST256p), - extra_payload={"uuid": str(uuid4())}, - ) - return dpop - - -def create_headers(method, url): - headers = { - "DPoP": create_headers_dpop(method, url), - "X-Platform": "web", - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate", - "Content-Type": "application/json; charset=utf-8", - "User-Agent": "python-mercari", - } - return headers - - -def extract_item_id(text): - match = re.match(r"m\d+|https://jp.mercari.com/item/(m\d+)", text) - if match: - return match.group(1) if match.group(1) else match.group(0) - return None - - -def timestamp_to_datetime(timestamp): - # 将时间戳转换为 datetime 对象 - return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") - - -@bot.message_handler(func=lambda message: True) -def process_message(message): - item_id = extract_item_id(message.text) - if item_id: - url = f"https://api.mercari.jp/items/get?id={item_id}" - headers = create_headers("GET", url) - response = requests.get(url, headers=headers) - if response.status_code == 200: - data = response.json() - buyer_id = data.get("data", {}).get("buyer", {}).get("id") - if buyer_id: - profile_url = f"https://jp.mercari.com/user/profile/{buyer_id}" - # 获取并转换时间戳 - updated = timestamp_to_datetime(data.get("data", {}).get("updated")) - created = timestamp_to_datetime(data.get("data", {}).get("created")) - reply_message = ( - f"买家主页: {profile_url}\n更新时间: {updated}\n创建时间: {created}" - ) - bot.reply_to(message, reply_message) - return - bot.reply_to( - message, - "Failed to retrieve item information. Please check the item ID or try again later.", - ) - else: - bot.reply_to(message, "Please send a valid Mercari item ID or link.") - - -if __name__ == "__main__": - bot.polling(none_stop=True) diff --git a/mercari_get_buyer.py b/mercari_get_buyer.py new file mode 100644 index 0000000..e91c10e --- /dev/null +++ b/mercari_get_buyer.py @@ -0,0 +1,95 @@ +import os +import re +import requests +from uuid import uuid4 +import ecdsa +from datetime import datetime +from jose import jws +from jose.constants import ALGORITHMS +from telebot import TeleBot +from typing import Optional, Dict + +# Use an environment variable for the API token +TG_TOKEN = "" +API_TOKEN = os.getenv("MERCARI_BOT_API_TOKEN") | TG_TOKEN +bot = TeleBot(API_TOKEN) + +# Constants +MERCARI_API_URL = "https://api.mercari.jp/items/get?id={}" +USER_PROFILE_URL = "https://jp.mercari.com/user/profile/{}" +ALGORITHM = ALGORITHMS.ES256 + + +def generate_dpop( + url: str, + method: str, + key: ecdsa.SigningKey, + extra_payload: Optional[Dict[str, str]] = None, +) -> str: + payload = { + "iat": int(datetime.now().timestamp()), + "jti": str(uuid4()), + "htu": url, + "htm": method, + **(extra_payload or {}), + } + + headers = { + "typ": "dpop+jwt", + "alg": ALGORITHM, + "jwk": key.get_verifying_key().to_jwk(), + } + + return jws.sign(payload, key, headers, ALGORITHM) + + +def create_headers(method: str, url: str) -> Dict[str, str]: + key = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p) + dpop_token = generate_dpop(url, method, key, extra_payload={"uuid": str(uuid4())}) + return { + "DPoP": dpop_token, + "X-Platform": "web", + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Content-Type": "application/json; charset=utf-8", + "User-Agent": "python-mercari", + } + + +def extract_item_id(text: str) -> Optional[str]: + match = re.search(r"(?<=item/)(m\d+)", text) + return match.group(1) if match else None + + +def timestamp_to_datetime(timestamp: int) -> str: + return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") + + +@bot.message_handler(func=lambda message: True) +def process_message(message): + item_id = extract_item_id(message.text) + if item_id: + try: + url = MERCARI_API_URL.format(item_id) + headers = create_headers("GET", url) + response = requests.get(url, headers=headers) + response.raise_for_status() # This will raise an exception for HTTP errors + data = response.json() + buyer_id = data.get("data", {}).get("buyer", {}).get("id") + if buyer_id: + profile_url = USER_PROFILE_URL.format(buyer_id) + updated = timestamp_to_datetime(data.get("data", {}).get("updated")) + created = timestamp_to_datetime(data.get("data", {}).get("created")) + reply_message = f"Buyer profile: {profile_url}\nUpdated at: {updated}\nCreated at: {created}" + bot.reply_to(message, reply_message) + except requests.RequestException as e: + bot.reply_to( + message, + "Failed to retrieve item information. Please check the item ID or try again later.", + ) + else: + bot.reply_to(message, "Please send a valid Mercari item ID or link.") + + +if __name__ == "__main__": + bot.polling(none_stop=True) diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..7c3433b --- /dev/null +++ b/start.sh @@ -0,0 +1,166 @@ +#!/bin/sh + +# 指定目标目录 +TARGET_DIR="/root/VintageVigil/user/user" + +# Base64编码的GitHub Tokens数组 +TOKENS_B64='WyJnaHBfYnBxYVVzbHFSWFJKZHZwVEpQRHlJcW5mYkZlRjJCM1BYcHdaIiwiZ2hwXzhGclJWSGRFSVdvYUpjNHpoVkFCb1prTUF2ZHZ2RjNOa1NjQyIsImdocF9kMmpZTFFGMkJTVmlKWGRWdlRoaXFHbFBaa1cxQW4wejNFOFYiLCJnaHBfZlltUk1nY0w4bGtKb3Q2aXlTT3A5NzBPNmV2U0JFMmdZRFplIiwiZ2hwX0VTTm9XTGtxUVFHVWpnQ1lVY3FiYkV0bTltbkx3UDJWMGhCMyJd' + +# 从环境变量读取监控间隔时间,默认为60秒 +CHECK_INTERVAL=${CHECK_INTERVAL:-60} + +# 从环境变量读取是否使用Authorization头,默认为True +USE_TOKEN=${USE_TOKEN:-True} + +# 解码Token数组 +get_tokens() { + echo "$TOKENS_B64" | base64 -d | jq -r '.[]' +} + +# 解析仓库信息和分支名称 +parse_config() { + REPO_OWNER=$(echo $CONFIG_PATH | sed -E 's|https://github.com/([^/]*)/.*|\1|') + REPO_NAME=$(echo $CONFIG_PATH | sed -E 's|https://github.com/[^/]*/([^/]*)/.*|\1|') + BRANCH=$(echo $CONFIG_PATH | sed -E 's|https://github.com/[^/]*/[^/]*/tree/([^/]*)/.*|\1|') + FOLDER_PATH=$(echo $CONFIG_PATH | sed -E 's|https://github.com/.*/tree/[^/]*/(.*)|\1|') +} + +# 使用多个Token尝试请求直到成功或尝试完所有Token +try_request_with_tokens() { + local url=$1 + local response_file="response.json" + for token in $(get_tokens); do + if [ "$USE_TOKEN" = "True" ]; then + response_code=$(curl -s -H "Authorization: Bearer $token" -o $response_file -w "%{http_code}" "$url") + else + response_code=$(curl -s -o $response_file -w "%{http_code}" "$url") + fi + echo "Trying token, HTTP status code: $response_code" + if [ "$response_code" -eq 200 ]; then + echo "Successful response with token: $token" + cat $response_file + return 0 + fi + done + echo "Failed to get a successful response." + return 1 +} + +# 递归下载目录内容 +download_folder_contents() { + local repo_owner=$1 + local repo_name=$2 + local branch=$3 + local folder_path=$4 + local target_dir=$5 + + echo "Downloading contents of $folder_path from $repo_owner/$repo_name at branch $branch to $target_dir" + + local files_url="https://api.github.com/repos/$repo_owner/$repo_name/contents/$folder_path?ref=$branch" + local files_response=$(try_request_with_tokens "$files_url") + if [ $? -ne 0 ]; then + echo "Failed to retrieve files list." + return + fi + local items=$(echo "$files_response" | jq -r '.[] | @base64') + + for item in $items; do + _jq() { + echo ${item} | base64 --decode | jq -r ${1} + } + item_type=$(_jq '.type') + item_path=$(_jq '.path') + item_download_url=$(_jq '.download_url') + file_target_path="$target_dir/${item_path#$FOLDER_PATH/}" + if [ "$item_type" = "dir" ]; then + mkdir -p "$file_target_path" + download_folder_contents "$repo_owner" "$repo_name" "$branch" "$item_path" "$target_dir" + else + echo "Downloading $item_download_url to $file_target_path" + curl -s -L "$item_download_url" -o "$file_target_path" + fi + done +} + +initialize_or_update_repo() { + # 检查CONFIG_PATH是否已设置 + if [ -z "$CONFIG_PATH" ]; then + echo "CONFIG_PATH未设置。仅运行python /root/VintageVigil/main.py。" + cd /root/VintageVigil + python main.py & + MAIN_PY_PID=$! + echo "python /root/VintageVigil/main.py 的 PID 为 $MAIN_PY_PID" + wait $MAIN_PY_PID + exit 0 + fi + + parse_config + + mkdir -p "$TARGET_DIR" + local repo_tree_url="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/git/trees/$BRANCH?recursive=1" + local repo_tree_response=$(try_request_with_tokens "$repo_tree_url") + if [ $? -ne 0 ]; then + echo "Failed to retrieve repository tree." + return + fi + local new_folder_hash=$(echo "$repo_tree_response" | jq -r ".tree[] | select(.path==\"$FOLDER_PATH\") | .sha") + + if [ -z "$new_folder_hash" ]; then + echo "Failed to retrieve FOLDER_HASH." + return + fi + + echo "Current folder hash: $LAST_FOLDER_HASH, New folder hash: $new_folder_hash" + + if [ "$new_folder_hash" != "$LAST_FOLDER_HASH" ]; then + echo "Detected changes in $CONFIG_PATH. Updating files..." + LAST_FOLDER_HASH="$new_folder_hash" + + # Terminate the current running main.py process + if [ ! -z "$MAIN_PY_PID" ]; then + kill $MAIN_PY_PID + while kill -0 $MAIN_PY_PID 2>/dev/null; do + echo "Waiting for process $MAIN_PY_PID to terminate" + sleep 1 + done + fi + # 确保目录存在,防止rm命令因路径不存在而失败 + if [ -d "$TARGET_DIR" ]; then + # 删除目录下所有内容 + rm -rf "$TARGET_DIR"/* + + # 等待直到$TARGET_DIR为空 + while [ "$(ls -A $TARGET_DIR)" ]; do + echo "Waiting for $TARGET_DIR to be fully cleared" + sleep 1 + done + fi + echo "All files have been deleted." + + download_folder_contents "$REPO_OWNER" "$REPO_NAME" "$BRANCH" "$FOLDER_PATH" "$TARGET_DIR" + echo "All files have been downloaded." + + cd /root/VintageVigil + if [ -f "main.py" ]; then + python main.py & + MAIN_PY_PID=$! + echo "Restarted python /root/VintageVigil/main.py with PID $MAIN_PY_PID" + else + echo "main.py does not exist in /root/VintageVigil" + fi + else + echo "No changes detected." + fi +} + +# 初始哈希值设置为一个空值,以便首次执行时触发更新 +LAST_FOLDER_HASH="" + +# 初始设置或更新 +initialize_or_update_repo + +# 间歇性监控循环 +while true; do + sleep "$CHECK_INTERVAL" + initialize_or_update_repo +done