add web
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
#badges.roblox.com
|
||||
import logging
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, abort, jsonify, make_response, after_this_request, Response
|
||||
from app.util import auth
|
||||
from app.extensions import db, csrf, limiter, redis_controller
|
||||
from flask_wtf.csrf import CSRFError, generate_csrf
|
||||
|
||||
from app.models.user import User
|
||||
from app.models.groups import Group
|
||||
from app.models.place import Place
|
||||
from app.models.userassets import UserAsset
|
||||
from app.models.place_badge import PlaceBadge, UserBadge
|
||||
from app.models.asset import Asset
|
||||
from app.models.game_session_log import GameSessionLog
|
||||
from app.models.placeserver_players import PlaceServerPlayer
|
||||
from app.models.placeservers import PlaceServer
|
||||
from app.models.universe import Universe
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import and_
|
||||
|
||||
BadgesAPIRoute = Blueprint( 'BadgesAPIRoute', __name__, url_prefix='/')
|
||||
|
||||
def CalculateBadgeRarity( badge_id : int, bypass_cache : bool = False ) -> float:
|
||||
"""
|
||||
Badges have a rarity value that is influenced by the ratio of the number of users in the past 24 hours
|
||||
to achieve the badge to the total number of players that have joined the experience within the same timespan;
|
||||
for example, if 99,900 of 100,000 daily visitors receive a badge, its difficulty will likely be Freebie (99.9%),
|
||||
while if only 100 players get a badge in the same experience, its difficulty will appear as Impossible (0.1%).
|
||||
|
||||
https://roblox.fandom.com/wiki/Player_badge
|
||||
"""
|
||||
|
||||
def _calculate_rarity() -> float:
|
||||
BadgeObj : PlaceBadge = PlaceBadge.query.filter_by( id = badge_id ).first()
|
||||
if BadgeObj is None:
|
||||
return 0.0
|
||||
PlaceObj : Place = Place.query.filter_by( placeid = BadgeObj.associated_place_id ).first()
|
||||
UniverseObj : Universe = Universe.query.filter_by( id = PlaceObj.parent_universe_id ).first()
|
||||
AwardedRecentlyCount : int = UserBadge.query.filter_by( badge_id = badge_id ).filter( UserBadge.awarded_at > datetime.utcnow() - timedelta( hours = 24 ) ).count()
|
||||
TotalPlayedRecentlyCount : int = GameSessionLog.query.filter_by( place_id = UniverseObj.root_place_id ).filter( GameSessionLog.joined_at > datetime.utcnow() - timedelta( hours = 24 ) ).distinct( GameSessionLog.user_id ).count()
|
||||
|
||||
if TotalPlayedRecentlyCount == 0 or AwardedRecentlyCount == 0:
|
||||
return 0.0
|
||||
|
||||
return round( AwardedRecentlyCount / TotalPlayedRecentlyCount, 3 )
|
||||
|
||||
CacheRedisKey = f"badge_rarity_{badge_id}"
|
||||
|
||||
if not bypass_cache:
|
||||
CachedValue = redis_controller.get( CacheRedisKey )
|
||||
if CachedValue is not None:
|
||||
return float( CachedValue )
|
||||
|
||||
RarityValue = _calculate_rarity()
|
||||
redis_controller.set( CacheRedisKey, str(RarityValue), ex = 60 )
|
||||
|
||||
return RarityValue
|
||||
|
||||
def GetBadgeAwardedPastDay( badge_id ) -> int:
|
||||
timenow = datetime.utcnow()
|
||||
start_of_yesterday = datetime(timenow.year, timenow.month, timenow.day) - timedelta(days=1)
|
||||
end_of_yesterday = datetime(timenow.year, timenow.month, timenow.day) - timedelta(seconds=1)
|
||||
return UserBadge.query.filter_by( badge_id = badge_id ).filter( and_( UserBadge.awarded_at > start_of_yesterday, UserBadge.awarded_at < end_of_yesterday ) ).count()
|
||||
|
||||
class UserAlreadyHasBadgeException( Exception ):
|
||||
pass
|
||||
|
||||
def AwardBadgeToUser( badge_id : int, user_obj : User ) -> UserBadge:
|
||||
""" Award a badge to a user. """
|
||||
BadgeObj : PlaceBadge | None = PlaceBadge.query.filter_by( id = badge_id ).first()
|
||||
if BadgeObj is None:
|
||||
raise ValueError("Badge does not exist.")
|
||||
|
||||
if UserBadge.query.filter_by( badge_id = badge_id, user_id = user_obj.id ).first() is not None:
|
||||
raise UserAlreadyHasBadgeException("User already has badge.")
|
||||
|
||||
BadgeAwarded = UserBadge( badge_id = badge_id, user_id = user_obj.id )
|
||||
db.session.add( BadgeAwarded )
|
||||
|
||||
try:
|
||||
if BadgeObj.asset_reward is not None:
|
||||
UserAssetObj : UserAsset | None = UserAsset.query.filter_by( userid = user_obj.id, assetid = BadgeObj.asset_reward ).first()
|
||||
if UserAssetObj is None:
|
||||
AssetObj : Asset | None = Asset.query.filter_by( id = BadgeObj.asset_reward ).first()
|
||||
if AssetObj is None:
|
||||
raise ValueError(f"Asset [{BadgeObj.asset_reward}] does not exist for badge {BadgeObj.id}")
|
||||
|
||||
UserAssetObj = UserAsset( userid = user_obj.id, assetid = BadgeObj.asset_reward )
|
||||
db.session.add( UserAssetObj )
|
||||
except Exception as e:
|
||||
logging.error(f"Error attempting to give user [{user_obj.id}] badge award [{badge_id}]: {e}")
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return BadgeAwarded
|
||||
|
||||
def GetAssetCreator( asset_obj : Asset ) -> User | Group | None:
|
||||
if asset_obj.creator_type == 0:
|
||||
return User.query.filter_by( id = asset_obj.creator_id ).first()
|
||||
elif asset_obj.creator_type == 1:
|
||||
return Group.query.filter_by( id = asset_obj.creator_id ).first()
|
||||
|
||||
return None
|
||||
|
||||
csrf.exempt(BadgesAPIRoute)
|
||||
@BadgesAPIRoute.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
|
||||
|
||||
@BadgesAPIRoute.errorhandler(429)
|
||||
def handle_ratelimit_reached(e):
|
||||
return jsonify({"errors": [{"code": 9,"message": "The flood limit has been exceeded."}]}), 429
|
||||
@BadgesAPIRoute.errorhandler(405)
|
||||
def handle_method_not_allowed(e):
|
||||
return jsonify({"errors": [{"code": 0,"message": "MethodNotAllowed"}]}), 405
|
||||
|
||||
@BadgesAPIRoute.before_request
|
||||
def before_request():
|
||||
if "Roblox/" not in request.user_agent.string:
|
||||
csrf.protect()
|
||||
|
||||
@BadgesAPIRoute.route('/v1/badges/<int:badge_id>', methods=['GET'])
|
||||
@limiter.limit("60/minute")
|
||||
def get_badge_info( badge_id : int ):
|
||||
BadgeObj : PlaceBadge = PlaceBadge.query.filter_by( id = badge_id ).first()
|
||||
if BadgeObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404
|
||||
|
||||
UniverseObj : Universe = Universe.query.filter_by( id = BadgeObj.universe_id ).first()
|
||||
|
||||
BadgeAwardedCount : int = UserBadge.query.filter_by( badge_id = badge_id ).count()
|
||||
PastAwardedCount : int = UserBadge.query.filter_by( badge_id = badge_id ).filter( UserBadge.awarded_at > datetime.utcnow() - timedelta( hours = 24 ) ).count()
|
||||
AssociatedPlaceAsset : Asset = Asset.query.filter_by( id = UniverseObj.root_place_id ).first()
|
||||
BadgeWinRatePercentage : float = CalculateBadgeRarity( badge_id = badge_id )
|
||||
|
||||
return jsonify({
|
||||
"id": BadgeObj.id,
|
||||
"name": BadgeObj.name,
|
||||
"description": BadgeObj.description,
|
||||
"displayName": BadgeObj.name,
|
||||
"displayDescription": BadgeObj.description,
|
||||
"enabled": BadgeObj.enabled,
|
||||
"iconImageId": BadgeObj.icon_image_id,
|
||||
"displayIconImageId": BadgeObj.icon_image_id,
|
||||
"created": BadgeObj.created_at.isoformat(),
|
||||
"updated": BadgeObj.updated_at.isoformat(),
|
||||
"statistics": {
|
||||
"pastAwardCount": PastAwardedCount,
|
||||
"awardCount": BadgeAwardedCount,
|
||||
"winRatePercentage": BadgeWinRatePercentage
|
||||
},
|
||||
"awardingUniverse": {
|
||||
"id": UniverseObj.id,
|
||||
"name": AssociatedPlaceAsset.name,
|
||||
"rootPlaceId": UniverseObj.root_place_id
|
||||
}
|
||||
})
|
||||
|
||||
# This endpoint should only be used by 2020 games, which should always have "AccessKey" in the request headers unlike 2014
|
||||
@BadgesAPIRoute.route('/v1/users/<int:user_id>/badges/<int:badge_id>/award-badge', methods=["POST"])
|
||||
@auth.gameserver_accesskey_required
|
||||
def server_award_badge_route( user_id : int, badge_id : int ):
|
||||
BadgeObj : PlaceBadge = PlaceBadge.query.filter_by( id = badge_id ).first()
|
||||
if BadgeObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404
|
||||
|
||||
UserObj : User | None = User.query.filter_by( id = user_id ).first()
|
||||
if UserObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User is invalid or does not exist."}]}), 404
|
||||
|
||||
PlaceServerPlayerObj : PlaceServerPlayer | None = PlaceServerPlayer.query.filter_by( userid = user_id ).first()
|
||||
if PlaceServerPlayerObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
|
||||
|
||||
PlaceServerObj : PlaceServer = PlaceServer.query.filter_by( serveruuid = PlaceServerPlayerObj.serveruuid ).first()
|
||||
# This should never happen, but just in case
|
||||
if PlaceServerObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
|
||||
ServerPlaceObj : Place = Place.query.filter_by( placeid = PlaceServerObj.serverPlaceId ).first()
|
||||
if ServerPlaceObj.parent_universe_id != BadgeObj.universe_id:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
|
||||
|
||||
RequestPlaceId = request.headers.get( key = "Roblox-Place-Id", default = None, type = int )
|
||||
if RequestPlaceId is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
|
||||
PlaceObj : Place = Place.query.filter_by( placeid = RequestPlaceId ).first()
|
||||
if PlaceObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
|
||||
if PlaceObj.parent_universe_id != BadgeObj.universe_id:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
|
||||
|
||||
try:
|
||||
AwardBadgeToUser( badge_id = badge_id, user_obj = UserObj )
|
||||
except UserAlreadyHasBadgeException:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User already has badge."}]}), 400
|
||||
except ValueError:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404
|
||||
except Exception as e:
|
||||
logging.error(f"Error awarding badge to user: {e}")
|
||||
return jsonify({"errors": [{"code": 1,"message": "Internal Server error."}]}), 500
|
||||
|
||||
return jsonify({ "success": True })
|
||||
|
||||
# 2018 RCC
|
||||
@BadgesAPIRoute.route('/assets/award-badge', methods=["POST"])
|
||||
@auth.gameserver_accesskey_required
|
||||
def server_award_badge_route_legacy():
|
||||
reqUserId = request.args.get("userId", type=int, default=None)
|
||||
reqBadgeId = request.args.get("badgeId", type=int, default=None)
|
||||
reqPlaceId = request.args.get("placeId", type=int, default=None)
|
||||
|
||||
if reqUserId is None or reqBadgeId is None or reqPlaceId is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Invalid request."}]}), 400
|
||||
|
||||
BadgeObj : PlaceBadge = PlaceBadge.query.filter_by( id = reqBadgeId ).first()
|
||||
if BadgeObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404
|
||||
|
||||
UserObj : User | None = User.query.filter_by( id = reqUserId ).first()
|
||||
if UserObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User is invalid or does not exist."}]}), 404
|
||||
|
||||
PlaceServerPlayerObj : PlaceServerPlayer | None = PlaceServerPlayer.query.filter_by( userid = reqUserId ).first()
|
||||
if PlaceServerPlayerObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
|
||||
|
||||
PlaceServerObj : PlaceServer = PlaceServer.query.filter_by( serveruuid = PlaceServerPlayerObj.serveruuid ).first()
|
||||
# This should never happen, but just in case
|
||||
if PlaceServerObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
|
||||
ServerPlaceObj : Place = Place.query.filter_by( placeid = PlaceServerObj.serverPlaceId ).first()
|
||||
if ServerPlaceObj.parent_universe_id != BadgeObj.universe_id:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
|
||||
|
||||
RequestPlaceId = request.headers.get( key = "Roblox-Place-Id", default = None, type = int )
|
||||
if RequestPlaceId is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
|
||||
if RequestPlaceId != reqPlaceId:
|
||||
return jsonify({"errors": [{"code": 1,"message": f"Roblox-Place-Id [{RequestPlaceId}] Header does not match placeId argument [{reqPlaceId}]"}]}), 400
|
||||
PlaceObj : Place = Place.query.filter_by( placeid = RequestPlaceId ).first()
|
||||
if PlaceObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
|
||||
if PlaceObj.parent_universe_id != BadgeObj.universe_id:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
|
||||
|
||||
try:
|
||||
AwardBadgeToUser( badge_id = reqBadgeId, user_obj = UserObj )
|
||||
except UserAlreadyHasBadgeException:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User already has badge."}]}), 400
|
||||
except ValueError:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404
|
||||
except Exception as e:
|
||||
logging.error(f"Error awarding badge to user: {e}")
|
||||
return jsonify({"errors": [{"code": 1,"message": "Internal Server error."}]}), 500
|
||||
PlaceAssetObj : Asset = Asset.query.filter_by( id = PlaceObj.placeid ).first()
|
||||
PlaceCreator : User | Group = GetAssetCreator( PlaceAssetObj )
|
||||
|
||||
return f"{UserObj.username} won {PlaceCreator.username if PlaceAssetObj.creator_type == 0 else PlaceCreator.name}'s \"{BadgeObj.name}\" award!" # This message is sent to client to be shown as a badge awarded notification
|
||||
|
||||
@BadgesAPIRoute.route("/Game/Badge/HasBadge.ashx", methods=["GET"])
|
||||
@auth.gameserver_authenticated_required # Only IP address is checked
|
||||
def query_user_has_badge():
|
||||
reqUserId = request.args.get("UserID", type=int, default=None)
|
||||
reqBadgeId = request.args.get("BadgeID", type=int, default=None)
|
||||
|
||||
if reqUserId is None or reqBadgeId is None:
|
||||
return "0"
|
||||
|
||||
BadgeObj : PlaceBadge | None = PlaceBadge.query.filter_by( id = reqBadgeId ).first()
|
||||
if BadgeObj is None:
|
||||
return "0"
|
||||
|
||||
UserObj : User | None = User.query.filter_by( id = reqUserId ).first()
|
||||
if UserObj is None:
|
||||
return "0"
|
||||
|
||||
if UserBadge.query.filter_by( badge_id = reqBadgeId, user_id = reqUserId ).first() is None:
|
||||
return "0"
|
||||
|
||||
return "1"
|
||||
|
||||
@BadgesAPIRoute.route("/Game/Badge/AwardBadge.ashx", methods=["POST"])
|
||||
@auth.gameserver_authenticated_required
|
||||
def award_badge_to_user():
|
||||
reqUserId = request.args.get("UserID", type=int, default=None)
|
||||
reqBadgeId = request.args.get("BadgeID", type=int, default=None)
|
||||
reqPlaceId = request.args.get("PlaceID", type=int, default=None)
|
||||
|
||||
if reqUserId is None or reqBadgeId is None or reqPlaceId is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Invalid request."}]}), 400
|
||||
|
||||
BadgeObj : PlaceBadge = PlaceBadge.query.filter_by( id = reqBadgeId ).first()
|
||||
if BadgeObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404
|
||||
|
||||
UserObj : User | None = User.query.filter_by( id = reqUserId ).first()
|
||||
if UserObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User is invalid or does not exist."}]}), 404
|
||||
|
||||
PlaceServerPlayerObj : PlaceServerPlayer | None = PlaceServerPlayer.query.filter_by( userid = reqUserId ).first()
|
||||
if PlaceServerPlayerObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
|
||||
|
||||
PlaceServerObj : PlaceServer = PlaceServer.query.filter_by( serveruuid = PlaceServerPlayerObj.serveruuid ).first()
|
||||
# This should never happen, but just in case
|
||||
if PlaceServerObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
|
||||
ServerPlaceObj : Place = Place.query.filter_by( placeid = PlaceServerObj.serverPlaceId ).first()
|
||||
if ServerPlaceObj.parent_universe_id != BadgeObj.universe_id:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
|
||||
|
||||
PlaceObj : Place = Place.query.filter_by( placeid = reqPlaceId ).first()
|
||||
if PlaceObj is None:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
|
||||
if PlaceObj.parent_universe_id != BadgeObj.universe_id:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
|
||||
|
||||
try:
|
||||
AwardBadgeToUser( badge_id = reqBadgeId, user_obj = UserObj )
|
||||
except UserAlreadyHasBadgeException:
|
||||
return jsonify({"errors": [{"code": 1,"message": "User already has badge."}]}), 400
|
||||
except ValueError:
|
||||
return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404
|
||||
except Exception as e:
|
||||
logging.error(f"Error awarding badge to user: {e}")
|
||||
return jsonify({"errors": [{"code": 1,"message": "Internal Server error."}]}), 500
|
||||
PlaceAssetObj : Asset = Asset.query.filter_by( id = PlaceObj.placeid ).first()
|
||||
PlaceCreator : User | Group = GetAssetCreator( PlaceAssetObj )
|
||||
|
||||
return f"{UserObj.username} won {PlaceCreator.username if PlaceAssetObj.creator_type == 0 else PlaceCreator.name}'s \"{BadgeObj.name}\" award!" # This message is sent to client to be shown as a badge awarded notification
|
||||
|
||||
Reference in New Issue
Block a user