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 sects_bp = Blueprint("sects", __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 @sects_bp.route("/api/sects/list", methods=["GET"]) def list_sects(): conn = get_user_db_conn() try: rows = conn.execute(""" SELECT s.id, s.name, s.slogan, s.badge, s.level, s.contribution, s.created_at, u.username as leader_name, (SELECT COUNT(*) FROM sect_members WHERE sect_id = s.id) as member_count FROM sects s LEFT JOIN users u ON s.leader_id = u.id ORDER BY s.contribution DESC, s.created_at DESC """).fetchall() user = get_current_user() user_requests = [] if user: req_rows = conn.execute("SELECT sect_id FROM sect_join_requests WHERE user_id = ? AND status = 'pending'", (user["id"],)).fetchall() user_requests = [r["sect_id"] for r in req_rows] return jsonify({ "sects": [serialize_row(r) for r in rows], "pending_requests": user_requests }) except Exception as e: logger.error(f"Error listing sects: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/create", methods=["POST"]) @jwt_required def create_sect(): user = get_current_user() data = request.json or {} name = data.get("name", "").strip() slogan = data.get("slogan", "").strip() badge = data.get("badge", "purple").strip() if not name: return jsonify({"error": "Tên tông môn không được để trống"}), 400 conn = get_user_db_conn() try: # Check if user is already in a sect in_sect = conn.execute("SELECT sect_id FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if in_sect: return jsonify({"error": "Bạn đã thuộc một tông môn khác, hãy rời tông cũ trước"}), 400 # Check if name exists exists = conn.execute("SELECT id FROM sects WHERE name = ?", (name,)).fetchone() if exists: return jsonify({"error": "Tên tông môn này đã được lập từ trước"}), 400 cursor = conn.execute( "INSERT INTO sects (name, slogan, badge, leader_id, announcement) VALUES (?, ?, ?, ?, ?)", (name, slogan, badge, user["id"], f"Tông môn {name} được thành lập. Chúc các đồng môn tu tiên đắc đạo!") ) sect_id = cursor.lastrowid conn.execute( "INSERT INTO sect_members (sect_id, user_id, role, contribution) VALUES (?, ?, 'leader', 100)", (sect_id, user["id"]) ) conn.execute( "UPDATE sects SET contribution = 100 WHERE id = ?", (sect_id,) ) # Delete any pending join requests of this user elsewhere conn.execute("DELETE FROM sect_join_requests WHERE user_id = ?", (user["id"],)) conn.commit() return jsonify({"success": True, "message": f"Sáng lập {name} tông thành công!", "sect_id": sect_id}) except Exception as e: logger.error(f"Error creating sect: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/join", methods=["POST"]) @jwt_required def join_sect(): user = get_current_user() data = request.json or {} sect_id = data.get("sect_id") if not sect_id: return jsonify({"error": "Thiếu sect_id"}), 400 conn = get_user_db_conn() try: # Check if user is already in a sect in_sect = conn.execute("SELECT sect_id FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if in_sect: return jsonify({"error": "Bạn đã ở trong một tông môn khác"}), 400 # Check if sect exists sect = conn.execute("SELECT name FROM sects WHERE id = ?", (sect_id,)).fetchone() if not sect: return jsonify({"error": "Tông môn không tồn tại"}), 404 # Check if already requested req_exists = conn.execute("SELECT id FROM sect_join_requests WHERE sect_id = ? AND user_id = ?", (sect_id, user["id"])).fetchone() if req_exists: return jsonify({"error": "Bạn đã gửi yêu cầu gia nhập tông này và đang chờ duyệt"}), 400 conn.execute( "INSERT INTO sect_join_requests (sect_id, user_id, status) VALUES (?, ?, 'pending')", (sect_id, user["id"]) ) conn.commit() return jsonify({"success": True, "message": f"Đã gửi yêu cầu bái kiến gia nhập {sect['name']}! Vui lòng chờ duyệt."}) except Exception as e: logger.error(f"Error joining sect: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/leave", methods=["POST"]) @jwt_required def leave_sect(): user = get_current_user() conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id, role FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member: return jsonify({"error": "Bạn chưa gia nhập tông môn nào"}), 400 sect_id = member["sect_id"] if member["role"] == "leader": # Leader leaving dissolves the sect conn.execute("DELETE FROM sect_chat_groups WHERE sect_id = ?", (sect_id,)) conn.execute("DELETE FROM sect_messages WHERE sect_id = ?", (sect_id,)) conn.execute("DELETE FROM sect_members WHERE sect_id = ?", (sect_id,)) conn.execute("DELETE FROM sect_join_requests WHERE sect_id = ?", (sect_id,)) conn.execute("DELETE FROM sect_books WHERE sect_id = ?", (sect_id,)) conn.execute("DELETE FROM sects WHERE id = ?", (sect_id,)) msg = "Bạn là tông chủ, rời đi đồng nghĩa với việc giải tán tông môn!" else: conn.execute("DELETE FROM sect_members WHERE user_id = ? AND sect_id = ?", (user["id"], sect_id)) msg = "Bạn đã rời khỏi tông môn thành công!" conn.commit() return jsonify({"success": True, "message": msg}) except Exception as e: logger.error(f"Error leaving sect: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/my-sect", methods=["GET"]) @jwt_required def my_sect(): user = get_current_user() conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id, role, contribution as my_contrib FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member: return jsonify({"in_sect": False}) sect_id = member["sect_id"] sect = conn.execute(""" SELECT s.id, s.name, s.slogan, s.announcement, s.badge, s.level, s.contribution, s.created_at, u.username as leader_name, u.id as leader_id FROM sects s LEFT JOIN users u ON s.leader_id = u.id WHERE s.id = ? """, (sect_id,)).fetchone() members = conn.execute(""" SELECT sm.user_id, sm.role, sm.contribution, sm.joined_at, u.username, u.avatar FROM sect_members sm LEFT JOIN users u ON sm.user_id = u.id WHERE sm.sect_id = ? ORDER BY CASE sm.role WHEN 'leader' THEN 1 WHEN 'elder' THEN 2 ELSE 3 END, sm.contribution DESC """, (sect_id,)).fetchall() return jsonify({ "in_sect": True, "role": member["role"], "my_contribution": member["my_contrib"], "sect": serialize_row(sect), "members": [serialize_row(m) for m in members] }) except Exception as e: logger.error(f"Error getting my sect: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/chat/history", methods=["GET"]) @jwt_required def get_sect_chat_history(): user = get_current_user() chat_type = request.args.get("chat_type", "general").strip() target_id = request.args.get("target_id") group_id = request.args.get("group_id") conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member: return jsonify({"error": "Bạn chưa gia nhập tông môn"}), 403 sect_id = member["sect_id"] if chat_type == "direct": if not target_id: return jsonify({"error": "Thiếu target_id cho chat riêng"}), 400 rows = conn.execute(""" SELECT sm.id, sm.sender_id, sm.message, sm.created_at, sm.chat_type, sm.target_id, u.username as sender_name, u.avatar as sender_avatar FROM sect_messages sm LEFT JOIN users u ON sm.sender_id = u.id WHERE sm.sect_id = ? AND sm.chat_type = 'direct' AND ((sm.sender_id = ? AND sm.target_id = ?) OR (sm.sender_id = ? AND sm.target_id = ?)) ORDER BY sm.created_at ASC LIMIT 100 """, (sect_id, user["id"], target_id, target_id, user["id"])).fetchall() elif chat_type == "book": if not target_id: return jsonify({"error": "Thiếu target_id cho chat truyện"}), 400 rows = conn.execute(""" SELECT sm.id, sm.sender_id, sm.message, sm.created_at, sm.chat_type, sm.target_id, u.username as sender_name, u.avatar as sender_avatar FROM sect_messages sm LEFT JOIN users u ON sm.sender_id = u.id WHERE sm.sect_id = ? AND sm.chat_type = 'book' AND sm.target_id = ? ORDER BY sm.created_at ASC LIMIT 100 """, (sect_id, target_id)).fetchall() elif chat_type == "group": if not group_id: return jsonify({"error": "Thiếu group_id cho chat nhóm"}), 400 # Check group access group = conn.execute("SELECT members_csv FROM sect_chat_groups WHERE id = ? AND sect_id = ?", (group_id, sect_id)).fetchone() if not group: return jsonify({"error": "Nhóm chat không tồn tại"}), 404 members_list = [int(x) for x in group["members_csv"].split(",") if x.strip()] if user["id"] not in members_list: return jsonify({"error": "Bạn không phải thành viên của nhóm chat này"}), 403 rows = conn.execute(""" SELECT sm.id, sm.sender_id, sm.message, sm.created_at, sm.chat_type, sm.group_id, u.username as sender_name, u.avatar as sender_avatar FROM sect_messages sm LEFT JOIN users u ON sm.sender_id = u.id WHERE sm.sect_id = ? AND sm.chat_type = 'group' AND sm.group_id = ? ORDER BY sm.created_at ASC LIMIT 100 """, (sect_id, group_id)).fetchall() else: # general chat rows = conn.execute(""" SELECT sm.id, sm.sender_id, sm.message, sm.created_at, sm.chat_type, u.username as sender_name, u.avatar as sender_avatar FROM sect_messages sm LEFT JOIN users u ON sm.sender_id = u.id WHERE sm.sect_id = ? AND (sm.chat_type IS NULL OR sm.chat_type = 'general') ORDER BY sm.created_at ASC LIMIT 100 """, (sect_id,)).fetchall() 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: logger.error(f"Error getting sect chat: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/chat/send", methods=["POST"]) @jwt_required def send_sect_chat(): user = get_current_user() data = request.json or {} message = data.get("message", "").strip() chat_type = data.get("chat_type", "general").strip() target_id = data.get("target_id") group_id = data.get("group_id") if not message: return jsonify({"error": "Nội dung tin nhắn trống"}), 400 conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member: return jsonify({"error": "Bạn chưa gia nhập tông môn"}), 403 sect_id = member["sect_id"] # Validate target / group access if chat_type == "group" and group_id: group = conn.execute("SELECT members_csv FROM sect_chat_groups WHERE id = ? AND sect_id = ?", (group_id, sect_id)).fetchone() if not group: return jsonify({"error": "Nhóm chat không tồn tại"}), 404 members_list = [int(x) for x in group["members_csv"].split(",") if x.strip()] if user["id"] not in members_list: return jsonify({"error": "Bạn không có quyền chat trong nhóm này"}), 403 enc_msg = encrypt_message(message) cursor = conn.execute( """INSERT INTO sect_messages (sect_id, sender_id, message, chat_type, target_id, group_id) VALUES (?, ?, ?, ?, ?, ?)""", (sect_id, user["id"], enc_msg, chat_type, target_id, group_id) ) msg_id = cursor.lastrowid conn.commit() return jsonify({"success": True, "msg_id": msg_id}) except Exception as e: logger.error(f"Error sending sect chat: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/chat/groups/create", methods=["POST"]) @jwt_required def create_sect_chat_group(): user = get_current_user() data = request.json or {} name = data.get("name", "").strip() member_ids = data.get("members", []) if not name: return jsonify({"error": "Tên nhóm chat không được để trống"}), 400 conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member: return jsonify({"error": "Bạn chưa gia nhập tông môn"}), 403 sect_id = member["sect_id"] if user["id"] not in member_ids: member_ids.append(user["id"]) valid_members = [] for uid in member_ids: is_valid = conn.execute("SELECT 1 FROM sect_members WHERE user_id = ? AND sect_id = ?", (uid, sect_id)).fetchone() if is_valid: valid_members.append(str(uid)) members_csv = ",".join(valid_members) cursor = conn.execute( "INSERT INTO sect_chat_groups (sect_id, name, creator_id, members_csv) VALUES (?, ?, ?, ?)", (sect_id, name, user["id"], members_csv) ) group_id = cursor.lastrowid conn.commit() return jsonify({"success": True, "message": f"Tạo nhóm chat '{name}' thành công!", "group_id": group_id}) except Exception as e: logger.error(f"Error creating sect chat group: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/chat/groups/list", methods=["GET"]) @jwt_required def list_sect_chat_groups(): user = get_current_user() conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member: return jsonify({"error": "Bạn chưa gia nhập tông môn"}), 403 sect_id = member["sect_id"] groups = conn.execute("SELECT * FROM sect_chat_groups WHERE sect_id = ?", (sect_id,)).fetchall() my_groups = [] for g in groups: members_list = [int(x) for x in g["members_csv"].split(",") if x.strip()] if user["id"] in members_list: my_groups.append(serialize_row(g)) return jsonify({"groups": my_groups}) except Exception as e: logger.error(f"Error listing sect groups: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/contribute", methods=["POST"]) @jwt_required def contribute_sect(): user = get_current_user() data = request.json or {} amount = int(data.get("amount", 0)) if amount <= 0: return jsonify({"error": "Số lượng linh thạch cống hiến phải lớn hơn 0"}), 400 conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member: return jsonify({"error": "Bạn không ở trong tông môn nào"}), 400 sect_id = member["sect_id"] # Increase user contribution conn.execute( "UPDATE sect_members SET contribution = contribution + ? WHERE user_id = ? AND sect_id = ?", (amount, user["id"], sect_id) ) # Increase sect total contribution conn.execute( "UPDATE sects SET contribution = contribution + ? WHERE id = ?", (amount, sect_id) ) # Calculate new level (1000 contribution per level) sect = conn.execute("SELECT contribution FROM sects WHERE id = ?", (sect_id,)).fetchone() new_level = max(1, int(sect["contribution"] / 1000) + 1) conn.execute("UPDATE sects SET level = ? WHERE id = ?", (new_level, sect_id)) conn.commit() return jsonify({ "success": True, "message": f"Cống hiến thành công {amount} linh thạch!", "new_level": new_level }) except Exception as e: logger.error(f"Error contributing to sect: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/kick", methods=["POST"]) @jwt_required def kick_member(): user = get_current_user() data = request.json or {} target_user_id = data.get("user_id") if not target_user_id: return jsonify({"error": "Thiếu user_id thành viên cần trục xuất"}), 400 conn = get_user_db_conn() try: # Check if user is leader/elder member = conn.execute("SELECT sect_id, role FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member or member["role"] not in ['leader', 'elder']: return jsonify({"error": "Bạn không đủ quyền hạn (Chỉ Tông chủ hoặc Trưởng lão)"}), 403 sect_id = member["sect_id"] # Check target target = conn.execute("SELECT role FROM sect_members WHERE user_id = ? AND sect_id = ?", (target_user_id, sect_id)).fetchone() if not target: return jsonify({"error": "Thành viên này không thuộc tông môn của bạn"}), 400 # Elder cannot kick leader or another elder if member["role"] == 'elder' and target["role"] in ['leader', 'elder']: return jsonify({"error": "Trưởng lão không thể trục xuất Tông chủ hoặc Trưởng lão khác"}), 403 if target["role"] == 'leader': return jsonify({"error": "Không thể trục xuất Tông chủ"}), 400 conn.execute("DELETE FROM sect_members WHERE user_id = ? AND sect_id = ?", (target_user_id, sect_id)) conn.commit() return jsonify({"success": True, "message": "Đã trục xuất thành viên ra khỏi tông môn!"}) except Exception as e: logger.error(f"Error kicking member: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() # ========================================== # ADVANCED SECTS & ALLIANCES FEATURES # ========================================== @sects_bp.route("/api/sects/announcement", methods=["POST"]) @jwt_required def update_announcement(): user = get_current_user() data = request.json or {} announcement = data.get("announcement", "").strip() conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id, role FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member or member["role"] not in ['leader', 'elder']: return jsonify({"error": "Chỉ Tông chủ hoặc Trưởng lão mới có quyền đổi thông cáo"}), 403 conn.execute("UPDATE sects SET announcement = ? WHERE id = ?", (announcement, member["sect_id"])) conn.commit() return jsonify({"success": True, "message": "Cập nhật thông cáo tông môn thành công!"}) except Exception as e: logger.error(f"Error updating announcement: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/promote", methods=["POST"]) @jwt_required def promote_member(): user = get_current_user() data = request.json or {} target_user_id = data.get("user_id") new_role = data.get("role", "member").lower() # 'elder' or 'member' if new_role not in ['elder', 'member']: return jsonify({"error": "Chức vị không hợp lệ"}), 400 conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id, role FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member or member["role"] != 'leader': return jsonify({"error": "Chỉ có Tông chủ mới được thăng/giáng chức đệ tử"}), 403 sect_id = member["sect_id"] target = conn.execute("SELECT role FROM sect_members WHERE user_id = ? AND sect_id = ?", (target_user_id, sect_id)).fetchone() if not target: return jsonify({"error": "Thành viên này không thuộc tông môn của bạn"}), 400 if target["role"] == 'leader': return jsonify({"error": "Không thể thay đổi chức vị của Tông chủ"}), 400 conn.execute("UPDATE sect_members SET role = ? WHERE user_id = ? AND sect_id = ?", (new_role, target_user_id, sect_id)) conn.commit() role_name = "Trưởng Lão" if new_role == 'elder' else "Môn đồ" return jsonify({"success": True, "message": f"Đã thăng/giáng chức thành viên thành {role_name}!"}) except Exception as e: logger.error(f"Error promoting member: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/requests/list", methods=["GET"]) @jwt_required def list_join_requests(): user = get_current_user() conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id, role FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member or member["role"] not in ['leader', 'elder']: return jsonify({"error": "Chỉ Tông chủ hoặc Trưởng lão mới xem được danh sách yêu cầu"}), 403 rows = conn.execute(""" SELECT r.id, r.user_id, r.status, r.created_at, u.username, u.avatar FROM sect_join_requests r LEFT JOIN users u ON r.user_id = u.id WHERE r.sect_id = ? AND r.status = 'pending' ORDER BY r.created_at DESC """, (member["sect_id"],)).fetchall() return jsonify({"requests": [serialize_row(r) for r in rows]}) except Exception as e: logger.error(f"Error listing requests: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/requests/respond", methods=["POST"]) @jwt_required def respond_join_request(): user = get_current_user() data = request.json or {} req_id = data.get("request_id") action = data.get("action", "").lower() # 'approve' or 'reject' if not req_id or action not in ['approve', 'reject']: return jsonify({"error": "Dữ liệu phản hồi không hợp lệ"}), 400 conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id, role FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member or member["role"] not in ['leader', 'elder']: return jsonify({"error": "Chỉ Tông chủ hoặc Trưởng lão mới được phê duyệt"}), 403 sect_id = member["sect_id"] req = conn.execute("SELECT user_id FROM sect_join_requests WHERE id = ? AND sect_id = ?", (req_id, sect_id)).fetchone() if not req: return jsonify({"error": "Yêu cầu gia nhập không tồn tại hoặc đã được xử lý"}), 404 target_user_id = req["user_id"] if action == 'approve': # Check if target is already in a sect already_in = conn.execute("SELECT sect_id FROM sect_members WHERE user_id = ?", (target_user_id,)).fetchone() if already_in: conn.execute("DELETE FROM sect_join_requests WHERE id = ?", (req_id,)) conn.commit() return jsonify({"error": "Người dùng này đã gia nhập một tông môn khác, yêu cầu tự động hủy."}), 400 # Add member conn.execute( "INSERT INTO sect_members (sect_id, user_id, role, contribution) VALUES (?, ?, 'member', 0)", (sect_id, target_user_id) ) # Delete target requests from all other sects conn.execute("DELETE FROM sect_join_requests WHERE user_id = ?", (target_user_id,)) msg = "Đã phê duyệt gia nhập tông môn!" else: # Reject conn.execute("DELETE FROM sect_join_requests WHERE id = ?", (req_id,)) msg = "Đã từ chối yêu cầu gia nhập!" conn.commit() return jsonify({"success": True, "message": msg}) except Exception as e: logger.error(f"Error responding join request: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() # ========================================== # SECT LIBRARY (BOOK SHELF SHARING) # ========================================== @sects_bp.route("/api/sects/library/list", methods=["GET"]) @jwt_required def list_sect_library(): user = get_current_user() conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member: return jsonify({"error": "Bạn chưa gia nhập tông môn"}), 403 sect_id = member["sect_id"] # We need to join with the main books database to fetch book title and cover # Since SQLite can ATTACH databases, or we can fetch metadata, we'll fetch from sect_books # and query titles from books database. Let's do it cleanly: rows = conn.execute(""" SELECT sb.id, sb.book_id, sb.added_at, u.username as added_by_name FROM sect_books sb LEFT JOIN users u ON sb.added_by = u.id WHERE sb.sect_id = ? ORDER BY sb.added_at DESC """, (sect_id,)).fetchall() from backend.database.db_manager import get_db main_conn = get_db() book_list = [] for r in rows: book_row = main_conn.execute(""" SELECT id, title, title_vietphrase, title_hanviet, author, cover, description FROM books WHERE id = ? """, (r["book_id"],)).fetchone() if book_row: b = serialize_row(book_row) b["added_by_name"] = r["added_by_name"] b["added_at"] = r["added_at"].isoformat() if hasattr(r["added_at"], "isoformat") else r["added_at"] book_list.append(b) return jsonify({"books": book_list}) except Exception as e: logger.error(f"Error listing sect library: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/library/add", methods=["POST"]) @jwt_required def add_to_sect_library(): user = get_current_user() data = request.json or {} book_id = data.get("book_id") if not book_id: return jsonify({"error": "Thiếu book_id truyện đóng góp"}), 400 conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member: return jsonify({"error": "Bạn không thuộc tông môn nào"}), 400 sect_id = member["sect_id"] # Check if already added exists = conn.execute("SELECT id FROM sect_books WHERE sect_id = ? AND book_id = ?", (sect_id, book_id)).fetchone() if exists: return jsonify({"error": "Truyện này đã được đệ tử khác đóng góp vào thư viện tông môn từ trước"}), 400 conn.execute( "INSERT INTO sect_books (sect_id, book_id, added_by) VALUES (?, ?, ?)", (sect_id, book_id, user["id"]) ) # Reward user 20 contribution points for sharing a book conn.execute( "UPDATE sect_members SET contribution = contribution + 20 WHERE user_id = ? AND sect_id = ?", (user["id"], sect_id) ) # Increase sect total contribution conn.execute( "UPDATE sects SET contribution = contribution + 20 WHERE id = ?", (sect_id,) ) conn.commit() return jsonify({"success": True, "message": "Chia sẻ truyện vào Thư viện Tông môn thành công! Nhận 20 điểm cống hiến."}) except Exception as e: logger.error(f"Error adding to sect library: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/library/remove", methods=["POST"]) @jwt_required def remove_from_sect_library(): user = get_current_user() data = request.json or {} book_id = data.get("book_id") if not book_id: return jsonify({"error": "Thiếu book_id truyện cần gỡ"}), 400 conn = get_user_db_conn() try: member = conn.execute("SELECT sect_id, role FROM sect_members WHERE user_id = ?", (user["id"],)).fetchone() if not member: return jsonify({"error": "Bạn không thuộc tông môn nào"}), 400 sect_id = member["sect_id"] # Check who added it book = conn.execute("SELECT added_by FROM sect_books WHERE sect_id = ? AND book_id = ?", (sect_id, book_id)).fetchone() if not book: return jsonify({"error": "Truyện không có trong thư viện tông môn"}), 404 # Only leader, elder, or the original adder can remove if member["role"] not in ['leader', 'elder'] and book["added_by"] != user["id"]: return jsonify({"error": "Bạn không có quyền gỡ truyện này (Chỉ Tông chủ, Trưởng lão hoặc người đóng góp)"}), 403 conn.execute("DELETE FROM sect_books WHERE sect_id = ? AND book_id = ?", (sect_id, book_id)) conn.commit() return jsonify({"success": True, "message": "Đã gỡ truyện khỏi thư viện Tông môn!"}) except Exception as e: logger.error(f"Error removing from sect library: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() # ========================================== # SECT SEARCH & PROFILE BY ID # ========================================== ROLE_ORDER = { "leader": 1, "vice_leader": 2, "elder": 3, "inner_disciple": 4, "member": 5, } ROLE_LABELS = { "leader": "Tông chủ", "vice_leader": "Phó Tông chủ", "elder": "Trưởng lão", "inner_disciple": "Nội môn đệ tử", "member": "Ngoại môn đệ tử", } VALID_ROLES = list(ROLE_ORDER.keys()) @sects_bp.route("/api/sects/search", methods=["GET"]) def search_sects(): """Search sects by name or slogan. No auth required.""" q = request.args.get("q", "").strip() page = int(request.args.get("page", 1)) per_page = int(request.args.get("per_page", 20)) offset = (page - 1) * per_page conn = get_user_db_conn() try: if q: rows = conn.execute(""" SELECT s.id, s.name, s.slogan, s.badge, s.level, s.contribution, s.created_at, u.username as leader_name, (SELECT COUNT(*) FROM sect_members WHERE sect_id = s.id) as member_count FROM sects s LEFT JOIN users u ON s.leader_id = u.id WHERE s.name LIKE ? OR s.slogan LIKE ? ORDER BY s.contribution DESC, s.created_at DESC LIMIT ? OFFSET ? """, (f"%{q}%", f"%{q}%", per_page, offset)).fetchall() total = conn.execute( "SELECT COUNT(*) as cnt FROM sects WHERE name LIKE ? OR slogan LIKE ?", (f"%{q}%", f"%{q}%") ).fetchone()["cnt"] else: rows = conn.execute(""" SELECT s.id, s.name, s.slogan, s.badge, s.level, s.contribution, s.created_at, u.username as leader_name, (SELECT COUNT(*) FROM sect_members WHERE sect_id = s.id) as member_count FROM sects s LEFT JOIN users u ON s.leader_id = u.id ORDER BY s.contribution DESC, s.created_at DESC LIMIT ? OFFSET ? """, (per_page, offset)).fetchall() total = conn.execute("SELECT COUNT(*) as cnt FROM sects").fetchone()["cnt"] return jsonify({ "sects": [serialize_row(r) for r in rows], "total": total, "page": page, "per_page": per_page, "total_pages": max(1, (total + per_page - 1) // per_page) }) except Exception as e: logger.error(f"Error searching sects: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/", methods=["GET"]) def get_sect_by_id(sect_id): """View full sect profile by ID. No auth required for basic info.""" conn = get_user_db_conn() try: sect = conn.execute(""" SELECT s.id, s.name, s.slogan, s.announcement, s.badge, s.level, s.contribution, s.created_at, u.username as leader_name, u.id as leader_id, u.avatar as leader_avatar FROM sects s LEFT JOIN users u ON s.leader_id = u.id WHERE s.id = ? """, (sect_id,)).fetchone() if not sect: return jsonify({"error": "Tông môn không tồn tại"}), 404 members = conn.execute(""" SELECT sm.user_id, sm.role, sm.contribution, sm.joined_at, u.username, u.avatar, COALESCE(sm.role, 'member') as role FROM sect_members sm LEFT JOIN users u ON sm.user_id = u.id WHERE sm.sect_id = ? ORDER BY CASE sm.role WHEN 'leader' THEN 1 WHEN 'vice_leader' THEN 2 WHEN 'elder' THEN 3 WHEN 'inner_disciple' THEN 4 ELSE 5 END, sm.contribution DESC """, (sect_id,)).fetchall() # Enrich members with Vietnamese role labels member_list = [] for m in members: md = serialize_row(m) md["role_label"] = ROLE_LABELS.get(m["role"], "Môn đồ") member_list.append(md) # Count by role role_counts = {} for m in member_list: r = m["role"] role_counts[r] = role_counts.get(r, 0) + 1 return jsonify({ "sect": serialize_row(sect), "members": member_list, "member_count": len(member_list), "role_counts": role_counts, "role_labels": ROLE_LABELS, }) except Exception as e: logger.error(f"Error fetching sect {sect_id}: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() # ========================================== # FULL RANK PROMOTION SYSTEM (5 Ranks) # ========================================== @sects_bp.route("/api/sects/promote/rank", methods=["POST"]) @jwt_required def promote_member_rank(): """ Full 5-rank promotion system: leader > vice_leader > elder > inner_disciple > member Only leader can set: vice_leader, elder, inner_disciple, member vice_leader can set: elder, inner_disciple, member elder can set: inner_disciple, member """ user = get_current_user() data = request.json or {} target_user_id = data.get("user_id") new_role = data.get("role", "").lower() if new_role not in VALID_ROLES or new_role == "leader": return jsonify({ "error": f"Chức vị không hợp lệ. Các chức vị có thể gán: {', '.join(VALID_ROLES[1:])}", "valid_roles": VALID_ROLES[1:], "role_labels": ROLE_LABELS, }), 400 if not target_user_id: return jsonify({"error": "Thiếu user_id"}), 400 conn = get_user_db_conn() try: caller = conn.execute( "SELECT sect_id, role FROM sect_members WHERE user_id = ?", (user["id"],) ).fetchone() if not caller: return jsonify({"error": "Bạn chưa gia nhập tông môn"}), 403 sect_id = caller["sect_id"] caller_role = caller["role"] caller_order = ROLE_ORDER.get(caller_role, 99) new_role_order = ROLE_ORDER.get(new_role, 99) # Permission matrix: caller must outrank new_role if caller_order >= new_role_order: return jsonify({ "error": f"Bạn ({ROLE_LABELS.get(caller_role)}) không đủ quyền gán chức vị {ROLE_LABELS.get(new_role)}" }), 403 target = conn.execute( "SELECT role FROM sect_members WHERE user_id = ? AND sect_id = ?", (target_user_id, sect_id) ).fetchone() if not target: return jsonify({"error": "Thành viên không thuộc tông môn của bạn"}), 404 if target["role"] == "leader": return jsonify({"error": "Không thể thay đổi chức vị của Tông chủ"}), 400 target_order = ROLE_ORDER.get(target["role"], 99) if caller_order >= target_order: return jsonify({ "error": f"Bạn ({ROLE_LABELS.get(caller_role)}) không thể thay đổi chức vị của người ngang hoặc cao hơn bạn" }), 403 conn.execute( "UPDATE sect_members SET role = ? WHERE user_id = ? AND sect_id = ?", (new_role, target_user_id, sect_id) ) conn.commit() return jsonify({ "success": True, "message": f"Đã gán chức vị {ROLE_LABELS.get(new_role)} thành công!", "new_role": new_role, "new_role_label": ROLE_LABELS.get(new_role), }) except Exception as e: logger.error(f"Error promoting rank: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/members", methods=["GET"]) @jwt_required def list_sect_members(): """List all members of current user's sect with full role info.""" user = get_current_user() role_filter = request.args.get("role") conn = get_user_db_conn() try: member = conn.execute( "SELECT sect_id FROM sect_members WHERE user_id = ?", (user["id"],) ).fetchone() if not member: return jsonify({"error": "Bạn chưa gia nhập tông môn"}), 403 sect_id = member["sect_id"] if role_filter and role_filter in VALID_ROLES: rows = conn.execute(""" SELECT sm.user_id, sm.role, sm.contribution, sm.joined_at, u.username, u.avatar FROM sect_members sm LEFT JOIN users u ON sm.user_id = u.id WHERE sm.sect_id = ? AND sm.role = ? ORDER BY sm.contribution DESC """, (sect_id, role_filter)).fetchall() else: rows = conn.execute(""" SELECT sm.user_id, sm.role, sm.contribution, sm.joined_at, u.username, u.avatar FROM sect_members sm LEFT JOIN users u ON sm.user_id = u.id WHERE sm.sect_id = ? ORDER BY CASE sm.role WHEN 'leader' THEN 1 WHEN 'vice_leader' THEN 2 WHEN 'elder' THEN 3 WHEN 'inner_disciple' THEN 4 ELSE 5 END, sm.contribution DESC """, (sect_id,)).fetchall() member_list = [] for r in rows: rd = serialize_row(r) rd["role_label"] = ROLE_LABELS.get(r["role"], "Môn đồ") member_list.append(rd) return jsonify({ "members": member_list, "total": len(member_list), "role_labels": ROLE_LABELS, "valid_roles": VALID_ROLES, }) except Exception as e: logger.error(f"Error listing members: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() # ========================================== # SECT CHAT GROUPS – MANAGE MEMBERS # ========================================== @sects_bp.route("/api/sects/chat/groups//members/add", methods=["POST"]) @jwt_required def add_to_chat_group(group_id): """Add member(s) to a sub-group chat.""" user = get_current_user() data = request.json or {} new_member_ids = data.get("user_ids", []) conn = get_user_db_conn() try: my_member = conn.execute( "SELECT sect_id, role FROM sect_members WHERE user_id = ?", (user["id"],) ).fetchone() if not my_member: return jsonify({"error": "Bạn chưa gia nhập tông môn"}), 403 sect_id = my_member["sect_id"] group = conn.execute( "SELECT id, creator_id, members_csv FROM sect_chat_groups WHERE id = ? AND sect_id = ?", (group_id, sect_id) ).fetchone() if not group: return jsonify({"error": "Nhóm chat không tồn tại"}), 404 # Only group creator, leader, vice_leader, elder can add if group["creator_id"] != user["id"] and my_member["role"] not in ["leader", "vice_leader", "elder"]: return jsonify({"error": "Chỉ người tạo nhóm hoặc Ban quản lý mới có thể thêm thành viên"}), 403 current_members = [int(x) for x in group["members_csv"].split(",") if x.strip()] added = [] for uid in new_member_ids: uid = int(uid) if uid not in current_members: # Check uid is in sect in_sect = conn.execute( "SELECT 1 FROM sect_members WHERE user_id = ? AND sect_id = ?", (uid, sect_id) ).fetchone() if in_sect: current_members.append(uid) added.append(uid) new_csv = ",".join(str(x) for x in current_members) conn.execute( "UPDATE sect_chat_groups SET members_csv = ? WHERE id = ?", (new_csv, group_id) ) conn.commit() return jsonify({"success": True, "added": added, "members": current_members}) except Exception as e: logger.error(f"Error adding to chat group: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/chat/groups//members/remove", methods=["POST"]) @jwt_required def remove_from_chat_group(group_id): """Remove a member from a sub-group chat.""" user = get_current_user() data = request.json or {} target_id = data.get("user_id") conn = get_user_db_conn() try: my_member = conn.execute( "SELECT sect_id, role FROM sect_members WHERE user_id = ?", (user["id"],) ).fetchone() if not my_member: return jsonify({"error": "Bạn chưa gia nhập tông môn"}), 403 sect_id = my_member["sect_id"] group = conn.execute( "SELECT id, creator_id, members_csv FROM sect_chat_groups WHERE id = ? AND sect_id = ?", (group_id, sect_id) ).fetchone() if not group: return jsonify({"error": "Nhóm chat không tồn tại"}), 404 if group["creator_id"] != user["id"] and my_member["role"] not in ["leader", "vice_leader", "elder"]: return jsonify({"error": "Không đủ quyền xóa thành viên khỏi nhóm chat"}), 403 current_members = [int(x) for x in group["members_csv"].split(",") if x.strip()] if int(target_id) not in current_members: return jsonify({"error": "Thành viên không có trong nhóm chat này"}), 400 current_members.remove(int(target_id)) new_csv = ",".join(str(x) for x in current_members) conn.execute("UPDATE sect_chat_groups SET members_csv = ? WHERE id = ?", (new_csv, group_id)) conn.commit() return jsonify({"success": True, "members": current_members}) except Exception as e: logger.error(f"Error removing from chat group: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/chat/groups/", methods=["GET"]) @jwt_required def get_chat_group_info(group_id): """Get info and member list of a sub-group chat.""" user = get_current_user() conn = get_user_db_conn() try: my_member = conn.execute( "SELECT sect_id FROM sect_members WHERE user_id = ?", (user["id"],) ).fetchone() if not my_member: return jsonify({"error": "Bạn chưa gia nhập tông môn"}), 403 sect_id = my_member["sect_id"] group = conn.execute( "SELECT * FROM sect_chat_groups WHERE id = ? AND sect_id = ?", (group_id, sect_id) ).fetchone() if not group: return jsonify({"error": "Nhóm chat không tồn tại"}), 404 member_ids = [int(x) for x in group["members_csv"].split(",") if x.strip()] if user["id"] not in member_ids: return jsonify({"error": "Bạn không phải thành viên của nhóm chat này"}), 403 # Fetch member details members = [] for uid in member_ids: row = conn.execute(""" SELECT sm.user_id, sm.role, sm.contribution, u.username, u.avatar FROM sect_members sm LEFT JOIN users u ON sm.user_id = u.id WHERE sm.user_id = ? AND sm.sect_id = ? """, (uid, sect_id)).fetchone() if row: rd = serialize_row(row) rd["role_label"] = ROLE_LABELS.get(row["role"], "Môn đồ") members.append(rd) return jsonify({ "group": serialize_row(group), "members": members, "member_count": len(members), }) except Exception as e: logger.error(f"Error fetching chat group: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close() @sects_bp.route("/api/sects/chat/groups", methods=["GET"]) @jwt_required def list_all_sect_chat_groups(): """List all sub-group chats the current user is part of in their sect.""" user = get_current_user() conn = get_user_db_conn() try: my_member = conn.execute( "SELECT sect_id FROM sect_members WHERE user_id = ?", (user["id"],) ).fetchone() if not my_member: return jsonify({"error": "Bạn chưa gia nhập tông môn"}), 403 sect_id = my_member["sect_id"] groups = conn.execute( "SELECT id, name, creator_id, members_csv, created_at FROM sect_chat_groups WHERE sect_id = ?", (sect_id,) ).fetchall() result = [] for g in groups: member_ids = [int(x) for x in g["members_csv"].split(",") if x.strip()] if user["id"] in member_ids: gd = serialize_row(g) gd["member_count"] = len(member_ids) result.append(gd) return jsonify({"groups": result}) except Exception as e: logger.error(f"Error listing chat groups: {e}") return jsonify({"error": str(e)}), 500 finally: conn.close()