from flask import Blueprint, request, jsonify from backend.core.decorators import jwt_required, get_current_user from backend.database.db_manager import get_user_db_conn from backend.core.logger import logger from backend.core.security import encrypt_message, decrypt_message from datetime import datetime import os user_features_bp = Blueprint("user_features", __name__) def serialize_row(row): if not row: return {} d = dict(row) for k, v in d.items(): if hasattr(v, "isoformat"): d[k] = v.isoformat() return d import time _history_cache = {} _stats_cache = {} # ========================================== # 1. Translation History Endpoints # ========================================== @user_features_bp.route("/api/user/history", methods=["GET"]) @jwt_required def get_translation_history(): user = get_current_user() user_id = user["id"] now = time.time() if user_id in _history_cache: cached_data, cached_time = _history_cache[user_id] if now - cached_time < 60: return jsonify(cached_data) conn = get_user_db_conn() try: rows = conn.execute( "SELECT * FROM translation_history WHERE user_id = ? ORDER BY created_at DESC LIMIT 100", (user_id,) ).fetchall() response_data = {"history": [serialize_row(r) for r in rows], "success": True} _history_cache[user_id] = (response_data, now) return jsonify(response_data) except Exception as e: logger.error(f"[User History] Failed to fetch history: {e}") return jsonify({"error": "Failed to fetch translation history", "success": False}), 500 finally: conn.close() @user_features_bp.route("/api/user/history", methods=["POST"]) @jwt_required def add_translation_history(): user = get_current_user() data = request.json or {} original = data.get("original_text", "").strip() translated = data.get("translated_text", "").strip() mode = data.get("mode", "fast").strip() chars = int(data.get("characters", len(original))) if not original or not translated: return jsonify({"error": "Original text and translation content cannot be empty", "success": False}), 400 conn = get_user_db_conn() try: conn.execute( "INSERT INTO translation_history (user_id, original_text, translated_text, mode, characters) VALUES (?, ?, ?, ?, ?)", (user["id"], original, translated, mode, chars) ) conn.commit() if user["id"] in _history_cache: del _history_cache[user["id"]] if user["id"] in _stats_cache: del _stats_cache[user["id"]] return jsonify({"message": "Translation history entry added", "success": True}) except Exception as e: logger.error(f"[User History] Failed to add entry: {e}") return jsonify({"error": "Failed to add translation history entry", "success": False}), 500 finally: conn.close() @user_features_bp.route("/api/user/history", methods=["DELETE"]) @jwt_required def clear_translation_history(): user = get_current_user() conn = get_user_db_conn() try: conn.execute("DELETE FROM translation_history WHERE user_id = ?", (user["id"],)) conn.commit() if user["id"] in _history_cache: del _history_cache[user["id"]] if user["id"] in _stats_cache: del _stats_cache[user["id"]] return jsonify({"message": "Translation history cleared", "success": True}) except Exception as e: logger.error(f"[User History] Failed to clear history: {e}") return jsonify({"error": "Failed to clear history", "success": False}), 500 finally: conn.close() # ========================================== # 2. Vocabulary/Word Notebook Endpoints # ========================================== @user_features_bp.route("/api/user/vocabulary", methods=["GET"]) @jwt_required def get_vocabulary(): user = get_current_user() conn = get_user_db_conn() try: rows = conn.execute( "SELECT * FROM vocabulary WHERE user_id = ? ORDER BY created_at DESC", (user["id"],) ).fetchall() return jsonify({"vocabulary": [serialize_row(r) for r in rows], "success": True}) except Exception as e: logger.error(f"[Vocabulary] Failed to fetch vocabulary: {e}") return jsonify({"error": "Failed to fetch vocabulary list", "success": False}), 500 finally: conn.close() @user_features_bp.route("/api/user/vocabulary", methods=["POST"]) @jwt_required def add_vocabulary(): user = get_current_user() data = request.json or {} original = data.get("original_text", "").strip() translation = data.get("translation", "").strip() pinyin_or_hanviet = data.get("pinyin_or_hanviet", "").strip() context = data.get("context_sentence", "").strip() notes = data.get("notes", "").strip() if not original or not translation: return jsonify({"error": "Original text and translation content cannot be empty", "success": False}), 400 conn = get_user_db_conn() try: conn.execute( "INSERT INTO vocabulary (user_id, original_text, pinyin_or_hanviet, translation, context_sentence, notes) VALUES (?, ?, ?, ?, ?, ?)", (user["id"], original, pinyin_or_hanviet, translation, context, notes) ) conn.commit() return jsonify({"message": "Word saved to vocabulary notebook", "success": True}) except Exception as e: logger.error(f"[Vocabulary] Failed to save word: {e}") return jsonify({"error": "Failed to save word notebook entry", "success": False}), 500 finally: conn.close() @user_features_bp.route("/api/user/vocabulary/", methods=["DELETE"]) @jwt_required def delete_vocabulary(item_id): user = get_current_user() conn = get_user_db_conn() try: # Verify ownership row = conn.execute("SELECT user_id FROM vocabulary WHERE id = ?", (item_id,)).fetchone() if not row: return jsonify({"error": "Entry not found", "success": False}), 404 if int(row["user_id"]) != int(user["id"]): return jsonify({"error": "Unauthorized action", "success": False}), 403 conn.execute("DELETE FROM vocabulary WHERE id = ?", (item_id,)) conn.commit() return jsonify({"message": "Word deleted from notebook", "success": True}) except Exception as e: logger.error(f"[Vocabulary] Failed to delete item {item_id}: {e}") return jsonify({"error": "Failed to delete item", "success": False}), 500 finally: conn.close() # ========================================== # 3. Personalization Settings Endpoints # ========================================== @user_features_bp.route("/api/user/settings", methods=["GET"]) @jwt_required def get_user_settings(): user = get_current_user() conn = get_user_db_conn() try: row = conn.execute("SELECT * FROM user_settings WHERE user_id = ?", (user["id"],)).fetchone() if not row: # Insert defaults conn.execute( "INSERT INTO user_settings (user_id, theme, default_language, auto_read, font_size) VALUES (?, 'dark', 'vi', 0, 14)", (user["id"],) ) conn.commit() row = conn.execute("SELECT * FROM user_settings WHERE user_id = ?", (user["id"],)).fetchone() return jsonify({"settings": serialize_row(row), "success": True}) except Exception as e: logger.error(f"[UserSettings] Failed to fetch settings: {e}") return jsonify({"error": "Failed to fetch settings", "success": False}), 500 finally: conn.close() @user_features_bp.route("/api/user/settings", methods=["POST"]) @jwt_required def update_user_settings(): user = get_current_user() data = request.json or {} theme = data.get("theme", "dark").strip() lang = data.get("default_language", "vi").strip() auto_read = int(data.get("auto_read", 0)) font_size = int(data.get("font_size", 14)) now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") conn = get_user_db_conn() try: conn.execute( """INSERT INTO user_settings (user_id, theme, default_language, auto_read, font_size, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET theme = excluded.theme, default_language = excluded.default_language, auto_read = excluded.auto_read, font_size = excluded.font_size, updated_at = excluded.updated_at""", (user["id"], theme, lang, auto_read, font_size, now) ) conn.commit() return jsonify({"message": "Settings updated successfully", "success": True}) except Exception as e: logger.error(f"[UserSettings] Failed to update settings: {e}") return jsonify({"error": "Failed to update settings", "success": False}), 500 finally: conn.close() # ========================================== # 4. Usage Tracking & System/API Statistics # ========================================== @user_features_bp.route("/api/user/track", methods=["POST"]) @jwt_required def track_user_usage(): user = get_current_user() data = request.json or {} source = data.get("source", "web").strip().lower() action = data.get("action", "read").strip().lower() duration = int(data.get("duration", 0)) mode = data.get("mode", "online").strip().lower() if not source or not action: return jsonify({"error": "Missing source or action", "success": False}), 400 conn = get_user_db_conn() try: conn.execute( "INSERT INTO usage_tracking (user_id, source, action, duration, mode) VALUES (?, ?, ?, ?, ?)", (user["id"], source, action, duration, mode) ) conn.commit() if user["id"] in _stats_cache: del _stats_cache[user["id"]] return jsonify({"message": "Usage tracked successfully", "success": True}) except Exception as e: logger.error(f"[Usage Tracking] Failed to log usage: {e}") return jsonify({"error": "Failed to track usage", "success": False}), 500 finally: conn.close() @user_features_bp.route("/api/user/stats", methods=["GET"]) @jwt_required def get_user_stats(): user = get_current_user() user_id = user["id"] now = time.time() if user_id in _stats_cache: cached_data, cached_time = _stats_cache[user_id] if now - cached_time < 60: return jsonify(cached_data) conn = get_user_db_conn() try: # 1. Total active/reading duration by source web_duration_row = conn.execute( "SELECT SUM(duration) as total FROM usage_tracking WHERE user_id = ? AND source = 'web'", (user["id"],) ).fetchone() web_duration = web_duration_row["total"] or 0 if web_duration_row else 0 ext_duration_row = conn.execute( "SELECT SUM(duration) as total FROM usage_tracking WHERE user_id = ? AND source = 'extension'", (user["id"],) ).fetchone() ext_duration = ext_duration_row["total"] or 0 if ext_duration_row else 0 # 2. Total active/reading duration by online/offline mode online_duration_row = conn.execute( "SELECT SUM(duration) as total FROM usage_tracking WHERE user_id = ? AND mode = 'online'", (user["id"],) ).fetchone() online_duration = online_duration_row["total"] or 0 if online_duration_row else 0 offline_duration_row = conn.execute( "SELECT SUM(duration) as total FROM usage_tracking WHERE user_id = ? AND mode = 'offline'", (user["id"],) ).fetchone() offline_duration = offline_duration_row["total"] or 0 if offline_duration_row else 0 # 3. Translation calls count (from translation_history) trans_calls_row = conn.execute( "SELECT COUNT(*) as cnt, SUM(characters) as total_chars FROM translation_history WHERE user_id = ?", (user["id"],) ).fetchone() trans_calls = trans_calls_row["cnt"] or 0 if trans_calls_row else 0 total_chars = trans_calls_row["total_chars"] or 0 if trans_calls_row else 0 # 4. API keys & API usage stats (from api_keys and api_usage) api_keys_count_row = conn.execute( "SELECT COUNT(*) as cnt FROM api_keys WHERE user_id = ?", (user["id"],) ).fetchone() api_keys_count = api_keys_count_row["cnt"] or 0 if api_keys_count_row else 0 api_usage_row = conn.execute( """SELECT COUNT(u.id) as cnt, SUM(u.tokens) as tokens FROM api_usage u JOIN api_keys k ON u.api_key = k.api_key WHERE k.user_id = ?""", (user["id"],) ).fetchone() api_usage_calls = api_usage_row["cnt"] or 0 if api_usage_row else 0 api_usage_chars = api_usage_row["tokens"] or 0 if api_usage_row else 0 # 5. Recent actions log recent_actions = conn.execute( "SELECT source, action, duration, mode, timestamp FROM usage_tracking WHERE user_id = ? ORDER BY timestamp DESC, id DESC LIMIT 20", (user_id,) ).fetchall() response_data = { "success": True, "stats": { "web_duration": web_duration, "ext_duration": ext_duration, "online_duration": online_duration, "offline_duration": offline_duration, "total_reading_time": web_duration + ext_duration, "translation_calls": trans_calls, "translation_chars": total_chars, "api_keys_count": api_keys_count, "api_usage_calls": api_usage_calls, "api_usage_chars": api_usage_chars }, "recent_actions": [serialize_row(r) for r in recent_actions] } _stats_cache[user_id] = (response_data, now) return jsonify(response_data) except Exception as e: logger.error(f"[User Stats] Failed to fetch stats: {e}") return jsonify({"error": "Failed to fetch stats", "success": False}), 500 finally: conn.close() # ========================================== # 5. Feedback Submissions # ========================================== @user_features_bp.route("/api/feedback/submit", methods=["POST"]) def submit_feedback(): data = request.json or {} email = data.get("email", "").strip() message = data.get("message", "").strip() if not email or not message: return jsonify({"error": "Email và nội dung phản hồi không được để trống", "success": False}), 400 # 1. Log to logs/feedback.log os.makedirs("logs", exist_ok=True) log_path = "logs/feedback.log" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_entry = f"[{timestamp}] EMAIL: {email} | MESSAGE: {message}\n" try: with open(log_path, "a", encoding="utf-8") as f: f.write(log_entry) except Exception as e: logger.error(f"Failed to write feedback to log file: {e}") # 2. Save to SQLite database conn = get_user_db_conn() try: conn.execute( """CREATE TABLE IF NOT EXISTS user_feedbacks ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT, message TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )""" ) conn.execute( "INSERT INTO user_feedbacks (email, message) VALUES (?, ?)", (email, message) ) conn.commit() return jsonify({"message": "Gửi phản hồi thành công!", "success": True}) except Exception as e: logger.error(f"[Feedback] Failed to save feedback: {e}") return jsonify({"error": "Gửi phản hồi thất bại", "success": False}), 500 finally: conn.close() @user_features_bp.route("/api/feedback/list", methods=["GET"]) def list_feedbacks(): conn = get_user_db_conn() try: conn.execute( """CREATE TABLE IF NOT EXISTS user_feedbacks ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT, message TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )""" ) rows = conn.execute("SELECT * FROM user_feedbacks ORDER BY id DESC").fetchall() feedbacks = [serialize_row(r) for r in rows] return jsonify({"feedbacks": feedbacks, "success": True}) except Exception as e: logger.error(f"[Feedback] Failed to list feedback: {e}") return jsonify({"error": "Không thể lấy danh sách phản hồi", "success": False}), 500 finally: conn.close() # ========================================== # 6. Social, Friends, and Private Messaging APIs # ========================================== @user_features_bp.route("/api/users/search", methods=["GET"]) def search_users(): q = request.args.get("q", "").strip() if not q: return jsonify({"users": []}) user = get_current_user() user_id = user["id"] if user else None conn = get_user_db_conn() try: like_q = "%" + q + "%" if user_id: rows = conn.execute( """SELECT id, username, user_code, display_name, avatar FROM users WHERE id != ? AND ( username LIKE ? OR email LIKE ? OR user_code = ? ) LIMIT 15""", (user_id, like_q, like_q, q) ).fetchall() else: rows = conn.execute( """SELECT id, username, user_code, display_name, avatar FROM users WHERE username LIKE ? OR email LIKE ? OR user_code = ? LIMIT 15""", (like_q, like_q, q) ).fetchall() return jsonify({"users": [serialize_row(r) for r in rows]}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @user_features_bp.route("/api/notifications/unread-counts", methods=["GET"]) def get_unread_counts(): """Separate unread counts: messages vs friend/other notifications.""" user = get_current_user() if not user: return jsonify({"messages": 0, "notifications": 0}) conn = get_user_db_conn() try: # Unread personal messages from direct_messages table msg_count = conn.execute( "SELECT COUNT(*) as c FROM direct_messages WHERE receiver_id = ? AND is_read = 0", (user["id"],) ).fetchone()["c"] # Unread other notifications (friend_request, friend_accept, book_share etc) notif_count = conn.execute( "SELECT COUNT(*) as c FROM personal_notifications WHERE user_id = ? AND is_read = 0 AND type != 'message'", (user["id"],) ).fetchone()["c"] return jsonify({"messages": msg_count, "notifications": notif_count}) except Exception as e: return jsonify({"messages": 0, "notifications": 0}) finally: conn.close() @user_features_bp.route("/api/friends/request", methods=["POST"]) def send_friend_request(): user = get_current_user() if not user: return jsonify({"error": "Unauthorized"}), 401 data = request.json or {} # Accept username, user_code, or email query = data.get("friend_username") or data.get("friend_code") or data.get("friend_email") or "" query = query.strip() if not query: return jsonify({"error": "Thiếu thông tin tìm kiếm bạn bè"}), 400 conn = get_user_db_conn() try: target = conn.execute( "SELECT id, username FROM users WHERE username = ? OR user_code = ? OR email = ?", (query, query, query.lower()) ).fetchone() if not target: return jsonify({"error": "Không tìm thấy người dùng"}), 404 target_id = target["id"] if target_id == user["id"]: return jsonify({"error": "Không thể kết bạn với chính mình"}), 400 exists = conn.execute("SELECT status FROM friendships WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)", (user["id"], target_id, target_id, user["id"])).fetchone() if exists: return jsonify({"error": f"Đã có lời mời kết bạn hoặc đã là bạn bè (Trạng thái: {exists['status']})"}), 400 conn.execute("INSERT INTO friendships (user_id, friend_id, status) VALUES (?, ?, 'pending')", (user["id"], target_id)) conn.execute( "INSERT INTO personal_notifications (user_id, sender_id, type, message, related_id) VALUES (?, ?, 'friend_request', ?, ?)", (target_id, user["id"], f"{user['username']} đã gửi cho bạn một lời mời kết bạn.", user["id"]) ) conn.commit() return jsonify({"success": True, "message": "Đã gửi lời mời kết bạn!", "to_user": target["username"]}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @user_features_bp.route("/api/friends/respond", methods=["POST"]) def respond_friend_request(): user = get_current_user() if not user: return jsonify({"error": "Unauthorized"}), 401 data = request.json or {} sender_id = data.get("sender_id") action = data.get("action", "").lower() if not sender_id or action not in ['accept', 'reject']: return jsonify({"error": "Dữ liệu không hợp lệ"}), 400 conn = get_user_db_conn() try: req = conn.execute("SELECT id FROM friendships WHERE user_id = ? AND friend_id = ? AND status = 'pending'", (sender_id, user["id"])).fetchone() if not req: return jsonify({"error": "Không tìm thấy lời mời kết bạn"}), 404 if action == 'accept': conn.execute("UPDATE friendships SET status = 'accepted' WHERE user_id = ? AND friend_id = ?", (sender_id, user["id"])) # Cross-compatibility check instead of INSERT OR IGNORE exists = conn.execute("SELECT 1 FROM friendships WHERE user_id = ? AND friend_id = ?", (user["id"], sender_id)).fetchone() if not exists: conn.execute("INSERT INTO friendships (user_id, friend_id, status) VALUES (?, ?, 'accepted')", (user["id"], sender_id)) conn.execute( "INSERT INTO personal_notifications (user_id, sender_id, type, message, related_id) VALUES (?, ?, 'friend_accept', ?, ?)", (sender_id, user["id"], f"{user['username']} đã chấp nhận lời mời kết bạn.", user["id"]) ) else: conn.execute("DELETE FROM friendships WHERE user_id = ? AND friend_id = ?", (sender_id, user["id"])) conn.execute( "UPDATE personal_notifications SET is_read = 1 WHERE user_id = ? AND sender_id = ? AND type = 'friend_request'", (user["id"], sender_id) ) conn.commit() return jsonify({"success": True, "message": f"Đã {'chấp nhận' if action == 'accept' else 'từ chối'} kết bạn."}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @user_features_bp.route("/api/friends/list", methods=["GET"]) def list_friends(): user = get_current_user() if not user: return jsonify({"error": "Unauthorized"}), 401 conn = get_user_db_conn() try: rows = conn.execute( """SELECT u.id, u.username, u.user_code, u.avatar, (SELECT COUNT(*) FROM direct_messages dm WHERE dm.sender_id = u.id AND dm.receiver_id = ? AND dm.is_read = 0) as unread_messages FROM friendships f JOIN users u ON f.friend_id = u.id WHERE f.user_id = ? AND f.status = 'accepted'""", (user["id"], user["id"]) ).fetchall() return jsonify({"friends": [serialize_row(r) for r in rows]}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @user_features_bp.route("/api/friends/unfriend", methods=["POST"]) @jwt_required def unfriend(): """Hủy kết bạn: Xóa quan hệ bạn bè cả hai chiều.""" user = get_current_user() if not user: return jsonify({"error": "Unauthorized"}), 401 data = request.json or {} friend_id = data.get("friend_id") if not friend_id: return jsonify({"error": "Thiếu friend_id"}), 400 conn = get_user_db_conn() try: # Kiểm tra quan hệ tồn tại và đang ở trạng thái accepted rel = conn.execute( """SELECT id FROM friendships WHERE ((user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)) AND status = 'accepted'""", (user["id"], friend_id, friend_id, user["id"]) ).fetchone() if not rel: return jsonify({"error": "Không tìm thấy quan hệ bạn bè"}), 404 # Xóa cả hai chiều conn.execute( "DELETE FROM friendships WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)", (user["id"], friend_id, friend_id, user["id"]) ) conn.commit() return jsonify({"success": True, "message": "Đã hủy kết bạn."}) except Exception as e: logger.error(f"Unfriend error: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @user_features_bp.route("/api/friends/block", methods=["POST"]) @jwt_required def block_user(): """Chặn người dùng: Xóa quan hệ bạn bè (nếu có) và ghi nhận trạng thái blocked.""" user = get_current_user() if not user: return jsonify({"error": "Unauthorized"}), 401 data = request.json or {} target_id = data.get("user_id") if not target_id: return jsonify({"error": "Thiếu user_id cần chặn"}), 400 if target_id == user["id"]: return jsonify({"error": "Không thể tự chặn chính mình"}), 400 conn = get_user_db_conn() try: # Kiểm tra đã chặn chưa already = conn.execute( "SELECT id FROM friendships WHERE user_id = ? AND friend_id = ? AND status = 'blocked'", (user["id"], target_id) ).fetchone() if already: return jsonify({"error": "Bạn đã chặn người dùng này rồi"}), 400 # Xóa quan hệ bạn bè hai chiều (nếu có) conn.execute( "DELETE FROM friendships WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)", (user["id"], target_id, target_id, user["id"]) ) # Ghi nhận block (một chiều: người chặn → người bị chặn) conn.execute( "INSERT OR REPLACE INTO friendships (user_id, friend_id, status) VALUES (?, ?, 'blocked')", (user["id"], target_id) ) conn.commit() return jsonify({"success": True, "message": "Đã chặn người dùng."}) except Exception as e: logger.error(f"Block user error: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @user_features_bp.route("/api/friends/unblock", methods=["POST"]) @jwt_required def unblock_user(): """Bỏ chặn người dùng.""" user = get_current_user() if not user: return jsonify({"error": "Unauthorized"}), 401 data = request.json or {} target_id = data.get("user_id") if not target_id: return jsonify({"error": "Thiếu user_id cần bỏ chặn"}), 400 conn = get_user_db_conn() try: rel = conn.execute( "SELECT id FROM friendships WHERE user_id = ? AND friend_id = ? AND status = 'blocked'", (user["id"], target_id) ).fetchone() if not rel: return jsonify({"error": "Bạn chưa chặn người dùng này"}), 404 conn.execute( "DELETE FROM friendships WHERE user_id = ? AND friend_id = ? AND status = 'blocked'", (user["id"], target_id) ) conn.commit() return jsonify({"success": True, "message": "Đã bỏ chặn người dùng."}) except Exception as e: logger.error(f"Unblock user error: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @user_features_bp.route("/api/friends/blocked-list", methods=["GET"]) @jwt_required def list_blocked(): """Lấy danh sách người dùng đang bị chặn.""" user = get_current_user() if not user: return jsonify({"error": "Unauthorized"}), 401 conn = get_user_db_conn() try: rows = conn.execute( """SELECT u.id, u.username, u.user_code, u.avatar, f.created_at as blocked_at FROM friendships f JOIN users u ON f.friend_id = u.id WHERE f.user_id = ? AND f.status = 'blocked'""", (user["id"],) ).fetchall() return jsonify({"blocked_users": [serialize_row(r) for r in rows]}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @user_features_bp.route("/api/messages/send", methods=["POST"]) @jwt_required def send_message(): user = get_current_user() if not user: return jsonify({"error": "Unauthorized"}), 401 data = request.json or {} receiver_id = data.get("receiver_id") message = data.get("message", "").strip() if not receiver_id or not message: return jsonify({"error": "Thiếu người nhận hoặc nội dung tin nhắn"}), 400 conn = get_user_db_conn() try: # Kiểm tra bảo mật: Người nhận có đang chặn người gửi không? blocked_by_receiver = conn.execute( "SELECT id FROM friendships WHERE user_id = ? AND friend_id = ? AND status = 'blocked'", (receiver_id, user["id"]) ).fetchone() if blocked_by_receiver: return jsonify({"error": "Không thể gửi tin nhắn đến người dùng này."}), 403 # Kiểm tra bảo mật: Người gửi có đang chặn người nhận không? blocked_by_sender = conn.execute( "SELECT id FROM friendships WHERE user_id = ? AND friend_id = ? AND status = 'blocked'", (user["id"], receiver_id) ).fetchone() if blocked_by_sender: return jsonify({"error": "Bạn đang chặn người dùng này. Bỏ chặn trước khi nhắn tin."}), 403 enc_msg = encrypt_message(message) cursor = conn.execute( "INSERT INTO direct_messages (sender_id, receiver_id, message) VALUES (?, ?, ?)", (user["id"], receiver_id, enc_msg) ) msg_id = cursor.lastrowid conn.execute( "INSERT INTO personal_notifications (user_id, sender_id, type, message, related_id) VALUES (?, ?, 'message', ?, ?)", (receiver_id, user["id"], f"{user['username']} đã gửi cho bạn một tin nhắn mới.", msg_id) ) conn.commit() return jsonify({"success": True, "message": "Gửi tin nhắn thành công!", "msg_id": msg_id}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @user_features_bp.route("/api/messages/chat/", methods=["GET"]) def get_chat_history(friend_id): user = get_current_user() if not user: return jsonify({"error": "Unauthorized"}), 401 conn = get_user_db_conn() try: rows = conn.execute( """SELECT id, sender_id, receiver_id, message, is_read, created_at FROM direct_messages WHERE (sender_id = ? AND receiver_id = ?) OR (sender_id = ? AND receiver_id = ?) ORDER BY created_at ASC""", (user["id"], friend_id, friend_id, user["id"]) ).fetchall() conn.execute( "UPDATE direct_messages SET is_read = 1 WHERE sender_id = ? AND receiver_id = ?", (friend_id, user["id"]) ) conn.execute( "UPDATE personal_notifications SET is_read = 1 WHERE user_id = ? AND sender_id = ? AND type = 'message'", (user["id"], friend_id) ) conn.commit() messages = [] for r in rows: d = dict(r) d["message"] = decrypt_message(d.get("message", "")) for k, v in d.items(): if hasattr(v, "isoformat"): d[k] = v.isoformat() messages.append(d) return jsonify({"messages": messages}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @user_features_bp.route("/api/books/share", methods=["POST"]) def share_book(): user = get_current_user() if not user: return jsonify({"error": "Unauthorized"}), 401 data = request.json or {} friend_id = data.get("friend_id") book_id = data.get("book_id") custom_message = data.get("message", "").strip() if not friend_id or not book_id: return jsonify({"error": "Thiếu thông tin người nhận hoặc truyện cần chia sẻ"}), 400 conn = get_user_db_conn() try: book_title = "Truyện" from backend.database.db_manager import get_db main_conn = get_db() row = main_conn.execute("SELECT title_vietphrase, title_hanviet, title FROM books WHERE id = ?", (book_id,)).fetchone() if row: book_title = row["title_vietphrase"] or row["title_hanviet"] or row["title"] msg_content = f"{user['username']} đã chia sẻ truyện '{book_title}' với bạn." if custom_message: msg_content += f" Lời nhắn: \"{custom_message}\"" conn.execute( "INSERT INTO personal_notifications (user_id, sender_id, type, message, related_id) VALUES (?, ?, 'book_share', ?, ?)", (friend_id, user["id"], msg_content, book_id) ) share_chat_msg = f"[Chia sẻ truyện] '{book_title}' - Xem chi tiết tại /book/{book_id}" if custom_message: share_chat_msg += f"\nLời nhắn: {custom_message}" enc_share_msg = encrypt_message(share_chat_msg) conn.execute( "INSERT INTO direct_messages (sender_id, receiver_id, message) VALUES (?, ?, ?)", (user["id"], friend_id, enc_share_msg) ) conn.commit() return jsonify({"success": True, "message": "Chia sẻ truyện thành công!"}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @user_features_bp.route("/api/notifications/personal", methods=["GET"]) def get_personal_notifications(): user = get_current_user() if not user: return jsonify({"notifications": []}) conn = get_user_db_conn() try: rows = conn.execute( """SELECT n.id, n.sender_id, n.type, n.message, n.related_id, n.is_read, n.created_at, u.username as sender_name FROM personal_notifications n LEFT JOIN users u ON n.sender_id = u.id WHERE n.user_id = ? ORDER BY n.created_at DESC LIMIT 50""", (user["id"],) ).fetchall() return jsonify({"notifications": [serialize_row(r) for r in rows]}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @user_features_bp.route("/api/notifications/personal/read", methods=["POST"]) def read_personal_notification(): user = get_current_user() if not user: return jsonify({"error": "Unauthorized"}), 401 data = request.json or {} notif_id = data.get("notification_id") if not notif_id: return jsonify({"error": "Thiếu notification_id"}), 400 conn = get_user_db_conn() try: conn.execute( "UPDATE personal_notifications SET is_read = 1 WHERE id = ? AND user_id = ?", (notif_id, user["id"]) ) conn.commit() return jsonify({"success": True}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close()