From bbcd520fbb7006403e64ac4a6fbea7a9ca50237f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:26:53 +0000 Subject: [PATCH] Security audit: remove eval() calls, fix hardcoded secret, add VPS guide - Replace all unsafe eval() calls with safe _SAFE_TYPE_MAP in settings.py, console.py, gui_utils.py - Replace hardcoded Flask secret key with os.urandom(24) in GUI.py - Replace os.system() with subprocess.run() in TTS/engine_wrapper.py - Fix install.sh Homebrew URL (was uninstall.sh instead of install.sh) - Create HUONG_DAN_VPS.md - comprehensive VPS deployment guide in Vietnamese Agent-Logs-Url: https://github.com/thaitien280401-stack/RedditVideoMakerBot/sessions/75dc3e66-3e99-4406-af07-9de9be3200b6 Co-authored-by: thaitien280401-stack <271128961+thaitien280401-stack@users.noreply.github.com> --- GUI.py | 5 +- HUONG_DAN_VPS.md | 358 ++++++++++++++++++++++++++++++++++++++++++ TTS/engine_wrapper.py | 17 +- install.sh | 2 +- utils/console.py | 2 +- utils/gui_utils.py | 19 ++- utils/settings.py | 38 ++++- 7 files changed, 428 insertions(+), 13 deletions(-) create mode 100644 HUONG_DAN_VPS.md diff --git a/GUI.py b/GUI.py index 4588083..a1d4d0c 100644 --- a/GUI.py +++ b/GUI.py @@ -1,3 +1,4 @@ +import os import webbrowser from pathlib import Path @@ -22,8 +23,8 @@ PORT = 4000 # Configure application app = Flask(__name__, template_folder="GUI") -# Configure secret key only to use 'flash' -app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' +# Configure secret key from environment variable or generate a random one +app.secret_key = os.environ.get("FLASK_SECRET_KEY", os.urandom(24)) # Ensure responses aren't cached diff --git a/HUONG_DAN_VPS.md b/HUONG_DAN_VPS.md new file mode 100644 index 0000000..0c8d90c --- /dev/null +++ b/HUONG_DAN_VPS.md @@ -0,0 +1,358 @@ +# 🇻🇳 HƯỚNG DẪN CÀI ĐẶT VÀ CHẠY TRÊN VPS + +## Mục lục + +1. [Yêu cầu hệ thống](#1-yêu-cầu-hệ-thống) +2. [Cài đặt trên VPS](#2-cài-đặt-trên-vps) +3. [Cấu hình bắt buộc](#3-cấu-hình-bắt-buộc-configtoml) +4. [Các chế độ chạy](#4-các-chế-độ-chạy) +5. [Chạy nền trên VPS](#5-chạy-nền-trên-vps-với-systemd) +6. [Chạy với Docker](#6-chạy-với-docker-tùy-chọn) +7. [Kiểm tra và khắc phục lỗi](#7-kiểm-tra-và-khắc-phục-lỗi) +8. [Bảng tóm tắt cấu hình](#8-bảng-tóm-tắt-cấu-hình-cần-cập-nhật) + +--- + +## 1. Yêu cầu hệ thống + +| Thành phần | Yêu cầu tối thiểu | +|---|---| +| **OS** | Ubuntu 20.04+ / Debian 11+ | +| **RAM** | 2 GB (khuyến nghị 4 GB) | +| **Disk** | 10 GB trống | +| **Python** | 3.10, 3.11 hoặc 3.12 | +| **FFmpeg** | Bắt buộc (cài tự động nếu thiếu) | + +--- + +## 2. Cài đặt trên VPS + +### Bước 1: Cập nhật hệ thống và cài đặt phụ thuộc + +```bash +sudo apt update && sudo apt upgrade -y +sudo apt install -y python3 python3-pip python3-venv ffmpeg git +``` + +### Bước 2: Clone dự án + +```bash +cd /opt +git clone https://github.com/thaitien280401-stack/RedditVideoMakerBot.git +cd RedditVideoMakerBot +``` + +### Bước 3: Tạo virtual environment + +```bash +python3 -m venv venv +source venv/bin/activate +``` + +### Bước 4: Cài đặt thư viện + +```bash +pip install -r requirements.txt +``` + +### Bước 5: Cài đặt Playwright browser (cần cho chế độ screenshot) + +```bash +python -m playwright install +python -m playwright install-deps +``` + +--- + +## 3. Cấu hình bắt buộc (`config.toml`) + +Khi chạy lần đầu, chương trình sẽ tự tạo file `config.toml` và hỏi bạn nhập thông tin. +Bạn cũng có thể tạo trước file `config.toml` trong thư mục gốc dự án: + +```toml +# ===== CẤU HÌNH BẮT BUỘC ===== + +[threads.creds] +access_token = "YOUR_THREADS_ACCESS_TOKEN" # Lấy từ Meta Developer Portal +user_id = "YOUR_THREADS_USER_ID" # Threads User ID + +[threads.thread] +target_user_id = "" # Để trống = dùng user của bạn +post_id = "" # Để trống = tự động chọn thread mới nhất +keywords = "viral, trending, hài hước" # Từ khóa lọc (tùy chọn) +max_comment_length = 500 +min_comment_length = 1 +post_lang = "vi" +min_comments = 5 +blocked_words = "spam, quảng cáo" +channel_name = "Threads Vietnam" + +[settings] +allow_nsfw = false +theme = "dark" +times_to_run = 1 +opacity = 0.9 +resolution_w = 1080 +resolution_h = 1920 + +[settings.background] +background_video = "minecraft" +background_audio = "lofi" +background_audio_volume = 0.15 + +[settings.tts] +voice_choice = "googletranslate" # Tốt nhất cho tiếng Việt +silence_duration = 0.3 +no_emojis = false + +# ===== SCHEDULER (lên lịch tự động) ===== + +[scheduler] +enabled = true # BẬT lên lịch tự động +cron = "0 */3 * * *" # Mỗi 3 giờ tạo 1 video +timezone = "Asia/Ho_Chi_Minh" # Múi giờ Việt Nam +max_videos_per_day = 8 # Tối đa 8 video/ngày + +# ===== UPLOAD TỰ ĐỘNG (tùy chọn) ===== + +[uploaders.youtube] +enabled = false +client_id = "" +client_secret = "" +refresh_token = "" + +[uploaders.tiktok] +enabled = false +client_key = "" +client_secret = "" +refresh_token = "" + +[uploaders.facebook] +enabled = false +page_id = "" +access_token = "" +``` + +### Cách lấy Threads API credentials + +1. Truy cập [Meta Developer Portal](https://developers.facebook.com/) +2. Tạo App mới → chọn "Business" type +3. Thêm product "Threads API" +4. Vào Settings → Basic → lấy **App ID** +5. Tạo Access Token cho Threads API +6. Lấy **User ID** từ Threads API endpoint: `GET /me?fields=id,username` + +--- + +## 4. Các chế độ chạy + +### 4.1. Manual (thủ công) — Mặc định +Tạo video 1 lần, không upload: +```bash +python main.py +``` + +### 4.2. Auto (tạo + upload) +Tạo video và tự động upload lên các platform đã cấu hình: +```bash +python main.py --mode auto +``` + +### 4.3. ⭐ Scheduled (lên lịch tự động) — KHUYẾN NGHỊ CHO VPS +Chạy liên tục trên VPS, tự động tạo video theo lịch trình: +```bash +python main.py --mode scheduled +``` + +**Mặc định:** +- Cron: `0 */3 * * *` → Tạo 1 video **mỗi 3 giờ** +- Lịch chạy: 00:00, 03:00, 06:00, 09:00, 12:00, 15:00, 18:00, 21:00 (giờ VN) +- **= 8 video/ngày** +- Timezone: `Asia/Ho_Chi_Minh` +- Tự động bỏ qua các chủ đề đã tạo video (title deduplication) +- Giới hạn tối đa `max_videos_per_day` video mỗi ngày + +### Tùy chỉnh lịch chạy + +Thay đổi `cron` trong `config.toml`: + +| Cron Expression | Mô tả | Video/ngày | +|---|---|---| +| `0 */3 * * *` | Mỗi 3 giờ (mặc định) | 8 | +| `0 */4 * * *` | Mỗi 4 giờ | 6 | +| `0 */6 * * *` | Mỗi 6 giờ | 4 | +| `0 8,14,20 * * *` | Lúc 8h, 14h, 20h | 3 | +| `0 */2 * * *` | Mỗi 2 giờ | 12 | + +--- + +## 5. Chạy nền trên VPS với systemd + +### Bước 1: Tạo systemd service + +```bash +sudo nano /etc/systemd/system/threads-video-bot.service +``` + +Dán nội dung sau: + +```ini +[Unit] +Description=Threads Video Maker Bot +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/RedditVideoMakerBot +ExecStart=/opt/RedditVideoMakerBot/venv/bin/python main.py --mode scheduled +Restart=always +RestartSec=30 +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target +``` + +### Bước 2: Kích hoạt và khởi động + +```bash +sudo systemctl daemon-reload +sudo systemctl enable threads-video-bot +sudo systemctl start threads-video-bot +``` + +### Bước 3: Kiểm tra trạng thái + +```bash +# Xem trạng thái +sudo systemctl status threads-video-bot + +# Xem log realtime +sudo journalctl -u threads-video-bot -f + +# Xem log gần nhất +sudo journalctl -u threads-video-bot --since "1 hour ago" + +# Restart +sudo systemctl restart threads-video-bot + +# Dừng +sudo systemctl stop threads-video-bot +``` + +--- + +## 6. Chạy với Docker (tùy chọn) + +### Build image + +```bash +cd /opt/RedditVideoMakerBot +docker build -t threads-video-bot . +``` + +### Chạy container + +```bash +docker run -d \ + --name threads-bot \ + --restart unless-stopped \ + -v $(pwd)/config.toml:/app/config.toml \ + -v $(pwd)/results:/app/results \ + -v $(pwd)/video_creation/data:/app/video_creation/data \ + threads-video-bot python3 main.py --mode scheduled +``` + +### Xem log + +```bash +docker logs -f threads-bot +``` + +--- + +## 7. Kiểm tra và khắc phục lỗi + +### Kiểm tra trạng thái + +```bash +# Service đang chạy? +sudo systemctl is-active threads-video-bot + +# Xem lỗi gần nhất +sudo journalctl -u threads-video-bot --since "30 min ago" --no-pager + +# Đếm video đã tạo +ls -la results/*/ +``` + +### Lỗi thường gặp + +| Lỗi | Nguyên nhân | Cách khắc phục | +|---|---|---| +| `ModuleNotFoundError` | Thiếu thư viện | `source venv/bin/activate && pip install -r requirements.txt` | +| `FileNotFoundError: ffmpeg` | Chưa cài FFmpeg | `sudo apt install ffmpeg` | +| `Threads API error 401` | Token hết hạn | Tạo access token mới từ Meta Developer Portal | +| `No suitable thread found` | Hết thread mới | Đợi có thread mới hoặc thay `target_user_id` | +| `playwright._impl._errors` | Thiếu browser | `python -m playwright install && python -m playwright install-deps` | +| `Đã đạt giới hạn X video/ngày` | Đã tạo đủ video | Bình thường, sẽ reset vào ngày hôm sau | + +### Lịch sử title (tránh trùng lặp) + +- File lưu: `video_creation/data/title_history.json` +- Xem title đã tạo: `cat video_creation/data/title_history.json | python -m json.tool` +- Reset (cho phép tạo lại tất cả): `echo "[]" > video_creation/data/title_history.json` + +--- + +## 8. Bảng tóm tắt cấu hình cần cập nhật + +### ⚠️ BẮT BUỘC phải thay đổi + +| Mục | Key trong config.toml | Mô tả | Cách lấy | +|---|---|---|---| +| **Threads Token** | `threads.creds.access_token` | Access token API | [Meta Developer Portal](https://developers.facebook.com/) | +| **Threads User ID** | `threads.creds.user_id` | User ID Threads | API endpoint `/me?fields=id` | + +### 📋 Nên tùy chỉnh + +| Mục | Key | Mặc định | Gợi ý | +|---|---|---|---| +| Tên kênh | `threads.thread.channel_name` | "Threads Vietnam" | Tên kênh của bạn | +| Từ khóa | `threads.thread.keywords` | "" | "viral, trending, hài hước" | +| Từ bị chặn | `threads.thread.blocked_words` | "" | "spam, quảng cáo, 18+" | +| Lịch chạy | `scheduler.cron` | `0 */3 * * *` | Xem bảng ở mục 4 | +| Max video/ngày | `scheduler.max_videos_per_day` | 8 | Tùy chỉnh | + +### 🔧 Tùy chọn: Upload tự động + +| Platform | Keys cần cấu hình | +|---|---| +| **YouTube** | `uploaders.youtube.client_id`, `client_secret`, `refresh_token` | +| **TikTok** | `uploaders.tiktok.client_key`, `client_secret`, `refresh_token` | +| **Facebook** | `uploaders.facebook.page_id`, `access_token` | + +--- + +## Tóm tắt nhanh + +```bash +# 1. Cài đặt +cd /opt/RedditVideoMakerBot +python3 -m venv venv && source venv/bin/activate +pip install -r requirements.txt +python -m playwright install && python -m playwright install-deps + +# 2. Cấu hình +nano config.toml # Nhập thông tin Threads API + +# 3. Test thử 1 video +python main.py + +# 4. Chạy tự động trên VPS (mỗi 3h = 8 video/ngày) +python main.py --mode scheduled + +# 5. Hoặc chạy nền với systemd (khuyến nghị) +sudo systemctl enable --now threads-video-bot +``` diff --git a/TTS/engine_wrapper.py b/TTS/engine_wrapper.py index fd0f9da..9020da9 100644 --- a/TTS/engine_wrapper.py +++ b/TTS/engine_wrapper.py @@ -1,5 +1,6 @@ import os import re +import subprocess from pathlib import Path from typing import Tuple @@ -127,12 +128,16 @@ class TTSEngine: split_files.append(str(f"{self.path}/{idx}-{idy}.part.mp3")) f.write("file " + f"'silence.mp3'" + "\n") - os.system( - "ffmpeg -f concat -y -hide_banner -loglevel panic -safe 0 " - + "-i " - + f"{self.path}/list.txt " - + "-c copy " - + f"{self.path}/{idx}.mp3" + subprocess.run( + [ + "ffmpeg", "-f", "concat", "-y", + "-hide_banner", "-loglevel", "panic", + "-safe", "0", + "-i", f"{self.path}/list.txt", + "-c", "copy", + f"{self.path}/{idx}.mp3", + ], + check=False, ) try: for i in range(0, len(split_files)): diff --git a/install.sh b/install.sh index 38c1708..08612c1 100644 --- a/install.sh +++ b/install.sh @@ -50,7 +50,7 @@ function install_macos(){ if [ ! command -v brew &> /dev/null ]; then echo "Installing Homebrew" # if it's is not installed, then install it in a NONINTERACTIVE way - NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)" + NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # Check for what arcitecture, so you can place path. if [[ "uname -m" == "x86_64" ]]; then echo "export PATH=/usr/local/bin:$PATH" >> ~/.bash_profile && source ~/.bash_profile diff --git a/utils/console.py b/utils/console.py index a9abf4b..f47a9b9 100644 --- a/utils/console.py +++ b/utils/console.py @@ -102,7 +102,7 @@ def handle_input( user_input = input("").strip() if check_type is not False: try: - isinstance(eval(user_input), check_type) # fixme: remove eval + check_type(user_input) return check_type(user_input) except: console.print( diff --git a/utils/gui_utils.py b/utils/gui_utils.py index 31c135f..1337c06 100644 --- a/utils/gui_utils.py +++ b/utils/gui_utils.py @@ -6,6 +6,14 @@ import toml import tomlkit from flask import flash +# Safe type mapping to replace eval() calls +_SAFE_TYPE_MAP = { + "int": int, + "float": float, + "str": str, + "bool": bool, +} + # Get validation checks from template def get_checks(): @@ -46,7 +54,16 @@ def check(value, checks): if not incorrect and "type" in checks: try: - value = eval(checks["type"])(value) # fixme remove eval + target_type = _SAFE_TYPE_MAP.get(checks["type"]) + if target_type is None: + incorrect = True + elif target_type is bool: + if isinstance(value, str): + value = value.lower() in ("true", "1", "yes") + else: + value = bool(value) + else: + value = target_type(value) except Exception: incorrect = True diff --git a/utils/settings.py b/utils/settings.py index 6b8242b..085a963 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -10,6 +10,40 @@ from utils.console import handle_input console = Console() config = dict # autocomplete +# Safe type mapping to replace eval() calls +_SAFE_TYPE_MAP = { + "int": int, + "float": float, + "str": str, + "bool": bool, + "False": False, +} + + +def _safe_type_cast(type_str, value): + """Safely cast value to type without eval(). + + Args: + type_str: String name of the type (e.g. 'int', 'float', 'str', 'bool'). + value: The value to cast. + + Returns: + The cast value. + + Raises: + ValueError: If the type string is not recognized. + """ + if type_str in _SAFE_TYPE_MAP: + target_type = _SAFE_TYPE_MAP[type_str] + if target_type is False: + return False + if target_type is bool: + if isinstance(value, str): + return value.lower() in ("true", "1", "yes") + return bool(value) + return target_type(value) + raise ValueError(f"Unknown type: {type_str}") + def crawl(obj: dict, func=lambda x, y: print(x, y, end="\n"), path=None): if path is None: # path Default argument value is mutable @@ -30,7 +64,7 @@ def check(value, checks, name): incorrect = True if not incorrect and "type" in checks: try: - value = eval(checks["type"])(value) # fixme remove eval + value = _safe_type_cast(checks["type"], value) except: incorrect = True @@ -78,7 +112,7 @@ def check(value, checks, name): + str(name) + "[#F7768E bold]=", extra_info=get_check_value("explanation", ""), - check_type=eval(get_check_value("type", "False")), # fixme remove eval + check_type=_SAFE_TYPE_MAP.get(get_check_value("type", "False"), False), default=get_check_value("default", NotImplemented), match=get_check_value("regex", ""), err_message=get_check_value("input_error", "Incorrect input"),