from app.models.follow_relationship import FollowRelationship from app.models.user import User from app.extensions import db, redis_controller from app.util.websiteFeatures import GetWebsiteFeature import redis_lock class FollowingExceptions(): class AlreadyFollowing(Exception): pass class CannotFollowSelf(Exception): pass class RedisLockAcquireError(Exception): pass class UserRateLimited(Exception): pass class UserNotFollowing(Exception): pass class FollowingIsDisabled(Exception): pass def follow_user( follower_user : User, followed_user : User, bypass_rate_limit : bool = False ) -> None: """ :param follower_user : User : The user that is following :param followed_user : User : The user that is being followed :param bypass_rate_limit : bool : Bypass the rate limit check :return None :raises FollowingExceptions.AlreadyFollowing : The user is already following the other user :raises FollowingExceptions.CannotFollowSelf : The user cannot follow themselves :raises FollowingExceptions.UserNotFound : The user is not found :raises FollowingExceptions.RedisLockAcquireError : The Redis lock cannot be acquired :raises FollowingExceptions.UserRateLimited : The user is rate limited :raises FollowingExceptions.FollowingIsDisabled : Following is disabled """ assert isinstance(follower_user, User), f"Expected follower_user to be of type User, got {follower_user.__class__.__name__}" assert isinstance(followed_user, User), f"Expected followed_user to be of type User, got {followed_user.__class__}" if follower_user.id == followed_user.id: raise FollowingExceptions.CannotFollowSelf if GetWebsiteFeature("FollowingUsers") is False: raise FollowingExceptions.FollowingIsDisabled try: with redis_lock.Lock( redis_client = redis_controller, name = f"services:followings:follow_user:{follower_user.id}", expire = 10 ): if not bypass_rate_limit: if redis_controller.get(f"rate_limit:followings:follow_user_action:{follower_user.id}") is not None: raise FollowingExceptions.UserRateLimited redis_controller.set(f"rate_limit:followings:follow_user_action:{follower_user.id}", "1", ex = 5) if FollowRelationship.query.filter_by(followerUserId = follower_user.id, followeeUserId = followed_user.id).first() is not None: raise FollowingExceptions.AlreadyFollowing follow_relationship = FollowRelationship( followerUserId = follower_user.id, followeeUserId = followed_user.id ) db.session.add(follow_relationship) db.session.commit() return None except AssertionError: raise FollowingExceptions.RedisLockAcquireError def unfollow_user( current_follower : User, followed_user : User, bypass_rate_limit : bool = False ) -> None: """ :param current_follower : User : The user that is unfollowing :param followed_user : User : The user that is being unfollowed :param bypass_rate_limit : bool : Bypass the rate limit check :return None :raises FollowingExceptions.UserNotFollowing : The user is not following the other user :raises FollowingExceptions.RedisLockAcquireError : The Redis lock cannot be acquired :raises FollowingExceptions.UserRateLimited : The user is rate limited """ assert isinstance(current_follower, User), f"Expected current_follower to be of type User, got {current_follower.__class__.__name__}" assert isinstance(followed_user, User), f"Expected followed_user to be of type User, got {followed_user.__class__}" try: with redis_lock.Lock( redis_client = redis_controller, name = f"services:followings:unfollow_user:{current_follower.id}", expire = 10 ): if not bypass_rate_limit: if redis_controller.get(f"rate_limit:followings:unfollow_user_action:{current_follower.id}") is not None: raise FollowingExceptions.UserRateLimited redis_controller.set(f"rate_limit:followings:unfollow_user_action:{current_follower.id}", "1", ex = 1) follow_relationship = FollowRelationship.query.filter_by(followerUserId = current_follower.id, followeeUserId = followed_user.id).first() if follow_relationship is None: raise FollowingExceptions.UserNotFollowing db.session.delete(follow_relationship) db.session.commit() return None except AssertionError: raise FollowingExceptions.RedisLockAcquireError def is_following( follower_user : User, followed_user : User ) -> bool: """ :param follower_user : User : The user that is following :param followed_user : User : The user that is being followed :return bool : Whether the user is following the other user """ assert isinstance(follower_user, User), f"Expected follower_user to be of type User, got {follower_user.__class__.__name__}" assert isinstance(followed_user, User), f"Expected followed_user to be of type User, got {followed_user.__class__}" return FollowRelationship.query.filter_by(followerUserId = follower_user.id, followeeUserId = followed_user.id).first() is not None def get_followers( requested_user : User, return_as_query : bool = False ) -> list[User]: """ :param requested_user : User : The user that is being requested :param return_as_query : bool : Whether to return the result as a query or a list :return list[User] : The list of users that are following the requested user """ assert isinstance(requested_user, User), f"Expected requested_user to be of type User, got {requested_user.__class__.__name__}" FollowersQuery = User.query.join(FollowRelationship, FollowRelationship.followerUserId == User.id).filter(FollowRelationship.followeeUserId == requested_user.id) if return_as_query: return FollowersQuery return FollowersQuery.all() def get_follower_count( requested_user : User, skip_cache : bool = False ) -> int: """ :param requested_user : User : The user that is being requested :param skip_cache : bool : Whether to skip the cache :return int : The number of users that are following the requested user """ assert isinstance(requested_user, User), f"Expected requested_user to be of type User, got {requested_user.__class__}" if not skip_cache: CachedFollowerCount = redis_controller.get(f"services:followings:follower_count:{requested_user.id}") if CachedFollowerCount is not None: return int(CachedFollowerCount) FollowerCount : int = get_followers(requested_user, return_as_query = True).count() redis_controller.set(f"services:followings:follower_count:{requested_user.id}", FollowerCount, ex = 10) return FollowerCount def get_following( requested_user : User, return_as_query : bool = False ) -> list[User]: """ :param requested_user : User : The user that is being requested :param return_as_query : bool : Whether to return the result as a query or a list :return list[User] : The list of users that the requested user is following """ assert isinstance(requested_user, User), f"Expected requested_user to be of type User, got {requested_user.__class__}" FollowingQuery = User.query.join(FollowRelationship, FollowRelationship.followeeUserId == User.id).filter(FollowRelationship.followerUserId == requested_user.id) if return_as_query: return FollowingQuery return FollowingQuery.all() def get_following_count( requested_user : User, skip_cache : bool = False ) -> int: """ :param requested_user : User : The user that is being requested :param skip_cache : bool : Whether to skip the cache :return int : The number of users that the requested user is following """ assert isinstance(requested_user, User), f"Expected requested_user to be of type User, got {requested_user.__class__}" if not skip_cache: CachedFollowingCount = redis_controller.get(f"services:followings:following_count:{requested_user.id}") if CachedFollowingCount is not None: return int(CachedFollowingCount) FollowingCount : int = get_following(requested_user, return_as_query = True).count() redis_controller.set(f"services:followings:following_count:{requested_user.id}", FollowingCount, ex = 10) return FollowingCount