# friends.roblox.com from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify, abort, after_this_request from app.util import auth, websiteFeatures, friends from app.models.user import User from app.models.friend_relationship import FriendRelationship from app.models.friend_request import FriendRequest from app.models.follow_relationship import FollowRelationship from app.extensions import limiter, db, redis_controller, get_remote_address, csrf, user_limiter from flask_wtf.csrf import CSRFError, generate_csrf from sqlalchemy import or_ from datetime import datetime, timedelta from app.services.user_relationships import followings FriendsAPIRoute = Blueprint('friendsapi', __name__, url_prefix="/") csrf.exempt(FriendsAPIRoute) @FriendsAPIRoute.errorhandler(CSRFError) def handle_csrf_error(e): ErrorResponse = make_response(jsonify({ "errors": [ { "code": 0, "message": "Token Validation Failed" } ] })) ErrorResponse.status_code = 403 ErrorResponse.headers["x-csrf-token"] = generate_csrf() return ErrorResponse @FriendsAPIRoute.errorhandler(429) def handle_ratelimit_reached(e): return jsonify({ "errors": [ { "code": 9, "message": "The flood limit has been exceeded." } ] }), 429 @FriendsAPIRoute.before_request def before_request(): if "Roblox/" not in request.user_agent.string: csrf.protect() @FriendsAPIRoute.route('/v1/my/friends/count', methods=['GET']) @limiter.limit("60/minute") @auth.authenticated_required_api def GetFriendCount(): AuthenticatedUser : User = auth.GetCurrentUser() return jsonify( { "count": friends.GetFriendCount( AuthenticatedUser.id ) } ), 200 @FriendsAPIRoute.route('/v1/users//friends/count', methods=['GET']) @limiter.limit("60/minute") def GetFriendCountByUserId( userId : int ): UserObj : User = User.query.filter_by( id = userId ).first() if UserObj is None: return jsonify( { "errors": [ { "code": 1, "message": "User not found" } ] } ), 404 return jsonify( { "count": friends.GetFriendCount( userId ) } ), 200 @FriendsAPIRoute.route("/v1/user/friend-requests/count", methods=["GET"]) @limiter.limit("60/minute") @auth.authenticated_required_api def GetMyFriendRequestsCount(): AuthenticatedUser : User = auth.GetCurrentUser() FriendRequestCount : int = max(FriendRequest.query.filter_by( requestee_id = AuthenticatedUser.id ).count(), 500) return jsonify( { "count": FriendRequestCount } ), 200 @FriendsAPIRoute.route("/v1/users//friends", methods=["GET"]) @limiter.limit("60/minute") def get_user_friends( userid : int ): UserObj : User = User.query.filter_by(id=userid).first() if UserObj is None: return jsonify({"success": False, "message": "Invalid request"}),400 FriendRelationshipList : list[FriendRelationship] = FriendRelationship.query.filter(or_(FriendRelationship.user_id == userid, FriendRelationship.friend_id == userid)).all() FriendList = [] for FriendRelationshipObj in FriendRelationshipList: FriendObj : User = User.query.filter_by(id=FriendRelationshipObj.user_id if FriendRelationshipObj.user_id != userid else FriendRelationshipObj.friend_id).first() if FriendObj is None: continue FriendList.append({ "isOnline": FriendObj.lastonline > datetime.utcnow() - timedelta(minutes=1), "isDeleted": False, "friendFrequentScore": 0, "friendFrequentRank": 1, "hasVerifiedBadge": False, "description": FriendObj.description, "created": FriendObj.created.isoformat(), "isBanned": False, "externalAppDisplayName": None, "id": FriendObj.id, "name": FriendObj.username, "displayName": FriendObj.username }) return jsonify({ "data": FriendList }) @FriendsAPIRoute.route("/v1/user/friend-requests/decline-all", methods=["POST"]) @limiter.limit("60/minute") @auth.authenticated_required_api def decline_all_friend_requests(): AuthenticatedUser : User = auth.GetCurrentUser() FriendRequestList : list[FriendRequest] = FriendRequest.query.filter_by( requestee_id = AuthenticatedUser.id ).all() for FriendRequestObj in FriendRequestList: db.session.delete( FriendRequestObj ) db.session.commit() return jsonify({}), 200 @FriendsAPIRoute.route("/v1/users//unfriend", methods=["POST"]) @limiter.limit("60/minute") @auth.authenticated_required_api @user_limiter.limit("60/minute") def unfriend_user( userId : int ): AuthenticatedUser : User = auth.GetCurrentUser() UserObj : User = User.query.filter_by(id=userId).first() if UserObj is None: return jsonify({ "errors": [{ "code": 1, "message": "The target user is invalid or does not exist"}] }), 400 FriendRelationshipObj : FriendRelationship = friends.GetFriendRelationship( AuthenticatedUser.id, UserObj.id ) if FriendRelationshipObj is not None: db.session.delete( FriendRelationshipObj ) db.session.commit() return jsonify({}), 200 @FriendsAPIRoute.route("/v1/users//request-friendship", methods=["POST"]) @limiter.limit("15/minute") @auth.authenticated_required_api @user_limiter.limit("15/minute") def request_friendship( userId : int ): AuthenticatedUser : User = auth.GetCurrentUser() UserObj : User = User.query.filter_by(id=userId).first() if UserObj is None: return jsonify({ "errors": [{ "code": 1, "message": "The target user is invalid or does not exist"}] }), 400 if userId == AuthenticatedUser.id: return jsonify({ "errors": [{ "code": 7, "message": "The user cannot be friends with itself."}] }), 400 if friends.GetFriendCount( AuthenticatedUser.id ) >= 200: return jsonify({ "errors": [{ "code": 31, "message": "User with max friends sent friend request."}] }), 400 if friends.IsFriends( AuthenticatedUser.id, UserObj.id ): return jsonify({ "errors": [{ "code": 5, "message": "The target user is already a friend."}] }), 400 OtherFriendRequestObj : FriendRequest = FriendRequest.query.filter_by( requestee_id = AuthenticatedUser.id, requester_id = UserObj.id ).first() if OtherFriendRequestObj is not None: if friends.GetFriendCount( UserObj.id ) >= 200: return jsonify({ "errors": [{ "code": 12, "message": "The target users friends limit has been exceeded."}] }), 400 db.session.delete( OtherFriendRequestObj ) NewFriendRelationshipObj : FriendRelationship = FriendRelationship( user_id = AuthenticatedUser.id, friend_id = UserObj.id ) db.session.add( NewFriendRelationshipObj ) db.session.commit() return jsonify({ "success": True }), 200 FriendRequestObj : FriendRequest = FriendRequest.query.filter_by( requestee_id = UserObj.id, requester_id = AuthenticatedUser.id ).first() if FriendRequestObj is None: FriendRequestObj = FriendRequest( requestee_id = UserObj.id, requester_id = AuthenticatedUser.id ) db.session.add( FriendRequestObj ) db.session.commit() return jsonify({ "success": True }), 200 @FriendsAPIRoute.route("/v1/users//accept-friend-request", methods=["POST"]) @limiter.limit("30/minute") @auth.authenticated_required_api @user_limiter.limit("30/minute") def accept_friend_request( userId : int ): AuthenticatedUser : User = auth.GetCurrentUser() UserObj : User = User.query.filter_by(id=userId).first() if UserObj is None: return jsonify({ "errors": [{ "code": 1, "message": "The target user is invalid or does not exist"}] }), 400 FriendRequestObj : FriendRequest = FriendRequest.query.filter_by( requestee_id = AuthenticatedUser.id, requester_id = UserObj.id ).first() if FriendRequestObj is None: return jsonify({ "errors": [{ "code": 10, "message": "The friend request does not exist."}] }), 400 if friends.GetFriendCount( AuthenticatedUser.id ) >= 200: return jsonify({ "errors": [{ "code": 11, "message": "The current users friends limit has been exceeded."}] }), 400 if friends.GetFriendCount( UserObj.id ) >= 200: return jsonify({ "errors": [{ "code": 12, "message": "The target users friends limit has been exceeded."}] }), 400 db.session.delete( FriendRequestObj ) NewFriendRelationshipObj : FriendRelationship = FriendRelationship( user_id = AuthenticatedUser.id, friend_id = UserObj.id ) db.session.add( NewFriendRelationshipObj ) db.session.commit() return jsonify({}), 200 @FriendsAPIRoute.route("/v1/users//decline-friend-request", methods=["POST"]) @limiter.limit("30/minute") @auth.authenticated_required_api @user_limiter.limit("30/minute") def decline_friend_request( userId : int ): AuthenticatedUser : User = auth.GetCurrentUser() UserObj : User = User.query.filter_by(id=userId).first() if UserObj is None: return jsonify({ "errors": [{ "code": 1, "message": "The target user is invalid or does not exist"}] }), 400 FriendRequestObj : FriendRequest = FriendRequest.query.filter_by( requestee_id = AuthenticatedUser.id, requester_id = UserObj.id ).first() if FriendRequestObj is None: return jsonify({ "errors": [{ "code": 10, "message": "The friend request does not exist."}] }), 400 db.session.delete( FriendRequestObj ) db.session.commit() return jsonify({}), 200 @FriendsAPIRoute.route("/v1/users//followers/count", methods=["GET"]) @limiter.limit("60/minute") def get_user_followers_count( userId : int ): UserObj : User = User.query.filter_by(id=userId).first() if UserObj is None: return jsonify({"success": False, "message": "Invalid request"}),400 return jsonify( { "count": followings.get_follower_count( requested_user = UserObj ) } ), 200 @FriendsAPIRoute.route("/v1/users//followings/count", methods=["GET"]) @limiter.limit("60/minute") def get_user_followings_count( userId : int ): UserObj : User = User.query.filter_by(id=userId).first() if UserObj is None: return jsonify({"success": False, "message": "Invalid request"}),400 return jsonify( { "count": followings.get_following_count( requested_user = UserObj ) } ), 200 @FriendsAPIRoute.route("/v1/user/following-exists", methods=["POST"]) @limiter.limit("60/minute") @auth.authenticated_required_api def following_exists(): AuthenticatedUser : User = auth.GetCurrentUser() if not request.is_json: return jsonify({ "errors": [{ "code": 0, "message": "An invalid userId was passed in."}] }), 400 if "targetUserIds" not in request.json: return jsonify({ "errors": [{ "code": 0, "message": "An invalid userId was passed in."}] }), 400 TargetUserIds : list[int] = request.json["targetUserIds"] ResponseList = [] if len(TargetUserIds) > 100: return jsonify({ "errors": [{ "code": 0, "message": "An invalid userId was passed in."}] }), 400 for TargetUserId in TargetUserIds: if TargetUserId < 1: return jsonify({ "errors": [{ "code": 0, "message": "An invalid userId was passed in."}] }), 400 TargetUser : User = User.query.filter_by( id = TargetUserId ).first() if TargetUser is None: return jsonify({ "errors": [{ "code": 0, "message": "An invalid userId was passed in."}] }), 400 ResponseList.append({ "isFollowing": followings.is_following( follower_user = AuthenticatedUser, followed_user = TargetUser ), "isFollowed": followings.is_following( follower_user = TargetUser, followed_user = AuthenticatedUser ), "userId": TargetUserId }) return jsonify({ "followings": ResponseList }), 200 @FriendsAPIRoute.route("/v1/users//unfollow", methods=["POST"]) @limiter.limit("60/minute") @auth.authenticated_required_api @user_limiter.limit("60/minute") def unfollow_user( userId : int ): AuthenticatedUser : User = auth.GetCurrentUser() UserObj : User = User.query.filter_by(id=userId).first() if UserObj is None: return jsonify({ "errors": [{ "code": 1, "message": "The target user is invalid or does not exist"}] }), 400 try: followings.unfollow_user( current_follower = AuthenticatedUser, followed_user = UserObj ) except followings.FollowingExceptions.UserNotFollowing: return jsonify({}), 200 except followings.FollowingExceptions.UserRateLimited: return jsonify({ "errors": [{ "code": 9, "message": "The flood limit has been exceeded."}] }), 429 except followings.FollowingExceptions.FollowingIsDisabled: return jsonify({ "errors": [{ "code": 0, "message": "Following is disabled."}] }), 500 return jsonify({}), 200 @FriendsAPIRoute.route("/v1/users//follow", methods=["POST"]) @limiter.limit("10/minute") @auth.authenticated_required_api @user_limiter.limit("10/minute") def follow_user( userId : int ): AuthenticatedUser : User = auth.GetCurrentUser() UserObj : User = User.query.filter_by(id=userId).first() if UserObj is None: return jsonify({ "errors": [{ "code": 1, "message": "The target user is invalid or does not exist"}] }), 400 if userId == AuthenticatedUser.id: return jsonify({ "errors": [{ "code": 8, "message": "The user cannot follow itself."}] }), 400 try: followings.follow_user( follow_user = AuthenticatedUser, followed_user = UserObj ) except followings.FollowingExceptions.AlreadyFollowing: return jsonify({ "success": True }), 200 except followings.FollowingExceptions.UserRateLimited: return jsonify({ "errors": [{ "code": 9, "message": "The flood limit has been exceeded."}] }), 429 except followings.FollowingExceptions.FollowingIsDisabled: return jsonify({ "errors": [{ "code": 0, "message": "Following is disabled."}] }), 500 return jsonify({ "success": True }), 200