tienhiep-api / backend /api /books.py
Cong123779
deploy: update backend production to new Space
22f303a
Raw
History Blame Contribute Delete
30.4 kB
import os
import time
import unicodedata
from flask import Blueprint, request, jsonify, session
from backend.config import Config
from backend.core.decorators import get_current_user
from backend.core.security import verify_access_token, encrypt_asymmetric_hybrid, decrypt_asymmetric_hybrid
from backend.core.rate_limit import check_rate_limit, check_ip_rate_limit, get_client_ip
from backend.database.db_manager import get_db, get_mode_connection, get_user_db_conn
from backend.services.book_service import search_books, clean_vietnamese_query
from backend.services.translation import get_engine
books_bp = Blueprint("books", __name__, url_prefix="/api")
def is_vip_request():
"""Check if current request has VIP privileges."""
user = get_current_user()
if user and user.get("vip_status", 0) == 1:
return True
vip_code = request.headers.get("X-VIP-Code", "")
if vip_code in Config.VALID_VIP_CODES:
return True
return False
@books_bp.route("/books", methods=["GET"])
def api_books():
ip = get_client_ip()
if not check_ip_rate_limit(ip, max_requests=45, period=60):
return jsonify({"error": "Too many requests. Please slow down."}), 429
q = request.args.get("q", "").strip()
category = request.args.get("category", "").strip()
source = request.args.get("source", "").strip()
dup = request.args.get("dup", "").strip()
sort = request.args.get("sort", "site_count DESC").strip()
search_field = request.args.get("field", "all").strip()
min_chapters = request.args.get("min_chapters", "").strip()
try:
page = max(1, int(request.args.get("page", 1)))
per_page = max(1, min(50, int(request.args.get("per_page", 20))))
limit_k = max(1, min(1000, int(request.args.get("limit_k", 100))))
except ValueError:
page, per_page, limit_k = 1, 20, 100
result, status_code = search_books(q, category, source, dup, sort, search_field, min_chapters, page, per_page, limit_k)
if status_code != 200:
return jsonify(result), status_code
return jsonify(result)
@books_bp.route("/stats", methods=["GET"])
def api_stats():
from backend.database.db_manager import cached_stats
import backend.database.db_manager as db_mod
if db_mod.cached_stats is None:
conn = get_db()
total = conn.execute("SELECT COUNT(*) FROM books").fetchone()[0]
cats = conn.execute(
"SELECT categories FROM books WHERE categories IS NOT NULL AND categories != ''"
).fetchall()
cat_counts = {}
for row in cats:
for c in row[0].split(","):
c = c.strip()
if c:
cat_counts[c] = cat_counts.get(c, 0) + 1
top_categories = sorted(cat_counts.items(), key=lambda x: x[1], reverse=True)[:20]
db_mod.cached_stats = {
"total_books": total,
"top_categories": [{"name": k, "count": v} for k, v in top_categories],
}
return jsonify(db_mod.cached_stats)
@books_bp.route("/book/<int:book_id>", methods=["GET"])
def get_book_detail(book_id):
conn = get_db()
row = conn.execute("SELECT * FROM books WHERE id = ?", (book_id,)).fetchone()
if not row:
return jsonify({"error": "Không tìm thấy truyện."}), 404
book_dict = dict(row)
# Parse sources
parsed_sources = []
if book_dict.get("urls"):
parts = book_dict["urls"].split(" | ")
for p in parts:
idx = p.find(":")
if idx > 0:
parsed_sources.append({
"source": p[:idx].strip(),
"url": p[idx+1:].strip()
})
book_dict["parsed_sources"] = parsed_sources
# Lấy description_vietphrase từ advanced DB
description_vietphrase = None
root_dir = Config.ROOT_DIR
adv_db_path = os.path.join(root_dir, "merged_books_advanced.db")
if os.path.exists(adv_db_path):
try:
from backend.database.db_manager import get_mode_connection
adv_conn = get_mode_connection(adv_db_path)
if adv_conn:
r_adv = adv_conn.execute("SELECT description_vietphrase FROM books WHERE id = ?", (book_id,)).fetchone()
if r_adv and r_adv["description_vietphrase"]:
description_vietphrase = r_adv["description_vietphrase"]
except Exception as e:
logger.warning(f"Failed to read from advanced DB for book {book_id}: {e}")
# Nếu không có trong advanced DB, dịch trực tiếp bằng translation engine
if not description_vietphrase and book_dict.get("description"):
try:
from backend.services.translation import get_engine
eng = get_engine()
description_vietphrase = eng.translate(book_dict["description"], multi_option=False)
except Exception as e:
logger.warning(f"Failed to translate book description live: {e}")
description_vietphrase = book_dict.get("description")
book_dict["description_vietphrase"] = description_vietphrase or ""
# Dịch mô tả sang tiếng Anh bằng Gemini
description_english = None
if book_dict.get("description"):
# Tạo fallback không dấu trước
fallback_en = ""
if book_dict["description_vietphrase"]:
try:
from backend.services.book_service import remove_vietnamese_accents
fallback_en = remove_vietnamese_accents(book_dict["description_vietphrase"])
except Exception:
pass
# Thử gọi Gemini để dịch chất lượng cao
key = os.environ.get("ADMIN_GEMINI_KEY", "") or os.environ.get("GOOGLE_API_KEY_BIGDATA", "") or os.environ.get("GEMINI_API_KEY_BIGDATA", "")
if key:
try:
import requests as py_requests
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={key}"
prompt_text = book_dict["description"][:800]
payload = {
"contents": [{
"role": "user",
"parts": [{"text": f"Translate the following novel synopsis to English. Only return the English translation, no other commentary:\n\n{prompt_text}"}]
}]
}
res = py_requests.post(url, json=payload, timeout=5)
if res.status_code == 200:
ans = res.json().get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "")
if ans.strip():
description_english = ans.strip()
except Exception as e:
logger.warning(f"Failed to translate book description to English using Gemini: {e}")
if not description_english:
description_english = fallback_en or book_dict.get("description")
book_dict["description_english"] = description_english or ""
# Dịch thể loại sang tiếng Việt và tiếng Anh, cùng chuẩn hóa tên tác giả tiếng Anh
try:
from backend.services.book_service import translate_categories, translate_categories_to_english, remove_vietnamese_accents
book_dict["categories_vietphrase"] = translate_categories(book_dict.get("categories", ""))
book_dict["categories_english"] = translate_categories_to_english(book_dict.get("categories", ""))
author_hv = book_dict.get("author_hanviet", "")
if author_hv:
book_dict["author_english"] = remove_vietnamese_accents(author_hv)
else:
book_dict["author_english"] = book_dict.get("author", "")
except Exception:
book_dict["categories_vietphrase"] = book_dict.get("categories", "")
book_dict["categories_english"] = book_dict.get("categories", "")
book_dict["author_english"] = book_dict.get("author", "")
return jsonify(book_dict)
@books_bp.route("/book/<int:book_id>/translations")
def api_book_translations(book_id):
root_dir = Config.ROOT_DIR
adv_db_path = os.path.join(root_dir, "merged_books_advanced.db")
fast_db_path = os.path.join(root_dir, "merged_books_fast.db")
vp_db_path = os.path.join(root_dir, "merged_books_vietphrase.db")
hv_db_path = os.path.join(root_dir, "merged_books_hanviet.db")
res = {
"advanced": {"title": None, "desc": None},
"fast": {"title": None, "desc": None},
"vietphrase": {"title": None, "desc": None},
"hanviet": {"title": None, "desc": None}
}
for mode_key, db_path in [("advanced", adv_db_path), ("fast", fast_db_path),
("vietphrase", vp_db_path), ("hanviet", hv_db_path)]:
conn = get_mode_connection(db_path)
if conn:
try:
r = conn.execute("SELECT title_vietphrase, description_vietphrase FROM books WHERE id = ?", (book_id,)).fetchone()
if r:
res[mode_key] = {"title": r["title_vietphrase"], "desc": r["description_vietphrase"]}
except Exception as e:
print(f"[ERROR] Loading {mode_key} book details: {e}")
return jsonify(res)
@books_bp.route("/bookshelf", methods=["GET"])
def get_bookshelf():
user = get_current_user()
user_id = user["id"] if user else None
if not user_id:
return jsonify({"error": "Vui lòng đăng nhập."}), 401
q = request.args.get("q", "").strip()
conn = get_user_db_conn()
rows = conn.execute("SELECT * FROM bookshelf WHERE user_id = ? ORDER BY added_at DESC", (user_id,)).fetchall()
conn.close()
books = [dict(r) for r in rows]
# Enrich with main book details if book_id is present
try:
from backend.database.db_manager import get_db
from backend.services.book_service import parse_book_sources
main_conn = get_db()
for b in books:
if b.get("book_id"):
row = main_conn.execute("SELECT * FROM books WHERE id = ?", (b["book_id"],)).fetchone()
if row:
r_dict = parse_book_sources(dict(row))
for key, val in r_dict.items():
if key != "id":
b[key] = val
except Exception as e:
logger.warning(f"Failed to enrich bookshelf items: {e}")
if q:
q_clean = clean_vietnamese_query(q)
books = [b for b in books if q_clean in clean_vietnamese_query(b.get("title", ""))
or q_clean in clean_vietnamese_query(b.get("author", ""))]
return jsonify(books)
@books_bp.route("/bookshelf/add", methods=["POST"])
def add_bookshelf():
user = get_current_user()
user_id = user["id"] if user else None
if not user_id:
return jsonify({"error": "Vui lòng đăng nhập."}), 401
data = request.json or {}
book_id = data.get("book_id")
if not book_id:
return jsonify({"error": "Thiếu ID truyện."}), 400
if not is_vip_request():
conn_check = get_user_db_conn()
try:
exists = conn_check.execute("SELECT 1 FROM bookshelf WHERE user_id = ? AND book_id = ?", (user_id, book_id)).fetchone() is not None
if not exists:
count = conn_check.execute("SELECT COUNT(*) as cnt FROM bookshelf WHERE user_id = ?", (user_id,)).fetchone()["cnt"]
if count >= 5:
return jsonify({"error": "Tủ sách Standard tối đa 5 truyện. Nâng cấp VIP để lưu không giới hạn!"}), 403
finally:
conn_check.close()
main_conn = get_db()
book = main_conn.execute("SELECT title_vietphrase, author_hanviet, cover FROM books WHERE id = ?", (book_id,)).fetchone()
if not book:
return jsonify({"error": "Không tìm thấy truyện."}), 404
title = book["title_vietphrase"] or "Không rõ"
author = book["author_hanviet"] or "Không rõ"
cover = book["cover"] or ""
conn = get_user_db_conn()
try:
conn.execute(
"INSERT OR IGNORE INTO bookshelf (user_id, book_id, title, author, cover) VALUES (?, ?, ?, ?, ?)",
(user_id, book_id, title, author, cover)
)
conn.commit()
finally:
conn.close()
return jsonify({"success": True, "message": "Đã thêm vào tủ sách."})
@books_bp.route("/bookshelf/remove", methods=["POST"])
def remove_bookshelf():
user = get_current_user()
user_id = user["id"] if user else None
if not user_id:
return jsonify({"error": "Vui lòng đăng nhập."}), 401
data = request.json or {}
book_id = data.get("book_id")
url = data.get("url")
if not book_id and not url:
return jsonify({"error": "Thiếu thông tin truyện để xóa."}), 400
conn = get_user_db_conn()
if book_id:
conn.execute("DELETE FROM bookshelf WHERE user_id = ? AND book_id = ?", (user_id, book_id))
else:
conn.execute("DELETE FROM bookshelf WHERE user_id = ? AND url = ?", (user_id, url))
conn.commit()
conn.close()
return jsonify({"success": True, "message": "Đã xóa khỏi tủ sách."})
@books_bp.route("/history", methods=["GET"])
def get_history():
user = get_current_user()
user_id = user["id"] if user else None
if not user_id:
return jsonify({"error": "Vui lòng đăng nhập."}), 401
q = request.args.get("q", "").strip()
conn = get_user_db_conn()
rows = conn.execute("SELECT * FROM reading_history WHERE user_id = ? ORDER BY timestamp DESC", (user_id,)).fetchall()
conn.close()
books = [dict(r) for r in rows]
if q:
q_clean = clean_vietnamese_query(q)
books = [b for b in books if q_clean in clean_vietnamese_query(b.get("title", ""))
or q_clean in clean_vietnamese_query(b.get("author", ""))]
struct_now = time.localtime()
today_str = f"{struct_now.tm_year:04d}-{struct_now.tm_mon:02d}-{struct_now.tm_mday:02d}"
struct_y = time.localtime(time.time() - 86400)
yesterday_str = f"{struct_y.tm_year:04d}-{struct_y.tm_mon:02d}-{struct_y.tm_mday:02d}"
this_month_prefix = f"{struct_now.tm_year:04d}-{struct_now.tm_mon:02d}"
groups = {"Hôm nay": [], "Hôm qua": [], "Tháng này": [], "Trước đây": []}
for b in books:
r_date = b.get("read_date", "")
if r_date == today_str:
groups["Hôm nay"].append(b)
elif r_date == yesterday_str:
groups["Hôm qua"].append(b)
elif r_date.startswith(this_month_prefix):
groups["Tháng này"].append(b)
else:
groups["Trước đây"].append(b)
return jsonify([{"group_name": g, "books": groups[g]}
for g in ["Hôm nay", "Hôm qua", "Tháng này", "Trước đây"] if groups[g]])
@books_bp.route("/history/add", methods=["POST"])
def add_history():
user = get_current_user()
user_id = user["id"] if user else None
if not user_id:
return jsonify({"error": "Vui lòng đăng nhập."}), 401
data = request.json or {}
book_id = data.get("book_id")
last_chapter = data.get("last_chapter", "Chương đầu")
if not book_id:
return jsonify({"error": "Thiếu ID truyện."}), 400
main_conn = get_db()
book = main_conn.execute("SELECT title_vietphrase, author_hanviet, cover FROM books WHERE id = ?", (book_id,)).fetchone()
if not book:
return jsonify({"error": "Không tìm thấy truyện."}), 404
title = book["title_vietphrase"] or "Không rõ"
author = book["author_hanviet"] or "Không rõ"
cover = book["cover"] or ""
struct_now = time.localtime()
today_str = f"{struct_now.tm_year:04d}-{struct_now.tm_mon:02d}-{struct_now.tm_mday:02d}"
conn = get_user_db_conn()
try:
conn.execute("""
INSERT OR REPLACE INTO reading_history (user_id, book_id, title, author, cover, last_chapter, read_date, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
""", (user_id, book_id, title, author, cover, last_chapter, today_str))
conn.commit()
finally:
conn.close()
return jsonify({"success": True})
@books_bp.route("/history/clear", methods=["POST"])
def clear_history():
user = get_current_user()
user_id = user["id"] if user else None
if not user_id:
return jsonify({"error": "Vui lòng đăng nhập."}), 401
conn = get_user_db_conn()
conn.execute("DELETE FROM reading_history WHERE user_id = ?", (user_id,))
conn.commit()
conn.close()
return jsonify({"success": True})
@books_bp.route("/history/remove", methods=["POST"])
def remove_history_item():
"""Xóa một mục khỏi lịch sử đọc (theo book_id hoặc url)."""
user = get_current_user()
user_id = user["id"] if user else None
if not user_id:
return jsonify({"error": "Vui lòng đăng nhập."}), 401
data = request.json or {}
book_id = data.get("book_id")
url = data.get("url")
if not book_id and not url:
return jsonify({"error": "Thiếu book_id hoặc url."}), 400
conn = get_user_db_conn()
try:
if book_id:
conn.execute(
"DELETE FROM reading_history WHERE user_id = ? AND book_id = ?",
(user_id, book_id)
)
else:
conn.execute(
"DELETE FROM reading_history WHERE user_id = ? AND url = ?",
(user_id, url)
)
conn.commit()
return jsonify({"success": True, "message": "Đã xóa khỏi lịch sử."})
except Exception as e:
logger.error(f"[History] Remove item failed: {e}")
return jsonify({"error": "Không thể xóa mục lịch sử."}), 500
finally:
conn.close()
@books_bp.route("/extension/sync", methods=["POST"])
def extension_sync():
user = get_current_user()
user_id = user["id"] if user else None
if not user_id:
return jsonify({"error": "Vui lòng đăng nhập trên website chính."}), 401
data = request.json or {}
title_zh = data.get("title", "").strip()
author_zh = data.get("author", "").strip()
cover = data.get("cover", "").strip()
last_chapter_zh = data.get("last_chapter", "").strip()
url = data.get("url", "").strip()
action = data.get("action", "history")
if not title_zh and not url:
return jsonify({"error": "Thiếu thông tin tên truyện hoặc liên kết đường dẫn."}), 400
book = None
book_id = None
title_vi = None
author_vi = None
eng = get_engine()
if not title_zh:
from urllib.parse import urlparse
try:
domain = urlparse(url).netloc
title_vi = f"Truyện ngoài ({domain})"
except Exception:
title_vi = "Truyện ngoài"
title_zh = title_vi
author_vi = "Không rõ"
else:
main_conn = get_db()
book = main_conn.execute(
"SELECT id, title_vietphrase, author_hanviet, cover FROM books WHERE title = ? LIMIT 1",
(title_zh,)
).fetchone()
if book:
book_id = book["id"]
title_vi = book["title_vietphrase"]
author_vi = book["author_hanviet"]
if not cover and book["cover"]:
cover = book["cover"]
else:
if not title_vi:
try:
title_vi = eng.translate(title_zh, multi_option=False)
except Exception:
title_vi = title_zh
if not author_vi:
if author_zh:
try:
author_vi = eng.translate(author_zh, multi_option=False)
except Exception:
author_vi = author_zh
else:
author_vi = "Không rõ"
last_chapter_vi = "Chương đầu"
if last_chapter_zh:
try:
last_chapter_vi = eng.translate(last_chapter_zh, multi_option=False)
except Exception:
last_chapter_vi = last_chapter_zh
title_vi = title_vi or title_zh
author_vi = author_vi or "Không rõ"
struct_now = time.localtime()
today_str = f"{struct_now.tm_year:04d}-{struct_now.tm_mon:02d}-{struct_now.tm_mday:02d}"
conn = get_user_db_conn()
try:
if action == "bookshelf":
if not is_vip_request():
exists = (
conn.execute("SELECT 1 FROM bookshelf WHERE user_id = ? AND book_id = ?", (user_id, book_id)).fetchone()
if book_id
else conn.execute("SELECT 1 FROM bookshelf WHERE user_id = ? AND url = ?", (user_id, url)).fetchone()
) is not None
if not exists:
count = conn.execute("SELECT COUNT(*) as cnt FROM bookshelf WHERE user_id = ?", (user_id,)).fetchone()["cnt"]
if count >= 5:
return jsonify({"error": "Tủ sách Standard tối đa 5 truyện. Nâng cấp VIP!"}), 403
if book_id:
conn.execute("INSERT OR REPLACE INTO bookshelf (user_id, book_id, title, author, cover, url) VALUES (?, ?, ?, ?, ?, ?)",
(user_id, book_id, title_vi, author_vi, cover, url))
else:
existing = conn.execute("SELECT id FROM bookshelf WHERE user_id = ? AND url = ?", (user_id, url)).fetchone()
if existing:
conn.execute("UPDATE bookshelf SET title = ?, author = ?, cover = ? WHERE id = ?",
(title_vi, author_vi, cover, existing["id"]))
else:
conn.execute("INSERT INTO bookshelf (user_id, book_id, title, author, cover, url) VALUES (?, NULL, ?, ?, ?, ?)",
(user_id, title_vi, author_vi, cover, url))
conn.commit()
msg = "Đã lưu vào Tủ sách!"
else:
if book_id:
conn.execute("INSERT OR REPLACE INTO reading_history (user_id, book_id, title, author, cover, last_chapter, read_date, url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(user_id, book_id, title_vi, author_vi, cover, last_chapter_vi, today_str, url))
else:
existing = conn.execute("SELECT id FROM reading_history WHERE user_id = ? AND url = ?", (user_id, url)).fetchone()
if existing:
conn.execute("UPDATE reading_history SET title = ?, author = ?, cover = ?, last_chapter = ?, read_date = ?, timestamp = CURRENT_TIMESTAMP WHERE id = ?",
(title_vi, author_vi, cover, last_chapter_vi, today_str, existing["id"]))
else:
conn.execute("INSERT INTO reading_history (user_id, book_id, title, author, cover, last_chapter, read_date, url) VALUES (?, NULL, ?, ?, ?, ?, ?, ?)",
(user_id, title_vi, author_vi, cover, last_chapter_vi, today_str, url))
conn.commit()
msg = "Đã cập nhật Lịch sử!"
finally:
conn.close()
return jsonify({
"success": True, "message": msg,
"matched": book is not None, "book_id": book_id,
"title_vi": title_vi, "author_vi": author_vi, "last_chapter_vi": last_chapter_vi
})
@books_bp.route("/system/notifications", methods=["GET"])
def get_system_notifications():
user = get_current_user()
notifications = [
{
"id": "sys-1",
"type": "server",
"message": "Hệ thống máy chủ dịch thuật AI đã được nâng cấp lên v2.4, cải thiện 45% tốc độ phản hồi.",
"time": "Vừa xong"
},
{
"id": "sys-2",
"type": "promo",
"message": "Sự kiện đặc biệt: Tặng ngay 20% số dư khi thực hiện nạp ví VIP qua chuyển khoản VietQR trong tuần này!",
"time": "5 phút trước"
}
]
if user:
user_id = user["id"]
conn = get_user_db_conn()
try:
rows = conn.execute("SELECT title FROM bookshelf WHERE user_id = ? ORDER BY added_at DESC LIMIT 3", (user_id,)).fetchall()
for i, row in enumerate(rows):
notifications.append({
"id": f"shelf-{i}",
"type": "update",
"message": f"Truyện '{row['title']}' trong Tủ sách của bạn vừa cập nhật thêm chương dịch mới.",
"time": "10 phút trước"
})
except Exception:
pass
finally:
conn.close()
notifications.append({
"id": "comment-1",
"type": "comment",
"message": "Độc giả Lê Hoàng Nam vừa bình luận truyện 'Trọng Sinh Chi Ma Giáo Giáo Chủ': 'Bản dịch AI đọc rất mượt mà...'",
"time": "20 phút trước"
})
notifications.append({
"id": "comment-2",
"type": "comment",
"message": "Thành viên VIP_SERVER vừa tối ưu hóa tốc độ load chương mới thành công.",
"time": "1 giờ trước"
})
return jsonify(notifications)
@books_bp.route("/author/<author_name>", methods=["GET"])
def get_author_details(author_name):
conn = get_db()
rows = conn.execute(
"SELECT * FROM books WHERE author = ? OR author_hanviet = ? ORDER BY chapters_max DESC",
(author_name, author_name)
).fetchall()
if not rows:
rows = conn.execute(
"SELECT * FROM books WHERE LOWER(author) = LOWER(?) OR LOWER(author_hanviet) = LOWER(?) ORDER BY chapters_max DESC",
(author_name, author_name)
).fetchall()
fallback_mode = False
if not rows:
# Fallback: if author has no books in database, fetch first 5 books to show as placeholder
rows = conn.execute("SELECT * FROM books LIMIT 5").fetchall()
fallback_mode = True
books = []
canonical_chinese = author_name if fallback_mode else (rows[0]["author"] or author_name)
canonical_hanviet = author_name if fallback_mode else (rows[0]["author_hanviet"] or author_name)
for row in rows:
book_dict = dict(row)
if fallback_mode:
book_dict["author"] = author_name
book_dict["author_hanviet"] = author_name
parsed_sources = []
raw_urls = book_dict.get("urls", "")
if raw_urls:
parts = raw_urls.split(" | ")
for part in parts:
if ":" in part:
subparts = part.split(":", 1)
src_name = subparts[0].strip()
src_url = subparts[1].strip()
# Keep raw scheme if complete, else handle
if src_url.startswith("//"):
src_url = "https:" + src_url
parsed_sources.append({
"source": src_name,
"url": src_url
})
book_dict["parsed_sources"] = parsed_sources
books.append(book_dict)
return jsonify({
"author_chinese": canonical_chinese,
"author_hanviet": canonical_hanviet,
"total_books": len(books),
"books": books
})
@books_bp.route("/books/<int:book_id>/comments", methods=["POST"])
def add_book_comment(book_id):
user = get_current_user()
data = request.json or {}
content = data.get("content", "").strip()
is_anonymous = int(data.get("is_anonymous", 0))
if not content:
return jsonify({"error": "Nội dung bình luận không được để trống"}), 400
user_id = user["id"] if user else None
# Mã hóa Hybrid RSA-AES bảo mật
enc_data = encrypt_asymmetric_hybrid(content)
conn = get_user_db_conn()
try:
conn.execute(
"""INSERT INTO book_comments
(book_id, user_id, is_anonymous, content_ciphertext, encrypted_aes_key, aes_nonce, aes_tag)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(book_id, user_id, is_anonymous,
enc_data["ciphertext"], enc_data["encrypted_key"], enc_data["nonce"], enc_data["tag"])
)
conn.commit()
return jsonify({"success": True, "message": "Gửi bình luận thành công!"})
except Exception as e:
return jsonify({"error": str(e)}), 500
finally:
conn.close()
@books_bp.route("/books/<int:book_id>/comments", methods=["GET"])
def get_book_comments(book_id):
conn = get_user_db_conn()
try:
rows = conn.execute(
"""SELECT c.id, c.user_id, c.is_anonymous, c.content_ciphertext, c.encrypted_aes_key,
c.aes_nonce, c.aes_tag, c.created_at, u.username, u.avatar
FROM book_comments c
LEFT JOIN users u ON c.user_id = u.id
WHERE c.book_id = ?
ORDER BY c.created_at DESC""",
(book_id,)
).fetchall()
comments = []
for r in rows:
# Giải mã Hybrid RSA-AES
decrypted_content = decrypt_asymmetric_hybrid(
r["content_ciphertext"],
r["encrypted_aes_key"],
r["aes_nonce"],
r["aes_tag"]
)
comment = {
"id": r["id"],
"created_at": r["created_at"].isoformat() if hasattr(r["created_at"], "isoformat") else r["created_at"],
}
if r["is_anonymous"] == 1:
comment["username"] = "Đạo hữu ẩn danh"
comment["avatar"] = None
comment["user_id"] = None
else:
comment["username"] = r["username"] or "Khách viếng thăm"
comment["avatar"] = r["avatar"]
comment["user_id"] = r["user_id"]
comment["content"] = decrypted_content
comments.append(comment)
return jsonify({"comments": comments})
except Exception as e:
return jsonify({"error": str(e)}), 500
finally:
conn.close()