from flask import Blueprint, flash, jsonify, redirect, request, url_for from flask_login import current_user, login_required from app.db import get_db friends_bp = Blueprint("friends", __name__) def _get_friendship_row(user_a_id: int, user_b_id: int): db = get_db() return db.execute( """ SELECT requester_id, addressee_id, status FROM friendships WHERE (requester_id = ? AND addressee_id = ?) OR (requester_id = ? AND addressee_id = ?) LIMIT 1 """, (user_a_id, user_b_id, user_b_id, user_a_id), ).fetchone() def _send_friend_request_or_accept(addressee_id: int): if addressee_id == current_user.id: return {"error": "cannot send a request to yourself"}, 400 db = get_db() user_exists = db.execute( "SELECT id FROM users WHERE id = ?", (addressee_id,), ).fetchone() if not user_exists: return {"error": "user not found"}, 404 friendship = _get_friendship_row(current_user.id, addressee_id) if friendship: if friendship["status"] == "accepted": return {"error": "already friends"}, 409 if friendship["status"] == "blocked": return {"error": "cannot send request"}, 403 if friendship["status"] == "pending": if friendship["addressee_id"] == current_user.id: db.execute( """ UPDATE friendships SET status = 'accepted' WHERE requester_id = ? AND addressee_id = ? """, (addressee_id, current_user.id), ) db.commit() return {"status": "accepted"}, 200 return {"error": "request already sent"}, 409 db.execute( """ INSERT INTO friendships (requester_id, addressee_id, status) VALUES (?, ?, 'pending') """, (current_user.id, addressee_id), ) db.commit() return {"status": "pending"}, 201 def _accept_friend_request(requester_id: int): db = get_db() updated = db.execute( """ UPDATE friendships SET status = 'accepted' WHERE requester_id = ? AND addressee_id = ? AND status = 'pending' """, (requester_id, current_user.id), ).rowcount if updated == 0: return {"error": "request not found"}, 404 db.commit() return {"status": "accepted"}, 200 def _decline_friend_request(requester_id: int): db = get_db() deleted = db.execute( """ DELETE FROM friendships WHERE requester_id = ? AND addressee_id = ? AND status = 'pending' """, (requester_id, current_user.id), ).rowcount if deleted == 0: return {"error": "request not found"}, 404 db.commit() return {"status": "declined"}, 200 def _cancel_outgoing_friend_request(addressee_id: int): db = get_db() deleted = db.execute( """ DELETE FROM friendships WHERE requester_id = ? AND addressee_id = ? AND status = 'pending' """, (current_user.id, addressee_id), ).rowcount if deleted == 0: return {"error": "request not found"}, 404 db.commit() return {"status": "canceled"}, 200 def _friends_page_data(search_query: str = ""): db = get_db() friends = db.execute( """ SELECT DISTINCT u.id, u.username, CASE WHEN u.last_seen_at IS NOT NULL AND u.last_seen_at >= datetime('now', '-35 seconds') THEN 1 ELSE 0 END AS is_online FROM friendships f JOIN users u ON ( (f.requester_id = ? AND f.addressee_id = u.id) OR (f.addressee_id = ? AND f.requester_id = u.id) ) WHERE f.status = 'accepted' ORDER BY u.username COLLATE NOCASE ASC """, (current_user.id, current_user.id), ).fetchall() incoming = db.execute( """ SELECT f.requester_id AS id, u.username FROM friendships f JOIN users u ON u.id = f.requester_id WHERE f.addressee_id = ? AND f.status = 'pending' ORDER BY u.username COLLATE NOCASE ASC """, (current_user.id,), ).fetchall() outgoing = db.execute( """ SELECT f.addressee_id AS id, u.username FROM friendships f JOIN users u ON u.id = f.addressee_id WHERE f.requester_id = ? AND f.status = 'pending' ORDER BY u.username COLLATE NOCASE ASC """, (current_user.id,), ).fetchall() search_results = [] normalized_query = search_query.strip() if len(normalized_query) >= 2: like_query = f"%{normalized_query}%" rows = db.execute( """ SELECT u.id, u.username FROM users u WHERE u.id != ? AND u.username LIKE ? ORDER BY u.username COLLATE NOCASE ASC LIMIT 20 """, (current_user.id, like_query), ).fetchall() for row in rows: relation = "none" friendship = _get_friendship_row(current_user.id, row["id"]) if friendship: if friendship["status"] == "accepted": relation = "accepted" elif friendship["status"] == "pending": relation = ( "incoming" if friendship["addressee_id"] == current_user.id else "outgoing" ) else: relation = friendship["status"] search_results.append( { "id": row["id"], "username": row["username"], "relation": relation, } ) return { "friends": friends, "incoming_requests": incoming, "outgoing_requests": outgoing, "search_results": search_results, "search_query": normalized_query, } @friends_bp.route("/friends/request", methods=["POST"]) @login_required def request_page_action(): try: addressee_id = int(request.form.get("addressee_id", "")) except ValueError: flash("Invalid user id", "error") return redirect(url_for("main.friends", q=request.form.get("q", ""))) payload, status = _send_friend_request_or_accept(addressee_id) if status in (200, 201): flash("Friend request updated", "success") else: flash(payload["error"], "error") return redirect(url_for("main.friends", q=request.form.get("q", ""))) @friends_bp.route("/friends/requests//accept", methods=["POST"]) @login_required def accept_page_action(requester_id: int): payload, status = _accept_friend_request(requester_id) if status == 200: flash("Friend request accepted", "success") else: flash(payload["error"], "error") return redirect(url_for("main.friends", q=request.form.get("q", ""))) @friends_bp.route("/friends/requests//decline", methods=["POST"]) @login_required def decline_page_action(requester_id: int): payload, status = _decline_friend_request(requester_id) if status == 200: flash("Friend request declined", "success") else: flash(payload["error"], "error") return redirect(url_for("main.friends", q=request.form.get("q", ""))) @friends_bp.route("/friends/requests//cancel", methods=["POST"]) @login_required def cancel_page_action(addressee_id: int): payload, status = _cancel_outgoing_friend_request(addressee_id) if status == 200: flash("Outgoing request canceled", "success") else: flash(payload["error"], "error") return redirect(url_for("main.friends", q=request.form.get("q", ""))) @friends_bp.route("/api/friends", methods=["GET"]) @login_required def list_friends(): data = _friends_page_data("") return jsonify({"friends": [dict(row) for row in data["friends"]]}) @friends_bp.route("/api/friends/search", methods=["GET"]) @login_required def search_people(): data = _friends_page_data(request.args.get("q", "")) return jsonify({"results": data["search_results"]}) @friends_bp.route("/api/friends/requests/incoming", methods=["GET"]) @login_required def incoming_friend_requests(): data = _friends_page_data("") return jsonify({"requests": [dict(row) for row in data["incoming_requests"]]}) @friends_bp.route("/api/friends/requests/outgoing", methods=["GET"]) @login_required def outgoing_friend_requests(): data = _friends_page_data("") return jsonify({"requests": [dict(row) for row in data["outgoing_requests"]]}) @friends_bp.route("/api/friends/requests", methods=["POST"]) @login_required def send_friend_request(): payload = request.get_json(silent=True) or {} addressee_id = payload.get("addressee_id") if not isinstance(addressee_id, int): return jsonify({"error": "addressee_id must be an integer"}), 400 response_payload, status = _send_friend_request_or_accept(addressee_id) return jsonify(response_payload), status @friends_bp.route("/api/friends/requests//accept", methods=["POST"]) @login_required def accept_friend_request(requester_id: int): payload, status = _accept_friend_request(requester_id) return jsonify(payload), status @friends_bp.route("/api/friends/requests//decline", methods=["POST"]) @login_required def decline_friend_request(requester_id: int): payload, status = _decline_friend_request(requester_id) return jsonify(payload), status