add web
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
from app.extensions import redis_controller, db
|
||||
from app.util import redislock
|
||||
|
||||
from app.models.usereconomy import UserEconomy
|
||||
from app.models.user import User
|
||||
from app.models.userassets import UserAsset
|
||||
from app.models.groups import Group, GroupEconomy
|
||||
from app.models.asset import Asset
|
||||
from app.models.asset_rap import AssetRap
|
||||
from app.services.groups import GetGroupFromId, GetUserFromId
|
||||
|
||||
import redis_lock
|
||||
import math
|
||||
|
||||
class InvalidCurrencyTypeException(Exception):
|
||||
pass
|
||||
class EconomyLockAcquireException(Exception):
|
||||
pass
|
||||
class InsufficientFundsException(Exception):
|
||||
pass
|
||||
class AssetNotLimitedException(Exception):
|
||||
pass
|
||||
class AssetDoesNotExistException(Exception):
|
||||
pass
|
||||
|
||||
def TaxCurrencyAmount( Amount : int ) -> int:
|
||||
return math.floor( Amount * 0.7 )
|
||||
|
||||
def GetAssetFromId( assetid : int | Asset ) -> Asset | None:
|
||||
"""
|
||||
Returns the Asset object for the given assetid
|
||||
"""
|
||||
if isinstance(assetid, Asset):
|
||||
return assetid
|
||||
AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
|
||||
if AssetObj is None:
|
||||
raise AssetDoesNotExistException("Asset does not exist")
|
||||
return AssetObj
|
||||
|
||||
def GetUserEconomyObj( TargetUser : User ) -> UserEconomy | None:
|
||||
"""
|
||||
Returns the UserEconomy object for the given user
|
||||
"""
|
||||
return UserEconomy.query.filter_by( userid=TargetUser.id ).first()
|
||||
|
||||
def GetGroupEconomyObj( TargetGroup : Group ) -> GroupEconomy | None:
|
||||
"""
|
||||
Returns the GroupEconomy object for the given group
|
||||
"""
|
||||
return GroupEconomy.query.filter_by( group_id=TargetGroup.id ).first()
|
||||
|
||||
def GetUserBalance( TargetUser : User ) -> tuple[int, int]:
|
||||
"""
|
||||
Returns the User's Robux and Tickets balance
|
||||
"""
|
||||
EconomyObj : UserEconomy = GetUserEconomyObj( TargetUser )
|
||||
return EconomyObj.robux, EconomyObj.tix
|
||||
|
||||
def GetGroupBalance( TargetGroup : Group ) -> tuple[int, int]:
|
||||
"""
|
||||
Returns the Group's Robux and Tickets balance
|
||||
"""
|
||||
EconomyObj : GroupEconomy = GetGroupEconomyObj( TargetGroup )
|
||||
return EconomyObj.robux_balance, EconomyObj.tix_balance
|
||||
|
||||
def UnsafeIncrementTargetBalance( Target : User | Group, Amount : int, CurrencyType : int ): # CurrencyType is 0 for Robux, 1 for Tickets
|
||||
"""
|
||||
Increments the Target Balance ( Not Recommended for normal use please instead use IncrementTargetBalance)
|
||||
"""
|
||||
if isinstance(Target, User):
|
||||
TargetEconomyObj : UserEconomy = GetUserEconomyObj( Target )
|
||||
if CurrencyType == 0:
|
||||
TargetEconomyObj.robux += Amount
|
||||
elif CurrencyType == 1:
|
||||
TargetEconomyObj.tix += Amount
|
||||
else:
|
||||
raise InvalidCurrencyTypeException("Invalid Currency Type")
|
||||
db.session.commit()
|
||||
elif isinstance(Target, Group):
|
||||
TargetEconomyObj : GroupEconomy = GetGroupEconomyObj( Target )
|
||||
if CurrencyType == 0:
|
||||
TargetEconomyObj.robux_balance += Amount
|
||||
elif CurrencyType == 1:
|
||||
TargetEconomyObj.tix_balance += Amount
|
||||
else:
|
||||
raise InvalidCurrencyTypeException("Invalid Currency Type")
|
||||
db.session.commit()
|
||||
else:
|
||||
raise TypeError("Invalid Target Type")
|
||||
|
||||
def UnsafeDecrementTargetBalance( Target : User | Group, Amount : int, CurrencyType : int ): # CurrencyType is 0 for Robux, 1 for Tickets
|
||||
"""
|
||||
Decrements the Target Balance ( Not Recommended for normal use please instead use DecrementTargetBalance)
|
||||
"""
|
||||
if isinstance(Target, User):
|
||||
TargetEconomyObj : UserEconomy = GetUserEconomyObj( Target )
|
||||
if CurrencyType == 0:
|
||||
TargetEconomyObj.robux -= Amount
|
||||
elif CurrencyType == 1:
|
||||
TargetEconomyObj.tix -= Amount
|
||||
else:
|
||||
raise InvalidCurrencyTypeException("Invalid Currency Type")
|
||||
db.session.commit()
|
||||
elif isinstance(Target, Group):
|
||||
TargetEconomyObj : GroupEconomy = GetGroupEconomyObj( Target )
|
||||
if CurrencyType == 0:
|
||||
TargetEconomyObj.robux_balance -= Amount
|
||||
elif CurrencyType == 1:
|
||||
TargetEconomyObj.tix_balance -= Amount
|
||||
else:
|
||||
raise InvalidCurrencyTypeException("Invalid Currency Type")
|
||||
db.session.commit()
|
||||
else:
|
||||
raise TypeError("Invalid Target Type")
|
||||
|
||||
def IncrementTargetBalance( Target : User | Group, Amount : int, CurrencyType : int ): # CurrencyType is 0 for Robux, 1 for Tickets
|
||||
"""
|
||||
Increments the Target Balance
|
||||
"""
|
||||
if Amount < 0:
|
||||
raise ValueError("Amount must be positive")
|
||||
if isinstance(Target, User):
|
||||
with redis_lock.Lock( redis_client = redis_controller, name = f"economy:{Target.id}", expire = 1, auto_renewal = True ):
|
||||
UnsafeIncrementTargetBalance( Target, Amount, CurrencyType )
|
||||
return
|
||||
elif isinstance(Target, Group):
|
||||
with redis_lock.Lock( redis_client = redis_controller, name = f"economy_group:{Target.id}", expire = 1, auto_renewal = True ):
|
||||
UnsafeIncrementTargetBalance( Target, Amount, CurrencyType )
|
||||
return
|
||||
else:
|
||||
raise TypeError("Invalid Target Type")
|
||||
|
||||
def DecrementTargetBalance( Target : User | Group, Amount : int, CurrencyType : int ): # CurrencyType is 0 for Robux, 1 for Tickets
|
||||
"""
|
||||
Decrements the Target Balance
|
||||
"""
|
||||
if Amount < 0:
|
||||
raise ValueError("Amount must be positive")
|
||||
if isinstance(Target, User):
|
||||
with redis_lock.Lock( redis_client = redis_controller, name = f"economy:{Target.id}", expire = 1, auto_renewal = True ):
|
||||
TargetEconomyObj : UserEconomy = GetUserEconomyObj( Target )
|
||||
if CurrencyType == 0:
|
||||
if TargetEconomyObj.robux < Amount:
|
||||
raise InsufficientFundsException("Insufficient Funds")
|
||||
elif CurrencyType == 1:
|
||||
if TargetEconomyObj.tix < Amount:
|
||||
raise InsufficientFundsException("Insufficient Funds")
|
||||
UnsafeDecrementTargetBalance( Target, Amount, CurrencyType )
|
||||
return
|
||||
elif isinstance(Target, Group):
|
||||
with redis_lock.Lock( redis_client = redis_controller, name = f"economy_group:{Target.id}", expire = 1, auto_renewal = True ):
|
||||
TargetEconomyObj : GroupEconomy = GroupEconomy.query.filter_by( group_id=Target.id ).first()
|
||||
if CurrencyType == 0:
|
||||
if TargetEconomyObj.robux_balance < Amount:
|
||||
raise InsufficientFundsException("Insufficient Funds")
|
||||
elif CurrencyType == 1:
|
||||
if TargetEconomyObj.tix_balance < Amount:
|
||||
raise InsufficientFundsException("Insufficient Funds")
|
||||
UnsafeDecrementTargetBalance( Target, Amount, CurrencyType )
|
||||
return
|
||||
else:
|
||||
raise TypeError("Invalid Target Type")
|
||||
|
||||
def TransferFunds( Source : User | Group, Target : User | Group, Amount : int, CurrencyType : int, ApplyTax : bool = False): # CurrencyType is 0 for Robux, 1 for Tickets
|
||||
"""
|
||||
Transfers funds from the source to the target
|
||||
"""
|
||||
|
||||
if Amount < 0:
|
||||
raise ValueError("Amount must be positive")
|
||||
if Source == Target:
|
||||
raise ValueError("Source and Target must be different")
|
||||
if CurrencyType not in [0,1]:
|
||||
raise InvalidCurrencyTypeException("Invalid Currency Type")
|
||||
if isinstance(Source, User):
|
||||
SourceEconomyObj : UserEconomy = GetUserEconomyObj( Source )
|
||||
if CurrencyType == 0:
|
||||
if SourceEconomyObj.robux < Amount:
|
||||
raise InsufficientFundsException("Insufficient Funds")
|
||||
elif CurrencyType == 1:
|
||||
if SourceEconomyObj.tix < Amount:
|
||||
raise InsufficientFundsException("Insufficient Funds")
|
||||
elif isinstance(Source, Group):
|
||||
SourceEconomyObj : GroupEconomy = GroupEconomy.query.filter_by( group_id=Source.id ).first()
|
||||
if CurrencyType == 0:
|
||||
if SourceEconomyObj.robux_balance < Amount:
|
||||
raise InsufficientFundsException("Insufficient Funds")
|
||||
elif CurrencyType == 1:
|
||||
if SourceEconomyObj.tix_balance < Amount:
|
||||
raise InsufficientFundsException("Insufficient Funds")
|
||||
else:
|
||||
raise TypeError("Invalid Source Type")
|
||||
|
||||
TakenAmount : int = Amount
|
||||
GivenAmount : int = Amount
|
||||
if ApplyTax:
|
||||
GivenAmount = TaxCurrencyAmount( Amount )
|
||||
try:
|
||||
DecrementTargetBalance( Source, TakenAmount, CurrencyType )
|
||||
except InsufficientFundsException:
|
||||
raise InsufficientFundsException("Insufficient Funds")
|
||||
IncrementTargetBalance( Target, GivenAmount, CurrencyType )
|
||||
|
||||
return
|
||||
|
||||
def AdjustAssetRap(AssetObj : Asset | int, robux : int):
|
||||
""" Adjusts the RAP of an asset
|
||||
https://roblox.fandom.com/wiki/Recent_Average_Price
|
||||
This will only work with assets that are limited
|
||||
"""
|
||||
AssetObj : Asset = GetAssetFromId(AssetObj)
|
||||
|
||||
AssetRapObject : AssetRap = AssetRap.query.filter_by(assetid=AssetObj.id).first()
|
||||
if AssetRapObject is None:
|
||||
if not AssetObj.is_limited:
|
||||
raise AssetNotLimitedException("Asset is not limited")
|
||||
AssetRapObject = AssetRap(assetid=AssetObj.id, rap=robux)
|
||||
db.session.add(AssetRapObject)
|
||||
db.session.commit()
|
||||
return True
|
||||
if AssetRapObject.rap <= 0:
|
||||
AssetRapObject.rap = robux
|
||||
CurrentRAP = AssetRapObject.rap
|
||||
AssetRapObject.rap = math.floor(CurrentRAP - ( CurrentRAP - robux ) / 10)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
def GetAssetRap(AssetObj : Asset | int ) -> int:
|
||||
"""
|
||||
Returns the RAP of an asset
|
||||
"""
|
||||
AssetObj : Asset = GetAssetFromId(AssetObj)
|
||||
if not AssetObj.is_limited:
|
||||
raise AssetNotLimitedException("Asset is not limited")
|
||||
|
||||
AssetRapObject : AssetRap = AssetRap.query.filter_by(assetid=AssetObj.id).first()
|
||||
if AssetRapObject is None:
|
||||
AssetRapObject = AssetRap(assetid=AssetObj.id, rap=0)
|
||||
db.session.add(AssetRapObject)
|
||||
db.session.commit()
|
||||
return AssetRapObject.rap
|
||||
|
||||
def GetCreatorOfAsset( AssetObj : Asset | int ) -> User | Group | None:
|
||||
"""
|
||||
Returns the creator of an asset
|
||||
"""
|
||||
AssetObj : Asset = GetAssetFromId(AssetObj)
|
||||
if AssetObj.creator_type == 1:
|
||||
return GetGroupFromId(AssetObj.creator_id)
|
||||
elif AssetObj.creator_type == 0:
|
||||
return GetUserFromId(AssetObj.creator_id)
|
||||
else:
|
||||
return None
|
||||
|
||||
def CalculateUserRAP( UserObj : User | int, skipCache : bool = False ) -> int:
|
||||
"""
|
||||
Calculates the RAP of a user
|
||||
"""
|
||||
if redis_controller.exists(f"rap_calculation:{UserObj.id}") and not skipCache:
|
||||
return int(redis_controller.get(f"rap_calculation:{UserObj.id}"))
|
||||
|
||||
UserObj : User = GetUserFromId(UserObj)
|
||||
UserRAP : int = 0
|
||||
UserLimitedAssets : list[UserAsset] = UserAsset.query.filter_by(userid=UserObj.id).outerjoin( Asset, Asset.id == UserAsset.assetid ).filter( Asset.is_limited == True ).all()
|
||||
|
||||
for UserLimitedAsset in UserLimitedAssets:
|
||||
UserRAP += GetAssetRap(UserLimitedAsset.assetid)
|
||||
|
||||
redis_controller.set(f"rap_calculation:{UserObj.id}", UserRAP, ex = 60)
|
||||
return UserRAP
|
||||
@@ -0,0 +1,117 @@
|
||||
import requests
|
||||
import time
|
||||
import base64
|
||||
import json
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
from app.models.gameservers import GameServer
|
||||
|
||||
from config import Config
|
||||
|
||||
config = Config()
|
||||
|
||||
def sign_content( content : bytes ) -> str:
|
||||
"""
|
||||
Signs the given content using the gameserver private key
|
||||
|
||||
:param content: The content to sign
|
||||
|
||||
:returns: str
|
||||
"""
|
||||
assert isinstance( content, bytes ), "content must be a bytes object"
|
||||
|
||||
with open( config.GAMESERVER_COMM_PRIVATE_KEY_LOCATION, "rb" ) as f:
|
||||
private_key = serialization.load_pem_private_key(
|
||||
f.read(),
|
||||
password=None,
|
||||
backend=default_backend()
|
||||
)
|
||||
|
||||
signature = private_key.sign(
|
||||
content,
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA256()
|
||||
)
|
||||
|
||||
return base64.b64encode( signature ).decode( "utf-8" )
|
||||
|
||||
def perform_get(
|
||||
TargetGameserver : GameServer,
|
||||
Endpoint : str,
|
||||
AdditionalHeaders : dict = {},
|
||||
|
||||
RequestTimeout : int = 10
|
||||
) -> requests.Response:
|
||||
"""
|
||||
Performs a GET request to the given gameserver
|
||||
|
||||
:param TargetGameserver: The gameserver to send the request to
|
||||
:param Endpoint: The endpoint to send the request to
|
||||
:param AdditionalHeaders: Additional headers to send with the request
|
||||
:param RequestTimeout: The amount of time before the request times out
|
||||
|
||||
:returns: requests.Response
|
||||
"""
|
||||
assert isinstance( TargetGameserver, GameServer ), "TargetGameserver must be an instance of GameServer"
|
||||
assert isinstance( Endpoint, str ), "Endpoint must be a string"
|
||||
assert isinstance( AdditionalHeaders, dict ), "AdditionalHeaders must be a dictionary"
|
||||
|
||||
RequestTimestamp = time.time()
|
||||
signed_content = sign_content( f'{RequestTimestamp}\nGET'.encode( 'utf-8' ) )
|
||||
ReqSignature = f"{str(RequestTimestamp)}|{ signed_content }"
|
||||
|
||||
headers = {
|
||||
"User-Agent": "SYNTAX-Gameserver-Communication/1.0",
|
||||
"X-Syntax-Request-Signature": ReqSignature
|
||||
}
|
||||
|
||||
return requests.get(
|
||||
url = f"http://{TargetGameserver.serverIP}:{TargetGameserver.serverPort}/{Endpoint}",
|
||||
headers = headers,
|
||||
timeout = RequestTimeout
|
||||
)
|
||||
|
||||
def perform_post(
|
||||
TargetGameserver : GameServer,
|
||||
Endpoint : str,
|
||||
JSONData : dict | list = {},
|
||||
AdditionalHeaders : dict = {},
|
||||
|
||||
RequestTimeout : int = 10
|
||||
) -> requests.Response:
|
||||
"""
|
||||
Performs a POST request to the given gameserver
|
||||
|
||||
:param TargetGameserver: The gameserver to send the request to
|
||||
:param Endpoint: The endpoint to send the request to
|
||||
:param JSONData: The JSON data to send with the request
|
||||
:param AdditionalHeaders: Additional headers to send with the request
|
||||
:param RequestTimeout: The amount of time before the request times out
|
||||
|
||||
:returns: requests.Response
|
||||
"""
|
||||
assert isinstance( TargetGameserver, GameServer ), "TargetGameserver must be an instance of GameServer"
|
||||
assert isinstance( Endpoint, str ), "Endpoint must be a string"
|
||||
assert isinstance( JSONData, ( dict, list ) ), "JSONData must be a dictionary or list"
|
||||
assert isinstance( AdditionalHeaders, dict ), "AdditionalHeaders must be a dictionary"
|
||||
|
||||
RequestTimestamp = time.time()
|
||||
signed_content = sign_content( f"{RequestTimestamp}\nPOST\n{json.dumps(JSONData)}".encode( "utf-8" ))
|
||||
ReqSignature = f"{str(RequestTimestamp)}|{signed_content}"
|
||||
|
||||
headers = {
|
||||
"User-Agent": "SYNTAX-Gameserver-Communication/1.0",
|
||||
"X-Syntax-Request-Signature": ReqSignature,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
headers.update( AdditionalHeaders )
|
||||
|
||||
return requests.post(
|
||||
url = f"http://{TargetGameserver.serverIP}:{TargetGameserver.serverPort}/{Endpoint}",
|
||||
headers = headers,
|
||||
data = json.dumps( JSONData ),
|
||||
timeout = RequestTimeout
|
||||
)
|
||||
@@ -0,0 +1,690 @@
|
||||
import logging
|
||||
|
||||
from app.extensions import db, redis_controller
|
||||
from app.util import redislock
|
||||
|
||||
from app.models.user import User
|
||||
from app.models.groups import Group, GroupIcon, GroupSettings, GroupRole, GroupRolePermission, GroupMember, GroupEconomy, GroupJoinRequest, GroupStatus, GroupWallPost
|
||||
|
||||
class GroupExceptions:
|
||||
class RolesetDoesNotExist(Exception):
|
||||
pass
|
||||
class GroupDoesNotExist(Exception):
|
||||
pass
|
||||
class UserDoesNotExist(Exception):
|
||||
pass
|
||||
class RolesetNameNotUnique(Exception):
|
||||
pass
|
||||
class InvalidRankNumber(Exception):
|
||||
pass
|
||||
class UserNotInGroup(Exception):
|
||||
pass
|
||||
class UserAlreadyInsideGroup(Exception):
|
||||
pass
|
||||
class CorruptedGroup(Exception):
|
||||
pass
|
||||
class GroupNameAlreadyTaken(Exception):
|
||||
pass
|
||||
class GroupNameNotAllowed(Exception):
|
||||
pass
|
||||
class InsufficientPermssions(Exception):
|
||||
pass
|
||||
|
||||
class UserGroupEntry:
|
||||
group_id : int = 0
|
||||
group_name : str = ""
|
||||
group_description : str = ""
|
||||
group_owner_id : int = 0
|
||||
|
||||
group_member_count : int = 0
|
||||
|
||||
user_rank : int = 0
|
||||
user_roleset : GroupRole | None = None
|
||||
|
||||
def __init__(self, Group : Group, UserRoleset : GroupRole | None):
|
||||
self.group_id = Group.id
|
||||
self.group_name = Group.name
|
||||
self.group_description = Group.description
|
||||
self.group_owner_id = Group.owner_id
|
||||
|
||||
self.user_rank = UserRoleset.rank
|
||||
self.user_roleset = UserRoleset
|
||||
|
||||
self.group_member_count = GetGroupMemberCount(Group)
|
||||
|
||||
def GetUserFromId( UserObj : User | int ) -> User | None:
|
||||
"""
|
||||
Returns a User object from a User ID.
|
||||
"""
|
||||
if isinstance(UserObj, User):
|
||||
return UserObj
|
||||
else:
|
||||
TargetUser : User | None = User.query.filter_by(id=UserObj).first()
|
||||
if TargetUser is None:
|
||||
raise GroupExceptions.UserDoesNotExist("User does not exist.")
|
||||
return TargetUser
|
||||
|
||||
def GetRolesetFromId( Role : GroupRole | int ) -> GroupRole | None:
|
||||
"""
|
||||
Returns a GroupRole object from a GroupRole ID.
|
||||
"""
|
||||
if isinstance(Role, GroupRole):
|
||||
return Role
|
||||
else:
|
||||
TargetGroupRole : GroupRole | None = GroupRole.query.filter_by(id=Role).first()
|
||||
if TargetGroupRole is None:
|
||||
raise GroupExceptions.RolesetDoesNotExist("Roleset does not exist.")
|
||||
return TargetGroupRole
|
||||
|
||||
def GetGroupFromId( GroupObj : Group | int ) -> Group | None:
|
||||
"""
|
||||
Returns a Group object from a Group ID.
|
||||
"""
|
||||
if isinstance(GroupObj, Group):
|
||||
return GroupObj
|
||||
else:
|
||||
TargetGroup : Group | None = Group.query.filter_by(id=GroupObj).first()
|
||||
if TargetGroup is None:
|
||||
raise GroupExceptions.GroupDoesNotExist("Group does not exist.")
|
||||
return TargetGroup
|
||||
|
||||
def RefreshRolesetCount( Role : GroupRole | int ) -> int:
|
||||
"""
|
||||
Recounts the amount of users in a GroupRole and updates the database.
|
||||
then returns the amount of users in the role.
|
||||
"""
|
||||
Role : GroupRole | None = GetRolesetFromId(Role)
|
||||
|
||||
TotalMembers : int = GroupMember.query.filter_by(group_id=Role.group_id, group_role_id=Role.id).count()
|
||||
Role.member_count = TotalMembers
|
||||
db.session.commit()
|
||||
|
||||
return TotalMembers
|
||||
|
||||
def GetGroupMemberCount( Group : Group | int ) -> int:
|
||||
"""
|
||||
Returns the amount of members in a group.
|
||||
"""
|
||||
Group : Group | None = GetGroupFromId(Group)
|
||||
TotalMembers : int = 0
|
||||
for Role in GroupRole.query.filter_by(group_id=Group.id).all():
|
||||
TotalMembers += Role.member_count
|
||||
|
||||
return TotalMembers
|
||||
|
||||
def GetUserRankInGroup( TargetUser : User | int, TargetGroup : Group | int ) -> int:
|
||||
"""
|
||||
Returns a integer between 0-255 representing the user rank in the group.
|
||||
0 - Guest ( Not in the group )
|
||||
1 - 244 - Custom ranks
|
||||
255 - Group Owner
|
||||
"""
|
||||
TargetUser : User = GetUserFromId(TargetUser)
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
|
||||
UserGroupMembership : GroupMember | None = GroupMember.query.filter_by(user_id=TargetUser.id, group_id=TargetGroup.id).first()
|
||||
if UserGroupMembership is None:
|
||||
return 0 # Guest
|
||||
try:
|
||||
UserAssignedRoleset : GroupRole = GetRolesetFromId(UserGroupMembership.group_role_id)
|
||||
except GroupExceptions.RolesetDoesNotExist:
|
||||
logging.warn(f"User {TargetUser.id} is in group {TargetGroup.id} but the roleset {UserGroupMembership.group_role_id} does not exist.")
|
||||
return 0 # Weird edge case where the user is in the group but the roleset doesn't exist
|
||||
return UserAssignedRoleset.rank
|
||||
|
||||
def GetUserRolesetInGroup( TargetUser : User | int, TargetGroup : Group | int ) -> GroupRole | None:
|
||||
"""
|
||||
Returns a GroupRole object representing the user's roleset in the group.
|
||||
"""
|
||||
TargetUser : User = GetUserFromId(TargetUser)
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
|
||||
UserGroupMembership : GroupMember | None = GroupMember.query.filter_by(user_id=TargetUser.id, group_id=TargetGroup.id).first()
|
||||
if UserGroupMembership is None:
|
||||
GuestRoleset : GroupRole | None = GroupRole.query.filter_by(group_id=TargetGroup.id, rank=0).first()
|
||||
if GuestRoleset is None:
|
||||
raise GroupExceptions.CorruptedGroup("Group has no guest roleset.")
|
||||
return GuestRoleset
|
||||
try:
|
||||
UserAssignedRoleset : GroupRole = GetRolesetFromId(UserGroupMembership.group_role_id)
|
||||
except GroupExceptions.RolesetDoesNotExist:
|
||||
logging.warn(f"User {TargetUser.id} is in group {TargetGroup.id} but the roleset {UserGroupMembership.group_role_id} does not exist.")
|
||||
return None
|
||||
return UserAssignedRoleset
|
||||
|
||||
def GetUserGroups( TargetUser : User | int ) -> list[UserGroupEntry]:
|
||||
"""
|
||||
Returns a list of UserGroupEntry objects representing the user's groups.
|
||||
"""
|
||||
TargetUser : User = GetUserFromId(TargetUser)
|
||||
|
||||
UserGroups : list[UserGroupEntry] = []
|
||||
for UserGroupMembership in GroupMember.query.filter_by(user_id=TargetUser.id).all():
|
||||
try:
|
||||
UserAssignedRoleset : GroupRole = GetRolesetFromId(UserGroupMembership.group_role_id)
|
||||
except GroupExceptions.RolesetDoesNotExist:
|
||||
logging.warn(f"User {TargetUser.id} is in group {UserGroupMembership.group_id} but the roleset {UserGroupMembership.group_role_id} does not exist.")
|
||||
continue
|
||||
GroupObj : Group = GetGroupFromId(UserGroupMembership.group_id)
|
||||
UserGroups.append(UserGroupEntry(
|
||||
Group=GroupObj,
|
||||
UserRoleset=UserAssignedRoleset
|
||||
))
|
||||
return UserGroups
|
||||
|
||||
def GetGroupRolesets( TargetGroup : Group | int ) -> list[GroupRole]:
|
||||
"""
|
||||
Returns a list of GroupRole objects representing the group's rolesets.
|
||||
"""
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
|
||||
return GroupRole.query.filter_by(group_id=TargetGroup.id).order_by(GroupRole.rank.asc()).all()
|
||||
|
||||
def CreateGroupRoleset( TargetGroup : Group | int, Name : str, Description : str, Rank : int ) -> GroupRole:
|
||||
"""
|
||||
Creates a new roleset in the group and returns the GroupRole object.
|
||||
"""
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
if GroupRole.query.filter_by(group_id=TargetGroup.id, name=Name).first() is not None:
|
||||
raise GroupExceptions.RolesetNameNotUnique("A roleset with that name already exists.")
|
||||
if Rank < 0 or Rank > 255:
|
||||
raise GroupExceptions.InvalidRankNumber("Rank must be between 0 and 255.")
|
||||
if len(Name) > 255:
|
||||
raise ValueError("Name must be less than 255 characters.")
|
||||
if len(Description) > 255:
|
||||
raise ValueError("Description must be less than 255 characters.")
|
||||
|
||||
NewRoleset : GroupRole = GroupRole(
|
||||
group_id=TargetGroup.id,
|
||||
name=Name,
|
||||
description=Description,
|
||||
rank=Rank,
|
||||
member_count=0
|
||||
)
|
||||
db.session.add(NewRoleset)
|
||||
db.session.commit()
|
||||
|
||||
RolesetPermission : GroupRolePermission = GroupRolePermission(
|
||||
group_role_id=NewRoleset.id
|
||||
)
|
||||
RolesetPermission.view_status = True
|
||||
db.session.add(RolesetPermission)
|
||||
db.session.commit()
|
||||
|
||||
return NewRoleset
|
||||
|
||||
def ChangeUserRole( TargetUser : User | int , TargetRoleset : GroupRole | int ) -> None:
|
||||
"""
|
||||
Changes the user's roleset in the group if possible.
|
||||
"""
|
||||
TargetUser : User = GetUserFromId(TargetUser)
|
||||
TargetRoleset : GroupRole = GetRolesetFromId(TargetRoleset)
|
||||
TargetGroup : Group = GetGroupFromId(TargetRoleset.group_id)
|
||||
|
||||
if GetUserRankInGroup(TargetUser, TargetRoleset.group_id) == 0:
|
||||
raise GroupExceptions.UserNotInGroup("User is not in the group.")
|
||||
|
||||
if GetUserRolesetInGroup(TargetUser, TargetRoleset.group_id) == TargetRoleset:
|
||||
return
|
||||
|
||||
if TargetGroup.owner_id == TargetUser.id and TargetRoleset.rank != 255:
|
||||
raise ValueError("Cannot change the owner's roleset.")
|
||||
|
||||
UserGroupMembership : GroupMember = GroupMember.query.filter_by(user_id=TargetUser.id, group_id=TargetGroup.id).first()
|
||||
OldRolesetId : int = UserGroupMembership.group_role.id
|
||||
UserGroupMembership.group_role_id = TargetRoleset.id
|
||||
db.session.commit()
|
||||
RefreshRolesetCount(OldRolesetId)
|
||||
RefreshRolesetCount(TargetRoleset.id)
|
||||
|
||||
return
|
||||
|
||||
def AddUserToGroup( TargetUser : User | int , TargetGroup : Group | int, ForceJoin : bool = False ) -> GroupMember | GroupJoinRequest | None:
|
||||
"""
|
||||
Adds the user to the group if possible.
|
||||
ForceJoin : bool ( If the group requires manual approval, this will bypass it. )
|
||||
"""
|
||||
TargetUser : User = GetUserFromId(TargetUser)
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
|
||||
if GetUserRankInGroup(TargetUser, TargetGroup) != 0:
|
||||
raise GroupExceptions.UserAlreadyInsideGroup("User is already in the group.")
|
||||
|
||||
TargetGroupSettings : GroupSettings | None = GroupSettings.query.filter_by(group_id=TargetGroup.id).first()
|
||||
if TargetGroupSettings is None:
|
||||
raise ValueError("Group settings do not exist.")
|
||||
if TargetGroupSettings.approval_required and not ForceJoin:
|
||||
ExisitingJoinRequest : GroupJoinRequest | None = GroupJoinRequest.query.filter_by(user_id=TargetUser.id, group_id=TargetGroup.id).first()
|
||||
if ExisitingJoinRequest is not None:
|
||||
return ExisitingJoinRequest
|
||||
NewJoinRequest : GroupJoinRequest = GroupJoinRequest(
|
||||
user_id=TargetUser.id,
|
||||
group_id=TargetGroup.id
|
||||
)
|
||||
db.session.add(NewJoinRequest)
|
||||
db.session.commit()
|
||||
return GroupJoinRequest
|
||||
|
||||
LowestRankRoleset : GroupRole | None = GroupRole.query.filter_by(group_id=TargetGroup.id).filter(GroupRole.rank > 0).order_by(GroupRole.rank.asc()).first()
|
||||
if LowestRankRoleset is None:
|
||||
raise GroupExceptions.CorruptedGroup("Group has no rolesets.")
|
||||
if LowestRankRoleset.rank == 255:
|
||||
raise GroupExceptions.CorruptedGroup("Group has no member roleset.")
|
||||
|
||||
NewGroupMember : GroupMember = GroupMember(
|
||||
user_id=TargetUser.id,
|
||||
group_id=TargetGroup.id,
|
||||
group_role_id=LowestRankRoleset.id
|
||||
)
|
||||
|
||||
db.session.add(NewGroupMember)
|
||||
db.session.commit()
|
||||
|
||||
RefreshRolesetCount(LowestRankRoleset)
|
||||
|
||||
return NewGroupMember
|
||||
|
||||
def GetJoinRequest( TargetUser : User | int, TargetGroup : Group | int ) -> GroupJoinRequest | None:
|
||||
"""
|
||||
Returns a GroupJoinRequest object from a user and group.
|
||||
"""
|
||||
TargetUser : User = GetUserFromId(TargetUser)
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
|
||||
return GroupJoinRequest.query.filter_by(user_id=TargetUser.id, group_id=TargetGroup.id).first()
|
||||
|
||||
def AcceptJoinRequest( TargetUser : User | int, TargetGroup : Group | int ) -> GroupMember | None:
|
||||
"""
|
||||
Accepts the user's join request.
|
||||
"""
|
||||
ExisitingJoinRequest : GroupJoinRequest | None = GroupJoinRequest.query.filter_by(user_id=TargetUser.id, group_id=TargetGroup.id).first()
|
||||
if ExisitingJoinRequest is None:
|
||||
raise ValueError("Join request does not exist.")
|
||||
db.session.delete(ExisitingJoinRequest)
|
||||
db.session.commit()
|
||||
return AddUserToGroup(TargetUser, TargetGroup, ForceJoin=True)
|
||||
|
||||
def GetRolesetPermission( GroupRoleset : GroupRole | int ) -> GroupRolePermission:
|
||||
"""
|
||||
Returns a GroupRolePermission object from a GroupRole.
|
||||
"""
|
||||
GroupRoleset : GroupRole = GetRolesetFromId(GroupRoleset)
|
||||
RolesetPermission : GroupRolePermission | None = GroupRolePermission.query.filter_by(group_role_id=GroupRoleset.id).first()
|
||||
if RolesetPermission is None:
|
||||
RolesetPermission : GroupRolePermission = GroupRolePermission(
|
||||
group_role_id=GroupRoleset.id
|
||||
)
|
||||
db.session.add(RolesetPermission)
|
||||
db.session.commit()
|
||||
return RolesetPermission
|
||||
|
||||
def ModifyRolesetPermission(
|
||||
GroupRoleset : GroupRole,
|
||||
DeleteFromWall : bool = None,
|
||||
PostToWall : bool = None,
|
||||
InviteMembers : bool = None,
|
||||
PostToStatus : bool = None,
|
||||
RemoveMembers : bool = None,
|
||||
ViewStatus : bool = None,
|
||||
ViewWall : bool = None,
|
||||
ChangeRank : bool = None,
|
||||
AdvertiseGroup : bool = None,
|
||||
ManageRelationships : bool = None,
|
||||
AddGroupPlaces : bool = None,
|
||||
ViewAuditLogs : bool = None,
|
||||
CreateItems : bool = None,
|
||||
ManageItems : bool = None,
|
||||
SpendGroupFunds : bool = None,
|
||||
ManageClan : bool = None,
|
||||
ManageGroupGames : bool = None
|
||||
) -> None:
|
||||
"""
|
||||
Modifies the roleset's permissions.
|
||||
"""
|
||||
RolesetPermission : GroupRolePermission = GetRolesetPermission(GroupRoleset)
|
||||
|
||||
if DeleteFromWall is not None:
|
||||
RolesetPermission.delete_from_wall = DeleteFromWall
|
||||
if PostToWall is not None:
|
||||
RolesetPermission.post_to_wall = PostToWall
|
||||
if InviteMembers is not None:
|
||||
RolesetPermission.invite_members = InviteMembers
|
||||
if PostToStatus is not None:
|
||||
RolesetPermission.post_to_status = PostToStatus
|
||||
if RemoveMembers is not None:
|
||||
RolesetPermission.remove_members = RemoveMembers
|
||||
if ViewStatus is not None:
|
||||
RolesetPermission.view_status = ViewStatus
|
||||
if ViewWall is not None:
|
||||
RolesetPermission.view_wall = ViewWall
|
||||
if ChangeRank is not None:
|
||||
RolesetPermission.change_rank = ChangeRank
|
||||
if AdvertiseGroup is not None:
|
||||
RolesetPermission.advertise_group = AdvertiseGroup
|
||||
if ManageRelationships is not None:
|
||||
RolesetPermission.manage_relationships = ManageRelationships
|
||||
if AddGroupPlaces is not None:
|
||||
RolesetPermission.add_group_places = AddGroupPlaces
|
||||
if ViewAuditLogs is not None:
|
||||
RolesetPermission.view_audit_logs = ViewAuditLogs
|
||||
if CreateItems is not None:
|
||||
RolesetPermission.create_items = CreateItems
|
||||
if ManageItems is not None:
|
||||
RolesetPermission.manage_items = ManageItems
|
||||
if SpendGroupFunds is not None:
|
||||
RolesetPermission.spend_group_funds = SpendGroupFunds
|
||||
if ManageClan is not None:
|
||||
RolesetPermission.manage_clan = ManageClan
|
||||
if ManageGroupGames is not None:
|
||||
RolesetPermission.manage_group_games = ManageGroupGames
|
||||
|
||||
db.session.commit()
|
||||
return
|
||||
|
||||
from app.util.textfilter import FilterText, TextNotAllowedException
|
||||
from sqlalchemy import func
|
||||
|
||||
def SearchGroupByName( GroupName : str ) -> Group | None :
|
||||
"""
|
||||
Returns a Group object from a group name.
|
||||
"""
|
||||
GroupName = GroupName.lower()
|
||||
return Group.query.filter(func.lower(Group.name) == GroupName).first()
|
||||
|
||||
def isGroupNameAllowed( GroupName : str ) -> bool:
|
||||
"""
|
||||
Returns True if the group name is allowed.
|
||||
"""
|
||||
if len(GroupName) > 255:
|
||||
return False
|
||||
if len(GroupName) < 3:
|
||||
return False
|
||||
|
||||
AlphanumericCharacters : int = 0
|
||||
for Character in GroupName:
|
||||
if Character.isalnum():
|
||||
AlphanumericCharacters += 1
|
||||
if AlphanumericCharacters < 3:
|
||||
return False
|
||||
try:
|
||||
FilterText( Text = GroupName, ThrowException = True)
|
||||
except TextNotAllowedException:
|
||||
return False
|
||||
return True
|
||||
|
||||
def AssertUserHasPermission( TargetUser : User | int, TargetGroup : Group | int, Permission ) -> None: # Permission should be like GroupRolePermission.post_to_status
|
||||
"""
|
||||
Raises an exception if the user does not have the permission.
|
||||
"""
|
||||
TargetUser : User = GetUserFromId(TargetUser)
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
|
||||
UserRoleset : GroupRole | None = GetUserRolesetInGroup(TargetUser, TargetGroup)
|
||||
if UserRoleset is None:
|
||||
raise GroupExceptions.CorruptedGroup("Group does not have a guest roleset.")
|
||||
RolesetPermission : GroupRolePermission = GetRolesetPermission(UserRoleset)
|
||||
|
||||
if not getattr(RolesetPermission, Permission.name):
|
||||
raise GroupExceptions.InsufficientPermssions("User does not have permission.")
|
||||
return
|
||||
|
||||
|
||||
def CreateGroup( GroupName : str, GroupDescription : str, GroupOwner : User | int, GroupIconContentHash : str ) -> Group | None :
|
||||
"""
|
||||
Creates a new group and returns the Group object.
|
||||
"""
|
||||
|
||||
CreateLock = redislock.acquire_lock("group_creation_lock", 30, 5)
|
||||
if not CreateLock:
|
||||
raise ValueError("Failed to acquire global creation lock.")
|
||||
|
||||
if SearchGroupByName(GroupName) is not None:
|
||||
redislock.release_lock("group_creation_lock", CreateLock)
|
||||
raise GroupExceptions.GroupNameAlreadyTaken("Group name is already taken.")
|
||||
|
||||
if isGroupNameAllowed(GroupName) is False:
|
||||
redislock.release_lock("group_creation_lock", CreateLock)
|
||||
raise GroupExceptions.GroupNameNotAllowed("Group name is not allowed.")
|
||||
if len(GroupDescription) > 1024:
|
||||
redislock.release_lock("group_creation_lock", CreateLock)
|
||||
raise ValueError("Group description must be less than 1024 characters.")
|
||||
GroupOwner : User = GetUserFromId(GroupOwner)
|
||||
|
||||
NewGroup : Group = Group(
|
||||
name=GroupName,
|
||||
description=GroupDescription,
|
||||
owner_id=GroupOwner.id
|
||||
)
|
||||
db.session.add(NewGroup)
|
||||
db.session.commit()
|
||||
redislock.release_lock("group_creation_lock", CreateLock)
|
||||
NewGroupSettings : GroupSettings = GroupSettings(
|
||||
group_id=NewGroup.id
|
||||
)
|
||||
db.session.add(NewGroupSettings)
|
||||
|
||||
NewGroupIcon : GroupIcon = GroupIcon(
|
||||
group_id=NewGroup.id,
|
||||
content_hash=GroupIconContentHash,
|
||||
moderation_status=1,
|
||||
creator_id=GroupOwner.id
|
||||
)
|
||||
db.session.add(NewGroupIcon)
|
||||
|
||||
NewGroupEconomy : GroupEconomy = GroupEconomy(
|
||||
group_id=NewGroup.id
|
||||
)
|
||||
db.session.add(NewGroupEconomy)
|
||||
db.session.commit()
|
||||
|
||||
GuestGroupRoleset : GroupRole = CreateGroupRoleset(
|
||||
TargetGroup=NewGroup,
|
||||
Name="Guest",
|
||||
Description="A non-group member.",
|
||||
Rank=0
|
||||
)
|
||||
ModifyRolesetPermission(GuestGroupRoleset, ViewStatus=True, ViewWall=True)
|
||||
MemberGroupRoleset : GroupRole = CreateGroupRoleset(
|
||||
TargetGroup=NewGroup,
|
||||
Name="Member",
|
||||
Description="A regular group member.",
|
||||
Rank=1
|
||||
)
|
||||
ModifyRolesetPermission(MemberGroupRoleset, ViewStatus=True, ViewWall=True, PostToWall=True)
|
||||
AdminGroupRoleset : GroupRole = CreateGroupRoleset(
|
||||
TargetGroup=NewGroup,
|
||||
Name="Admin",
|
||||
Description="A group administrator.",
|
||||
Rank=254
|
||||
)
|
||||
ModifyRolesetPermission(AdminGroupRoleset, ViewStatus=True, ViewWall=True, PostToWall=True)
|
||||
OwnerGroupRoleset : GroupRole = CreateGroupRoleset(
|
||||
TargetGroup=NewGroup,
|
||||
Name="Owner",
|
||||
Description="The group's owner.",
|
||||
Rank=255
|
||||
)
|
||||
ModifyRolesetPermission(
|
||||
OwnerGroupRoleset,
|
||||
DeleteFromWall=True,
|
||||
PostToWall=True,
|
||||
InviteMembers=True,
|
||||
PostToStatus=True,
|
||||
RemoveMembers=True,
|
||||
ViewStatus=True,
|
||||
ViewWall=True,
|
||||
ChangeRank=True,
|
||||
AdvertiseGroup=True,
|
||||
ManageRelationships=True,
|
||||
AddGroupPlaces=True,
|
||||
ViewAuditLogs=True,
|
||||
CreateItems=True,
|
||||
ManageItems=True,
|
||||
SpendGroupFunds=True,
|
||||
ManageClan=True,
|
||||
ManageGroupGames=True
|
||||
)
|
||||
|
||||
AddUserToGroup(GroupOwner, NewGroup, ForceJoin=True)
|
||||
ChangeUserRole(GroupOwner, OwnerGroupRoleset)
|
||||
|
||||
return NewGroup
|
||||
|
||||
def PostToGroupStatus( Poster : User | int, TargetGroup : Group | int, StatusMessage : str, ForcePost : bool = False ) -> GroupStatus:
|
||||
"""
|
||||
Posts a status to the group.
|
||||
ForcePost : bool ( If True this function will not check for the user permissions. )
|
||||
"""
|
||||
Poster : User = GetUserFromId(Poster)
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
if not ForcePost:
|
||||
AssertUserHasPermission(Poster, TargetGroup, GroupRolePermission.post_to_status)
|
||||
|
||||
FilteredMessage : str = FilterText(Text=StatusMessage)
|
||||
if len(FilteredMessage) > 1024:
|
||||
raise ValueError("Status message must be less than 1024 characters.")
|
||||
NewStatus : GroupStatus = GroupStatus(
|
||||
group_id=TargetGroup.id,
|
||||
poster_id=Poster.id,
|
||||
content=FilteredMessage
|
||||
)
|
||||
db.session.add(NewStatus)
|
||||
db.session.commit()
|
||||
|
||||
return NewStatus
|
||||
|
||||
def PostToGroupWall( Poster : User | int, TargetGroup : Group | int, WallMessage : str, ForcePost : bool = False ) -> GroupWallPost:
|
||||
"""
|
||||
Posts a message to the group wall.
|
||||
ForcePost : bool ( If True this function will not check for the user permissions. )
|
||||
"""
|
||||
Poster : User = GetUserFromId(Poster)
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
if not ForcePost:
|
||||
AssertUserHasPermission(Poster, TargetGroup, GroupRolePermission.post_to_wall)
|
||||
|
||||
FilteredMessage : str = FilterText(Text=WallMessage)
|
||||
if len(FilteredMessage) > 1024:
|
||||
raise ValueError("Wall message must be less than 1024 characters.")
|
||||
NewWallPost : GroupWallPost = GroupWallPost(
|
||||
group_id=TargetGroup.id,
|
||||
poster_id=Poster.id,
|
||||
content=FilteredMessage
|
||||
)
|
||||
db.session.add(NewWallPost)
|
||||
db.session.commit()
|
||||
|
||||
return NewWallPost
|
||||
|
||||
def GetGroupWallPosts( TargetGroup : Group | int, Page : int = 1, PerPage : int= 10 ) -> list[GroupWallPost]:
|
||||
"""
|
||||
Returns a list of GroupWallPost objects representing the group's wall posts.
|
||||
"""
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
|
||||
return GroupWallPost.query.filter_by(group_id=TargetGroup.id).order_by(GroupWallPost.created_at.desc()).paginate(Page, PerPage, False).items
|
||||
|
||||
def RemoveUserFromGroup( TargetUser : User | int, TargetGroup : Group | int, AllowOwnerRemove : bool = False ) -> bool:
|
||||
"""
|
||||
Removes the user from the group.
|
||||
"""
|
||||
TargetUser : User = GetUserFromId(TargetUser)
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
|
||||
if GetUserRankInGroup(TargetUser, TargetGroup.id) == 0:
|
||||
raise GroupExceptions.UserNotInGroup("User is not in the group.")
|
||||
|
||||
if TargetGroup.owner_id == TargetUser.id and not AllowOwnerRemove:
|
||||
raise ValueError("Cannot remove the owner from the group.")
|
||||
|
||||
if TargetGroup.owner_id == TargetUser.id:
|
||||
TargetGroup.owner_id = None
|
||||
|
||||
UserGroupMembership : GroupMember = GroupMember.query.filter_by(user_id=TargetUser.id, group_id=TargetGroup.id).first()
|
||||
OldRolesetId : int = UserGroupMembership.group_role.id
|
||||
db.session.delete(UserGroupMembership)
|
||||
db.session.commit()
|
||||
|
||||
RefreshRolesetCount(OldRolesetId)
|
||||
|
||||
return True
|
||||
|
||||
def isGroupNameAllowed( GroupName : str ) -> tuple[bool, str]:
|
||||
"""
|
||||
Returns True if the group name is allowed.
|
||||
If not it returns a bool and a string which is the reason why its not allowed
|
||||
"""
|
||||
if len(GroupName) > 50:
|
||||
return False, "Group name must be less than 40 characters."
|
||||
if len(GroupName) < 3:
|
||||
return False, "Group name must be more than 3 characters."
|
||||
|
||||
AlphanumericCharacters : int = 0
|
||||
for Character in GroupName:
|
||||
if Character.isalnum():
|
||||
AlphanumericCharacters += 1
|
||||
if AlphanumericCharacters < 3:
|
||||
return False, "Group name must contain at least 3 alphanumeric characters."
|
||||
try:
|
||||
FilterText( Text = GroupName, ThrowException = True)
|
||||
except TextNotAllowedException:
|
||||
return False, "Group name is not allowed."
|
||||
return True, ""
|
||||
|
||||
def DeleteGroupWallPost( TargetPost : GroupWallPost, Remover : User | int, ForceDelete : bool = False ) -> None:
|
||||
"""
|
||||
Deletes a group wall post.
|
||||
ForceDelete : bool ( If True this function will not check for the user permissions. )
|
||||
"""
|
||||
TargetPost : GroupWallPost = TargetPost
|
||||
Remover : User = GetUserFromId(Remover)
|
||||
TargetGroup : Group = GetGroupFromId(TargetPost.group_id)
|
||||
if not ForceDelete:
|
||||
AssertUserHasPermission(Remover, TargetGroup, GroupRolePermission.delete_from_wall)
|
||||
|
||||
db.session.delete(TargetPost)
|
||||
db.session.commit()
|
||||
return
|
||||
|
||||
def GetLatestGroupStatus( TargetGroup : Group | int, ignoreEmpty : bool = True ) -> GroupWallPost | None:
|
||||
"""
|
||||
Returns the latest group status.
|
||||
"""
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
|
||||
LatestGroupStatusPost : GroupStatus | None = GroupStatus.query.filter_by(group_id=TargetGroup.id).order_by(GroupStatus.created_at.desc()).first()
|
||||
if LatestGroupStatusPost is None:
|
||||
return None
|
||||
if ignoreEmpty and LatestGroupStatusPost.content == "":
|
||||
return None
|
||||
return LatestGroupStatusPost
|
||||
|
||||
def SetNewGroupIcon( TargetGroup : Group | int, NewIconHash : str, Uploader : User | int ) -> GroupIcon:
|
||||
"""
|
||||
Sets a new group icon.
|
||||
"""
|
||||
TargetGroup : Group = GetGroupFromId(TargetGroup)
|
||||
Uploader : User = GetUserFromId(Uploader)
|
||||
|
||||
if TargetGroup.icon is not None:
|
||||
db.session.delete(TargetGroup.icon)
|
||||
db.session.commit()
|
||||
|
||||
NewGroupIcon : GroupIcon = GroupIcon(
|
||||
group_id=TargetGroup.id,
|
||||
content_hash=NewIconHash,
|
||||
moderation_status=1,
|
||||
creator_id=Uploader.id
|
||||
)
|
||||
db.session.add(NewGroupIcon)
|
||||
db.session.commit()
|
||||
|
||||
return NewGroupIcon
|
||||
|
||||
def GetUserGroupCount( UserObj : User | int ) -> int:
|
||||
"""
|
||||
Gets the amount of groups a user is in.
|
||||
"""
|
||||
UserObj : User = GetUserFromId(UserObj)
|
||||
|
||||
return GroupMember.query.filter_by(user_id=UserObj.id).count()
|
||||
@@ -0,0 +1,70 @@
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from app.extensions import db
|
||||
|
||||
from app.models.invite_key import InviteKey
|
||||
from app.models.user import User
|
||||
|
||||
class InviteExceptions:
|
||||
class InvalidInviteKey(Exception):
|
||||
pass
|
||||
class UserDoesNotExist(Exception):
|
||||
pass
|
||||
class InviteKeyAlreadyUsed(Exception):
|
||||
pass
|
||||
|
||||
def _GenerateInviteKey() -> str:
|
||||
"""
|
||||
Returns a randomly generated invite key
|
||||
"""
|
||||
RandomUUID = str(uuid.uuid4())
|
||||
invite_key = "syntax" + RandomUUID[6:]
|
||||
InviteKeyObj : InviteKey = InviteKey.query.filter_by(key=invite_key).first()
|
||||
if InviteKeyObj is not None:
|
||||
return _GenerateInviteKey()
|
||||
return invite_key
|
||||
|
||||
def GetUserFromId( UserObj : User | int ) -> User | None:
|
||||
"""
|
||||
Returns a User object from a User ID.
|
||||
"""
|
||||
if isinstance(UserObj, User):
|
||||
return UserObj
|
||||
else:
|
||||
TargetUser : User | None = User.query.filter_by(id=UserObj).first()
|
||||
if TargetUser is None:
|
||||
raise InviteExceptions.UserDoesNotExist("User does not exist.")
|
||||
return TargetUser
|
||||
|
||||
def GetInviteKey( key : str ) -> InviteKey:
|
||||
InviteKeyObj : InviteKey = InviteKey.query.filter_by(key=key).first()
|
||||
if InviteKeyObj is None:
|
||||
raise InviteExceptions.InvalidInviteKey()
|
||||
return InviteKeyObj
|
||||
|
||||
def UseInviteKey( key : str, user : User ) -> InviteKey:
|
||||
InviteKeyObj : InviteKey = GetInviteKey(key)
|
||||
if InviteKeyObj.used_by is not None:
|
||||
raise InviteExceptions.InviteKeyAlreadyUsed()
|
||||
InviteKeyObj.used_by = user.id
|
||||
InviteKeyObj.used_on = datetime.utcnow()
|
||||
db.session.add(InviteKeyObj)
|
||||
db.session.commit()
|
||||
return InviteKeyObj
|
||||
|
||||
def CreateInviteKey( creator : User | int | None ) -> InviteKey:
|
||||
"""
|
||||
Create a new invite key.
|
||||
"""
|
||||
if creator is not None:
|
||||
creator = GetUserFromId(creator)
|
||||
invite_key = _GenerateInviteKey()
|
||||
InviteKeyObj = InviteKey(
|
||||
key = invite_key,
|
||||
created_by = creator.id if creator is not None else None
|
||||
)
|
||||
db.session.add(InviteKeyObj)
|
||||
db.session.commit()
|
||||
return InviteKeyObj
|
||||
@@ -0,0 +1,82 @@
|
||||
import requests
|
||||
import logging
|
||||
import json
|
||||
from app.extensions import redis_controller
|
||||
from app import Config
|
||||
|
||||
config = Config()
|
||||
|
||||
class IPHubQueryFailed(Exception):
|
||||
pass
|
||||
class IPHubRateLimited(Exception):
|
||||
pass
|
||||
|
||||
def lookup_address_info( address : str, skip_cache : bool = False, lookup_timeout : int = 10 ) -> dict:
|
||||
if not skip_cache:
|
||||
try:
|
||||
CachedResponse : str = redis_controller.get(f"iphub_lookup_{address}")
|
||||
if CachedResponse is not None:
|
||||
return json.loads(CachedResponse)
|
||||
except Exception as e:
|
||||
logging.error(f"lookup_address_info : IPHub cache lookup failed: {e}")
|
||||
redis_controller.delete(f"iphub_lookup_{address}")
|
||||
try:
|
||||
LookupResponse : requests.Response = requests.get(
|
||||
f"https://api.ipapi.is/?q={address}&key={config.IPAPI_AUTH_KEY}", # We NEED to put this in env (and not hardcode it)
|
||||
headers = { },
|
||||
timeout = lookup_timeout
|
||||
)
|
||||
if LookupResponse.status_code == 403 or LookupResponse.status_code == 429:
|
||||
logging.error("lookup_address_info : IPHub rate limited")
|
||||
raise IPHubRateLimited("IPHub rate limited")
|
||||
elif LookupResponse.status_code != 200:
|
||||
logging.error(f"lookup_address_info : IPHub query failed: {LookupResponse.status_code} {LookupResponse.text}")
|
||||
raise IPHubQueryFailed(f"IPHub query failed: {LookupResponse.status_code} {LookupResponse.text}")
|
||||
|
||||
LookupResponseJSON : dict = LookupResponse.json()
|
||||
redis_controller.setex(
|
||||
name = f"iphub_lookup_{address}",
|
||||
time = config.IPAPI_CACHE_LIFETIME,
|
||||
value = json.dumps(LookupResponseJSON)
|
||||
)
|
||||
return LookupResponseJSON
|
||||
except requests.exceptions.Timeout:
|
||||
logging.error(f"lookup_address_info : IPHub query timed out after {lookup_timeout} seconds")
|
||||
raise IPHubQueryFailed("IPHub query timed out")
|
||||
except Exception as e:
|
||||
logging.error(f"lookup_address_info : IPHub query failed: {e}")
|
||||
raise IPHubQueryFailed(f"IPHub query failed: {e}")
|
||||
|
||||
def fetch_address_risk( address : str, skip_cache : bool = False, lookup_timeout : int = 10, fallback_on_exception : bool = True ) -> int:
|
||||
"""
|
||||
:param address: The IP address to check
|
||||
:param skip_cache: Whether to skip the cache and query IPHub directly
|
||||
:param lookup_timeout: The timeout for the query
|
||||
:param fallback_on_exception: Whether to return 0 if the query fails
|
||||
|
||||
:return: The risk level of the IP address (0, 1, or 2)
|
||||
|
||||
https://iphub.info/api
|
||||
|
||||
0 - Residential or business IP (i.e. safe IP)
|
||||
1 - Non-residential IP (hosting provider, proxy, etc.)
|
||||
2 - Non-residential & residential IP (warning, may flag innocent people)
|
||||
"""
|
||||
|
||||
try:
|
||||
LookupResponseJSON: dict = lookup_address_info(address, skip_cache, lookup_timeout)
|
||||
if LookupResponseJSON.get("is_crawler", True) \
|
||||
or LookupResponseJSON.get("is_datacenter", False) \
|
||||
or LookupResponseJSON.get("is_tor", False) \
|
||||
or LookupResponseJSON.get("is_proxy", False) \
|
||||
or LookupResponseJSON.get("is_vpn", False) \
|
||||
or LookupResponseJSON.get("is_abuser", False):
|
||||
return 1
|
||||
else:
|
||||
return
|
||||
except Exception as e:
|
||||
if fallback_on_exception:
|
||||
logging.error(f"fetch_address_risk : Exception raised falling back: {e}")
|
||||
return 3 # Why would we let them pass????????????
|
||||
else:
|
||||
raise e
|
||||
@@ -0,0 +1,216 @@
|
||||
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
|
||||
@@ -0,0 +1,200 @@
|
||||
from app.models.friend_relationship import FriendRelationship
|
||||
from app.models.friend_request import FriendRequest
|
||||
from app.models.user import User
|
||||
|
||||
from app.extensions import db, redis_controller
|
||||
|
||||
import redis_lock
|
||||
|
||||
class FriendExceptions():
|
||||
class AlreadyFriends(Exception):
|
||||
pass
|
||||
class CannotFriendSelf(Exception):
|
||||
pass
|
||||
class RedisLockAcquireError(Exception):
|
||||
pass
|
||||
class UserRateLimited(Exception):
|
||||
pass
|
||||
class UserNotFriends(Exception):
|
||||
pass
|
||||
class FriendIsDisabled(Exception):
|
||||
pass
|
||||
class RecipientHasTooManyFriends(Exception):
|
||||
pass
|
||||
class SenderHasTooManyFriends(Exception):
|
||||
pass
|
||||
|
||||
def get_friend_count(
|
||||
user : User
|
||||
) -> int:
|
||||
"""
|
||||
:param user : User : The user
|
||||
|
||||
:return int : The amount of friends the user has
|
||||
"""
|
||||
|
||||
assert isinstance(user, User), f"Expected user to be of type User, got {user.__class__}"
|
||||
|
||||
return FriendRelationship.query.filter(
|
||||
(FriendRelationship.user_id == user.id) | (FriendRelationship.friend_id == user.id)
|
||||
).count()
|
||||
|
||||
def get_friend_relationship(
|
||||
user1 : User,
|
||||
user2 : User
|
||||
) -> FriendRelationship | None:
|
||||
"""
|
||||
:param user1 : User : The first user
|
||||
:param user2 : User : The second
|
||||
|
||||
:return FriendRelationship | None : The friend relationship or None
|
||||
"""
|
||||
|
||||
assert isinstance(user1, User), f"Expected user1 to be of type User, got {user1.__class__.__name__}"
|
||||
assert isinstance(user2, User), f"Expected user2 to be of type User, got {user2.__class__}"
|
||||
|
||||
return FriendRelationship.query.filter(
|
||||
(FriendRelationship.user_id == user1.id and FriendRelationship.friend_id == user2.id) |
|
||||
(FriendRelationship.user_id == user2.id and FriendRelationship.friend_id == user1.id)
|
||||
).first()
|
||||
|
||||
def create_friend_relationship(
|
||||
user1 : User,
|
||||
user2 : User,
|
||||
) -> FriendRelationship:
|
||||
"""
|
||||
:param user1 : User : The first user
|
||||
:param user2 : User : The second
|
||||
|
||||
:return FriendRelationship : The friend relationship
|
||||
"""
|
||||
|
||||
assert isinstance(user1, User), f"Expected user1 to be of type User, got {user1.__class__.__name__}"
|
||||
assert isinstance(user2, User), f"Expected user2 to be of type User, got {user2.__class__}"
|
||||
|
||||
FirstUser = user1 if user1.id < user2.id else user2
|
||||
SecondUser = user2 if user1.id < user2.id else user1
|
||||
|
||||
try:
|
||||
with redis_lock.Lock( redis_client = redis_controller, name = f"services:friends:create_friend_relationship:{FirstUser.id}:{SecondUser.id}", expire = 10 ):
|
||||
if get_friend_relationship(user1, user2) is not None:
|
||||
raise FriendExceptions.AlreadyFriends
|
||||
|
||||
friendRelationship = FriendRelationship(
|
||||
user_id = user1.id,
|
||||
friend_id = user2.id
|
||||
)
|
||||
|
||||
db.session.add(friendRelationship)
|
||||
db.session.commit()
|
||||
|
||||
return friendRelationship
|
||||
except AssertionError:
|
||||
raise FriendExceptions.RedisLockAcquireError
|
||||
|
||||
def remove_friend_relationship(
|
||||
user1 : User,
|
||||
user2 : User
|
||||
) -> None:
|
||||
"""
|
||||
:param user1 : User : The first user
|
||||
:param user2 : User : The second
|
||||
|
||||
:return None
|
||||
"""
|
||||
|
||||
assert isinstance(user1, User), f"Expected user1 to be of type User, got {user1.__class__}"
|
||||
assert isinstance(user2, User), f"Expected user2 to be of type User, got {user2.__class__}"
|
||||
|
||||
FirstUser = user1 if user1.id < user2.id else user2
|
||||
SecondUser = user2 if user1.id < user2.id else user1
|
||||
|
||||
try:
|
||||
with redis_lock.Lock( redis_client = redis_controller, name = f"services:friends:remove_friend_relationship:{FirstUser.id}:{SecondUser.id}", expire = 10 ):
|
||||
friendRelationship = get_friend_relationship(user1, user2)
|
||||
if friendRelationship is None:
|
||||
raise FriendExceptions.UserNotFriends
|
||||
|
||||
db.session.delete(friendRelationship)
|
||||
db.session.commit()
|
||||
|
||||
return None
|
||||
except AssertionError:
|
||||
raise FriendExceptions.RedisLockAcquireError
|
||||
|
||||
def send_friend_request(
|
||||
sender_user : User,
|
||||
recipient_user : User
|
||||
) -> FriendRequest | FriendRelationship:
|
||||
"""
|
||||
:param sender_user : User : The user sending the friend request
|
||||
:param recipient_user : User : The user receiving the friend request
|
||||
|
||||
:return FriendRequest | FriendRelationship : The friend request or friend relationship if there is already an existing friend request from the recipient user
|
||||
"""
|
||||
|
||||
assert isinstance(sender_user, User), f"Expected user1 to be of type User, got {sender_user.__class__}"
|
||||
assert isinstance(recipient_user, User), f"Expected user2 to be of type User, got {recipient_user.__class__}"
|
||||
|
||||
FirstUser = sender_user if sender_user.id < recipient_user.id else recipient_user
|
||||
SecondUser = recipient_user if sender_user.id < recipient_user.id else sender_user
|
||||
|
||||
try:
|
||||
with redis_lock.Lock( redis_client = redis_controller, name = f"services:friends:send_friend_request:{FirstUser.id}:{SecondUser.id}", expire = 10 ):
|
||||
if get_friend_relationship(sender_user, recipient_user) is not None:
|
||||
raise FriendExceptions.AlreadyFriends
|
||||
|
||||
if sender_user.id == recipient_user.id:
|
||||
raise FriendExceptions.CannotFriendSelf
|
||||
|
||||
if get_friend_count(recipient_user) >= 200:
|
||||
raise FriendExceptions.RecipientHasTooManyFriends
|
||||
if get_friend_count(sender_user) >= 200:
|
||||
raise FriendExceptions.SenderHasTooManyFriends
|
||||
|
||||
if redis_controller.get(f"rate_limit:friends:send_friend_request:{sender_user.id}") is not None:
|
||||
raise FriendExceptions.UserRateLimited
|
||||
redis_controller.set(f"rate_limit:friends:send_friend_request:{sender_user.id}", "1", ex = 3)
|
||||
|
||||
otherFriendRequest = FriendRequest.query.filter_by(requester_id = recipient_user.id, requestee_id = sender_user.id).first()
|
||||
if otherFriendRequest is not None:
|
||||
db.session.delete(otherFriendRequest)
|
||||
db.session.commit()
|
||||
|
||||
return create_friend_relationship( user1 = sender_user, user2 = recipient_user)
|
||||
|
||||
friendRequest = FriendRequest.query.filter_by(requester_id = sender_user.id, requestee_id = recipient_user.id).first()
|
||||
if friendRequest is not None:
|
||||
return friendRequest
|
||||
|
||||
friendRequest = FriendRequest(
|
||||
requester_id = sender_user.id,
|
||||
requestee_id = recipient_user.id
|
||||
)
|
||||
db.session.add(friendRequest)
|
||||
db.session.commit()
|
||||
|
||||
return friendRequest
|
||||
except AssertionError:
|
||||
raise FriendExceptions.RedisLockAcquireError
|
||||
|
||||
def decline_friend_request(
|
||||
sender_user : User,
|
||||
recipient_user : User
|
||||
) -> None:
|
||||
"""
|
||||
:param sender_user : User : The user sending the friend request
|
||||
:param recipient_user : User : The user receiving the friend request
|
||||
|
||||
:return None
|
||||
"""
|
||||
|
||||
assert isinstance(sender_user, User), f"Expected user1 to be of type User, got {sender_user.__class__}"
|
||||
assert isinstance(recipient_user, User), f"Expected user2 to be of type User, got {recipient_user.__class__}"
|
||||
|
||||
friendRequest = FriendRequest.query.filter_by(requester_id = sender_user.id, requestee_id = recipient_user.id).first()
|
||||
if friendRequest is not None:
|
||||
db.session.delete(friendRequest)
|
||||
db.session.commit()
|
||||
|
||||
return None
|
||||
Reference in New Issue
Block a user