From 976aa6b560da965687e12e510d0526ac3184d07d Mon Sep 17 00:00:00 2001 From: MEANINGLINK Date: Mon, 15 Apr 2024 01:35:27 +0800 Subject: [PATCH] CHUNI: Add more chunithm frontend features 1. Implemented profile, rating and playlog webpages. 2. Fixed bugs of version change api and name change api. --- core/frontend.py | 10 +- titles/chuni/frontend.py | 195 +++++++++++++++++++-- titles/chuni/schema/profile.py | 23 +++ titles/chuni/schema/score.py | 17 ++ titles/chuni/templates/chuni_header.jinja | 24 +++ titles/chuni/templates/chuni_index.jinja | 185 ++++++++++++++----- titles/chuni/templates/chuni_playlog.jinja | 184 +++++++++++++++++++ titles/chuni/templates/chuni_rating.jinja | 79 +++++++++ titles/chuni/templates/css/chuni_style.css | 195 +++++++++++++++++++++ 9 files changed, 853 insertions(+), 59 deletions(-) create mode 100644 titles/chuni/templates/chuni_header.jinja create mode 100644 titles/chuni/templates/chuni_playlog.jinja create mode 100644 titles/chuni/templates/chuni_rating.jinja create mode 100644 titles/chuni/templates/css/chuni_style.css diff --git a/core/frontend.py b/core/frontend.py index 9a00ca5..382a082 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -44,11 +44,12 @@ class ShopOwner(): self.permissions = perms class UserSession(): - def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7): + def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7, chunithm_ver: int = -1): self.user_id = usr_id self.current_ip = ip self.permissions = perms self.ongeki_version = ongeki_ver + self.chunithm_version = chunithm_ver class FrontendServlet(): def __init__(self, cfg: CoreConfig, config_dir: str) -> None: @@ -213,7 +214,8 @@ class FE_Base(): sesh.user_id = tk['user_id'] sesh.current_ip = tk['current_ip'] sesh.permissions = tk['permissions'] - + sesh.chunithm_version = tk['chunithm_version'] + if sesh.user_id <= 0: self.logger.error("User session failed to validate due to an invalid ID!") return UserSession() @@ -252,12 +254,12 @@ class FE_Base(): if usr_sesh.permissions <= 0 or usr_sesh.permissions > 255: self.logger.error(f"User session failed to validate due to an invalid permission value! {usr_sesh.permissions}") return None - + return usr_sesh def encode_session(self, sesh: UserSession, exp_seconds: int = 86400) -> str: try: - return jwt.encode({ "user_id": sesh.user_id, "current_ip": sesh.current_ip, "permissions": sesh.permissions, "ongeki_version": sesh.ongeki_version, "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds }, b64decode(self.core_config.frontend.secret), algorithm="HS256") + return jwt.encode({ "user_id": sesh.user_id, "current_ip": sesh.current_ip, "permissions": sesh.permissions, "ongeki_version": sesh.ongeki_version, "chunithm_version": sesh.chunithm_version, "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds }, b64decode(self.core_config.frontend.secret), algorithm="HS256") except jwt.InvalidKeyError: self.logger.error("Failed to encode User session because the secret is invalid!") return "" diff --git a/titles/chuni/frontend.py b/titles/chuni/frontend.py index 724d447..b0fa9bc 100644 --- a/titles/chuni/frontend.py +++ b/titles/chuni/frontend.py @@ -1,5 +1,5 @@ from typing import List -from starlette.routing import Route +from starlette.routing import Route, Mount from starlette.requests import Request from starlette.responses import Response, RedirectResponse from os import path @@ -29,7 +29,13 @@ class ChuniFrontend(FE_Base): def get_routes(self) -> List[Route]: return [ Route("/", self.render_GET, methods=['GET']), + Route("/rating", self.render_GET_rating, methods=['GET']), + Mount("/playlog", routes=[ + Route("/", self.render_GET_playlog, methods=['GET']), + Route("/{index}", self.render_GET_playlog, methods=['GET']), + ]), Route("/update.name", self.update_name, methods=['POST']), + Route("/version.change", self.version_change, methods=['POST']), ] async def render_GET(self, request: Request) -> bytes: @@ -39,27 +45,165 @@ class ChuniFrontend(FE_Base): usr_sesh = self.validate_session(request) if not usr_sesh: usr_sesh = UserSession() - - return Response(template.render( - title=f"{self.core_config.server.name} | {self.nav_name}", - game_list=self.environment.globals["game_list"], - sesh=vars(usr_sesh) - ), media_type="text/html; charset=utf-8") + + if usr_sesh.user_id > 0: + versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id) + profile = [] + if versions: + # chunithm_version is -1 means it is not initialized yet, select a default version from existing. + if usr_sesh.chunithm_version < 0: + usr_sesh.chunithm_version = versions[0] + profile = await self.data.profile.get_profile_data(usr_sesh.user_id, usr_sesh.chunithm_version) + + resp = Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + profile=profile, + version_list=ChuniConstants.VERSION_NAMES, + versions=versions, + cur_version=usr_sesh.chunithm_version + ), media_type="text/html; charset=utf-8") + + if usr_sesh.chunithm_version >= 0: + encoded_sesh = self.encode_session(usr_sesh) + resp.set_cookie("DIANA_SESH", encoded_sesh) + return resp + + else: + return RedirectResponse("/gate/", 303) + + async def render_GET_rating(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/chuni/templates/chuni_rating.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + if usr_sesh.chunithm_version < 0: + return RedirectResponse("/game/chuni/", 303) + profile = await self.data.profile.get_profile_data(usr_sesh.user_id, usr_sesh.chunithm_version) + rating = await self.data.profile.get_profile_rating(usr_sesh.user_id, usr_sesh.chunithm_version) + hot_list=[] + base_list=[] + if profile and rating: + song_records = [] + for song in rating: + music_chart = await self.data.static.get_music_chart(usr_sesh.chunithm_version, song.musicId, song.difficultId) + if music_chart: + if (song.score < 800000): + song_rating = 0 + elif (song.score >= 800000 and song.score < 900000): + song_rating = music_chart.level / 2 - 5 + elif (song.score >= 900000 and song.score < 925000): + song_rating = music_chart.level - 5 + elif (song.score >= 925000 and song.score < 975000): + song_rating = music_chart.level - 3 + elif (song.score >= 975000 and song.score < 1000000): + song_rating = (song.score - 975000) / 2500 * 0.1 + music_chart.level + elif (song.score >= 1000000 and song.score < 1005000): + song_rating = (song.score - 1000000) / 1000 * 0.1 + 1 + music_chart.level + elif (song.score >= 1005000 and song.score < 1007500): + song_rating = (song.score - 1005000) / 500 * 0.1 + 1.5 + music_chart.level + elif (song.score >= 1007500 and song.score < 1009000): + song_rating = (song.score - 1007500) / 100 * 0.01 + 2 + music_chart.level + elif (song.score >= 1009000): + song_rating = 2.15 + music_chart.level + song_rating = int(song_rating * 10 ** 2) / 10 ** 2 + song_records.append({ + "difficultId": song.difficultId, + "musicId": song.musicId, + "title": music_chart.title, + "level": music_chart.level, + "score": song.score, + "type": song.type, + "song_rating": song_rating, + }) + hot_list = [obj for obj in song_records if obj["type"] == "userRatingBaseHotList"] + base_list = [obj for obj in song_records if obj["type"] == "userRatingBaseList"] + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + profile=profile, + hot_list=hot_list, + base_list=base_list, + ), media_type="text/html; charset=utf-8") + else: + return RedirectResponse("/gate/", 303) + + async def render_GET_playlog(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/chuni/templates/chuni_playlog.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + if usr_sesh.chunithm_version < 0: + return RedirectResponse("/game/chuni/", 303) + path_index = request.path_params.get('index') + if not path_index or int(path_index) < 1: + index = 0 + else: + index = int(path_index) - 1 # 0 and 1 are 1st page + user_id = usr_sesh.user_id + playlog_count = await self.data.score.get_user_playlogs_count(user_id) + if playlog_count < index * 20 : + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + playlog_count=0 + ), media_type="text/html; charset=utf-8") + playlog = await self.data.score.get_playlogs_limited(user_id, index, 20) + playlog_with_title = [] + for record in playlog: + music_chart = await self.data.static.get_music_chart(usr_sesh.chunithm_version, record.musicId, record.level) + if music_chart: + difficultyNum=music_chart.level + artist=music_chart.artist + title=music_chart.title + else: + difficultyNum=0 + artist="unknown" + title="musicid: " + str(record.musicId) + playlog_with_title.append({ + "raw": record, + "title": title, + "difficultyNum": difficultyNum, + "artist": artist, + }) + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + playlog=playlog_with_title, + playlog_count=playlog_count + ), media_type="text/html; charset=utf-8") + else: + return RedirectResponse("/gate/", 303) async def update_name(self, request: Request) -> bytes: usr_sesh = self.validate_session(request) if not usr_sesh: return RedirectResponse("/gate/", 303) - - new_name: str = request.query_params.get('new_name', '') + + form_data = await request.form() + new_name: str = form_data.get("new_name") new_name_full = "" - + if not new_name: return RedirectResponse("/gate/?e=4", 303) - + if len(new_name) > 8: return RedirectResponse("/gate/?e=8", 303) - + for x in new_name: # FIXME: This will let some invalid characters through atm o = ord(x) try: @@ -72,12 +216,31 @@ class ChuniFrontend(FE_Base): return RedirectResponse("/gate/?e=4", 303) else: new_name_full += x - + except Exception as e: self.logger.error(f"Something went wrong parsing character {o:04X} - {e}") return RedirectResponse("/gate/?e=4", 303) - - if not await self.data.profile.update_name(usr_sesh, new_name_full): + + if not await self.data.profile.update_name(usr_sesh.user_id, new_name_full): return RedirectResponse("/gate/?e=999", 303) - return RedirectResponse("/gate/?s=1", 303) \ No newline at end of file + return RedirectResponse("/game/chuni/?s=1", 303) + + async def version_change(self, request: Request): + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + form_data = await request.form() + chunithm_version = form_data.get("version") + self.logger.info(f"version change to: {chunithm_version}") + if(chunithm_version.isdigit()): + usr_sesh.chunithm_version=int(chunithm_version) + encoded_sesh = self.encode_session(usr_sesh) + self.logger.info(f"Created session with JWT {encoded_sesh}") + resp = RedirectResponse("/game/chuni/", 303) + resp.set_cookie("DIANA_SESH", encoded_sesh) + return resp + else: + return RedirectResponse("/gate/", 303) \ No newline at end of file diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 9864928..2f8bce3 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -757,3 +757,26 @@ class ChuniProfileData(BaseData): return return result.lastrowid + + async def get_profile_rating(self, aime_id: int, version: int) -> Optional[List[Row]]: + sql = select(rating).where(and_( + rating.c.user == aime_id, + rating.c.version <= version, + )) + + result = await self.execute(sql) + if result is None: + self.logger.warning(f"Rating of user {aime_id}, version {version} was None") + return None + return result.fetchall() + + async def get_all_profile_versions(self, aime_id: int) -> Optional[List[Row]]: + sql = select([profile.c.version]).where(profile.c.user == aime_id) + result = await self.execute(sql) + if result is None: + self.logger.warning(f"user {aime_id}, has no profile") + return None + else: + versions_raw = result.fetchall() + versions = [row[0] for row in versions_raw] + return sorted(versions, reverse=True) \ No newline at end of file diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py index 0a6424c..fa95374 100644 --- a/titles/chuni/schema/score.py +++ b/titles/chuni/schema/score.py @@ -190,6 +190,23 @@ class ChuniScoreData(BaseData): return None return result.fetchall() + async def get_playlogs_limited(self, aime_id: int, index: int, count: int) -> Optional[Row]: + sql = select(playlog).where(playlog.c.user == aime_id).order_by(playlog.c.id.desc()).limit(count).offset(index * count) + + result = await self.execute(sql) + if result is None: + self.logger.warning(f" aime_id {aime_id} has no playlog ") + return None + return result.fetchall() + + async def get_user_playlogs_count(self, aime_id: int) -> Optional[Row]: + sql = select(func.count()).where(playlog.c.user == aime_id) + result = await self.execute(sql) + if result is None: + self.logger.warning(f" aime_id {aime_id} has no playlog ") + return None + return result.scalar() + async def put_playlog(self, aime_id: int, playlog_data: Dict, version: int) -> Optional[int]: # Calculate the ROM version that should be inserted into the DB, based on the version of the ggame being inserted # We only need from Version 10 (Plost) and back, as newer versions include romVersion in their upsert diff --git a/titles/chuni/templates/chuni_header.jinja b/titles/chuni/templates/chuni_header.jinja new file mode 100644 index 0000000..76acdc5 --- /dev/null +++ b/titles/chuni/templates/chuni_header.jinja @@ -0,0 +1,24 @@ +
+

Chunithm

+ +
+ \ No newline at end of file diff --git a/titles/chuni/templates/chuni_index.jinja b/titles/chuni/templates/chuni_index.jinja index e310054..69248e8 100644 --- a/titles/chuni/templates/chuni_index.jinja +++ b/titles/chuni/templates/chuni_index.jinja @@ -1,43 +1,150 @@ {% extends "core/templates/index.jinja" %} {% block content %} -

Chunithm

-{% if profile is defined and profile is not none and profile.id > 0 %} - -

Profile for {{ profile.userName }} 

-{% if error is defined %} -{% include "core/templates/widgets/err_banner.jinja" %} -{% endif %} -{% if success is defined and success == 1 %} -
-Update successful -
-{% endif %} -