708 lines
32 KiB
Python
708 lines
32 KiB
Python
from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify, abort
|
|
from app.extensions import db, redis_controller, csrf, get_remote_address
|
|
import requests
|
|
import logging
|
|
import json
|
|
import gzip
|
|
import json
|
|
import math
|
|
from datetime import datetime, timedelta
|
|
|
|
from app.models.user import User
|
|
from app.models.asset import Asset
|
|
from app.models.usereconomy import UserEconomy
|
|
from app.models.gameservers import GameServer
|
|
from app.models.placeservers import PlaceServer
|
|
from app.models.placeserver_players import PlaceServerPlayer
|
|
from app.models.place import Place
|
|
from app.models.groups import Group
|
|
from app.models.game_session_log import GameSessionLog
|
|
from app.models.universe import Universe
|
|
from app.models.user_ban import UserBan
|
|
|
|
from app.services.economy import IncrementTargetBalance
|
|
from app.services.gameserver_comm import perform_post
|
|
from app.pages.home.home import InsertRecentlyPlayed
|
|
from app.enums.TransactionType import TransactionType
|
|
from app.enums.PlaceYear import PlaceYear
|
|
from app.enums.BanType import BanType
|
|
from app.util.placeinfo import ClearPlayingCountCache, GetPlayingCount
|
|
from app.util.transactions import CreateTransaction
|
|
|
|
from config import Config
|
|
|
|
config = Config()
|
|
|
|
JobReportHandler = Blueprint('jobreporthandler', __name__, url_prefix='/')
|
|
|
|
def isValidAuthorizationToken( authtoken : str) -> GameServer:
|
|
if authtoken is None:
|
|
return None
|
|
RequestAddress = get_remote_address()
|
|
GameServerObject = GameServer.query.filter_by( accessKey = authtoken, serverIP = RequestAddress ).first()
|
|
return GameServerObject
|
|
|
|
class InvalidGZIPData(Exception):
|
|
pass
|
|
class InvalidJSONData(Exception):
|
|
pass
|
|
|
|
def IncrementPlaceVisits( PlaceObj : Place ):
|
|
PlaceObj.visitcount += 1
|
|
|
|
UniverseObj : Universe = Universe.query.filter_by( id = PlaceObj.parent_universe_id ).first()
|
|
if UniverseObj is None:
|
|
return
|
|
UniverseObj.visit_count += 1
|
|
|
|
db.session.commit()
|
|
|
|
def ParsePayloadData(throwException : bool = True):
|
|
"""
|
|
Handles RCC Post Data and returns the JSON data
|
|
"""
|
|
if request.headers.get("Content-Encoding") == "gzip":
|
|
try:
|
|
data = gzip.decompress(request.data)
|
|
except Exception as e:
|
|
raise InvalidGZIPData("Invalid gzip data")
|
|
try:
|
|
JSONData = json.loads(data)
|
|
except Exception as e:
|
|
raise InvalidJSONData("Invalid JSON data")
|
|
else:
|
|
JSONData = request.json
|
|
if JSONData is None:
|
|
raise InvalidJSONData("Invalid JSON data")
|
|
return JSONData
|
|
|
|
def EvictPlayer( PlaceServerObject : PlaceServer, UserId : int ):
|
|
PlaceObj : Place = Place.query.filter_by( placeid = PlaceServerObject.serverPlaceId ).first()
|
|
MasterServer : GameServer | None = GameServer.query.filter_by(serverId=PlaceServerObject.originServerId).first()
|
|
|
|
try:
|
|
if PlaceObj.placeyear in [PlaceYear.Eighteen, PlaceYear.Twenty, PlaceYear.Sixteen]:
|
|
if PlaceObj.placeyear in [PlaceYear.Eighteen, PlaceYear.Twenty]:
|
|
ExecutionScript = f"""{{
|
|
"Mode": "EvictPlayer",
|
|
"MessageVersion": 1,
|
|
"Settings": {{
|
|
"PlayerId": {str(UserId)}
|
|
}}
|
|
}}"""
|
|
elif PlaceObj.placeyear in [PlaceYear.Sixteen]:
|
|
ExecutionScript = f"""for _, Player in pairs(game:GetService("Players"):GetPlayers()) do if Player.UserId == {UserId} then Player:Kick("Disconnected from game, possibly due to game joined from another device") end end"""
|
|
else:
|
|
raise Exception("PlaceYear is not compatible")
|
|
|
|
ExecuteScriptRequest = perform_post(
|
|
TargetGameserver = MasterServer,
|
|
Endpoint = "Execute",
|
|
JSONData = {
|
|
"jobid": str(PlaceServerObject.serveruuid),
|
|
"script": ExecutionScript,
|
|
"arguments": []
|
|
}
|
|
)
|
|
|
|
if ExecuteScriptRequest.status_code != 200:
|
|
raise Exception(f"Unexpected Status Code: {ExecuteScriptRequest.status_code}, {ExecuteScriptRequest.content}")
|
|
elif PlaceObj.placeyear in [PlaceYear.Fourteen]:
|
|
redis_controller.set(f"EvictPlayerRequest:{PlaceServerObject.serveruuid}:{UserId}", 1, ex=60)
|
|
StartTime = datetime.utcnow()
|
|
|
|
while redis_controller.exists(f"EvictPlayerRequest:{PlaceServerObject.serveruuid}:{UserId}"):
|
|
if (datetime.utcnow() - StartTime).total_seconds() > 50:
|
|
raise Exception("Failed to evict player")
|
|
else:
|
|
raise Exception("PlaceYear is not compatible")
|
|
|
|
except Exception as e:
|
|
logging.error(f"EvictPlayer failed to send request to kick player, {e}")
|
|
|
|
@JobReportHandler.before_request
|
|
def before_request():
|
|
requesterAddress = get_remote_address()
|
|
if requesterAddress is None:
|
|
return abort(404)
|
|
gameserverObj : GameServer = GameServer.query.filter_by(serverIP=requesterAddress).first()
|
|
if gameserverObj is None:
|
|
return abort(404)
|
|
if "UserRequest" in request.headers.get( key = "accesskey", default = "" ):
|
|
return jsonify({
|
|
"success": False,
|
|
"message": "Invalid request"
|
|
}), 400
|
|
|
|
@JobReportHandler.route('/internal/gameserver/reportshutdown', methods=['POST'])
|
|
@csrf.exempt
|
|
def reportshutdown():
|
|
try:
|
|
JSONData = ParsePayloadData()
|
|
except InvalidGZIPData:
|
|
return jsonify({"status": "error", "message": "Invalid gzip data"}),400
|
|
except InvalidJSONData:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data"}),400
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "message": "Unknown error occured while parsing data"}),400
|
|
|
|
if "AuthToken" not in JSONData:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data"}),400
|
|
|
|
PlaceServerOwner : GameServer | None = isValidAuthorizationToken(JSONData["AuthToken"])
|
|
if PlaceServerOwner is None:
|
|
return jsonify({"status": "error", "message": "Invalid authorization token"}),400
|
|
|
|
placeId = JSONData["PlaceId"]
|
|
jobId = JSONData["JobId"]
|
|
|
|
PlaceServerObject : PlaceServer = PlaceServer.query.filter_by(serverPlaceId=placeId, serveruuid=jobId).first()
|
|
if PlaceServerObject is None:
|
|
return jsonify({"status": "error", "message": "Invalid place server"}),400
|
|
|
|
try:
|
|
logging.info(f"CloseJob : Closing {jobId} for place [{placeId}] because server reports shutdown")
|
|
perform_post(
|
|
TargetGameserver = PlaceServerOwner,
|
|
Endpoint = "CloseJob",
|
|
JSONData = {
|
|
"jobid": jobId,
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logging.error(f"Failed to close job ( {jobId} ) for place ( {placeId} ), {e}")
|
|
|
|
PlaceServerOwner : GameServer = GameServer.query.filter_by(serverId=PlaceServerObject.originServerId).first()
|
|
PlaceServerPlayers = PlaceServerPlayer.query.filter_by(serveruuid=jobId).all()
|
|
for PlaceServerPlayerObject in PlaceServerPlayers:
|
|
db.session.delete(PlaceServerPlayerObject)
|
|
db.session.delete(PlaceServerObject)
|
|
db.session.commit()
|
|
|
|
PlaceObj : Place = Place.query.filter_by(placeid=placeId).first()
|
|
if PlaceObj is not None:
|
|
ClearPlayingCountCache(PlaceObj)
|
|
|
|
return jsonify({"status": "success"}),200
|
|
|
|
@JobReportHandler.route('/internal/gameserver/reportstats', methods=['POST'])
|
|
@csrf.exempt
|
|
def reportstats():
|
|
try:
|
|
JSONData = ParsePayloadData()
|
|
except InvalidGZIPData:
|
|
return jsonify({"status": "error", "message": "Invalid gzip data"}),400
|
|
except InvalidJSONData:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data"}),400
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "message": "Unknown error occured while parsing data"}),400
|
|
|
|
if "AuthToken" not in JSONData:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data"}),400
|
|
|
|
PlaceServerOwner = isValidAuthorizationToken(JSONData["AuthToken"])
|
|
if PlaceServerOwner is None:
|
|
return jsonify({"status": "error", "message": "Invalid authorization token"}),400
|
|
|
|
placeId = JSONData["PlaceId"]
|
|
jobId = JSONData["JobId"]
|
|
serverAliveTime = JSONData["ServerAliveTime"]
|
|
|
|
PlaceServerObject : PlaceServer = PlaceServer.query.filter_by(serverPlaceId=placeId, serveruuid=jobId).first()
|
|
if PlaceServerObject is None:
|
|
return jsonify({"status": "success"}),200
|
|
PlaceServerObject.lastping = datetime.utcnow()
|
|
PlaceServerObject.serverRunningTime = serverAliveTime
|
|
db.session.commit()
|
|
|
|
return jsonify({"status": "success"}),200
|
|
|
|
@JobReportHandler.route('/internal/gameserver/reportplacevalidation', methods=['POST'])
|
|
@csrf.exempt
|
|
def reportplacevalidation():
|
|
try:
|
|
JSONData = ParsePayloadData()
|
|
except InvalidGZIPData:
|
|
return jsonify({"status": "error", "message": "Invalid gzip data"}),400
|
|
except InvalidJSONData:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data"}),400
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "message": "Unknown error occured while parsing data"}),400
|
|
|
|
if "AuthToken" not in JSONData:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data"}),400
|
|
|
|
PlaceServerOwner = isValidAuthorizationToken(JSONData["AuthToken"])
|
|
if PlaceServerOwner is None:
|
|
return jsonify({"status": "error", "message": "Invalid authorization token"}),400
|
|
|
|
ValidationRequestId = JSONData["ReqId"]
|
|
LoadSuccess = JSONData["LoadSuccess"]
|
|
ErrorMessage = None
|
|
if "ErrorMessage" in JSONData:
|
|
ErrorMessage = JSONData["ErrorMessage"]
|
|
|
|
redis_controller.set(f"ValidatePlaceFileRequest:{ValidationRequestId}", json.dumps({
|
|
"valid": LoadSuccess,
|
|
"error": ErrorMessage
|
|
}), ex=600)
|
|
logging.info(f"Place validation request ( {ValidationRequestId} ) has been completed")
|
|
return jsonify({"status": "success"}),200
|
|
|
|
def HandleUserTimePlayed( UserObj : User, Timeplayed : int, serverUUID : str = None, placeId : int = None ):
|
|
"""
|
|
For every 40 seconds the user has played a game we give them 1 ticket
|
|
however we limit the tickets to 100 per day and the user account must be
|
|
more than 2 days old
|
|
"""
|
|
if serverUUID is not None and placeId is not None:
|
|
GameSessionLogObject : GameSessionLog = GameSessionLog.query.filter_by(serveruuid=serverUUID, place_id=placeId, user_id=UserObj.id).first()
|
|
if GameSessionLogObject is None:
|
|
GameSessionLogObject = GameSessionLog(
|
|
user_id = UserObj.id,
|
|
serveruuid = serverUUID,
|
|
place_id = placeId,
|
|
joined_at = datetime.utcnow() - timedelta(seconds=Timeplayed),
|
|
left_at = datetime.utcnow()
|
|
)
|
|
db.session.add(GameSessionLogObject)
|
|
else:
|
|
GameSessionLogObject.left_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
if datetime.utcnow() - timedelta(days=2) < UserObj.created:
|
|
return
|
|
|
|
RawTicketsEarned = math.floor(Timeplayed / 40)
|
|
CurrentDay = datetime.utcnow().day
|
|
TicketsEarnedToday = redis_controller.get(f"UserTicketsEarned:{UserObj.id}:{CurrentDay}")
|
|
if TicketsEarnedToday is None:
|
|
TicketsEarnedToday = 0
|
|
else:
|
|
TicketsEarnedToday = int(TicketsEarnedToday)
|
|
|
|
if TicketsEarnedToday >= 500:
|
|
return
|
|
|
|
TicketsToGive = 500 - TicketsEarnedToday
|
|
if RawTicketsEarned > TicketsToGive:
|
|
RawTicketsEarned = TicketsToGive
|
|
|
|
if RawTicketsEarned <= 0:
|
|
return
|
|
|
|
IncrementTargetBalance(UserObj, RawTicketsEarned, 1)
|
|
CreateTransaction(
|
|
Reciever = UserObj,
|
|
Sender = User.query.filter_by(id=1).first(),
|
|
CurrencyAmount = RawTicketsEarned,
|
|
CurrencyType = 1,
|
|
TransactionType = TransactionType.BuildersClubStipend,
|
|
AssetId = None,
|
|
CustomText = f"Played game for {str(round(Timeplayed,1))} seconds"
|
|
)
|
|
AmountOfTicketsEarnedToday = TicketsEarnedToday + RawTicketsEarned
|
|
redis_controller.set(f"UserTicketsEarned:{UserObj.id}:{CurrentDay}", AmountOfTicketsEarnedToday, ex=86400)
|
|
|
|
@JobReportHandler.route('/internal/gameserver/verifyplayer', methods=['POST'])
|
|
@csrf.exempt
|
|
def verifyplayer():
|
|
try:
|
|
JSONData = ParsePayloadData()
|
|
except InvalidGZIPData:
|
|
return jsonify({"status": "error", "message": "Invalid gzip data"}),400
|
|
except InvalidJSONData:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data"}),400
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "message": "Unknown error occured while parsing data"}),400
|
|
|
|
if "AuthToken" not in JSONData:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data"}),400
|
|
|
|
PlaceServerOwner = isValidAuthorizationToken(JSONData["AuthToken"])
|
|
if PlaceServerOwner is None:
|
|
return jsonify({"status": "error", "message": "Invalid authorization token"}),400
|
|
|
|
jobId = JSONData["JobId"]
|
|
PlaceServerObject : PlaceServer = PlaceServer.query.filter_by(serveruuid=jobId).first()
|
|
if PlaceServerObject is None:
|
|
return jsonify({"status": "error", "message": "Invalid place server"}),400
|
|
PlaceServerObject.lastping = datetime.utcnow()
|
|
PlaceObject : Place = Place.query.filter_by(placeid=PlaceServerObject.serverPlaceId).first()
|
|
if PlaceObject is None:
|
|
return jsonify({"status": "error", "message": "Invalid place"}),400
|
|
|
|
UserId = JSONData["UserId"]
|
|
UserObject : User = User.query.filter_by(id=UserId).first()
|
|
if UserObject is None or UserObject.accountstatus != 1:
|
|
return jsonify({"status": "error", "message": "Invalid user", "authenticated": False}), 200
|
|
if "Username" not in JSONData or "CharacterAppearance" not in JSONData or "VerificationTicket" not in JSONData:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data", "authenticated": False}), 200
|
|
|
|
Username = JSONData["Username"]
|
|
CharacterAppearance = JSONData["CharacterAppearance"]
|
|
VerificationTicket = JSONData["VerificationTicket"]
|
|
|
|
authKeyName = f"joinashx-auth:{str(jobId)}:{str(UserId)}:{str(PlaceObject.placeid)}:{VerificationTicket}"
|
|
|
|
if not redis_controller.exists(authKeyName):
|
|
return jsonify({"status": "error", "message": "Invalid join request", "authenticated": False}), 200
|
|
|
|
JoinInfo = json.loads(redis_controller.get(authKeyName))
|
|
if JoinInfo is None:
|
|
return jsonify({"status": "error", "message": "Invalid join request", "authenticated": False}), 200
|
|
redis_controller.delete(authKeyName)
|
|
if JoinInfo["CharacterAppearance"] != CharacterAppearance or JoinInfo["Username"] != Username:
|
|
return jsonify({"status": "error", "message": "Invalid join request", "authenticated": False}), 200
|
|
return jsonify({"status": "success", "message": "Valid join request", "authenticated": True}), 200
|
|
|
|
@JobReportHandler.route('/internal/gameserver/reportplayers', methods=['POST'])
|
|
@csrf.exempt
|
|
def reportplayers():
|
|
try:
|
|
JSONData = ParsePayloadData()
|
|
except InvalidGZIPData:
|
|
return jsonify({"status": "error", "message": "Invalid gzip data"}),400
|
|
except InvalidJSONData:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data"}),400
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "message": "Unknown error occured while parsing data"}),400
|
|
|
|
if "AuthToken" not in JSONData:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data"}),400
|
|
|
|
PlaceServerOwner = isValidAuthorizationToken(JSONData["AuthToken"])
|
|
if PlaceServerOwner is None:
|
|
return jsonify({"status": "error", "message": "Invalid authorization token"}),400
|
|
|
|
jobId = JSONData["JobId"]
|
|
players = JSONData["Players"] # Array of players in the server which each player is a dictionary ( { "UserId": 1, "Name": "test" } )
|
|
playerCount = len(players)
|
|
|
|
PlaceServerObject : PlaceServer = PlaceServer.query.filter_by(serveruuid=jobId).first()
|
|
if PlaceServerObject is None:
|
|
return jsonify({"status": "error", "message": "Invalid place server"}),400
|
|
PlaceServerObject.lastping = datetime.utcnow()
|
|
PlaceObject : Place = Place.query.filter_by(placeid=PlaceServerObject.serverPlaceId).first()
|
|
if PlaceObject is None:
|
|
return jsonify({"status": "error", "message": "Invalid place"}),400
|
|
AssetObject : Asset = Asset.query.filter_by(id=PlaceObject.placeid).first()
|
|
CreatorObject : User | Group = None
|
|
if AssetObject.creator_type == 0:
|
|
CreatorObject = User.query.filter_by(id=AssetObject.creator_id).first()
|
|
else:
|
|
CreatorObject = Group.query.filter_by(id=AssetObject.creator_id).first()
|
|
|
|
BadPlayers = [] # Array of players which must be kicked from the server
|
|
for player in players:
|
|
UserObject : User = User.query.filter_by(id=player["UserId"]).first()
|
|
if UserObject is None or UserObject.username != player["Name"] or UserObject.accountstatus != 1:
|
|
logging.info(f"/internal/gameserver/reportplayers - {jobId} - Invalid Player ( {player['UserId']} ) - {player['Name']}")
|
|
BadPlayers.append(player["UserId"])
|
|
playerCount -= 1
|
|
continue
|
|
|
|
if redis_controller.exists(f"EvictPlayerRequest:{jobId}:{player['UserId']}"):
|
|
redis_controller.delete(f"EvictPlayerRequest:{jobId}:{player['UserId']}")
|
|
logging.info(f"/internal/gameserver/reportplayers - {jobId} - Player ( {player['UserId']} ) requested kick")
|
|
BadPlayers.append(player["UserId"])
|
|
playerCount -= 1
|
|
continue
|
|
|
|
PlaceServerPlayerObject : PlaceServerPlayer = PlaceServerPlayer.query.filter_by(userid=player["UserId"]).first()
|
|
if PlaceServerPlayerObject is None:
|
|
if not redis_controller.exists(f"allow_join:{str(player['UserId'])}:{PlaceObject.placeid}:{str(jobId)}"):
|
|
logging.info(f"/internal/gameserver/reportplayers - {jobId} - Player ( {player['UserId']} ) is not allowed to join")
|
|
BadPlayers.append(player["UserId"])
|
|
playerCount -= 1
|
|
continue
|
|
|
|
PlaceServerPlayerObject = PlaceServerPlayer(serveruuid=jobId, userid=player["UserId"], joinTime= datetime.utcnow())
|
|
IncrementPlaceVisits(PlaceObject)
|
|
if CreatorObject is not None:
|
|
IncrementTargetBalance(CreatorObject, 1, 1)
|
|
UserObject.lastonline = datetime.utcnow()
|
|
InsertRecentlyPlayed(UserObj = UserObject, PlaceId = PlaceObject.placeid)
|
|
db.session.add(PlaceServerPlayerObject)
|
|
else:
|
|
UserId = player["UserId"]
|
|
if str(PlaceServerPlayerObject.serveruuid) == jobId:
|
|
PlaceServerPlayerObject.lastHeartbeat = datetime.utcnow()
|
|
UserObject.lastonline = datetime.utcnow()
|
|
else:
|
|
OtherPlaceServerObj = PlaceServer.query.filter_by(serveruuid=PlaceServerPlayerObj.serveruuid).first()
|
|
if OtherPlaceServerObj is not None:
|
|
EvictPlayer( OtherPlaceServerObj, UserId )
|
|
|
|
TotalTimePlayed = (datetime.utcnow() - PlaceServerPlayerObject.joinTime).total_seconds()
|
|
UserObj : User = User.query.filter_by(id=PlaceServerPlayerObject.userid).first()
|
|
HandleUserTimePlayed(UserObj, TotalTimePlayed, serverUUID=jobId, placeId=PlaceObject.placeid)
|
|
db.session.delete(PlaceServerPlayerObj)
|
|
|
|
PlaceServerPlayerObj = PlaceServerPlayer(serveruuid=jobId, userid=UserId, joinTime= datetime.utcnow())
|
|
IncrementPlaceVisits(PlaceObject)
|
|
if CreatorObject is not None:
|
|
IncrementTargetBalance(CreatorObject, 1, 1)
|
|
UserObject.lastonline = datetime.utcnow()
|
|
InsertRecentlyPlayed(UserObj = UserObject, PlaceId = PlaceObject.placeid)
|
|
|
|
PlaceServerPlayers = PlaceServerPlayer.query.filter_by(serveruuid=jobId).all()
|
|
for PlaceServerPlayerObject in PlaceServerPlayers:
|
|
playerFound = False
|
|
for player in players:
|
|
if PlaceServerPlayerObject.userid == player["UserId"]:
|
|
playerFound = True
|
|
break
|
|
if playerFound == False:
|
|
TotalTimePlayed = (datetime.utcnow() - PlaceServerPlayerObject.joinTime).total_seconds()
|
|
UserObj : User = User.query.filter_by(id=PlaceServerPlayerObject.userid).first()
|
|
HandleUserTimePlayed(UserObj, TotalTimePlayed, serverUUID=jobId, placeId=PlaceObject.placeid)
|
|
db.session.delete(PlaceServerPlayerObject)
|
|
|
|
PlaceServerObject.playerCount = playerCount
|
|
db.session.commit()
|
|
ClearPlayingCountCache(PlaceObject)
|
|
return jsonify({"status": "success", "bad": BadPlayers}),200
|
|
|
|
@JobReportHandler.route('/internal/gameserver/reportfailure', methods=['POST'])
|
|
@csrf.exempt
|
|
def reportfailure():
|
|
# TODO: Implement this
|
|
return jsonify({"status": "success"}),200
|
|
|
|
# 2018+ endpoints
|
|
|
|
@JobReportHandler.route("/v2/CreateOrUpdate/", methods=['POST'])
|
|
@csrf.exempt
|
|
def CreateOrUpdate():
|
|
try:
|
|
JSONData = ParsePayloadData()
|
|
except InvalidGZIPData:
|
|
return jsonify({"status": "error", "message": "Invalid gzip data"}),400
|
|
except InvalidJSONData:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data"}),400
|
|
except Exception as e:
|
|
return jsonify({"status": "error", "message": "Unknown error occured while parsing data"}),400
|
|
|
|
RequestHost = request.headers.get('Host')
|
|
if RequestHost is None:
|
|
return abort(404)
|
|
if not RequestHost.startswith("gameinstances.api."):
|
|
return abort(404)
|
|
|
|
AccessKey = request.args.get( key = 'apiKey', default = None, type = str)
|
|
JobId = request.args.get( key = 'gameId', default = None, type = str)
|
|
if AccessKey is None or JobId is None:
|
|
return abort(404)
|
|
|
|
# Newer RCCs uses temporary access keys to report their game status and stuff
|
|
if not redis_controller.exists(f"GameServerAccessKey:{AccessKey}:{JobId}"):
|
|
logging.warning(f"Invalid access key ( {AccessKey} ) for server ( {JobId} )")
|
|
return abort(404)
|
|
|
|
PlaceServerObj : PlaceServer = PlaceServer.query.filter_by(serveruuid=JobId).first()
|
|
if PlaceServerObj is None:
|
|
originServerUUID = redis_controller.get(f"place:{JobId}:origin")
|
|
if originServerUUID is not None:
|
|
PlaceServerOwner : GameServer = GameServer.query.filter_by(serverId=originServerUUID).first()
|
|
if PlaceServerOwner is not None:
|
|
logging.info(f"CloseJob : Closing {JobId} because server does not exist in database")
|
|
perform_post(
|
|
TargetGameserver = PlaceServerOwner,
|
|
Endpoint = "CloseJob",
|
|
JSONData = {
|
|
"jobid": JobId,
|
|
}
|
|
)
|
|
|
|
logging.warning(f"Invalid server ( {JobId} )")
|
|
return abort(404)
|
|
PlaceServerOwner : GameServer = GameServer.query.filter_by(serverId=PlaceServerObj.originServerId).first()
|
|
|
|
PlaceObject : Place = Place.query.filter_by(placeid=PlaceServerObj.serverPlaceId).first()
|
|
if PlaceObject is None:
|
|
logging.warning(f"Invalid place ( {PlaceServerObj.serverPlaceId} ) for server ( {JobId} )")
|
|
return jsonify({"status": "error", "message": "Invalid place"}),400
|
|
AssetObject : Asset = Asset.query.filter_by(id=PlaceObject.placeid).first()
|
|
CreatorObject : User | Group = None
|
|
if AssetObject.creator_type == 0:
|
|
CreatorObject = User.query.filter_by(id=AssetObject.creator_id).first()
|
|
else:
|
|
CreatorObject = Group.query.filter_by(id=AssetObject.creator_id).first()
|
|
|
|
PlaceServerObj.lastping = datetime.utcnow()
|
|
GameSessions : list = JSONData["GameSessions"]
|
|
if GameSessions is None:
|
|
return jsonify({"status": "error", "message": "Invalid JSON data"}),400
|
|
|
|
for GameSession in GameSessions:
|
|
UserId = GameSession["UserId"]
|
|
UserObject : User = User.query.filter_by(id=UserId).first()
|
|
if UserObject is None or UserObject.accountstatus != 1:
|
|
EvictPlayer( PlaceServerObj, UserId )
|
|
logging.warning(f"User ( {UserId} ) is not a valid user, on server ( {PlaceServerObj.serveruuid} )")
|
|
continue
|
|
|
|
PlaceServerPlayerObj : PlaceServerPlayer = PlaceServerPlayer.query.filter_by(serveruuid=JobId, userid=UserId).first()
|
|
if PlaceServerPlayerObj is None:
|
|
PlaceServerPlayerObj = PlaceServerPlayer(serveruuid=JobId, userid=UserId, joinTime=datetime.utcnow())
|
|
db.session.add(PlaceServerPlayerObj)
|
|
IncrementPlaceVisits(PlaceObject)
|
|
if CreatorObject is not None:
|
|
IncrementTargetBalance(CreatorObject, 1, 1)
|
|
UserObject.lastonline = datetime.utcnow()
|
|
InsertRecentlyPlayed(UserObj = UserObject, PlaceId = PlaceObject.placeid)
|
|
else:
|
|
if str(PlaceServerPlayerObj.serveruuid) == JobId:
|
|
PlaceServerPlayerObj.lastHeartbeat = datetime.utcnow()
|
|
UserObject.lastonline = datetime.utcnow()
|
|
else:
|
|
OtherPlaceServerObj = PlaceServer.query.filter_by(serveruuid=PlaceServerPlayerObj.serveruuid).first()
|
|
if OtherPlaceServerObj is not None:
|
|
EvictPlayer( OtherPlaceServerObj, UserId )
|
|
|
|
TotalTimePlayed = (datetime.utcnow() - PlaceServerPlayerObject.joinTime).total_seconds()
|
|
UserObj : User = User.query.filter_by(id=PlaceServerPlayerObject.userid).first()
|
|
HandleUserTimePlayed(UserObj, TotalTimePlayed, serverUUID=JobId, placeId=PlaceObject.placeid)
|
|
db.session.delete(PlaceServerPlayerObj)
|
|
|
|
PlaceServerPlayerObj = PlaceServerPlayer(serveruuid=JobId, userid=UserId, joinTime= datetime.utcnow())
|
|
IncrementPlaceVisits(PlaceObject)
|
|
if CreatorObject is not None:
|
|
IncrementTargetBalance(CreatorObject, 1, 1)
|
|
UserObject.lastonline = datetime.utcnow()
|
|
InsertRecentlyPlayed(UserObj = UserObject, PlaceId = PlaceObject.placeid)
|
|
|
|
PlaceServerPlayers = PlaceServerPlayer.query.filter_by(serveruuid=JobId).all()
|
|
for PlaceServerPlayerObject in PlaceServerPlayers:
|
|
playerFound = False
|
|
for GameSession in GameSessions:
|
|
if PlaceServerPlayerObject.userid == GameSession["UserId"]:
|
|
playerFound = True
|
|
break
|
|
if playerFound == False:
|
|
TotalTimePlayed = (datetime.utcnow() - PlaceServerPlayerObject.joinTime).total_seconds()
|
|
UserObj : User = User.query.filter_by(id=PlaceServerPlayerObject.userid).first()
|
|
HandleUserTimePlayed(UserObj, TotalTimePlayed, serverUUID=JobId, placeId=PlaceObject.placeid)
|
|
db.session.delete(PlaceServerPlayerObject)
|
|
|
|
if PlaceServerObj.playerCount == 0 and len(GameSessions) == 0 and PlaceServerObj.serverRunningTime > 60:
|
|
logging.info(f"CloseJob : Closing {JobId} for place [{PlaceServerObj.serverPlaceId}] because there was no players in the server for more than 60 seconds")
|
|
perform_post(
|
|
TargetGameserver = PlaceServerOwner,
|
|
Endpoint = "CloseJob",
|
|
JSONData = {
|
|
"jobid": JobId,
|
|
}
|
|
)
|
|
logging.info(f"Server ( {JobId} ) has been shutdown because there was no players in the server")
|
|
db.session.delete(PlaceServerObj)
|
|
db.session.commit()
|
|
return jsonify({"status": "success"}),200
|
|
|
|
PlaceServerObj.serverRunningTime = int(float(request.args.get('gameTime', '1', type=str))) + 1
|
|
PlaceServerObj.playerCount = len(GameSessions)
|
|
db.session.commit()
|
|
|
|
ClearPlayingCountCache(PlaceObject)
|
|
return jsonify({"status": "success"}),200
|
|
|
|
@JobReportHandler.route("/v2.0/Refresh", methods=['POST'])
|
|
@csrf.exempt
|
|
def Refresh():
|
|
return jsonify({"status": "success"}),200 # We don't care about this endpoint since the above endpoint will handle it
|
|
|
|
@JobReportHandler.route("/v1/Close/", methods=['POST'])
|
|
@csrf.exempt
|
|
def CloseJob():
|
|
AccessKey = request.args.get( key = 'apiKey', default = None, type = str)
|
|
JobId = request.args.get( key = 'gameId', default = None, type = str)
|
|
if AccessKey is None or JobId is None:
|
|
return abort(404)
|
|
|
|
if not redis_controller.exists(f"GameServerAccessKey:{AccessKey}:{JobId}"):
|
|
logging.warning(f"Invalid access key ( {AccessKey} ) for server ( {JobId} )")
|
|
return abort(404)
|
|
|
|
PlaceServerObj : PlaceServer = PlaceServer.query.filter_by(serveruuid=JobId).first()
|
|
if PlaceServerObj is not None:
|
|
AllPlaceServerPlayers : list[PlaceServerPlayer] = PlaceServerPlayer.query.filter_by(serveruuid=JobId).all()
|
|
for PlaceServerPlayerObj in AllPlaceServerPlayers:
|
|
TotalTimePlayed = (datetime.utcnow() - PlaceServerPlayerObj.joinTime).total_seconds()
|
|
UserObj : User = User.query.filter_by(id=PlaceServerPlayerObj.userid).first()
|
|
HandleUserTimePlayed(UserObj, TotalTimePlayed, serverUUID=JobId, placeId=PlaceServerObj.serverPlaceId)
|
|
db.session.delete(PlaceServerPlayerObj)
|
|
db.session.delete(PlaceServerObj)
|
|
db.session.commit()
|
|
|
|
return jsonify({"status": "success"}),200
|
|
|
|
@JobReportHandler.route("/game/report-water-sys", methods=['GET'])
|
|
@csrf.exempt
|
|
def ReportCheatersHandler():
|
|
RequestHost = request.headers.get('Host')
|
|
if RequestHost is None:
|
|
return abort(404)
|
|
if not RequestHost.startswith("gameinstances.api."):
|
|
return abort(404)
|
|
|
|
ReportingUserId = request.args.get( key = 'UserID', default = None, type = int)
|
|
if ReportingUserId is None:
|
|
return abort(404)
|
|
ReportingMessage = request.args.get( key = 'Message', default = None, type = str)
|
|
if ReportingMessage is None:
|
|
return abort(404)
|
|
AccessKey = request.args.get( key = 'AccessKey', default = '', type = str )
|
|
if "UserRequest" in AccessKey:
|
|
return abort(404)
|
|
|
|
ReportingRemoteAddress = get_remote_address()
|
|
ReportingGameServer : GameServer = GameServer.query.filter_by(serverIP=ReportingRemoteAddress).first()
|
|
|
|
if ReportingGameServer is None:
|
|
return abort(404)
|
|
|
|
UserObj : User = User.query.filter_by(id=ReportingUserId).first()
|
|
if UserObj is None:
|
|
return abort(404)
|
|
|
|
BannableErrorCodes = {
|
|
"carol": "Lua vm hooked (20)",
|
|
"murdle": "Cheat Engine Stable Methods (0)",
|
|
"olivia": "Debugger found (10)"
|
|
}
|
|
|
|
isBanned = False
|
|
if ReportingMessage.lower() in BannableErrorCodes:
|
|
LastestUserBanObj : UserBan = UserBan.query.filter_by(userid=UserObj.id, acknowledged = False).order_by(UserBan.id.desc()).first()
|
|
if LastestUserBanObj is None and UserObj.accountstatus == 1:
|
|
NewUserBanObj = UserBan(
|
|
userid = UserObj.id,
|
|
author_userid = 1,
|
|
ban_type = BanType.Deleted,
|
|
reason = "Exploiting in games is not tolerated on SYNTAX",
|
|
moderator_note = f"Automatic ban, received detection from gameserver. Error Code: {BannableErrorCodes[ReportingMessage.lower()]} / {BannableErrorCodes[ReportingMessage.lower()]}",
|
|
expires_at = None
|
|
)
|
|
db.session.add(NewUserBanObj)
|
|
UserObj.accountstatus = 3
|
|
db.session.commit()
|
|
|
|
isBanned = True
|
|
|
|
try:
|
|
requests.post(
|
|
url = config.CHEATER_REPORTS_DISCORD_WEBHOOK,
|
|
json = {
|
|
"content": f"Received Cheater Report from GameServer {ReportingGameServer.serverName} ( {ReportingGameServer.serverId} ) for User {UserObj.username} ( {UserObj.id} )\n```{ReportingMessage}```\n Was the user banned? **{isBanned}**",
|
|
"username": "Cheater Reports"
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logging.error(f"jobreporthandler : ReportCheatersHandler: Failed to send cheater report to discord, {e}")
|
|
|
|
return "OK", 200
|
|
|
|
@JobReportHandler.route("/Game/ClientPresence.ashx", methods=['GET'])
|
|
def ClientPresence(): # Does nothing
|
|
return "OK", 200 |