syntaxwebsite/app/services/user_relationships/followings.py

216 lines
8.6 KiB
Python

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