30 Commits
Author SHA1 Message Date
SquadleDude e0fc73df51 Merge pull request #30 from nexiumfit/squadledude-patch-13
Add files via upload
2026-08-06 10:16:44 +03:00
SquadleDude 92be439e27 Add files via upload 2026-08-06 10:16:07 +03:00
SquadleDude 65a7c846ab Add files via upload 2026-08-06 10:15:04 +03:00
SquadleDude de9aee85be Merge pull request #29 from nexiumfit/squadledude-patch-12
delete long png
2026-08-06 10:13:34 +03:00
SquadleDude fe7a904fa0 delete long png 2026-08-06 10:13:19 +03:00
copyrighttxt baf5a64d35 Update badwords.py 2026-08-05 17:25:08 -04:00
copyrighttxt f969daf1f7 Update badwords.py 2026-08-05 17:21:13 -04:00
copyrighttxt e79463ce20 ye 2026-08-04 18:24:32 -04:00
copyrighttxt cb2bcb56d4 FUcK NIGGERS 2026-08-04 18:05:17 -04:00
copyrighttxt a176948ecb Yeah 2026-08-04 17:57:44 -04:00
copyrighttxt 872a2ffe2b t 2026-08-04 17:52:46 -04:00
copyrighttxt 9d44d1d377 ttest 2026-08-04 17:50:26 -04:00
copyrighttxt 75d3babf27 Update downloads.html 2026-08-04 14:38:59 -04:00
copyrighttxt 2c7f6cc959 Update clientinfo.py 2026-08-04 14:35:15 -04:00
copyrighttxt bc37a1b87e it'll be a long time before you ever see me again 2026-08-03 21:12:29 -04:00
copyrighttxt bb49f6b943 Update start.sh 2026-08-03 18:41:10 -04:00
lx c226ed2da9 killing myself on live 2026-08-03 22:34:17 +00:00
copyrighttxt 296a0ad58a s 2026-08-03 17:51:55 -04:00
copyrighttxt e4c189aec0 s 2026-08-03 17:50:41 -04:00
copyrighttxt cec64d598f Word 2026-08-03 17:49:31 -04:00
copyrighttxt 7ddc6a3346 booty 2026-08-03 17:47:45 -04:00
copyrighttxt 9c8f38ab8c you been putting up with my shit just way too long 2026-08-03 17:44:28 -04:00
copyrighttxt c5a17b1d1b y 2026-08-03 17:43:44 -04:00
copyrighttxt 518ceb0af2 Word? 2026-08-03 17:42:43 -04:00
copyrighttxt 2d341a3029 a 2026-08-03 17:41:13 -04:00
copyrighttxt 9d7913bf21 i also had ai do this btw 2026-08-03 17:40:16 -04:00
copyrighttxt 7c7085c038 i had ai do this btw 2026-08-03 17:38:38 -04:00
copyrighttxt b8c141cde9 Word? 2026-08-03 17:32:42 -04:00
copyrighttxt d2a0d2ba88 ye 2026-08-03 17:26:50 -04:00
SquadleDude a1facc1247 Merge pull request #28 from nexiumfit/patch2/2
Rename NEXIUM.pn to NEXIUM.png
2026-08-03 23:23:09 +03:00
15 changed files with 381 additions and 3765 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+19 -4
View File
@@ -26,7 +26,7 @@
} }
.main-content { .main-content {
margin-top: -50px !important; margin-top: 0 !important;
} }
@@ -88,6 +88,9 @@
{% endif %} {% endif %}
</a> </a>
<a href="/download" class="nav-link"><i class="bi bi-download"></i> Downloads</a> <a href="/download" class="nav-link"><i class="bi bi-download"></i> Downloads</a>
{% if InviteKeysEnabled %}
<a href="/invite-keys" class="nav-link"><i class="bi bi-key-fill"></i> Invite Keys</a>
{% endif %}
<a href="https://discord.gg/REPLACEME" class="nav-link"><i class="bi bi-discord"></i> Discord</a> <a href="https://discord.gg/REPLACEME" class="nav-link"><i class="bi bi-discord"></i> Discord</a>
{% if currentuser.is_admin: %} {% if currentuser.is_admin: %}
<a href="/admin/" class="nav-link"> <a href="/admin/" class="nav-link">
@@ -110,9 +113,9 @@
</a> </a>
</div> </div>
<div class="d-flex align-items-center gap-2"> <div class="navbar-actions d-flex align-items-center gap-2 flex-nowrap">
<div <div
class=" d-inline-flex align-items-center bg-dark text-white rounded-pill px-3 py-1 border shadow-sm gap-3"> class="navbar-currency d-none d-md-inline-flex align-items-center bg-dark text-white rounded-pill px-3 py-1 border shadow-sm gap-3">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="rbx-icon-nav-robux me-1 text-white"></span> <span class="rbx-icon-nav-robux me-1 text-white"></span>
<span class="fw-semibold" id="robux-amount">{{currentuser.robux}}</span> <span class="fw-semibold" id="robux-amount">{{currentuser.robux}}</span>
@@ -125,7 +128,7 @@
</div> </div>
<div <div
class="nav-item dropdown d-inline-flex align-items-center rounded-pill px-3 py-1 bg-dark border shadow-sm"> class="navbar-profile nav-item dropdown d-inline-flex align-items-center rounded-pill px-3 py-1 bg-dark border shadow-sm">
<a class="nav-link dropdown-toggle d-flex align-items-center p-0" href="#" role="button" <a class="nav-link dropdown-toggle d-flex align-items-center p-0" href="#" role="button"
data-bs-toggle="dropdown" aria-expanded="false"> data-bs-toggle="dropdown" aria-expanded="false">
<img class="rounded-circle me-2" width="32" height="32" <img class="rounded-circle me-2" width="32" height="32"
@@ -133,6 +136,18 @@
<b><span class="d-none d-md-inline">{{currentuser.username}}</span></b> <b><span class="d-none d-md-inline">{{currentuser.username}}</span></b>
</a> </a>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li class="d-md-none px-3 py-2">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<span class="rbx-icon-nav-robux me-1 text-dark"></span>
<span class="fw-semibold" id="mobile-robux-amount">{{currentuser.robux}}</span>
</div>
<div class="d-flex align-items-center">
<span class="rbx-icon-nav-tix me-1 text-dark"></span>
<span class="fw-semibold" id="mobile-tix-amount">{{currentuser.tix}}</span>
</div>
</div>
</li>
<li><a class="dropdown-item" href="/settings">Settings</a></li> <li><a class="dropdown-item" href="/settings">Settings</a></li>
<li><a class="dropdown-item" href="/membership">Membership</a></li> <li><a class="dropdown-item" href="/membership">Membership</a></li>
<li><a class="dropdown-item" href="/giftcard-redeem">Redeem Codes</a></li> <li><a class="dropdown-item" href="/giftcard-redeem">Redeem Codes</a></li>
+2 -20
View File
@@ -22,7 +22,7 @@ from config import Config
config = Config() config = Config()
from app.routes.asset import migrateAsset from app.routes.asset import migrateAsset
from app.util import auth, assetversion, discord, redislock, announcement from app.util import auth, assetversion, discord, redislock
from app.services import economy, gameserver_comm from app.services import economy, gameserver_comm
from app.models.admin_permissions import AdminPermissions from app.models.admin_permissions import AdminPermissions
from app.models.user import User from app.models.user import User
@@ -297,25 +297,7 @@ def websitemessage():
def websitemessage_post(): def websitemessage_post():
AdminPermissionRequired('UpdateWebsiteMessage') AdminPermissionRequired('UpdateWebsiteMessage')
raw_message = request.form.get('message', '').strip() redis_controller.set("website_wide_message", request.form['message'])
is_valid, error_msg = announcement.validate_announcement(raw_message)
if not is_valid:
flash(f"Invalid announcement: {error_msg}", "danger")
return redirect("/admin/websitemessage")
sanitized_message = announcement.sanitize_announcement(raw_message)
AuthenticatedUser: User = auth.GetCurrentUser()
announcement.log_announcement_to_discord(
username=AuthenticatedUser.username,
user_id=AuthenticatedUser.id,
message=sanitized_message,
action="updated"
)
redis_controller.set("website_wide_message", sanitized_message)
flash("Announcement updated successfully", "success")
return redirect("/admin/websitemessage") return redirect("/admin/websitemessage")
@AdminRoute.route('/fflags', methods=['GET']) @AdminRoute.route('/fflags', methods=['GET'])
+1 -1
View File
@@ -23,7 +23,7 @@ html[data-theme="dark"] .tux-icon {
</p> </p>
</div> </div>
<div class="ms-auto"> <div class="ms-auto">
<a href="https://clients.orcbin.com/NEXIUMPlayerLauncher.exe" target="_blank" class="text-decoration-none m-0 btn btn-outline-primary btn fw-bold"><i class="bi bi-download me-2"></i>Download</a> <a href="https://clients.orcbin.com/comingsoon.txt" target="_blank" class="text-decoration-none m-0 btn btn-outline-primary btn fw-bold"><i class="bi bi-download me-2"></i>Download</a>
</div> </div>
</div> </div>
<div class="p-3 border d-flex align-items-center mt-3 rounded"> <div class="p-3 border d-flex align-items-center mt-3 rounded">
+44 -32
View File
@@ -4,8 +4,8 @@
<link href="/static/css/home.css" rel="stylesheet"/> <!-- Its just easier to resuse this--> <link href="/static/css/home.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="main"> <div id="main" class="home-page-shell">
<div class="container"> <div class="home-page-container container">
<!-- {% if not isEmailVerified: %} <!-- {% if not isEmailVerified: %}
<div class="alert p-2 alert-warning d-flex align-items-center"> <div class="alert p-2 alert-warning d-flex align-items-center">
<i class="bi bi-envelope-plus ms-2" style="font-size: 30px;"></i> <i class="bi bi-envelope-plus ms-2" style="font-size: 30px;"></i>
@@ -24,30 +24,35 @@
<a href="/settings/discord_link" class="ms-auto text-decoration-none me-2"><button class="btn btn-primary btn-sm">Link Discord</button></a> <a href="/settings/discord_link" class="ms-auto text-decoration-none me-2"><button class="btn btn-primary btn-sm">Link Discord</button></a>
</div> </div>
{%endif%} {%endif%}
<div class="d-flex align-items-center"> <div class="home-hero-subtle d-flex align-items-center">
<img class="rounded-5 overflow-hidden me-4" src="/Thumbs/Head.ashx?x=150&y=150&userId={{currentuser.id}}" style="width:auto;max-height: 130px;"/> <img class="rounded-5 overflow-hidden me-4 home-hero-avatar" src="/Thumbs/Head.ashx?x=150&y=150&userId={{currentuser.id}}" alt="{{currentuser.username}}"/>
<div class="mt-2"> <div class="mt-2">
<h1> <p class="home-kicker mb-1">Welcome back</p>
<h1 class="home-greeting mb-2">
<span id="greetings"></span>, {{currentuser.username}}! <span id="greetings"></span>, {{currentuser.username}}!
</h1> </h1>
{% if membershipValue == 1: %} <div class="home-status-row">
<span class="rbx-icon-bc"></span> {% if membershipValue == 1: %}
{%endif%} <span class="rbx-icon-bc"></span>
{% if membershipValue == 2: %} {%endif%}
<span class="rbx-icon-tbc"></span> {% if membershipValue == 2: %}
{%endif%} <span class="rbx-icon-tbc"></span>
{% if membershipValue == 3: %} {%endif%}
<span class="rbx-icon-negative-obc"></span> {% if membershipValue == 3: %}
{%endif%} <span class="rbx-icon-negative-obc"></span>
{%endif%}
<span class="home-status-text">You should play something!</span>
</div>
</div> </div>
</div> </div>
<h4>Friends ( {{friendcount}} )</h4> <h4 class="home-section-title mt-4">Friends ( {{friendcount}} )</h4>
<div class="w-100 p-2 d-flex card-container" style="overflow-x: hidden;min-height: 100px;"> <p class="home-section-meta mb-2">People you have added.</p>
<div class="w-100 p-2 d-flex flex-nowrap card-container friends-scroll home-soft-strip" style="min-height: 100px;">
{% for friend in friends %} {% for friend in friends %}
<a href="/users/{{friend.id}}/profile"> <a href="/users/{{friend.id}}/profile">
<div style="height: fit-content; width:90px;" class="me-3"> <div style="height: fit-content; width:90px;" class="me-3 home-friend-card">
<div class="image-container"> <div class="image-container home-friend-avatar-frame">
<img class="rounded w-100 d-table" src="/Thumbs/Head.ashx?x=100&y=100&userId={{friend.id}}" style="margin: 0 auto;height: fit-content; max-width: 100px;"/> <img class="rounded w-100 d-table home-friend-avatar" src="/Thumbs/Head.ashx?x=100&y=100&userId={{friend.id}}" style="margin: 0 auto;height: fit-content; max-width: 100px;" alt="{{friend.username}}"/>
{% if friend.isonline: %}<div class="status-icon {% if friend.ingame: %}status-icon-green{% endif %}"></div>{% endif %} {% if friend.isonline: %}<div class="status-icon {% if friend.ingame: %}status-icon-green{% endif %}"></div>{% endif %}
</div> </div>
<p class="text-body w-100 text-center d-inline-block m-0 text-truncate" style="font-size: 12px;">{{friend.username}}</p> <p class="text-body w-100 text-center d-inline-block m-0 text-truncate" style="font-size: 12px;">{{friend.username}}</p>
@@ -55,27 +60,31 @@
</a> </a>
{% endfor %} {% endfor %}
{% if friendcount == 0: %} {% if friendcount == 0: %}
<p style="margin: auto auto;" class="d-table align-items-center d-flex">You have no friends <img width="21" height="21" src="https://media.tenor.com/jeYb8iK3YfsAAAAi/skull-skullgif.gif"></p> <div class="home-empty-state home-empty-inline">
<p class="mb-0">You have no friends yet.</p>
<a href="/games" class="btn btn-sm btn-primary mt-2">Explore games</a>
</div>
{% endif %} {% endif %}
</div> </div>
<h4 class="mt-2">Recently Played</h4> <h4 class="home-section-title mt-4">Recently Played</h4>
<div class="w-100 p-2 d-flex" style="overflow-x: hidden;min-height: 100px;overflow-x: auto;"> <p class="home-section-meta mb-2">Pick up where you left off or try something new.</p>
<div class="w-100 p-2 d-flex home-soft-strip" style="overflow-x: hidden;min-height: 100px;overflow-x: auto;">
{% for place in recentlyplayed %} {% for place in recentlyplayed %}
<a class="text-decoration-none" href="/games/{{place.id}}/"> <a class="text-decoration-none" href="/games/{{place.id}}/">
<div class="me-3 overflow-hidden rounded place-card"> <div class="me-3 overflow-hidden rounded place-card home-place-card">
<div class="position-relative"> <div class="position-relative home-place-thumb-wrap">
<img width="100%" style="aspect-ratio: 1/1;" src="/Thumbs/PlaceIcon.ashx?assetId={{place.id}}&x=150&y=150"> <img class="home-place-thumb" src="/Thumbs/PlaceIcon.ashx?assetId={{place.id}}&x=150&y=150" alt="{{place.name}}">
<div class="position-absolute" style="bottom: 0px;left: 0px;"> <div class="position-absolute" style="bottom: 0px;left: 0px;">
<div class="fw-bold bg-surface text-body" style="font-size: 12px;padding: 3px;border-top-right-radius: 4px;">{{place.placeyear.value}}</div> <div class="fw-bold bg-surface text-body home-place-year">{{place.placeyear.value}}</div>
</div> </div>
</div> </div>
<div class="p-1"> <div class="p-2 home-place-copy">
<h5 class="mb-0" style="font-size: 17px;text-overflow: ellipsis;">{{place.name}}</h5> <h5 class="mb-1 home-place-name" title="{{place.name}}">{{place.name}}</h5>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<p style="margin:0;font-size: 14px;" class="text-secondary"><i class="bi bi-people"></i> {{place.playercount}} <span style="font-size: 13px;">Playing</span></p> <p class="mb-2 text-secondary home-place-playing"><i class="bi bi-people"></i> {{place.playercount}} <span>Playing</span></p>
</div> </div>
<div class="w-100 votePercentageBackground"> <div class="w-100 votePercentageBackground home-place-votes">
<div class="votePercentageFill" style="width: {{place.likePercentage}}%;"></div> <div class="votePercentageFill home-place-vote-fill" data-like-percentage="{{place.likePercentage}}"></div>
<div> <div>
<div class="segment" style="left: 18%;"></div> <div class="segment" style="left: 18%;"></div>
<div class="segment" style="left: 38%;"></div> <div class="segment" style="left: 38%;"></div>
@@ -88,7 +97,10 @@
</a> </a>
{% endfor %} {% endfor %}
{% if recentlyplayedcount == 0: %} {% if recentlyplayedcount == 0: %}
<p style="margin: auto auto;" class="d-table">Looks like you havent played any games yet, maybe you should go <a class="text-decoration-none" href="/games">play</a> one right now!</p> <div class="home-empty-state home-empty-wide home-empty-inline">
<p class="mb-0">Looks like you havent played any games yet.</p>
<a class="btn btn-sm btn-primary mt-2" href="/games">Go and play something!</a>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
+3 -2
View File
@@ -13,7 +13,8 @@ from app.pages.login.login import CreateLoginRecord
config = Config() config = Config()
AuthenticationRoute = Blueprint('authentication', __name__, url_prefix='/') AuthenticationRoute = Blueprint('authentication', __name__, url_prefix='/')
@AuthenticationRoute.route('/Login/Negotiate.ashx', methods=['GET']) @AuthenticationRoute.route('/Login/Negotiate.ashx', methods=['GET', 'POST'])
@csrf.exempt
def loginNegotiate(): def loginNegotiate():
AuthenticationTicket = request.args.get( AuthenticationTicket = request.args.get(
key = 'suggest', key = 'suggest',
@@ -94,7 +95,7 @@ def gameGetCurrentUser():
return "Bad Request", 200 return "Bad Request", 200
return str(AuthenticatedUser.id), 200 return str(AuthenticatedUser.id), 200
@AuthenticationRoute.route('/login/RequestAuth.ashx', methods=['GET']) @AuthenticationRoute.route('/login/RequestAuth.ashx', methods=['GET', 'POST'])
@limiter.limit("6/minute") @limiter.limit("6/minute")
def loginRequestAuth(): def loginRequestAuth():
AuthenticatedUser : User = auth.GetCurrentUser() AuthenticatedUser : User = auth.GetCurrentUser()
+3 -3
View File
@@ -42,17 +42,17 @@ def GetAllowedMD5Hashes():
"data": [ "data": [
"aa",# 2020 "aa",# 2020
"a", "a",
"89ae5307ca8bcd4059fa8b61399b6dba" # 2021 "04cb8a13a2d0eda3b7ae2b4e3984e424" # 2021
] ]
}), 200 }), 200
return jsonify({ return jsonify({
"data": [ "data": [
# 2016 # 2016
"7c4caa74baf2100a2e2b2ba34eb9d69b", "4004fb391db720edcdf520907f4cf663",
"aaa", "aaa",
# 2018, # 2018,
"cb625aa02e932bdeb11ac84001ed5475" "763fade9fabe4fcd64fc1676393c173e"
] ]
}) })
+12 -54
View File
@@ -22,12 +22,9 @@ from app.models.place import Place
from app.models.asset import Asset from app.models.asset import Asset
from app.models.login_records import LoginRecord from app.models.login_records import LoginRecord
from app.models.user_hwid_log import UserHWIDLog from app.models.user_hwid_log import UserHWIDLog
from app.models.user_ban import UserBan
from app.models.admin_permissions import AdminPermissions
from app.models.universe import Universe from app.models.universe import Universe
from app.models.asset_version import AssetVersion from app.models.asset_version import AssetVersion
from app.enums.AssetType import AssetType from app.enums.AssetType import AssetType
from app.enums.BanType import BanType
from app.enums.MembershipType import MembershipType from app.enums.MembershipType import MembershipType
from app.enums.PlaceYear import PlaceYear from app.enums.PlaceYear import PlaceYear
from app.services.gameserver_comm import perform_post from app.services.gameserver_comm import perform_post
@@ -319,48 +316,6 @@ def ReturnPlaceLauncher( message : str, status : int, authenticated_userid : int
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
return response return response
def BanUsersForSharedHwid(hwid: str, triggering_user_id: int | None = None) -> bool:
MatchingHWIDLogs : list[UserHWIDLog] = UserHWIDLog.query.filter_by(hwid=hwid).distinct(UserHWIDLog.user_id).all()
if len(MatchingHWIDLogs) < 2:
return False
MatchingUserIds = {log.user_id for log in MatchingHWIDLogs if log.user_id is not None}
if len(MatchingUserIds) < 2:
return False
for user_id in MatchingUserIds:
UserObj : User | None = User.query.filter_by(id=user_id).first()
if UserObj is None:
continue
if AdminPermissions.query.filter_by(userid=UserObj.id).first() is not None:
return False
for user_id in MatchingUserIds:
UserObj : User | None = User.query.filter_by(id=user_id).first()
if UserObj is None:
continue
if UserObj.accountstatus != 1:
continue
ExistingActiveBan : UserBan | None = UserBan.query.filter_by(userid=UserObj.id, acknowledged=False).order_by(UserBan.id.desc()).first()
if ExistingActiveBan is not None:
continue
BanObj = UserBan(
userid=UserObj.id,
author_userid=1,
reason="Alting is not permitted on NEXIUM, contact us if you believe this is unfair",
ban_type=BanType.Deleted,
moderator_note=f"Automatic HWID alt ban for shared HWID {hwid}",
expires_at=None
)
db.session.add(BanObj)
UserObj.accountstatus = 3
if MatchingUserIds:
db.session.commit()
return True
@GameJoinRoute.route('/game/PlaceLauncher.ashx', methods=['GET', 'POST']) @GameJoinRoute.route('/game/PlaceLauncher.ashx', methods=['GET', 'POST'])
@GameJoinRoute.route('/Game/PlaceLauncher.ashx', methods=['GET', 'POST']) @GameJoinRoute.route('/Game/PlaceLauncher.ashx', methods=['GET', 'POST'])
@GameJoinRoute.route('/game/placelauncher.ashx', methods=['GET', 'POST']) @GameJoinRoute.route('/game/placelauncher.ashx', methods=['GET', 'POST'])
@@ -368,7 +323,7 @@ def BanUsersForSharedHwid(hwid: str, triggering_user_id: int | None = None) -> b
@csrf.exempt @csrf.exempt
def placelauncher(): def placelauncher():
if not websiteFeatures.GetWebsiteFeature("GameJoinAPI"): if not websiteFeatures.GetWebsiteFeature("GameJoinAPI"):
logging.warning("opsec") return ReturnPlaceLauncher("GameJoinAPI is disabled", 12)
AuthenticatdUser = None AuthenticatdUser = None
placeid = request.args.get( key = 'placeId', default = None, type = int) or request.args.get( key = 'placeid', default = None, type = int) placeid = request.args.get( key = 'placeId', default = None, type = int) or request.args.get( key = 'placeid', default = None, type = int)
@@ -391,6 +346,12 @@ def placelauncher():
if authticketInfo is None and AuthenticatdUser is None: if authticketInfo is None and AuthenticatdUser is None:
return ReturnPlaceLauncher("Invalid authentication ticket", 12) return ReturnPlaceLauncher("Invalid authentication ticket", 12)
#UserIPHash = hashlib.md5(get_remote_address().encode("utf-8")).hexdigest()
#LoginRecords : list[LoginRecord] = LoginRecord.query.filter(LoginRecord.ip == UserIPHash).distinct(LoginRecord.userid).all()
#for record in LoginRecords:
# if record.User.accountstatus != 1:
# return ReturnPlaceLauncher("Invalid authentication ticket", 12)
userId = int(authticketInfo) if authticketInfo is not None else AuthenticatdUser.id userId = int(authticketInfo) if authticketInfo is not None else AuthenticatdUser.id
if PlaceServerPlayer.query.filter_by(userid=userId).first() is not None and not isTeleport: if PlaceServerPlayer.query.filter_by(userid=userId).first() is not None and not isTeleport:
CurrentPlaceServerPlayerObj : PlaceServerPlayer = PlaceServerPlayer.query.filter_by(userid=userId).first() CurrentPlaceServerPlayerObj : PlaceServerPlayer = PlaceServerPlayer.query.filter_by(userid=userId).first()
@@ -427,9 +388,6 @@ def placelauncher():
redis_controller.delete(f"hwid:{str(Tracking_Cookie)}") redis_controller.delete(f"hwid:{str(Tracking_Cookie)}")
if BanUsersForSharedHwid(UserHWIDHash, UserObj.id):
return ReturnPlaceLauncher("Invalid session", 12)
UserMembershipStatus : MembershipType = GetUserMembership(UserObj) UserMembershipStatus : MembershipType = GetUserMembership(UserObj)
if UniverseObj.bc_required and UserMembershipStatus == MembershipType.NonBuildersClub: if UniverseObj.bc_required and UserMembershipStatus == MembershipType.NonBuildersClub:
return ReturnPlaceLauncher("Builders Club required", 12) return ReturnPlaceLauncher("Builders Club required", 12)
@@ -461,10 +419,11 @@ def placelauncher():
if PlaceServerObj is False: if PlaceServerObj is False:
logging.info(f"Placelauncher.ashx : {str(placeid)} : {UserObj.username} [{UserObj.id}] : No available place servers found yet") logging.info(f"Placelauncher.ashx : {str(placeid)} : {UserObj.username} [{UserObj.id}] : No available place servers found yet")
return ReturnPlaceLauncher(None, 1, authenticated_userid=userId) return ReturnPlaceLauncher(None, 1, authenticated_userid=userId)
redis_controller.delete(f"authticket:{ticket}")
authenticatedTicketUUID = str(uuid.uuid4()) authenticatedTicketUUID = str(uuid.uuid4())
redis_controller.setex(f"place:{placeid}:ticket:{authenticatedTicketUUID}", 60, json.dumps({"id": userId, "jobid": str(PlaceServerObj.serveruuid)})) redis_controller.setex(f"place:{placeid}:ticket:{authenticatedTicketUUID}", 60, json.dumps({"id": userId, "jobid": str(PlaceServerObj.serveruuid)}))
authticket = ''.join(random.choices(string.ascii_uppercase + string.digits, k=256)) authticket = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(256))
redis_controller.set(f"authticket:{authticket}", userId, 60*10) redis_controller.set(f"authticket:{authticket}", userId, 60*10)
resp = make_response(jsonify({ resp = make_response(jsonify({
"jobId": PlaceServerObj.serveruuid, "jobId": PlaceServerObj.serveruuid,
@@ -612,7 +571,7 @@ def GenerateClientTicket( UserObj : User, JobId : str, CharacterURL : str = None
if TicketVersion == 2: if TicketVersion == 2:
CharacterURL = str(UserObj.id) CharacterURL = str(UserObj.id)
elif TicketVersion == 1: elif TicketVersion == 1:
CharacterURL = Config.BaseURL + "/Asset/CharacterFetch.ashx?userId=" + str(UserObj.id) # f"http://www.nexium.fit/v1.1/avatar-fetch?userId={str(UserObj.id)}&placeId={str(PlaceId)}" CharacterURL = Config.BaseURL + "/Asset/CharacterFetch.ashx?userId=" + str(UserObj.id) # f"http://www.vortexi.cc/v1.1/avatar-fetch?userId={str(UserObj.id)}&placeId={str(PlaceId)}"
elif TicketVersion == 4: elif TicketVersion == 4:
CharacterURL = f"http://www.nexium.fit/v1/avatar-fetch?userId={str(UserObj.id)}&placeId={str(PlaceId)}" CharacterURL = f"http://www.nexium.fit/v1/avatar-fetch?userId={str(UserObj.id)}&placeId={str(PlaceId)}"
@@ -664,8 +623,7 @@ def join():
PlaceObj : Place = Place.query.filter_by(placeid=placeid).first() PlaceObj : Place = Place.query.filter_by(placeid=placeid).first()
AssetObj : Asset = Asset.query.filter_by(id=placeid).first() AssetObj : Asset = Asset.query.filter_by(id=placeid).first()
UniverseObj : Universe = Universe.query.filter_by(id=PlaceObj.parent_universe_id).first() UniverseObj : Universe = Universe.query.filter_by(id=PlaceObj.parent_universe_id).first()
if UniverseObj.place_year == PlaceYear.Sixteen:
return 'Who is bro', 400
ClientTicket = GenerateClientTicket(UserObj, jobid, TicketVersion = 1 if UniverseObj.place_year in [PlaceYear.Sixteen, PlaceYear.Fourteen] else ( 2 if UniverseObj.place_year == PlaceYear.Eighteen else 4), PlaceId = placeid) ClientTicket = GenerateClientTicket(UserObj, jobid, TicketVersion = 1 if UniverseObj.place_year in [PlaceYear.Sixteen, PlaceYear.Fourteen] else ( 2 if UniverseObj.place_year == PlaceYear.Eighteen else 4), PlaceId = placeid)
AuthenticationTicket = auth.CreateToken(UserObj.id, get_remote_address() , (60*60*24) ) AuthenticationTicket = auth.CreateToken(UserObj.id, get_remote_address() , (60*60*24) )
@@ -764,4 +722,4 @@ def join():
expires = datetime.utcnow() + timedelta(days=3), expires = datetime.utcnow() + timedelta(days=3),
domain = f".{config.BaseDomain}" domain = f".{config.BaseDomain}"
) )
return joinResposne return joinResposne
+39 -2
View File
@@ -190,11 +190,25 @@ body {
.navbar .container-fluid { .navbar .container-fluid {
justify-content: space-between; justify-content: space-between;
padding: 0; padding: 0;
flex-wrap: nowrap;
align-items: center;
gap: 0.75rem;
} }
.navbar-brand { .navbar-brand {
display: flex; display: flex;
align-items: center; align-items: center;
min-width: 0;
}
.navbar-actions {
margin-left: auto;
flex-shrink: 0;
}
.navbar-currency,
.navbar-profile {
flex-shrink: 0;
} }
.navbar-brand:hover { .navbar-brand:hover {
@@ -301,7 +315,7 @@ body {
} }
body.has-alert .main-content { body.has-alert .main-content {
padding-top: 0px; padding-top: 30px;
} }
@media (max-width: 992px) { @media (max-width: 992px) {
@@ -326,7 +340,30 @@ body.has-alert .main-content {
} }
body.has-alert .main-content { body.has-alert .main-content {
padding-top: 0px; padding-top: 30px;
}
}
@media (max-width: 767.98px) {
.navbar {
padding-inline: 0.75rem;
}
.navbar-brand {
flex: 1 1 auto;
}
.title-logo {
height: 2rem;
}
.navbar-profile {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.navbar-profile .nav-link {
white-space: nowrap;
} }
} }
+216 -1
View File
@@ -1,7 +1,61 @@
#main { #main {
display: flex; display: flex;
min-height: 100vh; min-height: 100vh;
margin-top: 120px; }
.home-page-shell {
width: 100%;
padding: 1.75rem 1rem 1.75rem;
}
.home-page-container {
width: 100%;
max-width: 980px;
margin: 0 auto;
padding: 0 0.25rem;
}
.home-hero-subtle {
padding: 0.5rem 0 0.75rem;
}
.home-hero-avatar {
width: 76px;
height: 76px;
object-fit: cover;
}
.home-kicker {
color: var(--text-secondary, #9c9c9c);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.68rem;
}
.home-greeting {
font-size: clamp(1.35rem, 2.1vw, 1.85rem);
font-weight: 700;
line-height: 1.1;
word-break: break-word;
}
.home-status-row {
display: flex;
align-items: center;
gap: 0.6rem;
flex-wrap: wrap;
}
.home-status-text,
.home-section-meta {
color: var(--text-secondary, #9c9c9c);
font-size: 0.85rem;
}
.home-section-title {
font-size: 1.05rem;
line-height: 1.1;
margin-bottom: 0;
} }
.card-container { .card-container {
@@ -10,6 +64,117 @@
border-radius: 5px; border-radius: 5px;
} }
.home-soft-strip {
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.home-friends-strip {
display: flex;
gap: 0.75rem;
align-items: flex-start;
min-height: 116px;
}
.home-friend-card {
width: 96px;
flex: 0 0 auto;
text-decoration: none;
}
.home-friend-avatar-frame {
width: 96px;
height: 96px;
}
.home-friend-avatar {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 16px;
}
.home-empty-state {
min-height: 116px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: var(--text-secondary, #9c9c9c);
border: 1px dashed rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1rem;
flex: 1 1 auto;
}
.home-empty-wide {
width: 100%;
}
.home-empty-inline {
margin: 0.25rem 0;
}
.home-place-card {
width: 170px;
flex: 0 0 auto;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.07);
}
.home-place-thumb-wrap {
overflow: hidden;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.home-place-thumb {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
display: block;
}
.home-place-year {
font-size: 11px;
padding: 3px 6px;
border-top-right-radius: 6px;
}
.home-place-copy {
min-height: 90px;
}
.home-place-name {
font-size: 0.95rem;
line-height: 1.15;
line-clamp: 2;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.home-place-playing {
margin: 0;
font-size: 0.84rem;
}
.home-place-playing span {
font-size: 0.8rem;
}
.home-place-votes {
margin-top: auto;
}
.home-places-strip {
display: flex;
gap: 0.75rem;
overflow-x: auto;
padding-bottom: 0.25rem;
}
.status-icon { .status-icon {
position: absolute; position: absolute;
bottom : -8px; bottom : -8px;
@@ -30,4 +195,54 @@
display: table; display: table;
margin: 0 auto; margin: 0 auto;
}
.friends-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
white-space: nowrap;
}
.friends-scroll a {
flex: 0 0 auto;
}
.friends-scroll::-webkit-scrollbar { display: none; }
.friends-scroll { -ms-overflow-style: none; scrollbar-width: none; }
@media (max-width: 768px) {
.home-page-shell {
padding: 1rem 0.75rem 1.5rem;
}
.home-hero-subtle {
align-items: flex-start;
}
.home-hero-avatar {
width: 64px;
height: 64px;
}
.home-kicker {
font-size: 0.63rem;
}
.home-greeting {
font-size: 1.4rem;
}
.home-status-text,
.home-section-meta {
font-size: 0.8rem;
}
.home-section-title {
font-size: 1rem;
}
.home-place-card {
width: 160px;
}
} }
+36 -1
View File
@@ -6,7 +6,42 @@
} }
.settings-container { .settings-container {
min-width: 800px; width: min(800px, 100%);
max-width: 800px;
min-width: 0;
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
} }
@media (max-width: 767.98px) {
#main {
align-items: flex-start;
min-height: auto;
padding-top: 0.5rem;
}
.settings-container {
width: 100%;
padding: 0.75rem;
}
.settings-container .d-flex.align-items-center.mb-2 {
align-items: flex-start !important;
flex-wrap: wrap;
gap: 0.5rem;
}
.settings-container .d-flex.align-items-center.mb-2 > p {
flex: 1 1 100%;
margin-right: 0 !important;
}
.settings-container .d-flex.align-items-center.mb-2 > a,
.settings-container .d-flex.align-items-center.mb-2 > form {
margin-left: 0 !important;
}
.settings-container h1 {
font-size: clamp(1.5rem, 6vw, 2rem);
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 106 KiB

-113
View File
@@ -1,113 +0,0 @@
import re
import logging
import requests
import threading
from datetime import datetime
from config import Config
config = Config()
def sanitize_announcement(message: str) -> str:
if not isinstance(message, str):
return ""
message = message.strip()
if not message:
return ""
message = re.sub(r'<[^>]+>', '', message)
message = re.sub(r'(javascript|data|vbscript):', '', message, flags=re.IGNORECASE)
message = re.sub(r'\s*on\w+\s*=', '', message, flags=re.IGNORECASE)
message = message[:500]
return message
def validate_announcement(message: str) -> tuple[bool, str]:
if not isinstance(message, str):
return False, "Message must be a string"
if len(message.strip()) == 0:
return False, "Message cannot be empty"
if len(message) > 500:
return False, "Message is too long (max 500 characters)"
suspicious_patterns = [
r'<script',
r'javascript:',
r'onerror\s*=',
r'onclick\s*=',
r'onload\s*=',
r'data:text/html',
r'vbscript:',
r'<iframe',
r'<object',
r'<embed',
]
for pattern in suspicious_patterns:
if re.search(pattern, message, re.IGNORECASE):
return False, "Message contains potentially dangerous content"
return True, ""
def log_announcement_to_discord(
username: str,
user_id: int,
message: str,
action: str = "updated",
avatar_url: str = None
) -> bool:
if not config.DISCORD_ADMIN_LOGS_WEBHOOK:
logging.warning("Discord webhook not configured for announcement logging")
return False
if avatar_url is None:
avatar_url = f"https://www.nexium.fit/Thumbs/Head.ashx?x=48&y=48&userId={str(user_id)}"
embed = {
"type": "rich",
"title": f"Website Announcement {action.capitalize()}",
"description": message if message else "(empty message)",
"color": 0x2196F3,
"author": {
"name": username,
"icon_url": avatar_url
},
"footer": {
"text": "NEXIUM - Announcement Logs"
},
"timestamp": datetime.utcnow().isoformat()
}
def send_webhook():
try:
response = requests.post(
url=config.DISCORD_ADMIN_LOGS_WEBHOOK,
json={
"username": "NEXIUM - Announcement Logs",
"embeds": [embed],
"avatar_url": avatar_url
},
timeout=15
)
if response.status_code not in [200, 204]:
logging.warning(
f"Failed to log announcement to Discord: "
f"status={response.status_code}, response={response.text}"
)
return False
return True
except Exception as e:
logging.error(f"Exception while logging announcement to Discord: {str(e)}")
return False
thread = threading.Thread(target=send_webhook)
thread.daemon = True
thread.start()
return True
+5 -3531
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -13,5 +13,5 @@ $$ \ /$$//$$$$$$ |/$$$$$$ |$$$$$$/ /$$$$$$ |$$ \/$$/ $$ |
EOF EOF
echo "Version - $(git rev-parse --short HEAD) (prod)" echo "Version - $(git rev-parse --short HEAD) (prod)"
echo "Running NEXIUM OSS at $(date)" echo "Running NEXIUM at $(date)"
gunicorn -b 0.0.0.0:3003 --preload --workers=8 --threads=20 "app:create_app()" gunicorn -b 0.0.0.0:3003 --preload --workers=8 --threads=20 "app:create_app()"