syntaxwebsite/app/pages/admin/admin.py

2780 lines
125 KiB
Python

from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify, abort
from functools import wraps
from app.extensions import db, redis_controller, csrf, limiter
import uuid
import json
import base64
import re
from datetime import datetime, timedelta, timezone
from app.enums.AssetType import AssetType
import hashlib
import os
import requests
import logging
import random
import time
import threading
import random
import string
from sqlalchemy import or_, and_, func
from config import Config
config = Config()
from app.routes.asset import migrateAsset
from app.util import auth, assetversion, discord, redislock
from app.services import economy, gameserver_comm
from app.models.admin_permissions import AdminPermissions
from app.models.user import User
from app.models.gameservers import GameServer
from app.models.placeservers import PlaceServer
from app.models.placeserver_players import PlaceServerPlayer
from app.models.fflag_group import FflagGroup
from app.models.fflag_value import FflagValue
from app.models.asset import Asset
from app.models.asset_version import AssetVersion
from app.models.asset_moderation_link import AssetModerationLink
from app.models.asset_thumbnail import AssetThumbnail
from app.models.place_icon import PlaceIcon
from app.models.past_usernames import PastUsername
from app.models.usereconomy import UserEconomy
from app.models.login_records import LoginRecord
from app.models.place import Place
from app.models.linked_discord import LinkedDiscord
from app.models.user_ban import UserBan
from app.models.giftcard_key import GiftcardKey
from app.models.invite_key import InviteKey
from app.models.groups import Group, GroupIcon
from app.models.userassets import UserAsset
from app.models.user_avatar_asset import UserAvatarAsset
from app.models.user_hwid_log import UserHWIDLog
from app.models.game_session_log import GameSessionLog
from app.models.user_transactions import UserTransaction
from app.models.moderator_note import ModeratorNote
from app.models.universe import Universe
from app.models.limited_item_transfers import LimitedItemTransfer
from app.enums.GiftcardType import GiftcardType
from app.enums.BanType import BanType
from app.enums.TransactionType import TransactionType
from app.enums.LimitedItemTransferMethod import LimitedItemTransferMethod
from app.routes.jobreporthandler import EvictPlayer
from app.routes.thumbnailer import TakeThumbnail, TakeUserThumbnail
from app.pages.admin.permissionsdefinition import PermissionsDefinition
from app.pages.messages.messages import CreateSystemMessage
def GetCreatorOfAsset( AssetObj : Asset ) -> User | Group | None:
if AssetObj.creator_type == 0:
return User.query.filter_by(id=AssetObj.creator_id).first()
elif AssetObj.creator_type == 1:
return Group.query.filter_by(id=AssetObj.creator_id).first()
return None
def AdminPermissionRequired(permission):
UserObj : User = auth.GetCurrentUser()
UserAdminPermission = AdminPermissions.query.filter_by(userid=UserObj.id, permission=permission).first()
if UserAdminPermission is None:
abort(403)
def HasAdminPermission(permission) -> bool:
UserObj : User = auth.GetCurrentUser()
UserAdminPermission = AdminPermissions.query.filter_by(userid=UserObj.id, permission=permission).first()
if UserAdminPermission is None:
return False
return True
def IsUserAnAdministrator( UserObj : User, AvoidCache : bool = False) -> bool:
if not AvoidCache:
CachedResult = redis_controller.get(f"IsUserAnAdministrator:Lookup:{UserObj.id}")
if CachedResult is not None:
try:
return CachedResult == "1"
except:
pass
isUserAdministrator = AdminPermissions.query.filter_by(userid=UserObj.id).first() is not None
redis_controller.set(f"IsUserAnAdministrator:Lookup:{UserObj.id}", int(isUserAdministrator), ex = 60)
return isUserAdministrator
def GetAmountOfPendingAssets( AvoidCache : bool = False ) -> int:
if not AvoidCache:
CachedResult = redis_controller.get("GetAmountOfPendingAssets:Lookup")
if CachedResult is not None:
try:
return int(CachedResult)
except:
pass
PendingAssetsCount = Asset.query.filter_by(moderation_status=1).count() + PlaceIcon.query.filter_by(moderation_status=1).count() + AssetThumbnail.query.filter_by(moderation_status=1).count() + GroupIcon.query.filter_by(moderation_status=1).count()
redis_controller.set("GetAmountOfPendingAssets:Lookup", str(PendingAssetsCount), ex = 10)
return PendingAssetsCount
def MustBeAdmin(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not auth.isAuthenticated():
return redirect("/login")
UserObj : User = auth.GetCurrentUser()
UserAdminPermission = AdminPermissions.query.filter_by(userid=UserObj.id).first()
if UserAdminPermission is None:
return redirect("/home")
return f(*args, **kwargs)
return decorated_function
def GetAdminPermissions():
# Get admin permissions for the current user
# and returns them in a array
UserObj : User = auth.GetCurrentUser()
UserAdminPermissions = AdminPermissions.query.filter_by(userid=UserObj.id).all()
if UserAdminPermissions is None:
return []
UserAdminPermissionsArray = []
for UserAdminPermission in UserAdminPermissions:
UserAdminPermissionsArray.append(UserAdminPermission.permission)
return UserAdminPermissionsArray
AdminRoute = Blueprint('admin', __name__, url_prefix='/admin')
@AdminRoute.before_request
def before_request():
if not auth.isAuthenticated():
return redirect("/login")
AuthTokenInfo = auth.GetCurrentUser()
UserAdminPermission = AdminPermissions.query.filter_by(userid=int(AuthTokenInfo[0])).first()
if UserAdminPermission is None:
return redirect("/home")
@AdminRoute.route('/', methods=['GET'])
def admin():
userPermissions = []
for permission in GetAdminPermissions():
if permission in PermissionsDefinition:
userPermissions.append(PermissionsDefinition[permission])
AuthenticatedUser : User = auth.GetCurrentUser()
stats = {}
stats['UsersPlaying'] = PlaceServerPlayer.query.count()
stats['PlaceServersCount'] = PlaceServer.query.count()
stats['UsersOnline'] = User.query.filter(User.lastonline > (datetime.utcnow() - timedelta(minutes=1))).count()
stats['UsersSignedUpToday'] = User.query.filter(User.created > (datetime.utcnow() - timedelta(days=1))).count()
stats['PendingAssets'] = Asset.query.filter_by(moderation_status=1).count() + PlaceIcon.query.filter_by(moderation_status=1).count() + AssetThumbnail.query.filter_by(moderation_status=1).count() + GroupIcon.query.filter_by(moderation_status=1).count()
stats['SystemTime'] = datetime.utcnow()
return render_template('admin/index.html', permissions = userPermissions, stats = stats, user = AuthenticatedUser)
@AdminRoute.route('/gameservers', methods=['GET'])
def gameservers():
AdminPermissionRequired('GameServerManager')
gameServers = GameServer.query.all()
return render_template('admin/gameservers/index.html', gameservers = gameServers)
@AdminRoute.route('/gameservers/<serverid>', methods=['GET'])
def gameservers_view(serverid):
AdminPermissionRequired('GameServerManager')
gameServer : GameServer = GameServer.query.filter_by(serverId=serverid).first()
if gameServer is None:
return redirect("/admin/gameservers")
return render_template('admin/gameservers/view.html', gameserver = gameServer)
@AdminRoute.route('/gameservers/<serverid>/delete', methods=['GET'])
def gameservers_delete(serverid):
AdminPermissionRequired('GameServerManager')
gameServer : GameServer = GameServer.query.filter_by(serverId=serverid).first()
if gameServer is None:
return redirect("/admin/gameservers")
return render_template("/admin/gameservers/delete.html", gameserver = gameServer)
@AdminRoute.route('/gameservers/<serverid>/delete', methods=['POST'])
def gameservers_delete_post(serverid):
AdminPermissionRequired('GameServerManager')
gameServer : GameServer = GameServer.query.filter_by(serverId=serverid).first()
if gameServer is None:
return redirect("/admin/gameservers")
db.session.delete(gameServer)
db.session.commit()
return redirect("/admin/gameservers")
@AdminRoute.route('/gameservers/<serverid>/edit', methods=['POST'])
def gameservers_edit(serverid):
AdminPermissionRequired('GameServerManager')
gameServer : GameServer = GameServer.query.filter_by(serverId=serverid).first()
if gameServer is None:
return redirect("/admin/gameservers")
if request.form['name'] == "" or request.form['serverip'] == "" or request.form['serverport'] == "" or request.form['accesskey'] == "":
flash("Please fill in all fields", "danger")
return redirect("/admin/gameservers/" + serverid)
gameServer.serverName = request.form['name']
gameServer.serverIP = request.form['serverip']
gameServer.serverPort = request.form['serverport']
gameServer.accessKey = request.form['accesskey']
gameServer.allowThumbnailGen = True if 'isThumbnailer' in request.form else False
gameServer.allowGameServerHost = True if 'isGameHoster' in request.form else False
db.session.commit()
flash("Game server updated", "success")
return redirect("/admin/gameservers/" + serverid)
@AdminRoute.route("/gameservers/<serverid>/refresh-accesskey", methods=['GET', 'POST'])
def gameservers_refresh_accesskey(serverid):
AdminPermissionRequired('GameServerManager')
GameServerObj : GameServer = GameServer.query.filter_by(serverId=serverid).first()
if GameServerObj is None:
return abort(404)
if request.method == "GET":
return render_template("admin/gameservers/refresh_accesskey.html", gameserver = GameServerObj)
else:
NewAccessKey = ''.join(random.choices(string.ascii_letters + string.digits, k=random.randint(60, 90)))
req_response = gameserver_comm.perform_post(
TargetGameserver = GameServerObj,
Endpoint = "ResetAccessKeyAndRestart",
JSONData = {
"NewAccessKey": NewAccessKey
},
RequestTimeout = 30
)
if req_response.status_code != 200:
flash("Failed to send request to game server", "danger")
return redirect(f"/admin/gameservers/{serverid}")
GameServerObj.accessKey = NewAccessKey
db.session.commit()
flash("Access key reset and game server restarted", "success")
return redirect(f"/admin/gameservers/{serverid}")
@AdminRoute.route('/gameservers/create', methods=['GET'])
def gameservers_create():
AdminPermissionRequired('GameServerManager')
return render_template('admin/gameservers/create.html')
@AdminRoute.route('/gameservers/create', methods=['POST'])
def gameservers_create_post():
AdminPermissionRequired('GameServerManager')
if request.form['name'] == "" or request.form['serverip'] == "" or request.form['serverport'] == "" or request.form['accesskey'] == "":
flash("Please fill in all fields", "danger")
return redirect("/admin/gameservers/create")
newServerID = str(uuid.uuid4())
gameServer = GameServer(
serverId = newServerID,
serverName= request.form['name'],
serverIP = request.form['serverip'],
serverPort = request.form['serverport'],
accessKey = request.form['accesskey'],
allowThumbnailGen = True if 'isThumbnailer' in request.form else False,
allowGameServerHost = True if 'isGameHoster' in request.form else False
)
db.session.add(gameServer)
db.session.commit()
return redirect("/admin/gameservers/" + newServerID)
@AdminRoute.route('/websitemessage', methods=['GET'])
def websitemessage():
AdminPermissionRequired('UpdateWebsiteMessage')
return render_template('admin/websitewidemsg.html', message = redis_controller.get("website_wide_message") or "")
@AdminRoute.route('/websitemessage', methods=['POST'])
def websitemessage_post():
AdminPermissionRequired('UpdateWebsiteMessage')
redis_controller.set("website_wide_message", request.form['message'])
return redirect("/admin/websitemessage")
@AdminRoute.route('/fflags', methods=['GET'])
def fflags():
AdminPermissionRequired('ManageFFlags')
return render_template('admin/fflagsettings/index.html', groups = FflagGroup.query.all())
@AdminRoute.route('/fflags/<int:groupid>', methods=['GET'])
def fflags_view(groupid : int):
AdminPermissionRequired('ManageFFlags')
FFlagGroupObj : FflagGroup = FflagGroup.query.filter_by(group_id=groupid).first()
if FFlagGroupObj is None:
return redirect("/admin/fflags")
PageNumber = max( request.args.get('page', default=1, type=int), 1 )
SearchQuery = request.args.get('search', default=None, type=str)
GroupFlagsPagination = FflagValue.query.filter_by(group_id=groupid)
if SearchQuery is not None:
GroupFlagsPagination = GroupFlagsPagination.filter(FflagValue.name.ilike(f"%{SearchQuery}%"))
GroupFlagsPagination = GroupFlagsPagination.paginate(
page = PageNumber,
per_page = 50,
error_out = False
)
return render_template('admin/fflagsettings/view.html', group = FFlagGroupObj, FFlagsLookupResults = GroupFlagsPagination, flagcount = GroupFlagsPagination.total, search = SearchQuery)
@AdminRoute.route('/fflags/<int:groupid>/import', methods=['POST'])
def fflags_import(groupid : int):
from app.routes.fflagssettings import ClearCache
AdminPermissionRequired('ManageFFlags')
group : FflagGroup = FflagGroup.query.filter_by(group_id=groupid).first()
if group is None:
return redirect("/admin/fflags")
if 'file' not in request.files:
flash("No file uploaded", "danger")
return redirect("/admin/fflags/" + groupid)
file = request.files['file']
if file.filename == '':
flash("Filename empty", "danger")
return redirect("/admin/fflags/" + groupid)
try:
json.loads(file.read())
except:
flash("Invalid JSON file", "danger")
return redirect("/admin/fflags/" + groupid)
FflagValue.query.filter_by(group_id=groupid).delete()
db.session.commit()
file.seek(0)
data = json.loads(file.read())
for flag in data:
FlagName = flag
FlagValue = data[flag]
FlagType = 1
try:
if FlagValue.lower() == "true":
FlagValue = True
elif FlagValue.lower() == "false":
FlagValue = False
else:
raise Exception()
except:
try:
FlagValue = int(FlagValue)
FlagType = 2
except:
FlagType = 3
newFlag = FflagValue(
group_id = groupid,
name = FlagName,
flag_type= FlagType,
flag_value = base64.b64encode(str(FlagValue).encode('utf-8')).decode('utf-8')
)
db.session.add(newFlag)
group.updated_at = datetime.utcnow()
db.session.commit()
ClearCache( groupid )
flash("Flags imported", "success")
return redirect("/admin/fflags/" + groupid)
@AdminRoute.route("/asset-copier", methods=['GET'])
def asset_copier():
AdminPermissionRequired('CopyAssets')
return render_template('admin/assetcopier.html')
@AdminRoute.route("/asset-copier", methods=['POST'])
def asset_copier_post():
AdminPermissionRequired('CopyAssets')
AssetURL = request.form.get('asseturl')
if AssetURL is None:
flash("No URL provided", "danger")
return redirect("/admin/asset-copier")
if AssetURL == "":
flash("URL is empty", "danger")
return redirect("/admin/asset-copier")
AssetRegex = re.compile(r".com\/catalog\/(\d+)\/")
AssetMatch = AssetRegex.search(request.form.get("asseturl"))
if AssetMatch is None:
flash("Invalid asset URL", "error")
return redirect("/admin/asset-copier")
AssetID = AssetMatch.group(1)
# Check if asset exists
AssetObj = Asset.query.filter_by(roblox_asset_id=int(AssetID)).first()
if AssetObj is not None:
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
AuthenticatedUser = auth.GetAuthenticatedUser(request.cookies.get(".ROBLOSECURITY"))
AssetMigrationCooldown = redis_controller.get(f"asset_migration_cooldown_{str(AuthenticatedUser.id)}")
if AssetMigrationCooldown is not None and AuthenticatedUser.id != 1:
flash(f"You are copying assets too fast!", "danger")
return redirect("/admin/asset-copier")
redis_controller.set(f"asset_migration_cooldown_{str(AuthenticatedUser.id)}", "1", 20)
# Migrate asset
NewAsset : Asset = migrateAsset(int(AssetID), forceMigration=False, allowedTypes=[2, 8, 11, 12, 17, 18, 19, 41, 42, 43, 44, 45, 46, 47], creatorId=1, keepRobloxId=False, migrateInfo=True)
if NewAsset is None:
flash("Failed to migrate asset", "danger")
return redirect("/admin/asset-copier")
TakeThumbnail(NewAsset.id)
return redirect(f"/admin/manage-assets/{str(NewAsset.id)}")
def IsItemInReleasePool( AssetId : int ) -> bool:
return redis_controller.lrange("ItemReleasePool:Items", 0, -1).count(str(AssetId)) > 0
def GetNextItemDropDateTime() -> datetime:
LastItemDropTimestamp : str | None = redis_controller.get("ItemReleasePool:LastDropTimestamp")
if LastItemDropTimestamp is None:
LastItemDropTimestamp = round(time.time())
else:
try:
LastItemDropTimestamp = int(LastItemDropTimestamp)
except Exception as e:
logging.warn(f"Admin > GetNextItemDropDateTime: Exception raised while trying to convert to integer, {str(e)}")
LastItemDropTimestamp = round(time.time())
RandomSeed = f"{LastItemDropTimestamp}-{config.FLASK_SESSION_KEY}"
random.seed( RandomSeed )
NextDropDatetime = datetime.utcfromtimestamp( LastItemDropTimestamp + random.randint( 60 * 60 * 6, 60 * 60 * 20 ) )
return NextDropDatetime
def InsertItemIntoItemReleasePool( AssetObj : Asset, Name : str, Description : str, RobuxPrice : int = 0, TicketsPrice : int = 0, IsLimited : bool = False, IsLimitedUnique : bool = False, SerialCount : int | None = None, OffsaleAfter : timedelta | None = None ):
if IsItemInReleasePool( AssetObj.id ):
raise Exception("InsertItemIntoItemReleasePool: Attempted to insert item which is already inside item release pool")
if RobuxPrice < 0 or TicketsPrice < 0:
raise Exception("InsertItemIntoItemReleasePool: RobuxPrice and TicketsPrice cannot be a negative integer")
if IsLimitedUnique:
IsLimited = True
ItemReleaseMetaData = json.dumps({
"Name" : Name,
"Description" : Description,
"RobuxPrice" : RobuxPrice,
"TicketsPrice" : TicketsPrice,
"IsLimited" : IsLimited,
"IsLimitedUnique" : IsLimitedUnique,
"SerialCount": SerialCount,
"OffsaleAfter" : None if OffsaleAfter is None else OffsaleAfter.total_seconds()
})
redis_controller.set(f"ItemReleasePool:Item_Metadata:{AssetObj.id}", ItemReleaseMetaData)
redis_controller.rpush("ItemReleasePool:Items", str(AssetObj.id))
logging.info(f"Inserted Item {AssetObj.id} into Item Release Pool")
AssetObj.name = "Asset"
AssetObj.description = ""
AssetObj.price_robux = 0
AssetObj.price_tix = 0
AssetObj.is_limited = False
AssetObj.is_limited_unique = False
AssetObj.moderation_status = 2 # Hides the asset from people scraping every asset
db.session.commit()
@AdminRoute.route("/manage-assets/<int:assetid>", methods=['GET'])
@AdminRoute.route("/manage-assets", methods=['GET'])
def manage_assets(assetid = None):
AdminPermissionRequired('ManageAsset')
if assetid is not None:
AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
if AssetObj is None:
flash("Asset does not exist", "danger")
return redirect("/admin/manage-assets")
if (AssetObj.creator_id != 1 and AssetObj.creator_id != 2) and AssetObj.creator_type != 0: # Only allows assets under ROBLOX and UGC accounts
flash("Asset is not a offical asset", "danger")
return redirect("/admin/manage-assets")
if AssetObj.asset_type not in [AssetType(1), AssetType(2), AssetType(3), AssetType(4), AssetType(8), AssetType(11), AssetType(12), AssetType(17), AssetType(18), AssetType(19), AssetType(27), AssetType(28), AssetType(29), AssetType(30), AssetType(31), AssetType(32), AssetType(41), AssetType(42), AssetType(43), AssetType(44), AssetType(45), AssetType(46), AssetType(47), AssetType(57), AssetType(58)]:#[1,2,3,4,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
flash("Asset type is not supported", "danger")
return redirect("/admin/manage-assets")
CanManageItemReleasePool = HasAdminPermission("ManageItemReleases")
IsItemEligibleForItemReleasePool = AssetObj.is_for_sale is False and AssetObj.is_limited is False and AssetObj.sale_count == 0
return render_template('admin/manageassets.html', asset = AssetObj, CanManageItemReleasePool = CanManageItemReleasePool, IsItemEligibleForItemReleasePool = IsItemEligibleForItemReleasePool)
return render_template('admin/manageassets.html')
@AdminRoute.route("/manage-assets/<int:assetid>/insert-item-pool", methods=['GET'])
def insert_item_into_pool( assetid : int ):
AdminPermissionRequired('ManageAsset')
AdminPermissionRequired('ManageItemReleases')
AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
if AssetObj is None:
flash("Asset does not exist", "danger")
return redirect("/admin/manage-assets")
if (AssetObj.creator_id != 1 and AssetObj.creator_id != 2) and AssetObj.creator_type != 0: # Only allows assets under ROBLOX and UGC accounts
flash("Asset is not a offical asset", "danger")
return redirect("/admin/manage-assets")
if AssetObj.asset_type not in [AssetType(1), AssetType(2), AssetType(3), AssetType(4), AssetType(8), AssetType(11), AssetType(12), AssetType(17), AssetType(18), AssetType(19), AssetType(27), AssetType(28), AssetType(29), AssetType(30), AssetType(31), AssetType(32), AssetType(41), AssetType(42), AssetType(43), AssetType(44), AssetType(45), AssetType(46), AssetType(47), AssetType(57), AssetType(58)]:#[1,2,3,4,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
flash("Asset type is not supported", "danger")
return redirect("/admin/manage-assets")
if IsItemInReleasePool(AssetObj.id):
flash("Item is already inside the item release pool", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
IsItemEligibleForItemReleasePool = AssetObj.is_for_sale is False and AssetObj.is_limited is False and AssetObj.sale_count == 0
if not IsItemEligibleForItemReleasePool:
flash("Item is not eligible for item release pool", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
return render_template('admin/insertitemrelasepool.html', asset = AssetObj)
@AdminRoute.route("/manage-assets/<int:assetid>/insert-item-pool", methods=['POST'])
def insert_item_into_pool_post( assetid : int ):
AdminPermissionRequired('ManageAsset')
AdminPermissionRequired('ManageItemReleases')
AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
if AssetObj is None:
flash("Asset does not exist", "danger")
return redirect("/admin/manage-assets")
if (AssetObj.creator_id != 1 and AssetObj.creator_id != 2) and AssetObj.creator_type != 0: # Only allows assets under ROBLOX and UGC accounts
flash("Asset is not a offical asset", "danger")
return redirect("/admin/manage-assets")
if AssetObj.asset_type not in [AssetType(1), AssetType(2), AssetType(3), AssetType(4), AssetType(8), AssetType(11), AssetType(12), AssetType(17), AssetType(18), AssetType(19), AssetType(27), AssetType(28), AssetType(29), AssetType(30), AssetType(31), AssetType(32), AssetType(41), AssetType(42), AssetType(43), AssetType(44), AssetType(45), AssetType(46), AssetType(47), AssetType(57), AssetType(58)]:#[1,2,3,4,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
flash("Asset type is not supported", "danger")
return redirect("/admin/manage-assets")
if IsItemInReleasePool(AssetObj.id):
flash("Item is already inside the item release pool", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
IsItemEligibleForItemReleasePool = AssetObj.is_for_sale is False and AssetObj.is_limited is False and AssetObj.sale_count == 0
if not IsItemEligibleForItemReleasePool:
flash("Item is not eligible for item release pool", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
AssetName = request.form.get('assetname', default=AssetObj.name, type=str)
AssetDescription = request.form.get('assetdescription', default=AssetObj.description, type=str)
AssetPriceRobux = request.form.get('assetpricerobux', default = None, type=int)
AssetPriceTickets = request.form.get('assetpricetix', default = None, type=int)
AssetSerialAmount = request.form.get('assetserialamount', default = None, type=int)
isLimited = request.form.get('isLimited') == "on"
isLimitedUnique = request.form.get('isLimitedUnique') == "on"
if AssetName is None or AssetName == "":
flash("Asset name is empty", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
if AssetDescription is None:
flash("Asset description is empty", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
if isLimitedUnique:
isLimited = True
if not HasAdminPermission("ModifyLimitedAssets") and isLimited:
flash("You do not have permission to create Limiteds", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
if AssetSerialAmount < 0:
flash("Asset serial amount cannot be negative", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
if AssetPriceRobux < 0 or AssetPriceRobux > 1000000:
flash("Asset Robux price must be in range ( 0 - 1000000)", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
if AssetPriceTickets < 0 or AssetPriceTickets > 10000000:
flash("Asset Tix price must be in range ( 0 - 10000000)", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
if AssetSerialAmount > 0 and not isLimitedUnique:
flash("Asset serial cannot be greater than 0 if not a limited unique", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
AssetOffsaleHours = request.form.get('offsale-at-hours', default=None, type=int)
AssetOffsaleMinutes = request.form.get('offsale-at-minutes', default=None, type=int)
AssetOffsaleSeconds = request.form.get('offsale-at-seconds', default=None, type=int)
if AssetOffsaleHours is not None or AssetOffsaleMinutes is not None or AssetOffsaleSeconds is not None:
AssetOffsaleHours = AssetOffsaleHours or 0
AssetOffsaleMinutes = AssetOffsaleMinutes or 0
AssetOffsaleSeconds = AssetOffsaleSeconds or 0
OffsaleDelta = timedelta(
hours = AssetOffsaleHours,
minutes = AssetOffsaleMinutes,
seconds = AssetOffsaleSeconds
)
if timedelta(minutes = 5) > OffsaleDelta:
flash("Offsale Time cannot be lesser than 5 minutes", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
else:
OffsaleDelta = None
if OffsaleDelta and isLimited:
flash("Limited unique assets cannot have a offsale date", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
InsertItemIntoItemReleasePool(
AssetObj = AssetObj,
Name = AssetName,
Description = AssetDescription,
RobuxPrice = AssetPriceRobux,
TicketsPrice = AssetPriceTickets,
IsLimited = isLimited,
IsLimitedUnique = isLimitedUnique,
SerialCount = AssetSerialAmount
)
flash("Successfully inserted item into item release pool", "success")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
@AdminRoute.route("/item-release-pool", methods=["GET"])
def item_release_pool_view():
AdminPermissionRequired('ManageItemReleases')
PlannedItems = []
AllAssetIds = redis_controller.lrange("ItemReleasePool:Items", 0, -1)
for AssetId in AllAssetIds:
AssetMetadata = redis_controller.get(f"ItemReleasePool:Item_Metadata:{AssetId}")
if AssetMetadata is None:
logging.warn(f"item_release_pool_view failed to get asset metadata for {AssetId}, removing it from list")
redis_controller.lrem("ItemReleasePool:Items", 0, AssetId)
return
try:
AssetMetadata = json.loads(AssetMetadata)
except Exception as e:
logging.warn(f"item_release_pool_view failed to parse json from item metadata {AssetId}, {str(e)}")
redis_controller.lrem("ItemReleasePool:Items", 0, AssetId)
return
PlannedItems.append({
"id": int(AssetId),
"name": AssetMetadata["Name"],
"description": AssetMetadata["Description"],
"robux_price": AssetMetadata["RobuxPrice"],
"tickets_price": AssetMetadata["TicketsPrice"],
"is_limited": AssetMetadata["IsLimited"],
"is_limited_unique": AssetMetadata["IsLimitedUnique"],
"serial_count": AssetMetadata["SerialCount"],
"offsale_after": AssetMetadata["OffsaleAfter"]
})
if HasAdminPermission("ViewItemReleasePoolDrop"):
NextItemDropDatetime = f"{GetNextItemDropDateTime()} UTC"
else:
NextItemDropDatetime = "You do not have permission to view the next Drop Time"
return render_template("admin/itemreleasepool.html", PlannedItems=PlannedItems, NextItemDropDatetime=NextItemDropDatetime)
@AdminRoute.route("/manage-assets", methods=['POST'])
def manage_assets_post():
AdminPermissionRequired('ManageAsset')
AssetID = request.form.get('assetid')
if AssetID is None:
flash("No asset ID provided", "danger")
return redirect("/admin/manage-assets")
if AssetID == "":
flash("Asset ID is empty", "danger")
return redirect("/admin/manage-assets")
AssetObj : Asset = Asset.query.filter_by(id=int(AssetID)).first()
if AssetObj is None:
flash("Asset does not exist", "danger")
return redirect("/admin/manage-assets")
if (AssetObj.creator_id != 1 and AssetObj.creator_id != 2) and AssetObj.creator_type != 0: # Only allows assets under ROBLOX and UGC accounts
flash("Asset is not a offical asset", "danger")
return redirect("/admin/manage-assets")
if AssetObj.asset_type not in [AssetType(1), AssetType(2), AssetType(3), AssetType(4), AssetType(8), AssetType(11), AssetType(12), AssetType(17), AssetType(18), AssetType(19), AssetType(27), AssetType(28), AssetType(29), AssetType(30), AssetType(31), AssetType(32), AssetType(41), AssetType(42), AssetType(43), AssetType(44), AssetType(45), AssetType(46), AssetType(47), AssetType(57), AssetType(58)]:
flash("Asset type is not supported", "danger")
return redirect("/admin/manage-assets")
return redirect(f"/admin/manage-assets/{str(AssetID)}")
from app.extensions import scheduler
def SetAssetOffsaleJob( assetId : int ):
with scheduler.app.app_context():
AssetObj : Asset = Asset.query.filter_by(id=assetId).first()
if AssetObj is None:
return
if AssetObj.offsale_at is None:
return
if AssetObj.offsale_at > datetime.utcnow() + timedelta(minutes=5):
logging.warning(f"Asset {str(AssetObj.id)} has a offsale date in the future more than 5 minutes diff, skipping")
return
AssetObj.is_for_sale = False
AssetObj.offsale_at = None
db.session.commit()
logging.info(f"Asset {str(AssetObj.id)} has been set offsale at {str(datetime.utcnow())}")
redis_controller.delete(f"APSchedulerTaskJobUUID:{str(AssetObj.id)}")
@AdminRoute.route("/manage-assets/<int:assetid>", methods=['POST'])
def manage_assets_post_update(assetid):
AdminPermissionRequired('ManageAsset')
AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
if AssetObj is None:
flash("Asset does not exist", "danger")
return redirect("/admin/manage-assets")
if (AssetObj.creator_id != 1 and AssetObj.creator_id != 2) and AssetObj.creator_type != 0: # Only allows assets under ROBLOX and UGC accounts
flash("Asset is not a offical asset", "danger")
return redirect("/admin/manage-assets")
if AssetObj.asset_type not in [AssetType(1), AssetType(2), AssetType(3), AssetType(4), AssetType(8), AssetType(11), AssetType(12), AssetType(17), AssetType(18), AssetType(19), AssetType(27), AssetType(28), AssetType(29), AssetType(30), AssetType(31), AssetType(32), AssetType(41), AssetType(42), AssetType(43), AssetType(44), AssetType(45), AssetType(46), AssetType(47), AssetType(57), AssetType(58)]:
flash("Asset type is not supported", "danger")
return redirect("/admin/manage-assets")
if AssetObj.is_limited or AssetObj.is_limited_unique:
if not HasAdminPermission("ModifyLimitedAssets"):
flash("You do not have permission to modify limited assets", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
if IsItemInReleasePool(AssetObj.id):
flash("You cannot edit an item which is in the item release pool", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
# Validate form
AssetName = request.form.get('assetname')
AssetDescription = request.form.get('assetdescription')
AssetPriceRobux = request.form.get('assetpricerobux')
AssetPriceTickets = request.form.get('assetpricetix')
AssetSerialAmount = request.form.get('assetserialamount')
AssetOffsaleAt = request.form.get('offsale-at', None) # Year-Month-DayTHour:Minute
OffsaleTimezone = request.form.get('offsale-timezone', 0, int) # 0, 1, 2, ... or -1, -2, ...
if AssetOffsaleAt is not None and AssetOffsaleAt != "":
try:
AssetOffsaleAt = datetime.strptime(AssetOffsaleAt, "%Y-%m-%dT%H:%M")
# We need to convert the time to UTC
AssetOffsaleAt = AssetOffsaleAt.replace(tzinfo=timezone(timedelta(hours=OffsaleTimezone)))
AssetOffsaleAt = AssetOffsaleAt.astimezone(timezone.utc)
AssetOffsaleAt = datetime(
AssetOffsaleAt.year,
AssetOffsaleAt.month,
AssetOffsaleAt.day,
AssetOffsaleAt.hour,
AssetOffsaleAt.minute,
AssetOffsaleAt.second,
AssetOffsaleAt.microsecond
)
except:
flash("Invalid offsale date", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
if datetime.utcnow() > AssetOffsaleAt:
flash("Offsale date cannot be in the past", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
else:
AssetOffsaleAt = None
isForSale = request.form.get('isForsale') == "on"
isLimited = request.form.get('isLimited') == "on"
isLimitedUnique = request.form.get('isLimitedUnique') == "on"
if AssetName is None or AssetName == "":
flash("Asset name is empty", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
if AssetDescription is None:
flash("Asset description is empty", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
if AssetPriceRobux is None or AssetPriceRobux == "":
flash("Asset price is empty", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
else:
try:
AssetPriceRobux = int(AssetPriceRobux)
except:
flash("Asset price is not a number", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
if AssetPriceTickets is None or AssetPriceTickets == "":
flash("Asset price is empty", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
else:
try:
AssetPriceTickets = int(AssetPriceTickets)
except:
flash("Asset price is not a number", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
if AssetSerialAmount is None or AssetSerialAmount == "":
flash("Asset serial amount is empty", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
else:
try:
AssetSerialAmount = int(AssetSerialAmount)
except:
flash("Asset serial amount is not a number", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
if AssetSerialAmount < 0:
flash("Asset serial amount cannot be negative", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
if AssetPriceRobux < 0 or AssetPriceRobux > 1000000:
flash("Asset Robux price must be in range ( 0 - 1000000)", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
if AssetPriceTickets < 0 or AssetPriceTickets > 10000000:
flash("Asset Tix price must be in range ( 0 - 10000000)", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
if AssetSerialAmount > 0 and not isLimitedUnique:
flash("Asset serial cannot be greater than 0 if not a limited unique", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
AuthenticatedUser : User = auth.GetCurrentUser()
# Check for cooldown
AdministratorCooldown = redis_controller.get(f"Asset_AdministratorCooldown:{str(AuthenticatedUser.id)}")
if AdministratorCooldown is not None and AuthenticatedUser.id != 1:
flash("You are updating assets too quickly!", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
redis_controller.set(f"Asset_AdministratorCooldown:{str(AuthenticatedUser.id)}", "1", ex=20)
if isLimitedUnique:
isLimited = True
if isLimited:
if not HasAdminPermission("ModifyLimitedAssets"):
flash("You do not have permission to modify limited assets", "danger")
return redirect("/admin/manage-assets")
if isLimitedUnique and AssetOffsaleAt is not None:
flash("Limited unique assets cannot have a offsale date", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
if not isForSale and AssetOffsaleAt is not None:
flash("Asset cannot have a offsale date if not for sale", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
if AssetOffsaleAt is not None and AssetOffsaleAt < datetime.utcnow() + timedelta( minutes = 5 ):
flash("Offsale time cannot be shorter than 5 minutes", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
# Update asset
AssetObj.name = AssetName
AssetObj.description = AssetDescription
AssetObj.price_robux = AssetPriceRobux
AssetObj.price_tix = AssetPriceTickets
AssetObj.serial_count = AssetSerialAmount
AssetObj.is_for_sale = isForSale
AssetObj.is_limited = isLimited
AssetObj.is_limited_unique = isLimitedUnique
AssetObj.updated_at = datetime.utcnow()
if AssetOffsaleAt is not None and AssetObj.offsale_at != AssetOffsaleAt:
AssetObj.offsale_at = AssetOffsaleAt
if redis_controller.exists(f"APSchedulerTaskJobUUID:{str(AssetObj.id)}"):
try:
scheduler.remove_job(redis_controller.get(f"APSchedulerTaskJobUUID:{str(AssetObj.id)}"))
except:
logging.warning(f"Failed to remove job {redis_controller.get(f'APSchedulerTaskJobUUID:{str(AssetObj.id)}')}")
APSchedulerTaskJobUUID = str(uuid.uuid4())
scheduler.add_job(id=APSchedulerTaskJobUUID, func=SetAssetOffsaleJob, trigger='date', run_date=AssetOffsaleAt, args=[AssetObj.id])
redis_controller.set(f"APSchedulerTaskJobUUID:{str(AssetObj.id)}", APSchedulerTaskJobUUID)
logging.info(f"Asset {str(AssetObj.id)} has been set to go offsale at {str(AssetOffsaleAt)}, job UUID: {APSchedulerTaskJobUUID}")
if not isForSale:
AssetObj.offsale_at = None
db.session.commit()
flash("Asset updated!", "success")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
@AdminRoute.route("/manage-assets/<int:assetid>/rerender", methods=['POST'])
def ReRenderAsset(assetid):
AdminPermissionRequired('ManageAsset')
AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
if AssetObj is None:
flash("Asset does not exist", "danger")
return redirect("/admin/manage-assets")
if (AssetObj.creator_id != 1 and AssetObj.creator_id != 2) and AssetObj.creator_type != 0:
flash("Asset is not a offical asset", "danger")
return redirect("/admin/manage-assets")
# Check for cooldown
AuthenticatedUser : User = auth.GetAuthenticatedUser(request.cookies.get(".ROBLOSECURITY"))
AdministratorCooldown = redis_controller.get(f"AssetRerender_AdministratorCooldown:{str(AuthenticatedUser.id)}")
if AdministratorCooldown is not None and AuthenticatedUser.id != 1:
flash("You are rerendering assets too quickly!", "danger")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
redis_controller.set(f"AssetRerender_AdministratorCooldown:{str(AuthenticatedUser.id)}", "1", ex=5)
TakeThumbnail(AssetObj.id, bypassCooldown=True, bypassCache=True)
flash("Asset queued for rerendering", "success")
return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
@AdminRoute.route("/pending-assets", methods=['GET'])
def PendingAssets():
AdminPermissionRequired('AssetModeration')
PendingAssets = []
# Only get assets that have their moderation_status set to 1
AssetsPendingList : list[Asset] = Asset.query.filter_by(moderation_status=1).order_by(Asset.created_at.desc()).all()
for AssetObj in AssetsPendingList:
if AssetObj.asset_type not in [AssetType.Image, AssetType.Audio]: # Only get images and audio
continue
LatestAssetVersion : AssetVersion = assetversion.GetLatestAssetVersion(AssetObj)
if LatestAssetVersion is None:
continue
ParentAsset : Asset = None
if AssetObj.asset_type == AssetType.Image:
# Try to get the parent asset
ParentAssetLink : AssetModerationLink = AssetModerationLink.query.filter_by(ChildAssetId=AssetObj.id).first()
if ParentAssetLink is not None:
ParentAsset = Asset.query.filter_by(id=ParentAssetLink.ParentAssetId).first()
PendingAssets.append({
"Asset": AssetObj,
"AssetVersion": LatestAssetVersion,
"ParentAsset": ParentAsset
})
if len(PendingAssets) >= 15:
break
PlaceIconPendingList : list[PlaceIcon] = PlaceIcon.query.filter_by(moderation_status=1).order_by(PlaceIcon.updated_at.desc()).all()
for PlaceIconObj in PlaceIconPendingList:
AssetObj : Asset = Asset.query.filter_by(id=PlaceIconObj.placeid).first()
if AssetObj is None:
continue
PendingAssets.append({
"Asset": AssetObj,
"AssetVersion": PlaceIconObj,
"ParentAsset": None,
"PlaceIcon": True,
"AssetThumbnail": False
})
if len(PendingAssets) >= 15:
break
AssetThumbnailPendingList : list[AssetThumbnail] = AssetThumbnail.query.filter_by(moderation_status=1).order_by(AssetThumbnail.updated_at.desc()).all()
for AssetThumbnailObj in AssetThumbnailPendingList:
AssetObj : Asset = Asset.query.filter_by(id=AssetThumbnailObj.asset_id).first()
if AssetObj is None:
continue
PendingAssets.append({
"Asset": AssetObj,
"AssetVersion": AssetThumbnailObj,
"ParentAsset": None,
"PlaceIcon": False,
"AssetThumbnail": True
})
GroupIconPendingList : list[GroupIcon] = GroupIcon.query.filter_by(moderation_status=1).order_by(GroupIcon.created_at.desc()).all()
for GroupIconObj in GroupIconPendingList:
GroupObj : Group = GroupIconObj.group
if GroupObj is None:
continue
PendingAssets.append({
"Group": GroupObj,
"Creator": GroupIconObj.creator,
"Icon": GroupIconObj,
"ParentAsset": None,
"PlaceIcon": False,
"AssetThumbnail": False
})
return render_template("admin/assetmoderation.html", PendingAssets=PendingAssets)
def LogModerationAction(
Actor : User,
isApproved : bool = True,
relatedAssets : list = []
):
"""
relatedAssets = [
{
"mainId": 1,
"relatedId": 2,
"type": "Image",
"view_name": "Awesome Shirt",
"source": "https://cdn.syntax.eco/",
"page": "https://www.syntax.eco/catalog/1/",
"creator": User | Group
}
]
"""
fields = []
for asset in relatedAssets:
fields.append({
"name": asset["view_name"],
"value": f"Type: **{asset['type']}** - { 'AssetId' if asset['type'] != 'GroupIcon' else 'GroupId' }: **{asset['mainId']}** - Creator: [{ asset['creator'].username if isinstance(asset['creator'], User) else asset['creator'].name }](https://www.syntax.eco/{ 'users/'+str(asset['creator'].id)+'/profile' if isinstance(asset['creator'], User) else 'groups/'+str(asset['creator'].id)+'/' }){ ' - Related AssetId: **' + str(asset['relatedId']) +'**' if asset['relatedId'] else ''} - [View]({asset['page']}){ ' - [Source](' + asset['source'] +')' if asset['source'] else ''}",
"inline": False
})
embed = {
"type": "rich",
"title": f"{'Approved' if isApproved else 'Denied'} {str(len(relatedAssets))} Asset{'s' if len(relatedAssets) > 1 else ''}",
"description": "",
"color": 0x26ff00 if isApproved else 0xef0000,
"fields": fields,
"author": {
"name": Actor.username,
"icon_url": f"https://www.syntax.eco/Thumbs/Head.ashx?x=48&y=48&userId={str(Actor.id)}"
},
"footer": {
"text": f"Syntax Asset Moderation Log"
},
"timestamp": datetime.utcnow().isoformat()
}
def thread_func():
try:
requests.post(
url = config.DISCORD_ADMIN_LOGS_WEBHOOK,
json = {
"username": "Syntax Asset Moderation Log",
"embeds": [embed],
"avatar_url": f"https://www.syntax.eco/Thumbs/Head.ashx?x=48&y=48&userId={str(Actor.id)}"
}
)
except Exception as e:
logging.warn(f"Admin > LogModerationAction: Exception raised when sending webhook - {str(e)}")
threading.Thread(target=thread_func).start()
@AdminRoute.route("/pending-assets/<content_hash>/approve-group-icon", methods=['POST'])
@csrf.exempt
def ApprovePendingGroupIcon( content_hash ):
AdminPermissionRequired('AssetModeration')
GroupIconObj : list[GroupIcon] = GroupIcon.query.filter_by(content_hash=content_hash).all()
if len(GroupIconObj) == 0:
return redirect("/admin/pending-assets")
for iconObj in GroupIconObj:
if iconObj.moderation_status != 1:
continue
LogModerationAction(auth.GetCurrentUser(), isApproved=True, relatedAssets=[{
"mainId": iconObj.group.id,
"relatedId": None,
"type": "GroupIcon",
"view_name": iconObj.group.name,
"source": f"{config.CDN_URL}/{iconObj.content_hash}",
"page": f"https://www.syntax.eco/groups/{str(iconObj.group.id)}/",
"creator": iconObj.creator
}])
iconObj.moderation_status = 0
db.session.commit()
return redirect("/admin/pending-assets")
@AdminRoute.route("/pending-assets/<content_hash>/deny-group-icon", methods=['POST'])
@csrf.exempt
def DenyPendingGroupIcon( content_hash ):
AdminPermissionRequired('AssetModeration')
GroupIconObj : list[GroupIcon] = GroupIcon.query.filter_by(content_hash=content_hash).all()
if len(GroupIconObj) == 0:
return redirect("/admin/pending-assets")
for iconObj in GroupIconObj:
if iconObj.moderation_status != 1:
continue
LogModerationAction(auth.GetCurrentUser(), isApproved=False, relatedAssets=[{
"mainId": iconObj.group.id,
"relatedId": None,
"type": "GroupIcon",
"view_name": iconObj.group.name,
"source": f"{config.CDN_URL}/{iconObj.content_hash}",
"page": f"https://www.syntax.eco/groups/{str(iconObj.group.id)}/",
"creator": iconObj.creator
}])
iconObj.moderation_status = 2
db.session.commit()
return redirect("/admin/pending-assets")
@AdminRoute.route("/pending-assets/<int:assetid>/approve", methods=['POST'])
@csrf.exempt
def ApprovePendingAsset(assetid):
AdminPermissionRequired('AssetModeration')
AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
if AssetObj is None:
flash("Asset does not exist", "danger")
return redirect("/admin/pending-assets")
if AssetObj.moderation_status != 1:
flash("Asset is not pending", "danger")
return redirect("/admin/pending-assets")
if AssetObj.asset_type not in [AssetType.Image, AssetType.Audio, AssetType.Place]:
flash("Asset type is not supported", "danger")
return redirect("/admin/pending-assets")
RelatedAssets = []
AssetObj.moderation_status = 0
RelatedAssets.append({
"mainId": AssetObj.id,
"relatedId": None,
"type": "Asset",
"view_name": AssetObj.name,
"source": None,
"page": f"https://www.syntax.eco/catalog/{str(AssetObj.id)}/",
"creator": GetCreatorOfAsset(AssetObj)
})
# Get the AssetThumbnail
AssetThumbnailObj : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=AssetObj.id).first()
if AssetThumbnailObj is not None:
if AssetThumbnailObj.moderation_status == 1:
AssetThumbnailObj.moderation_status = 0
RelatedAssets.append({
"mainId": AssetObj.id,
"relatedId": None,
"type": "AssetThumbnail",
"view_name": AssetObj.name,
"source": f"{config.CDN_URL}/{AssetThumbnailObj.content_hash}",
"page": f"https://www.syntax.eco/catalog/{str(AssetObj.id)}/",
"creator": GetCreatorOfAsset(AssetObj)
})
# Get the parent asset
ParentAssetLink : AssetModerationLink = AssetModerationLink.query.filter_by(ChildAssetId=AssetObj.id).first()
if ParentAssetLink is not None:
ParentAsset : Asset = Asset.query.filter_by(id=ParentAssetLink.ParentAssetId).first()
if ParentAsset is not None:
if ParentAsset.moderation_status == 1:
ParentAsset.moderation_status = 0
RelatedAssets.append({
"mainId": ParentAsset.id,
"relatedId": AssetObj.id,
"type": AssetObj.asset_type.name,
"view_name": ParentAsset.name,
"source": None,
"page": f"https://www.syntax.eco/catalog/{str(ParentAsset.id)}/",
"creator": GetCreatorOfAsset(AssetObj)
})
# Get the AssetThumbnail
ParentAssetThumbnail : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=ParentAsset.id).first()
if ParentAssetThumbnail is not None:
if ParentAssetThumbnail.moderation_status == 1:
ParentAssetThumbnail.moderation_status = 0
RelatedAssets.append({
"mainId": ParentAsset.id,
"relatedId": AssetObj.id,
"type": "AssetThumbnail",
"view_name": ParentAsset.name,
"source": f"{config.CDN_URL}/{ParentAssetThumbnail.content_hash}",
"page": f"https://www.syntax.eco/catalog/{str(ParentAsset.id)}/",
"creator": GetCreatorOfAsset(AssetObj)
})
LogModerationAction(auth.GetCurrentUser(), isApproved=True, relatedAssets=RelatedAssets)
db.session.commit()
return redirect("/admin/pending-assets")
@AdminRoute.route("/pending-assets/<int:assetid>/decline", methods=['POST'])
@csrf.exempt
def DeclinePendingAsset(assetid):
AdminPermissionRequired('AssetModeration')
AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
if AssetObj is None:
flash("Asset does not exist", "danger")
return redirect("/admin/pending-assets")
if AssetObj.moderation_status != 1:
flash("Asset is not pending", "danger")
return redirect("/admin/pending-assets")
if AssetObj.asset_type not in [AssetType.Image, AssetType.Audio, AssetType.Place]:
flash("Asset type is not supported", "danger")
return redirect("/admin/pending-assets")
RelatedAssets = []
AssetObj.moderation_status = 2
RelatedAssets.append({
"mainId": AssetObj.id,
"relatedId": None,
"type": "Asset",
"view_name": AssetObj.name,
"source": None,
"page": f"https://www.syntax.eco/catalog/{str(AssetObj.id)}/",
"creator": GetCreatorOfAsset(AssetObj)
})
# Get the AssetThumbnail
AssetThumbnailObj : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=AssetObj.id).first()
if AssetThumbnailObj is not None:
if AssetThumbnailObj.moderation_status == 1:
AssetThumbnailObj.moderation_status = 2
RelatedAssets.append({
"mainId": AssetObj.id,
"relatedId": None,
"type": "AssetThumbnail",
"view_name": AssetObj.name,
"source": f"{config.CDN_URL}/{AssetThumbnailObj.content_hash}",
"page": f"https://www.syntax.eco/catalog/{str(AssetObj.id)}/",
"creator": GetCreatorOfAsset(AssetObj)
})
# Get the parent asset
ParentAssetLink : AssetModerationLink = AssetModerationLink.query.filter_by(ChildAssetId=AssetObj.id).first()
if ParentAssetLink is not None:
ParentAsset : Asset = Asset.query.filter_by(id=ParentAssetLink.ParentAssetId).first()
if ParentAsset is not None:
if ParentAsset.moderation_status == 1:
ParentAsset.moderation_status = 2
RelatedAssets.append({
"mainId": ParentAsset.id,
"relatedId": AssetObj.id,
"type": AssetObj.asset_type.name,
"view_name": ParentAsset.name,
"source": None,
"page": f"https://www.syntax.eco/catalog/{str(ParentAsset.id)}/",
"creator": GetCreatorOfAsset(AssetObj)
})
# Get the AssetThumbnail
ParentAssetThumbnail : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=ParentAsset.id).first()
if ParentAssetThumbnail is not None:
ParentAssetThumbnail.moderation_status = 2
RelatedAssets.append({
"mainId": ParentAsset.id,
"relatedId": AssetObj.id,
"type": "AssetThumbnail",
"view_name": ParentAsset.name,
"source": f"{config.CDN_URL}/{ParentAssetThumbnail.content_hash}",
"page": f"https://www.syntax.eco/catalog/{str(ParentAsset.id)}/",
"creator": GetCreatorOfAsset(AssetObj)
})
LogModerationAction(auth.GetCurrentUser(), isApproved=False, relatedAssets=RelatedAssets)
db.session.commit()
return redirect("/admin/pending-assets")
@AdminRoute.route("/pending-assets/<int:assetid>/approve-icon", methods=['POST'])
@csrf.exempt
def ApprovePendingIcon(assetid):
AdminPermissionRequired('AssetModeration')
PlaceIconObj : PlaceIcon = PlaceIcon.query.filter_by(placeid=int(assetid)).first()
if PlaceIconObj is None:
flash("Icon does not exist", "danger")
return redirect("/admin/pending-assets")
if PlaceIconObj.moderation_status != 1:
flash("Icon is not pending", "danger")
return redirect("/admin/pending-assets")
PlaceAssetObj : Asset = Asset.query.filter_by(id=PlaceIconObj.placeid).first()
if PlaceAssetObj is None:
flash("Icon does not exist", "danger")
return redirect("/admin/pending-assets")
if PlaceAssetObj.moderation_status == 1:
PlaceAssetObj.moderation_status = 0
PlaceIconObj.moderation_status = 0
LogModerationAction(auth.GetCurrentUser(), isApproved=True, relatedAssets=[{
"mainId": PlaceAssetObj.id,
"relatedId": None,
"type": "PlaceIcon",
"view_name": PlaceAssetObj.name,
"source": f"{config.CDN_URL}/{PlaceIconObj.contenthash}",
"page": f"https://www.syntax.eco/games/{str(PlaceAssetObj.id)}/",
"creator": GetCreatorOfAsset(PlaceAssetObj)
}])
db.session.commit()
return redirect("/admin/pending-assets")
@AdminRoute.route("/pending-assets/<int:assetid>/decline-icon", methods=['POST'])
@csrf.exempt
def DeclinePendingIcon(assetid):
AdminPermissionRequired('AssetModeration')
PlaceIconObj : PlaceIcon = PlaceIcon.query.filter_by(placeid=int(assetid)).first()
if PlaceIconObj is None:
flash("Icon does not exist", "danger")
return redirect("/admin/pending-assets")
if PlaceIconObj.moderation_status != 1:
flash("Icon is not pending", "danger")
return redirect("/admin/pending-assets")
PlaceIconObj.moderation_status = 2
LogModerationAction(auth.GetCurrentUser(), isApproved=False, relatedAssets=[{
"mainId": PlaceIconObj.placeid,
"relatedId": None,
"type": "PlaceIcon",
"view_name": PlaceIconObj.asset.name,
"source": f"{config.CDN_URL}/{PlaceIconObj.contenthash}",
"page": f"https://www.syntax.eco/games/{str(PlaceIconObj.placeid)}/",
"creator": GetCreatorOfAsset(PlaceIconObj.asset)
}])
db.session.commit()
return redirect("/admin/pending-assets")
@AdminRoute.route("/pending-assets/<int:thumbnailid>/approve-thumbnail", methods=['POST'])
@csrf.exempt
def ApprovePendingThumbnail(thumbnailid : int):
AdminPermissionRequired('AssetModeration')
AssetThumbnailObj : AssetThumbnail = AssetThumbnail.query.filter_by(id=int(thumbnailid)).first()
if AssetThumbnailObj is None:
flash("Thumbnail does not exist", "danger")
return redirect("/admin/pending-assets")
if AssetThumbnailObj.moderation_status != 1:
flash("Thumbnail is not pending", "danger")
return redirect("/admin/pending-assets")
AssetThumbnailObj.moderation_status = 0
LogModerationAction(auth.GetCurrentUser(), isApproved=True, relatedAssets=[{
"mainId": AssetThumbnailObj.asset_id,
"relatedId": None,
"type": "AssetThumbnail",
"view_name": AssetThumbnailObj.asset.name,
"source": f"{config.CDN_URL}/{AssetThumbnailObj.content_hash}",
"page": f"https://www.syntax.eco/catalog/{str(AssetThumbnailObj.asset_id)}/",
"creator": GetCreatorOfAsset(AssetThumbnailObj.asset)
}])
db.session.commit()
return redirect("/admin/pending-assets")
@AdminRoute.route("/pending-assets/<int:thumbnailid>/decline-thumbnail", methods=['POST'])
@csrf.exempt
def DeclinePendingThumbnail(thumbnailid : int):
AdminPermissionRequired('AssetModeration')
AssetThumbnailObj : AssetThumbnail = AssetThumbnail.query.filter_by(id=int(thumbnailid)).first()
if AssetThumbnailObj is None:
flash("Thumbnail does not exist", "danger")
return redirect("/admin/pending-assets")
if AssetThumbnailObj.moderation_status != 1:
flash("Thumbnail is not pending", "danger")
return redirect("/admin/pending-assets")
AssetThumbnailObj.moderation_status = 2
LogModerationAction(auth.GetCurrentUser(), isApproved=False, relatedAssets=[{
"mainId": AssetThumbnailObj.asset_id,
"relatedId": None,
"type": "AssetThumbnail",
"view_name": AssetThumbnailObj.asset.name,
"source": f"{config.CDN_URL}/{AssetThumbnailObj.content_hash}",
"page": f"https://www.syntax.eco/catalog/{str(AssetThumbnailObj.asset_id)}/",
"creator": GetCreatorOfAsset(AssetThumbnailObj.asset)
}])
db.session.commit()
return redirect("/admin/pending-assets")
@AdminRoute.route("/manage-users", methods=['GET'])
def ManageUsers():
AdminPermissionRequired('ManageUsers')
query = request.args.get(key = "query", default = None, type = str)
page = request.args.get(key = "page", default = 1, type = int)
searchType = request.args.get(key = "searchType", default = "userid", type = str) # userid, username
orderBy = request.args.get(key = "orderBy", default = "userid", type = str) # userid, creation, lastonline, robux, tix
orderType = request.args.get(key = "orderType", default = "asc", type = str) # asc, desc
if searchType not in ["userid", "username", "discordid"]:
searchType = "userid"
if orderBy not in ["userid", "creation", "lastonline", "robux", "tix"]:
orderBy = "userid"
if orderType not in ["asc", "desc"]:
orderType = "desc"
if type(query) is not str:
query = None
def CreateUserInfo( userObj : User ):
userEconomyObj : UserEconomy = UserEconomy.query.filter_by(userid=userObj.id).first()
if userEconomyObj is None:
userEconomyObj = UserEconomy(userid=userObj.id)
db.session.add(userEconomyObj)
db.session.commit()
return {
"id": userObj.id,
"username": userObj.username,
"creation": userObj.created,
"lastonline": userObj.lastonline,
"robux": userEconomyObj.robux,
"tix": userEconomyObj.tix,
"accountstatus": userObj.accountstatus
}
returnList = []
if query is not None:
if searchType == "userid":
userObject : list[User] = [User.query.filter_by(id=int(query)).first()]
elif searchType == "username":
userObject : list[User] = User.query.filter(User.username.ilike(f"%{query}%")).offset((page-1)*15).limit(15).all()
elif searchType == "discordid":
LinkedDiscordObj : LinkedDiscord = LinkedDiscord.query.filter_by(discord_id=int(query)).first()
if LinkedDiscordObj is not None:
userObject = [User.query.filter_by(id=LinkedDiscordObj.user_id).first()]
else:
userObject = []
for userObj in userObject:
if userObj is None:
continue
if len(returnList) >= 15:
break
returnList.append(CreateUserInfo(userObj))
else:
UserQuery = None
if orderBy == "userid":
UserQuery = User.id
elif orderBy == "creation":
UserQuery = User.created
elif orderBy == "lastonline":
UserQuery = User.lastonline
elif orderBy == "robux":
UserQuery = UserEconomy.robux
elif orderBy == "tix":
UserQuery = UserEconomy.tix
if orderType == "asc":
UserQuery = UserQuery.asc()
elif orderType == "desc":
UserQuery = UserQuery.desc()
# If we are ordering by robux or tix, we need to join the UserEconomy table and only select the userEconomy column for that user by selecting its id
if orderBy in ["robux", "tix"]:
UserQuery = User.query.join(UserEconomy, User.id == UserEconomy.userid).order_by(UserQuery)
else:
UserQuery = User.query.order_by(UserQuery)
UserQuery = UserQuery.paginate(page=page, per_page=15, error_out=False)
for userObj in UserQuery.items:
returnList.append(CreateUserInfo(userObj))
isThereNextPage = False
if len(returnList) == 15:
isThereNextPage = True
return render_template("admin/usermanage/search.html",
query=query,
searchType=searchType,
orderBy=orderBy,
orderType=orderType,
returnList=returnList,
isThereNextPage=isThereNextPage,
page=page
)
@AdminRoute.route("/manage-users", methods=['POST'])
def ManageUserQuery():
AdminPermissionRequired('ManageUsers')
query = request.form.get(key = "user-search-input", default=None, type=str)
searchType = request.form.get(key = "user-search-type", default = "userid", type = str) # userid, username
orderBy = request.form.get(key = "user-order-by", default = "userid", type = str) # userid, creation, lastonline, robux, tix
orderType = request.form.get(key = "user-order-direction", default = "asc", type = str) # asc, desc
if searchType not in ["userid", "username", "discordid"]:
searchType = "userid"
if orderBy not in ["userid", "creation", "lastonline", "robux", "tix"]:
orderBy = "userid"
if orderType not in ["asc", "desc"]:
orderType = "desc"
if searchType == "userid" and query != "":
try:
query = int(query)
except:
searchType = "username"
if query == "":
query = None
if query is None:
return redirect(f"/admin/manage-users?searchType={searchType}&orderBy={orderBy}&orderType={orderType}")
else:
return redirect(f"/admin/manage-users?query={query}&searchType={searchType}&orderBy={orderBy}&orderType={orderType}")
@AdminRoute.route("/manage-users/<int:userid>", methods=['GET'])
def ManageUser(userid : int):
AdminPermissionRequired('ManageUsers')
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
isAdministrator = AdminPermissions.query.filter_by(userid=userid).first() is not None
TotalVisits = 0
UniverseList : list[Universe] = Universe.query.filter_by( creator_id = userObj.id, creator_type = 0 ).all()
for UniverseObj in UniverseList:
TotalVisits += UniverseObj.visit_count
UserEconomyObj : UserEconomy = UserEconomy.query.filter_by(userid=userObj.id).first()
if UserEconomyObj is None:
UserEconomyObj = UserEconomy(userid=userObj.id)
db.session.add(UserEconomyObj)
db.session.commit()
DescriptionLines = userObj.description.split("\n")
if HasAdminPermission('ViewLoginHistoryDetailed'):
LastLogin : LoginRecord = LoginRecord.query.filter_by(userid=userObj.id).order_by(LoginRecord.timestamp.desc()).first()
else:
LastLogin = None
LinkedDiscordObj : LinkedDiscord = LinkedDiscord.query.filter_by(user_id=userObj.id).first()
if LinkedDiscordObj is not None:
DiscordUserInfo : discord.DiscordUserInfo = discord.DiscordUserInfo(
UserId = LinkedDiscordObj.discord_id,
Username = LinkedDiscordObj.discord_username,
Discriminator = LinkedDiscordObj.discord_discriminator,
AvatarHash = LinkedDiscordObj.discord_avatar,
)
else:
DiscordUserInfo = None
InviteKeyUsed : InviteKey = InviteKey.query.filter_by(used_by=userObj.id).first()
LastestUserBanObj : UserBan = UserBan.query.filter_by(userid=userObj.id, acknowledged = False).order_by(UserBan.id.desc()).first()
return render_template(
"admin/usermanage/view.html",
userObj=userObj,
isAdministrator=isAdministrator,
TotalVisits=TotalVisits,
UserEconomyObj=UserEconomyObj,
DescriptionLines=DescriptionLines,
LastLogin=LastLogin,
LinkedDiscordObj=LinkedDiscordObj,
DiscordUserInfo=DiscordUserInfo,
LastestUserBanObj=LastestUserBanObj,
InviteKeyUsed=InviteKeyUsed,
HasAdminPermission=HasAdminPermission
)
@AdminRoute.route("/manage-users/<int:userid>/manage-admin-permissions", methods=["GET"])
def ManageUserAdminPermissions( userid : int ):
AdminPermissionRequired('ManageAdminPermissions')
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
return render_template(
"admin/usermanage/manage-admin-perms.html",
userObj = userObj
)
@AdminRoute.route("/manage-users/<int:userid>/manage-admin-permissions/api/fetch-permissions", methods=["GET"])
def ManageUserAdminPermissionsFetchPermissions( userid : int ):
AdminPermissionRequired('ManageAdminPermissions')
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
PermissionsData : list[dict] = []
for permission in PermissionsDefinition:
permissionData = PermissionsDefinition[permission]
PermissionsData.append({
"internal_name": permission,
"friendly_name": permissionData["Name"],
"description": permissionData["Description"],
"is_hidden": permissionData["Hidden"] if "Hidden" in permissionData else False,
"hasPermission": AdminPermissions.query.filter_by(userid=userObj.id, permission=permission).first() is not None,
"bi_icon": permissionData["icon"] if "icon" in permissionData else None
})
data_response = make_response(json.dumps(PermissionsData))
data_response.headers['Content-Type'] = 'application/json'
data_response.headers['Cache-Control'] = 'no-cache'
return data_response
@AdminRoute.route("/manage-users/<int:userid>/manage-admin-permissions/api/set-permissions", methods=["POST"])
@limiter.limit("30/minute")
def ManageUserAdminPermissionsSetPermission( userid : int ):
AdminPermissionRequired('ManageAdminPermissions')
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
AuthenticatedUser : User = auth.GetCurrentUser()
UpdatedPermissionData = request.json
if "permissions" not in UpdatedPermissionData:
return jsonify({
"success": False,
"reason": f"Bad Data"
}), 200
if "2fa_code" not in UpdatedPermissionData:
return jsonify({
"success": False,
"reason": f"Bad Data"
}), 200
if not auth.Validate2FACode( AuthenticatedUser.id, str( UpdatedPermissionData["2fa_code"] ) ):
return jsonify({
"success": False,
"reason": f"Invalid 2FA Code"
}), 200
for permission in UpdatedPermissionData["permissions"]:
if permission not in PermissionsDefinition:
return jsonify({
"success": False,
"reason": f"Unknown permission {permission}"
}), 200
AdminPermissions.query.filter_by(userid=userObj.id).delete()
db.session.commit()
for permission in UpdatedPermissionData["permissions"]:
db.session.add(AdminPermissions(userid=userObj.id, permission=permission))
db.session.commit()
return jsonify({
"success": True,
"reason": ""
}), 200
@AdminRoute.route("/manage-users/<int:userid>/ban-history", methods=['GET'])
def ManageUserBanHistory( userid : int ):
AdminPermissionRequired('ManageUsers')
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
UserBanHistory : list[UserBan] = UserBan.query.filter_by(userid=userObj.id).order_by(UserBan.id.desc()).paginate(page=1, per_page=10, error_out=False)
def GetBanAuthorName( banObj : UserBan ):
Author : User = User.query.filter_by(id=banObj.author_userid).first()
if Author is None:
return "Unknown"
return Author.username
return render_template(
"admin/usermanage/banhistory.html",
userObj = userObj,
BanHistory = UserBanHistory,
GetBanAuthorName = GetBanAuthorName
)
@AdminRoute.route("/manage-users/<int:userid>/invite-keys", methods=['GET'])
def ManageUserInviteKeysview(userid : int):
AdminPermissionRequired('ManageUsers')
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
PageNumber = request.args.get( key = "page", default = 1, type = int )
if PageNumber < 1:
PageNumber = 1
UserInviteKeys = InviteKey.query.filter_by(created_by = userObj.id).order_by(InviteKey.created_at.desc()).paginate( page = PageNumber, per_page = 15, error_out = False )
return render_template(
"admin/usermanage/invitekeys.html",
userObj = userObj,
InviteKeys = UserInviteKeys
)
def LogUserBanAction( BannedUser : User, Actor : User, BanObject : UserBan ):
BanEmbed = {
"type": "rich",
"title": f"{BannedUser.username} ({BannedUser.id}) Banned",
"description": "",
"color": 0xef0000,
"fields": [
{
"name": "Ban Author",
"value": f"[{Actor.username}]({config.BaseURL}/admin/manage-users/{Actor.id}) ({Actor.id})",
"inline": False
},
{
"name": "Banned User",
"value": f"[{BannedUser.username}]({config.BaseURL}/admin/manage-users/{BannedUser.id}) ({BannedUser.id})",
"inline": False
},
{
"name": "Ban Type",
"value": f"{BanObject.ban_type.name}",
"inline": False
},
{
"name": "Ban Reason",
"value": f"{BanObject.reason}",
"inline": False
},
{
"name": "Internal Reason",
"value": f"{BanObject.moderator_note}",
"inline": False
},
{
"name": "Expiration",
"value": f"{BanObject.expires_at}",
"inline": False
}
],
"author": {
"name": BannedUser.username,
"icon_url": f"https://www.syntax.eco/Thumbs/Head.ashx?x=48&y=48&userId={str(BannedUser.id)}"
},
"footer": {
"text": "Syntax Asset Moderation Log"
},
"timestamp": datetime.utcnow().isoformat()
}
def thread_func():
try:
requests.post(
url = config.DISCORD_ADMIN_LOGS_WEBHOOK,
json = {
"username": "Syntax Moderation Log",
"embeds": [BanEmbed],
"avatar_url": f"https://www.syntax.eco/Thumbs/Head.ashx?x=48&y=48&userId={str(BannedUser.id)}"
}
)
except Exception as e:
logging.warn(f"Admin > LogUserBanAction: Exception raised when sending webhook - {str(e)}")
threading.Thread(target=thread_func).start()
def LogUserUnbanAction( TargetUser : User, Actor : User, BanObj : UserBan ):
Author : User = User.query.filter_by( id = BanObj.author_userid ).first()
BanEmbed = {
"type": "rich",
"title": f"{TargetUser.username} ({TargetUser.id}) Unbanned",
"description": "",
"color": 0xfcdb03,
"fields": [
{
"name": "Ban Author",
"value": f"[{Author.username}]({config.BaseURL}/admin/manage-users/{Author.id}) ({Author.id})",
"inline": False
},
{
"name": "Unbanned By",
"value": f"[{Actor.username}]({config.BaseURL}/admin/manage-users/{Actor.id}) ({Actor.id})",
"inline": False
},
{
"name": "Banned User",
"value": f"[{TargetUser.username}]({config.BaseURL}/admin/manage-users/{TargetUser.id}) ({TargetUser.id})",
"inline": False
},
{
"name": "Ban Type",
"value": f"{BanObj.ban_type.name}",
"inline": False
},
{
"name": "Ban Reason",
"value": f"{BanObj.reason}",
"inline": False
},
{
"name": "Internal Reason",
"value": f"{BanObj.moderator_note}",
"inline": False
},
{
"name": "Expiration",
"value": f"{BanObj.expires_at}",
"inline": False
}
],
"author": {
"name": TargetUser.username,
"icon_url": f"https://www.syntax.eco/Thumbs/Head.ashx?x=48&y=48&userId={str(TargetUser.id)}"
},
"footer": {
"text": "Syntax Asset Moderation Log"
},
"timestamp": datetime.utcnow().isoformat()
}
def thread_func():
try:
requests.post(
url = config.DISCORD_ADMIN_LOGS_WEBHOOK,
json = {
"username": "Syntax Moderation Log",
"embeds": [BanEmbed],
"avatar_url": f"https://www.syntax.eco/Thumbs/Head.ashx?x=48&y=48&userId={str(TargetUser.id)}"
}
)
except Exception as e:
logging.warn(f"Admin > LogUserUnbanAction: Exception raised when sending webhook - {str(e)}")
threading.Thread(target=thread_func).start()
@AdminRoute.route("/manage-users/<int:userid>/ban-user", methods=['GET'])
def BanUser(userid : int):
AdminPermissionRequired('ManageUsers')
AdminPermissionRequired('BanUser')
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
isAdministrator = AdminPermissions.query.filter_by(userid=userid).first() is not None # Check if they have at least one admin permission
if isAdministrator:
flash("You cannot ban an administrator", "danger")
return redirect(f"/admin/manage-users/{str(userid)}")
LastestUserBanObj : UserBan = UserBan.query.filter_by(userid=userObj.id, acknowledged = False).order_by(UserBan.id.desc()).first()
return render_template("admin/usermanage/ban.html", userObj=userObj, LastestUserBanObj=LastestUserBanObj)
@AdminRoute.route("/manage-users/<int:userid>/ban-user", methods=['POST'])
def BanUserPost(userid : int):
AdminPermissionRequired('ManageUsers')
AdminPermissionRequired('BanUser')
AuthenticatedUser : User = auth.GetCurrentUser()
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
isAdministrator = AdminPermissions.query.filter_by(userid=userid).first() is not None # Check if they have at least one admin permission
if isAdministrator:
flash("You cannot ban an administrator", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
LastestUserBanObj : UserBan = UserBan.query.filter_by(userid=userObj.id, acknowledged = False).order_by(UserBan.id.desc()).first()
if LastestUserBanObj is not None:
flash("User is already banned", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
BanTypeInput : int = request.form.get( key='ban_type', default=None, type=int )
BanReason = request.form.get( key='ban_reason', default=None, type=str )
ModeratorNote : str = request.form.get( key='ban_notes', default=None, type=str )
TOTPCode : str = request.form.get( key='totp_code', default=None, type=str )
if BanTypeInput is None or BanTypeInput not in [0,1,2,3,4,5,6] or BanReason is None or ModeratorNote is None or TOTPCode is None:
flash("Invalid request", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
isValidTOTPCode = auth.Validate2FACode(AuthenticatedUser.id, TOTPCode)
if not isValidTOTPCode:
flash("Invalid 2FA code", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
BanTypeInput : BanType = BanType(BanTypeInput)
if len(BanReason) > 512:
flash("Ban reason is too long", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
if len(ModeratorNote) > 512:
flash("Moderator note is too long", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
if len(BanReason) < 10:
flash("Ban reason is too short", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
if len(ModeratorNote) < 10:
flash("Moderator note is too short", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
if redis_controller.exists(f"admin_ban_cooldown:{AuthenticatedUser.id}"):
flash("You are banning users too fast!", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
BanTypeToTimeLength = {
BanType.Warning : timedelta(days=0),
BanType.Day1Ban : timedelta(days=1),
BanType.Day3Ban : timedelta(days=3),
BanType.Day7Ban : timedelta(days=7),
BanType.Day14Ban : timedelta(days=14),
BanType.Day30Ban : timedelta(days=30)
}
ExpirationDate = None
if BanTypeInput in BanTypeToTimeLength:
ExpirationDate = datetime.utcnow() + BanTypeToTimeLength[BanTypeInput]
BanObj : UserBan = UserBan(
userid = userObj.id,
author_userid = AuthenticatedUser.id,
ban_type = BanTypeInput,
reason = BanReason,
moderator_note = ModeratorNote,
expires_at = ExpirationDate
)
db.session.add(BanObj)
if BanTypeInput == BanType.Deleted:
userObj.accountstatus = 3
else:
userObj.accountstatus = 2
db.session.commit()
LogUserBanAction( userObj, AuthenticatedUser, BanObj )
redis_controller.set(f"admin_ban_cooldown:{AuthenticatedUser.id}", "1", ex = 15)
flash("User banned", "success")
PlaceserverPlayerObj : PlaceServerPlayer = PlaceServerPlayer.query.filter_by( userid = userObj.id ).first()
if PlaceserverPlayerObj is not None:
CurrentPlaceServerObj : PlaceServer = PlaceServer.query.filter_by( serveruuid = PlaceserverPlayerObj.serveruuid).first()
EvictPlayer(CurrentPlaceServerObj, userObj.id)
return redirect(f"/admin/manage-users/{str(userid)}")
@AdminRoute.route("/manage-users/<int:userid>/ban-user/revoke-ban", methods=["POST"])
def UnbanUserPost( userid : int):
AdminPermissionRequired('ManageUsers')
AdminPermissionRequired('BanUser')
AuthenticatedUser : User = auth.GetCurrentUser()
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
banid = request.args.get( key = "banid", default = None, type = int )
if banid is None:
flash("Invalid BanID", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
if userObj.accountstatus == 1:
flash("User does not have an active ban", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
if userObj.accountstatus == 4:
flash("User is forgotten, cannot revoke any bans", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
TargetBanObj : UserBan = UserBan.query.filter_by( id = banid, userid = userid ).first()
if TargetBanObj is None:
flash("Invalid BanID", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
if TargetBanObj.acknowledged:
flash("Ban has already been acknowledged by user", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
if redis_controller.exists(f"admin_unban_user_cooldown:{AuthenticatedUser.id}"):
flash("You are unbanning users too quickly!", "danger")
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
LogUserUnbanAction( userObj, AuthenticatedUser, TargetBanObj )
db.session.delete(TargetBanObj)
userObj.accountstatus = 1
db.session.commit()
redis_controller.set(f"admin_unban_user_cooldown:{AuthenticatedUser.id}", "1", ex=30)
return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
@AdminRoute.route("/manage-users/<int:userid>/login-history", methods=['GET'])
def ViewUserLoginHistory( userid : int ):
AdminPermissionRequired('ManageUsers')
AdminPermissionRequired('ViewUserLoginHistory')
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
PageNumber = request.args.get(key = "page", default = 1, type = int)
if PageNumber < 1:
PageNumber = 1
LoginHistory : list[LoginRecord] = LoginRecord.query.filter_by(userid=userObj.id).order_by(LoginRecord.timestamp.desc()).paginate(page=PageNumber, per_page=15, error_out=False)
AllUniqueLoginRecords : list[LoginRecord] = LoginRecord.query.filter_by( userid = userObj.id ).order_by(LoginRecord.ip, LoginRecord.session_token).distinct(LoginRecord.ip, LoginRecord.session_token).all()
AlreadySearchedIPs : list[str] = []
AlreadySearchedSessionTokens : list[str] = []
AlternateAccounts : list[User] = []
for LoginRecordObj in AllUniqueLoginRecords:
if LoginRecordObj.ip in AlreadySearchedIPs and LoginRecordObj.session_token in AlreadySearchedSessionTokens:
continue
AlreadySearchedIPs.append(LoginRecordObj.ip)
AlreadySearchedSessionTokens.append(LoginRecordObj.session_token)
MatchingLoginRecords : list[LoginRecord] = LoginRecord.query.filter(
and_(
or_(
LoginRecord.ip == LoginRecordObj.ip,
LoginRecord.session_token == LoginRecordObj.session_token
),
LoginRecord.userid != userObj.id
)
).distinct(LoginRecord.userid).all()
for MatchingLoginRecord in MatchingLoginRecords:
MatchingUserObj : User = User.query.filter_by(id=MatchingLoginRecord.userid).first()
if MatchingUserObj is not None and MatchingUserObj not in AlternateAccounts and MatchingLoginRecord.session_token is not None:
MatchingUserObj.flags = {
"ipmatch" : MatchingLoginRecord.ip == LoginRecordObj.ip,
"sessiontokenmatch" : MatchingLoginRecord.session_token == LoginRecordObj.session_token,
"useragentmatch" : MatchingLoginRecord.useragent == LoginRecordObj.useragent,
"hwidmatch" : False
}
AlternateAccounts.append(MatchingUserObj)
elif MatchingUserObj is not None and MatchingUserObj in AlternateAccounts and MatchingLoginRecord.session_token is not None:
MatchingUserIndex = AlternateAccounts.index(MatchingUserObj)
AlternateAccounts[MatchingUserIndex].flags = {
"ipmatch" : MatchingLoginRecord.ip == LoginRecordObj.ip or AlternateAccounts[MatchingUserIndex].flags["ipmatch"],
"sessiontokenmatch" : MatchingLoginRecord.session_token == LoginRecordObj.session_token or AlternateAccounts[MatchingUserIndex].flags["sessiontokenmatch"],
"useragentmatch" : MatchingLoginRecord.useragent == LoginRecordObj.useragent or AlternateAccounts[MatchingUserIndex].flags["useragentmatch"],
"hwidmatch" : False
}
AllUniqueHWIDLogs : list[UserHWIDLog] = UserHWIDLog.query.filter_by( user_id = userObj.id ).order_by(UserHWIDLog.hwid).distinct(UserHWIDLog.hwid).all()
for HWIDLog in AllUniqueHWIDLogs:
MatchingHWIDLogs : list[UserHWIDLog] = UserHWIDLog.query.filter(
and_(
UserHWIDLog.hwid == HWIDLog.hwid,
UserHWIDLog.user_id != userObj.id
)
).distinct(UserHWIDLog.user_id).all()
for MatchingHWIDLog in MatchingHWIDLogs:
MatchingUserObj : User = User.query.filter_by(id=MatchingHWIDLog.user_id).first()
if MatchingUserObj is not None and MatchingUserObj not in AlternateAccounts:
MatchingUserObj.flags = {
"ipmatch" : False,
"sessiontokenmatch" : False,
"useragentmatch" : False,
"hwidmatch" : True
}
AlternateAccounts.append(MatchingUserObj)
elif MatchingUserObj is not None and MatchingUserObj in AlternateAccounts:
MatchingUserIndex = AlternateAccounts.index(MatchingUserObj)
AlternateAccounts[MatchingUserIndex].flags["hwidmatch"] = True
canViewSensitiveInfo = HasAdminPermission('ViewLoginHistoryDetailed')
return render_template("admin/usermanage/loginhistory.html", userObj=userObj, LoginHistory=LoginHistory, AlternateAccounts=AlternateAccounts, canViewSensitiveInfo=canViewSensitiveInfo)
@AdminRoute.route("/manage-users/<int:userid>/game-sessions", methods=['GET'])
def ViewUserGameSessions( userid : int ):
AdminPermissionRequired('ManageUsers')
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
PageNumber = request.args.get(key = "page", default = 1, type = int)
if PageNumber < 1:
PageNumber = 1
SessionsLogs : list[GameSessionLog] = GameSessionLog.query.filter_by( user_id = userObj.id ).order_by(GameSessionLog.joined_at.desc()).paginate(page=PageNumber, per_page=15, error_out=False)
def get_place_name( place_id : int ):
PlaceAssetObj : Asset = Asset.query.filter_by( id = place_id ).first()
if PlaceAssetObj is None:
return "Unknown Place"
return PlaceAssetObj.name
return render_template("admin/usermanage/gamesessions.html", userObj=userObj, GameSessions=SessionsLogs, get_place_name=get_place_name)
CategoryToEnum = {
"purchase": TransactionType.Purchase,
"sale": TransactionType.Sale,
"group-payout": TransactionType.GroupPayout,
"stipends": TransactionType.BuildersClubStipend,
}
@AdminRoute.route("/manage-users/<int:userid>/transactions", methods=['GET'])
def ViewUserTransactions( userid : int ):
AdminPermissionRequired('ManageUsers')
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
CategoryArg = request.args.get('category', default="purchase", type=str)
if CategoryArg not in CategoryToEnum:
Category : TransactionType = TransactionType.Purchase
else:
Category : TransactionType = CategoryToEnum[CategoryArg]
PageNumber = request.args.get('page', default=1, type=int)
if PageNumber < 1:
PageNumber = 1
TransactionQuery = UserTransaction.query.filter_by( transaction_type = Category)
CategoryQueryDict = {
TransactionType.Purchase: lambda queryObj: queryObj.filter_by(
sender_id = userObj.id,
sender_type = 0
),
TransactionType.Sale: lambda queryObj: queryObj.filter_by(
reciever_id = userObj.id,
reciever_type = 0
),
TransactionType.GroupPayout: lambda queryObj: queryObj.filter_by(
reciever_id = userObj.id,
reciever_type = 0
),
TransactionType.BuildersClubStipend: lambda queryObj: queryObj.filter_by(
reciever_id = userObj.id,
reciever_type = 0
),
}
TransactionQuery = CategoryQueryDict[Category](TransactionQuery)
TransactionQuery = TransactionQuery.order_by(UserTransaction.created_at.desc())
TransactionQuery = TransactionQuery.paginate( page=PageNumber, per_page=15, error_out=False )
FormattedTransactions = []
for Transaction in TransactionQuery.items:
Transaction : UserTransaction = Transaction
TransactionInfo = {}
#TransactionInfo["source"] = {
# "id": Transaction.sender_id if Transaction.sender_id != userObj.id else Transaction.reciever_id,
# "type": Transaction.sender_type if Transaction.sender_id != userObj.id else Transaction.reciever_type, # VV I know this is bad but im too lazy to think of another way to do it
# "name": ( User.query.filter_by(id = Transaction.sender_id).first().username if Transaction.sender_type != 1 else Group.query.filter_by( id = Transaction.sender_id ) ) if Transaction.sender_id != userObj.id else ( User.query.filter_by(id = Transaction.reciever_id).first().username if Transaction.reciever_type != 1 else Group.query.filter_by( id = Transaction.reciever_id ) ),
#}
TransactionInfo["source"] = {
"id": Transaction.sender_id if Transaction.sender_id != userObj.id or Transaction.sender_type != 0 else Transaction.reciever_id,
"type": Transaction.sender_type if Transaction.sender_id != userObj.id or Transaction.sender_type != 0 else Transaction.reciever_type, # VV I know this is bad but im too lazy to think of another way to do it
"name": ( User.query.filter_by(id = Transaction.sender_id).first().username if Transaction.sender_type != 1 else Group.query.filter_by( id = Transaction.sender_id ).first().name ) if Transaction.sender_id != userObj.id or Transaction.sender_type != 0 else ( User.query.filter_by(id = Transaction.reciever_id).first().username if Transaction.reciever_type != 1 else Group.query.filter_by( id = Transaction.reciever_id ).first().name ),
}
TransactionInfo["currency_amount"] = Transaction.currency_amount
TransactionInfo["currency_type"] = Transaction.currency_type
TransactionInfo["created_at"] = Transaction.created_at.strftime("%d/%m/%Y %H:%M:%S UTC")
TransactionInfo["custom_text"] = Transaction.custom_text
if Transaction.assetId:
TransactionInfo["asset"] = {
"id": Transaction.assetId,
"name": Asset.query.filter_by(id = Transaction.assetId).first().name,
}
else:
TransactionInfo["asset"] = None
FormattedTransactions.append(TransactionInfo)
return render_template(
"admin/usermanage/transactions.html",
userObj = userObj,
PageCategory = CategoryArg,
TransactionInfo = FormattedTransactions,
Pagination = TransactionQuery
)
@AdminRoute.route("/manage-users/<int:userid>/moderator-notes", methods=['GET'])
def ViewUserModeratorNotes( userid : int ):
AdminPermissionRequired('ManageUsers')
userObj : User = User.query.filter_by(id=userid).first()
if userObj is None:
return abort(404)
PageNumber = request.args.get('page', default=1, type=int)
if PageNumber < 1:
PageNumber = 1
UserModeratorNotes : list[ModeratorNote] = ModeratorNote.query.filter_by( user_id = userObj.id ).order_by(ModeratorNote.created_at.desc()).paginate(page=PageNumber, per_page=15, error_out=False)
def GetUserName( TargetUserId : int ) -> str:
return User.query.filter_by( id = TargetUserId ).first().username
return render_template(
"admin/usermanage/moderatornotes.html",
userObj = userObj,
UserModeratorNotes = UserModeratorNotes,
GetUserName = GetUserName
)
from app.pages.admin.websitefeaturesdefinition import WebsiteFeaturesDefinition
from app.util.websiteFeatures import GetWebsiteFeature, SetWebsiteFeature
@AdminRoute.route("/manage-website-features", methods=['GET'])
def ManageWebsiteFeatures():
AdminPermissionRequired('ManageWebsiteFeatures')
SitesFeaturesStatus = []
for feature in WebsiteFeaturesDefinition:
SitesFeaturesStatus.append( {
"name" : feature["name"],
"enabled" : GetWebsiteFeature(feature["name"]),
})
return render_template("admin/websitefeatures.html", SitesFeaturesStatus=SitesFeaturesStatus)
@AdminRoute.route("/manage-website-features/<featurename>/disable", methods=['POST'])
def ManageWebsiteFeaturesDisable(featurename : str):
AdminPermissionRequired('ManageWebsiteFeatures')
SetWebsiteFeature(featurename, False)
return redirect("/admin/manage-website-features")
@AdminRoute.route("/manage-website-features/<featurename>/enable", methods=['POST'])
def ManageWebsiteFeaturesEnable(featurename : str):
AdminPermissionRequired('ManageWebsiteFeatures')
SetWebsiteFeature(featurename, True)
return redirect("/admin/manage-website-features")
@AdminRoute.route("/create-user", methods=['GET'])
def CreateUser():
AdminPermissionRequired('CreateUser')
return render_template("admin/createuser.html")
@AdminRoute.route("/create-user", methods=['POST'])
@limiter.limit("5/minute")
def CreateUserPost():
AdminPermissionRequired('CreateUser')
Username = request.form.get( key='username', default=None, type=str )
Password = request.form.get( key='password', default=None, type=str )
if Username is None or Password is None:
flash("Invalid request", "error")
return redirect("/admin/create-user")
from app.pages.signup.signup import isUsernameAllowed
from app.util.textfilter import FilterText, TextNotAllowedException
from app.models.user_avatar import UserAvatar
import hashlib
from sqlalchemy import func
isAllowed, Reason = isUsernameAllowed(Username)
if not isAllowed:
flash(Reason, "error")
return redirect("/admin/create-user")
if len(Password) < 8:
flash("Password must be at least 8 characters long")
return redirect("/signup")
try:
FilterText( Text = Username, ThrowException = True)
except TextNotAllowedException:
flash("Username is not friendly for Syntax")
return redirect("/signup")
UserSignupLock = redislock.acquire_lock("UserSignupLock", acquire_timeout = 20, lock_timeout=1)
if not UserSignupLock:
flash("Failed to acquire user signup lock, please contact administrator", "error")
return redirect("/admin/create-user")
user = User.query.filter(func.lower(User.username) == func.lower(Username)).first()
if user is not None:
redislock.release_lock("UserSignupLock", UserSignupLock)
flash("Username already taken", "error")
return redirect("/admin/create-user")
pastUsername = PastUsername.query.filter(func.lower(PastUsername.username) == func.lower(Username)).first()
if pastUsername is not None:
redislock.release_lock("UserSignupLock", UserSignupLock)
flash("Username already taken", "error")
return redirect("/admin/create-user")
hashedPassword = hashlib.sha512(Password.encode("utf-8")).hexdigest()
user = User(username=Username, password=hashedPassword, created=datetime.utcnow(), lastonline=datetime.utcnow())
db.session.add(user)
db.session.commit()
userEconomy = UserEconomy(userid=user.id, robux=0, tix=10)
db.session.add(userEconomy)
userAvatar = UserAvatar(user_id=user.id)
db.session.add(userAvatar)
db.session.commit()
redislock.release_lock("UserSignupLock", UserSignupLock)
TakeUserThumbnail(user.id)
flash("User created", "success")
return redirect("/admin/create-user")
@AdminRoute.route("/create-giftcard", methods=['GET'])
def CreateGiftcard():
AdminPermissionRequired('CreateGiftcard')
return render_template("admin/creategiftcard.html")
@AdminRoute.route("/create-giftcard", methods=['POST'])
def CreateGiftcardPost():
AdminPermissionRequired('CreateGiftcard')
GiftcardTypeInput = request.form.get( key='giftcard-type', default=None, type=int )
CopiesAmount = request.form.get( key='copies', default=None, type=int )
GiftcardValue = request.form.get( key='value', default=None, type=int )
if GiftcardTypeInput is None or CopiesAmount is None or GiftcardValue is None:
flash("Invalid request", "error")
return redirect("/admin/create-giftcard")
if CopiesAmount < 1 or CopiesAmount > 15:
flash("Copies value must be between 1 and 15", "error")
return redirect("/admin/create-giftcard")
if GiftcardTypeInput not in [0,1,2,3,4]:
flash("Invalid giftcard type", "error")
return redirect("/admin/create-giftcard")
if GiftcardValue < 1:
flash("Giftcard value must be at least 1", "error")
return redirect("/admin/create-giftcard")
GiftcardTypeInput : GiftcardType = GiftcardType(GiftcardTypeInput)
if GiftcardTypeInput in [GiftcardType.RobuxCurrency, GiftcardType.TixCurrency]:
if GiftcardValue > 10000:
flash("Giftcard value must be at most 10000", "error")
return redirect("/admin/create-giftcard")
elif GiftcardTypeInput in [GiftcardType.Outrageous_BuildersClub, GiftcardType.Turbo_BuildersClub]:
if GiftcardValue > 12:
flash("Giftcard value must be at most 12", "error")
return redirect("/admin/create-giftcard")
elif GiftcardTypeInput == GiftcardType.Item:
AssetObj : Asset = Asset.query.filter_by(id=GiftcardValue).first()
if AssetObj is None:
flash("Invalid asset", "error")
return redirect("/admin/create-giftcard")
if AssetObj.is_limited:
flash("Cannot create giftcard for limited items", "error")
return redirect("/admin/create-giftcard")
else:
flash("Invalid giftcard type", "error")
return redirect("/admin/create-giftcard")
AuthenticatedUser : User = auth.GetCurrentUser()
if redis_controller.get(f"GiftcardCooldown_{AuthenticatedUser.id}") is not None:
flash("You are on cooldown", "error")
return redirect("/admin/create-giftcard")
redis_controller.set(f"GiftcardCooldown_{AuthenticatedUser.id}", "1", ex=60)
def GenerateCode():
Code = ""
for i in range(0, 5):
Chunk = ''.join(random.choices(string.ascii_uppercase + string.digits, k=5))
Code += Chunk
if i != 4:
Code += "-"
return Code
AllCodes = []
for i in range(0, CopiesAmount):
Code = GenerateCode()
GiftcardObj : GiftcardKey = GiftcardKey(key=Code, value=GiftcardValue, type=GiftcardTypeInput)
db.session.add(GiftcardObj)
AllCodes.append(Code)
db.session.commit()
flash(f"Created {CopiesAmount} giftcards", "success")
return render_template("admin/creategiftcard.html", codes=AllCodes)
@AdminRoute.route("/update-asset-file", methods=['GET'])
def UpdateAssetFile():
AdminPermissionRequired('UpdateAssetFile')
return render_template("admin/updateassetfile.html")
from app.util import s3helper
@AdminRoute.route("/update-asset-file", methods=['POST'])
def UpdateAssetFilePost():
AdminPermissionRequired('UpdateAssetFile')
AuthenticatedUser : User = auth.GetCurrentUser()
AssetID = request.form.get( key='asset-id', default=None, type=int )
if AssetID is None:
flash("Invalid request", "error")
return redirect("/admin/update-asset-file")
AssetObj : Asset = Asset.query.filter_by(id=AssetID).first()
if AssetObj is None:
flash("Invalid asset", "error")
return redirect("/admin/update-asset-file")
if AssetObj.creator_id not in [1,2] or AssetObj.creator_type != 0:
flash("Asset is not owned by Syntax", "error")
return redirect("/admin/update-asset-file")
if AssetObj.asset_type not in [AssetType(1), AssetType(2), AssetType(3), AssetType(4), AssetType(8), AssetType(11), AssetType(12), AssetType(17), AssetType(18), AssetType(19), AssetType(24), AssetType(27), AssetType(28), AssetType(29), AssetType(30), AssetType(31), AssetType(32), AssetType(41), AssetType(42), AssetType(43), AssetType(44), AssetType(45), AssetType(46), AssetType(47), AssetType(57), AssetType(58)]:
if AuthenticatedUser.id != 1:
flash("You are not allowed to update this type of asset", "error")
return redirect("/admin/update-asset-file")
if 'file' not in request.files:
flash("No file uploaded", "error")
return redirect("/admin/update-asset-file")
AssetFile = request.files['file']
if AssetFile.filename == '':
flash("No file uploaded", "error")
return redirect("/admin/update-asset-file")
AssetFile.seek(0)
AssetFileContent = AssetFile.read()
AssetFileHash = hashlib.sha512(AssetFileContent).hexdigest()
from app.util.assetvalidation import ValidateClothingImage, ValidatePlaceFile
if AssetObj.asset_type == AssetType.Image:
isAllowed = ValidateClothingImage( AssetFile, verifyResolution=False)
if not isAllowed:
flash("Invalid image", "error")
return redirect("/admin/update-asset-file")
else:
isValidPlaceFile = ValidatePlaceFile( AssetFile)
if type(isValidPlaceFile) is str:
flash(f"Validation Failed: {isValidPlaceFile}", "error")
return redirect("/admin/update-asset-file")
if not s3helper.DoesKeyExist(AssetFileHash):
s3helper.UploadBytesToS3(AssetFileContent, AssetFileHash)
NewAssetVersion : AssetVersion = assetversion.CreateNewAssetVersion(AssetObj, AssetFileHash)
AssetObj.updated_at = datetime.utcnow()
db.session.commit()
flash("Asset file updated", "success")
return redirect("/admin/update-asset-file")
@AdminRoute.route("/copy-bundle", methods=['GET'])
def CopyBundle():
AdminPermissionRequired('CopyBundle')
return render_template("admin/bundlecopier.html")
@AdminRoute.route("/copy-bundle", methods=['POST'])
def CopyBundlePost():
AdminPermissionRequired('CopyBundle')
BundleID = request.form.get( key='bundle-id', default=None, type=int )
if BundleID is None:
flash("Invalid request", "error")
return redirect("/admin/copy-bundle")
from app.routes.asset import MigrateBundle, MigrateBundleException
try:
NewBundle : Asset = MigrateBundle(BundleID)
except MigrateBundleException as e:
flash(e.message, "error")
return redirect("/admin/copy-bundle")
except Exception as e:
flash(f"An error occured, {str(e)}", "error")
return redirect("/admin/copy-bundle")
return redirect(f"/admin/manage-assets/{str(NewBundle.id)}")
@AdminRoute.route("/create-asset", methods=['GET'])
def CreateAsset():
AdminPermissionRequired('CreateAsset')
return render_template("admin/createasset.html")
@AdminRoute.route("/create-asset", methods=['POST'])
def CreateAssetPost():
AdminPermissionRequired('CreateAsset')
AssetTypeInput = request.form.get( key='asset-type', default=None, type=int )
AssetName = request.form.get( key='asset-name', default=None, type=str )
AssetFile = request.files.get(key='file', default=None)
if AssetTypeInput is None or AssetName is None or AssetFile is None:
flash("Invalid request", "error")
return redirect("/admin/create-asset")
if AssetTypeInput not in [8,17,18,19,41,42,43,44,45,46,47,62]:
flash("Invalid asset type", "error")
return redirect("/admin/create-asset")
if AssetName == "":
flash("Asset name cannot be empty", "error")
return redirect("/admin/create-asset")
if len(AssetName) > 64:
flash("Asset name is too long", "error")
return redirect("/admin/create-asset")
if AssetFile.content_length > 1024*1024*5:
flash("File is too big", "error")
return redirect("/admin/create-asset")
from app.util.assetvalidation import ValidatePlaceFile
if AssetTypeInput not in [62]:
isValidBinaryFile = ValidatePlaceFile( AssetFile )
if isValidBinaryFile != True:
flash(f"Validation Failed: {isValidBinaryFile}", "error")
return redirect("/admin/create-asset")
AssetTypeInput : AssetType = AssetType(AssetTypeInput)
NewAssetObj : Asset = Asset(
name = AssetName,
creator_id = 1,
creator_type = 0,
asset_type = AssetTypeInput,
created_at = datetime.utcnow(),
updated_at = datetime.utcnow(),
description = "",
moderation_status = 0,
asset_genre = 1
)
db.session.add(NewAssetObj)
db.session.commit()
AssetFile.seek(0)
AssetFileContent = AssetFile.read()
AssetFileHash = hashlib.sha512(AssetFileContent).hexdigest()
s3helper.UploadBytesToS3(AssetFileContent, AssetFileHash)
NewAssetVersion : AssetVersion = assetversion.CreateNewAssetVersion(NewAssetObj, AssetFileHash)
return redirect(f"/admin/manage-assets/{str(NewAssetObj.id)}")
@AdminRoute.route("/moderate-asset", methods=['GET'])
def ModerateAsset():
AdminPermissionRequired('ModerateAsset')
return render_template("admin/moderateUGC.html")
@AdminRoute.route("/moderate-asset/<int:assetid>", methods=['GET'])
def ModerateAssetView( assetid : int ):
AdminPermissionRequired('ModerateAsset')
AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObj is None:
flash("Invalid asset", "error")
return redirect("/admin/moderate-asset")
if AssetObj.creator_id in [1] and AssetObj.creator_type == 0:
if not HasAdminPermission("ManageAsset"):
flash("You are not allowed to manage a Admin owned asset", "error")
return redirect("/admin/moderate-asset")
AllAssetThumbnails : list[AssetThumbnail] = AssetThumbnail.query.filter_by(asset_id=assetid).order_by(AssetThumbnail.id.desc()).all()
AllAssetVersions : list[AssetVersion] = AssetVersion.query.filter_by(asset_id=assetid).order_by(AssetVersion.id.desc()).all()
RelatedAssetsLink : list[AssetModerationLink] = AssetModerationLink.query.filter(or_(AssetModerationLink.ParentAssetId == AssetObj.id, AssetModerationLink.ChildAssetId == AssetObj.id)).all()
if AssetObj.creator_type == 0:
CreatorObj : User = User.query.filter_by(id=AssetObj.creator_id).first()
else:
CreatorObj : Group = Group.query.filter_by(id=AssetObj.creator_id).first()
RelatedAssets : list[Asset] = []
for assetlink in RelatedAssetsLink:
if assetlink.ParentAssetId == AssetObj.id:
RelatedAssets.append(assetlink.ChildAsset)
else:
RelatedAssets.append(assetlink.ParentAsset)
return render_template(
"admin/moderateUGC.html",
AssetObj=AssetObj,
AllAssetThumbnails=AllAssetThumbnails,
AllAssetVersions=AllAssetVersions,
CreatorObj=CreatorObj,
RelatedAssets=RelatedAssets
)
StatusToColor = {
0 : {
"color" : 0x0ef05d,
"name" : "Approved"
},
1 : {
"color" : 0xffc107,
"name" : "Pending Review"
},
2 : {
"color" : 0xff0000,
"name" : "Deleted"
}
}
def LogAssetModerationAction( Actor : User, AssetObj : Asset ):
def thread_func():
EmbedObj = {
"type": "rich",
"title": "Asset Moderation",
"description": f"Asset [{AssetObj.id}](https://www.syntax.eco/admin/moderate-asset/{AssetObj.id}) moderation status has been updated by **{Actor.username}** ({Actor.id})",
"color": StatusToColor[AssetObj.moderation_status]["color"],
"fields": [
{
"name": "Asset Type",
"value": AssetObj.asset_type.name,
"inline": False
},
{
"name": "Asset Moderation Status",
"value": StatusToColor[AssetObj.moderation_status]["name"],
"inline": False
}
],
"author": {
"name": Actor.username,
"icon_url": f"https://www.syntax.eco/Thumbs/Head.ashx?x=48&y=48&userId={str(Actor.id)}"
},
"footer": {
"text": "Syntax Asset Moderation Log"
},
"timestamp": datetime.utcnow().isoformat()
}
try:
requests.post(
url = config.DISCORD_ADMIN_LOGS_WEBHOOK,
json = {
"username": "Syntax Asset Moderation Log",
"embeds" : [EmbedObj],
"avatar_url": f"https://www.syntax.eco/Thumbs/Head.ashx?x=48&y=48&userId={str(Actor.id)}"
}
)
except Exception as e:
logging.warn(f"Admin > LogAssetModerationAction: Exception raised when sending webhook - {str(e)}")
threading.Thread(target=thread_func).start()
@AdminRoute.route("/moderate-asset/<int:assetid>/quick-delete", methods=['POST'])
def ModerateAssetQuickDelete( assetid : int ):
AdminPermissionRequired('ModerateAsset')
AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObj is None:
flash("Invalid asset", "error")
return redirect("/admin/moderate-asset")
if AssetObj.creator_id in [1] and AssetObj.creator_type == 0:
if not HasAdminPermission("ManageAsset"):
flash("You are not allowed to manage a Admin owned asset", "error")
return redirect("/admin/moderate-asset")
if AssetObj.moderation_status == 2:
flash("Asset is already deleted", "error")
return redirect(f"/admin/moderate-asset/{str(assetid)}")
AuthenticatedUser : User = auth.GetCurrentUser()
if redis_controller.exists(f"ModerateAssetCooldown_{AuthenticatedUser.id}") and AuthenticatedUser.id != 1:
flash("You are on cooldown", "error")
return redirect(f"/admin/moderate-asset/{str(assetid)}")
redis_controller.set(f"ModerateAssetCooldown_{AuthenticatedUser.id}", "1", ex=20)
def ContentDeleteAssetObj( AssetObj : Asset ):
AssetObj.moderation_status = 2 # Deleted
AssetObj.name = f"[ Content Deleted {AssetObj.id} ]"
AssetObj.description = ""
AssetObj.updated_at = datetime.utcnow()
AssetObj.is_for_sale = False
AllAssetThumbnails : list[AssetThumbnail] = AssetThumbnail.query.filter_by(asset_id=AssetObj.id).order_by(AssetThumbnail.id.desc()).all()
for assetthumbnail in AllAssetThumbnails:
assetthumbnail.moderation_status = 2
assetthumbnail.updated_at = datetime.utcnow()
if AssetObj.asset_type == AssetType.Place:
PlaceIconObj : PlaceIcon = PlaceIcon.query.filter_by(placeid=AssetObj.id).first()
if PlaceIconObj is not None:
PlaceIconObj.moderation_status = 2
PlaceIconObj.updated_at = datetime.utcnow()
db.session.commit()
ContentDeleteAssetObj(AssetObj)
RelatedAssetsLink : list[AssetModerationLink] = AssetModerationLink.query.filter(or_(AssetModerationLink.ParentAssetId == AssetObj.id, AssetModerationLink.ChildAssetId == AssetObj.id)).all()
for assetlink in RelatedAssetsLink:
if assetlink.ParentAssetId == AssetObj.id:
ContentDeleteAssetObj(assetlink.ChildAsset)
flash(f"Asset [{assetlink.ChildAssetId}] deleted as it was linked.", "success")
else:
ContentDeleteAssetObj(assetlink.ParentAsset)
flash(f"Asset [{assetlink.ParentAssetId}] deleted as it was linked.", "success")
flash("Asset Content deleted", "success")
LogAssetModerationAction(AuthenticatedUser, AssetObj)
return redirect(f"/admin/moderate-asset/{str(assetid)}")
@AdminRoute.route("/moderate-asset/<int:assetid>/quick-approve", methods=['POST'])
def ModerateAssetQuickApporve( assetid : int ):
AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObj is None:
flash("Invalid asset", "error")
return redirect("/admin/moderate-asset")
if AssetObj.creator_id in [1] and AssetObj.creator_type == 0:
if not HasAdminPermission("ManageAsset"):
flash("You are not allowed to manage a Admin owned asset", "error")
return redirect("/admin/moderate-asset")
if AssetObj.moderation_status == 0:
flash("Asset is already approved", "error")
return redirect(f"/admin/moderate-asset/{str(assetid)}")
AuthenticatedUser : User = auth.GetCurrentUser()
if redis_controller.exists(f"ModerateAssetCooldown_{AuthenticatedUser.id}") and AuthenticatedUser.id != 1:
flash("You are on cooldown", "error")
return redirect(f"/admin/moderate-asset/{str(assetid)}")
redis_controller.set(f"ModerateAssetCooldown_{AuthenticatedUser.id}", "1", ex=20)
def AllowContentAssetObj( AssetObj : Asset ):
AssetObj.moderation_status = 0
AssetObj.updated_at = datetime.utcnow()
AllAssetThumbnails : list[AssetThumbnail] = AssetThumbnail.query.filter_by(asset_id=AssetObj.id).order_by(AssetThumbnail.id.desc()).all()
for assetthumbnail in AllAssetThumbnails:
assetthumbnail.moderation_status = 0
assetthumbnail.updated_at = datetime.utcnow()
if AssetObj.asset_type == AssetType.Place:
PlaceIconObj : PlaceIcon = PlaceIcon.query.filter_by(placeid=AssetObj.id).first()
if PlaceIconObj is not None:
PlaceIconObj.moderation_status = 0
PlaceIconObj.updated_at = datetime.utcnow()
db.session.commit()
AllowContentAssetObj(AssetObj)
RelatedAssetsLink : list[AssetModerationLink] = AssetModerationLink.query.filter(or_(AssetModerationLink.ParentAssetId == AssetObj.id, AssetModerationLink.ChildAssetId == AssetObj.id)).all()
for assetlink in RelatedAssetsLink:
if assetlink.ParentAssetId == AssetObj.id:
AllowContentAssetObj(assetlink.ChildAsset)
flash(f"Asset [{assetlink.ChildAssetId}] approved as it was linked.", "success")
else:
AllowContentAssetObj(assetlink.ParentAsset)
flash(f"Asset [{assetlink.ParentAssetId}] approved as it was linked.", "success")
flash("Asset approved", "success")
LogAssetModerationAction(AuthenticatedUser, AssetObj)
return redirect(f"/admin/moderate-asset/{str(assetid)}")
@AdminRoute.route("/moderate-asset/<int:assetid>/quick-pending", methods=['POST'])
def ModerateAssetQuickPending( assetid : int ):
AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
if AssetObj is None:
flash("Invalid asset", "error")
return redirect("/admin/moderate-asset")
if AssetObj.creator_id in [1] and AssetObj.creator_type == 0:
if not HasAdminPermission("ManageAsset"):
flash("You are not allowed to manage a Admin owned asset", "error")
return redirect("/admin/moderate-asset")
if AssetObj.moderation_status == 1:
flash("Asset is already pending", "error")
return redirect(f"/admin/moderate-asset/{str(assetid)}")
AuthenticatedUser : User = auth.GetCurrentUser()
if redis_controller.exists(f"ModerateAssetCooldown_{AuthenticatedUser.id}") and AuthenticatedUser.id != 1:
flash("You are on cooldown", "error")
return redirect(f"/admin/moderate-asset/{str(assetid)}")
redis_controller.set(f"ModerateAssetCooldown_{AuthenticatedUser.id}", "1", ex=20)
def PendingContentAssetObj( AssetObj : Asset ):
AssetObj.moderation_status = 1
AssetObj.updated_at = datetime.utcnow()
AllAssetThumbnails : list[AssetThumbnail] = AssetThumbnail.query.filter_by(asset_id=AssetObj.id).order_by(AssetThumbnail.id.desc()).all()
for assetthumbnail in AllAssetThumbnails:
assetthumbnail.moderation_status = 1
assetthumbnail.updated_at = datetime.utcnow()
if AssetObj.asset_type == AssetType.Place:
PlaceIconObj : PlaceIcon = PlaceIcon.query.filter_by(placeid=AssetObj.id).first()
if PlaceIconObj is not None:
PlaceIconObj.moderation_status = 1
PlaceIconObj.updated_at = datetime.utcnow()
db.session.commit()
PendingContentAssetObj(AssetObj)
RelatedAssetsLink : list[AssetModerationLink] = AssetModerationLink.query.filter(or_(AssetModerationLink.ParentAssetId == AssetObj.id, AssetModerationLink.ChildAssetId == AssetObj.id)).all()
for assetlink in RelatedAssetsLink:
if assetlink.ParentAssetId == AssetObj.id:
PendingContentAssetObj(assetlink.ChildAsset)
flash(f"Asset [{assetlink.ChildAssetId}] pending as it was linked.", "success")
else:
PendingContentAssetObj(assetlink.ParentAsset)
flash(f"Asset [{assetlink.ParentAssetId}] pending as it was linked.", "success")
flash("Asset set to pending", "success")
LogAssetModerationAction(AuthenticatedUser, AssetObj)
return redirect(f"/admin/moderate-asset/{str(assetid)}")
@AdminRoute.route("/moderate-asset", methods=['POST'])
def ModerateAssetSearch():
AdminPermissionRequired('ModerateAsset')
AssetID = request.form.get( key='asset-id', default=None, type=int )
if AssetID is None:
flash("Invalid request", "error")
return redirect("/admin/moderate-asset")
if AssetID < 0:
flash("Invalid request", "error")
return redirect("/admin/moderate-asset")
return redirect(f"/admin/moderate-asset/{str(AssetID)}")
@AdminRoute.route("/lottery", methods=["GET"])
def LotteryIndex():
AdminPermissionRequired('Lottery')
EligibleUsers = User.query.filter(or_( User.accountstatus == 3, User.accountstatus == 4, User.lastonline < datetime.utcnow() - timedelta( days = 31 ) ))
EligibleUsers = EligibleUsers.outerjoin( UserAsset, UserAsset.userid == User.id ).outerjoin( Asset, Asset.id == UserAsset.assetid ).filter( Asset.is_limited == True ).group_by( User.id ).all()
def GetTotalLimiteds( UserObject : User ) -> int:
if redis_controller.exists(f"LotteryLimitedCountCache:{UserObject.id}"):
return int(redis_controller.get(f"LotteryLimitedCountCache:{UserObject.id}"))
TotalLimiteds : int = UserAsset.query.filter_by( userid = UserObject.id ).join( Asset, Asset.id == UserAsset.assetid ).filter( Asset.is_limited == True ).count()
redis_controller.set(f"LotteryLimitedCountCache:{UserObject.id}", str(TotalLimiteds), ex=120)
return TotalLimiteds
return render_template("admin/lottery/index.html", EligibleUsers=EligibleUsers, GetTotalLimiteds=GetTotalLimiteds, InactiveTime = (datetime.utcnow() - timedelta( days = 31 )) )
@AdminRoute.route("/lottery", methods=["POST"])
def LotteryPost():
AdminPermissionRequired('Lottery')
SelectedUser : int = request.form.get( key='selected-user', default=None, type=int )
if SelectedUser is None:
flash("Invalid request", "error")
return redirect("/admin/lottery")
UserObject : User = User.query.filter_by( id = SelectedUser ).first()
if UserObject is None:
flash("Invalid user", "error")
return redirect("/admin/lottery")
if UserObject.accountstatus not in [3,4] and UserObject.lastonline > datetime.utcnow() - timedelta( days = 31 ):
flash("User is not eligible", "error")
return redirect("/admin/lottery")
TotalUsersOnline : int = User.query.filter( User.lastonline > datetime.utcnow() - timedelta( minutes = 2 ) ).count()
if TotalUsersOnline < 10:
flash("Not enough users online", "error")
return redirect("/admin/lottery")
EligibleLimiteds : list[UserAsset] = UserAsset.query.filter_by( userid = UserObject.id ).join( Asset, Asset.id == UserAsset.assetid ).filter( Asset.is_limited == True ).all()
if len(EligibleLimiteds) < 1:
flash("User does not have any limiteds", "error")
return redirect("/admin/lottery")
AuthenticatedUser : User = auth.GetCurrentUser()
PreviousWinner = None
for UserAssetObj in EligibleLimiteds:
RandomWinner : User = User.query.filter(and_(
User.id != UserObject.id,
User.id != AuthenticatedUser.id,
User.id != PreviousWinner,
User.lastonline > datetime.utcnow() - timedelta( minutes = 2 )
)).order_by(func.random()).first()
if RandomWinner is None:
flash("Not enough users online", "error")
return redirect("/admin/lottery")
PreviousWinner = RandomWinner.id
UserAssetObj.userid = RandomWinner.id
UserAssetObj.updated = datetime.utcnow()
CreateSystemMessage(
f"You won a free limited!",
f"""You have been randomly selected to be given a free limited by Syntax.
You have won: {UserAssetObj.asset.name} ( {UserAssetObj.asset.id} ) [ UAID: {UserAssetObj.id} / Serial: { UserAssetObj.serial if UserAssetObj.serial is not None else 'None'} ]
- Syntax Team
""",
RandomWinner.id
)
CreateSystemMessage(
f"Limited Item removed",
f""""Your limited {UserAssetObj.asset.name} ( {UserAssetObj.asset.id} ) [ UAID: {UserAssetObj.id} / Serial: { UserAssetObj.serial if UserAssetObj.serial is not None else 'None'} ] has been removed from your account and was given to another SYNTAX User
This was because you are either:
- Inactive for over 31 days
- Terminated
We apologize for any inconvenience this may have caused.
- Syntax Team
""",
UserObject.id
)
TargetUserAvatarAsset : UserAvatarAsset = UserAvatarAsset.query.filter_by( user_id = UserObject.id, asset_id = UserAssetObj.asset.id ).first()
if TargetUserAvatarAsset is not None:
db.session.delete(TargetUserAvatarAsset)
db.session.commit()
try:
newLimitedTransfer = LimitedItemTransfer(
original_owner_id = UserObject.id,
new_owner_id = RandomWinner.id,
asset_id = UserAssetObj.assetid,
user_asset_id = UserAssetObj.id,
transfer_method = LimitedItemTransferMethod.WonByLottery,
purchased_price = None
)
db.session.add(newLimitedTransfer)
db.session.commit()
except Exception as e:
flash(f"Failed to create LimitedItemTransfer: {str(e)}, but lottery items has still been given away", "error")
TakeUserThumbnail( UserObject.id )
flash("Lottery completed", "success")
return redirect("/admin/lottery")