From f8db1e21494bcd157872083356b5ccbbeef510e7 Mon Sep 17 00:00:00 2001 From: akanyan Date: Mon, 27 May 2024 06:42:08 +0900 Subject: [PATCH 01/14] ongeki: fix clearstatus type --- titles/ongeki/schema/score.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/ongeki/schema/score.py b/titles/ongeki/schema/score.py index f5ab4e1..6867133 100644 --- a/titles/ongeki/schema/score.py +++ b/titles/ongeki/schema/score.py @@ -30,7 +30,7 @@ score_best = Table( Column("isFullCombo", Boolean, nullable=False), Column("isAllBreake", Boolean, nullable=False), Column("isLock", Boolean, nullable=False), - Column("clearStatus", Boolean, nullable=False), + Column("clearStatus", Integer, nullable=False), Column("isStoryWatched", Boolean, nullable=False), Column("platinumScoreMax", Integer), UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"), From df2a4d3074f35c97ece456a017a5be91e8b4267b Mon Sep 17 00:00:00 2001 From: akanyan Date: Wed, 29 May 2024 19:15:22 +0900 Subject: [PATCH 02/14] ongeki: clearstatus db migration --- .../8ad40a6e7be2_ongeki_fix_clearstatus.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py diff --git a/core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py b/core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py new file mode 100644 index 0000000..8b6ec51 --- /dev/null +++ b/core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py @@ -0,0 +1,30 @@ +"""ongeki: fix clearStatus + +Revision ID: 8ad40a6e7be2 +Revises: 7dc13e364e53 +Create Date: 2024-05-29 19:03:30.062157 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '8ad40a6e7be2' +down_revision = '7dc13e364e53' +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column('ongeki_score_best', 'clearStatus', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Integer(), + existing_nullable=False) + + +def downgrade(): + op.alter_column('ongeki_score_best', 'clearStatus', + existing_type=sa.Integer(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False) From d57aa934017c1272bb9f940504bf968a699b8dc0 Mon Sep 17 00:00:00 2001 From: ThatzOkay Date: Thu, 30 May 2024 09:28:07 +0200 Subject: [PATCH 03/14] Fix for diva reader when trying to read modded content. When it can't parse a number. So instead of crashing give a friendly error and continue --- changelog.md | 5 +++++ titles/diva/read.py | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 0e8b39a..3f8c6ba 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,12 @@ # Changelog Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. +## 20240530 +### DIVA ++ Fix reader for when dificulty is not a int + ## 20240526 +### DIVA + Fixed missing awaits causing coroutine error ## 20240524 diff --git a/titles/diva/read.py b/titles/diva/read.py index 97c9481..64603ca 100644 --- a/titles/diva/read.py +++ b/titles/diva/read.py @@ -183,7 +183,11 @@ class DivaReader(BaseReader): pv_list[pv_id] = self.add_branch(pv_list[pv_id], key_args, val) for pv_id, pv_data in pv_list.items(): - song_id = int(pv_id.split("_")[1]) + try: + song_id = int(pv_id.split("_")[1]) + except ValueError: + self.logger.error(f"Invalid song ID format: {pv_id}") + continue if "songinfo" not in pv_data: continue if "illustrator" not in pv_data["songinfo"]: From efd249d808f5091adda58537850829f63790ce2a Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 8 Jun 2024 17:26:51 -0400 Subject: [PATCH 04/14] maimai: some housekeeping --- titles/mai2/base.py | 249 ++++++++++++++++++++++++++++++++++++ titles/mai2/buddies.py | 13 -- titles/mai2/festival.py | 14 +- titles/mai2/festivalplus.py | 19 --- titles/mai2/universe.py | 206 ----------------------------- 5 files changed, 250 insertions(+), 251 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 90597d1..ee46ae9 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -4,6 +4,7 @@ import logging from base64 import b64decode from os import path, stat, remove from PIL import ImageFile +from random import randint import pytz from core.config import CoreConfig @@ -886,3 +887,251 @@ class Mai2Base: self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually") return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'} + + async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: + user_id = data.get("userId", 0) + kind = data.get("kind", 2) # 1 is fav music, 2 is rival user IDs + next_index = data.get("nextIndex", 0) + max_ct = data.get("maxCount", 100) # always 100 + is_all = data.get("isAllFavoriteItem", False) # always false + + """ + class userFavoriteItemList: + orderId: int, never checked + id: int, either song ID for kind 1, or rival user ID for kind 2 + """ + return { + "userId": user_id, + "kind": kind, + "nextIndex": 0, + "userFavoriteItemList": [], + } + + async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: + """ + userRecommendRateMusicIdList: list[int] + """ + return {"userId": data["userId"], "userRecommendRateMusicIdList": []} + + async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: + """ + userRecommendSelectionMusicIdList: list[int] + """ + return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []} + + async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict: + # TODO: Added in 1.41, implement this? + user_id = data["userId"] + version = data.get("version", 1041000) + user_playlog_list = data.get("userPlaylogList", []) + + return { + "userId": user_id, + "itemKind": -1, + "itemId": -1, + } + + # CardMaker support added in Universe + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_detail(data["userId"], self.version) + if p is None: + return {} + + return { + "userName": p["userName"], + "rating": p["playerRating"], + # hardcode lastDataVersion for CardMaker + "lastDataVersion": "1.20.00", # Future versiohs should replace this with the correct version + # checks if the user is still logged in + "isLogin": False, + "isExistSellingCard": True, + } + + async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # user already exists, because the preview checks that already + p = await self.data.profile.get_profile_detail(data["userId"], self.version) + + cards = await self.data.card.get_user_cards(data["userId"]) + if cards is None or len(cards) == 0: + # This should never happen + self.logger.error( + f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" + ) + return {} + + # get the dict representation of the row so we can modify values + user_data = p._asdict() + + # remove the values the game doesn't want + user_data.pop("id") + user_data.pop("user") + user_data.pop("version") + + return {"userId": data["userId"], "userData": user_data} + + async def handle_cm_login_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + async def handle_cm_logout_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: + selling_cards = await self.data.static.get_enabled_cards(self.version) + if selling_cards is None: + return {"length": 0, "sellingCardList": []} + + selling_card_list = [] + for card in selling_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("version") + tmp.pop("cardName") + tmp.pop("enabled") + + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["noticeStartDate"] = datetime.strftime( + tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["noticeEndDate"] = datetime.strftime( + tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT + ) + + selling_card_list.append(tmp) + + return {"length": len(selling_card_list), "sellingCardList": selling_card_list} + + async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = await self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_cards[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = 0 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) + card_list.append(tmp) + + return { + "returnCode": 1, + "length": len(card_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx], + } + + async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + await self.handle_get_user_item_api_request(data) + + async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + characters = await self.data.item.get_characters(data["userId"]) + + chara_list = [] + for chara in characters: + chara_list.append( + { + "characterId": chara["characterId"], + # no clue why those values are even needed + "point": 0, + "count": 0, + "level": chara["level"], + "nextAwake": 0, + "nextAwakePercent": 0, + "favorite": False, + "awakening": chara["awakening"], + "useCount": chara["useCount"], + } + ) + + return { + "returnCode": 1, + "length": len(chara_list), + "userCharacterList": chara_list, + } + + async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + return {"length": 0, "userPrintDetailList": []} + + async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + upsert = data["userPrintDetail"] + + # set a random card serial number + serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) + + # calculate start and end date of the card + start_date = datetime.utcnow() + end_date = datetime.utcnow() + timedelta(days=15) + + user_card = upsert["userCard"] + await self.data.item.put_card( + user_id, + user_card["cardId"], + user_card["cardTypeId"], + user_card["charaId"], + user_card["mapId"], + # add the correct start date and also the end date in 15 days + start_date, + end_date, + ) + + # get the profile extend to save the new bought card + extend = await self.data.profile.get_profile_extend(user_id, self.version) + if extend: + extend = extend._asdict() + # parse the selectedCardList + # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId) + selected_cards: List = extend["selectedCardList"] + + # if no pass is already added, add the corresponding pass + if not user_card["cardTypeId"] in selected_cards: + selected_cards.insert(0, user_card["cardTypeId"]) + + extend["selectedCardList"] = selected_cards + await self.data.profile.put_profile_extend(user_id, self.version, extend) + + # properly format userPrintDetail for the database + upsert.pop("userCard") + upsert.pop("serialId") + upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") + + await self.data.item.put_user_print_detail(user_id, serial_id, upsert) + + return { + "returnCode": 1, + "orderId": 0, + "serialId": serial_id, + "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT), + "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT), + } + + async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": data["userPrintlog"]["serialId"], + } + + async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} diff --git a/titles/mai2/buddies.py b/titles/mai2/buddies.py index f04b215..38049a1 100644 --- a/titles/mai2/buddies.py +++ b/titles/mai2/buddies.py @@ -17,16 +17,3 @@ class Mai2Buddies(Mai2FestivalPlus): # hardcode lastDataVersion for CardMaker user_data["lastDataVersion"] = "1.40.00" return user_data - - async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict: - # TODO: Added in 1.41, implement this? - user_id = data["userId"] - version = data.get("version", 1041000) - user_playlog_list = data.get("userPlaylogList", []) - - return { - "userId": user_id, - "itemKind": -1, - "itemId": -1, - } - diff --git a/titles/mai2/festival.py b/titles/mai2/festival.py index 94ce3ec..451f0ba 100644 --- a/titles/mai2/festival.py +++ b/titles/mai2/festival.py @@ -20,18 +20,6 @@ class Mai2Festival(Mai2UniversePlus): async def handle_user_login_api_request(self, data: Dict) -> Dict: user_login = await super().handle_user_login_api_request(data) - # useless? + # TODO: Make use of this user_login["Bearer"] = "ARTEMiSTOKEN" return user_login - - async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: - """ - userRecommendRateMusicIdList: list[int] - """ - return {"userId": data["userId"], "userRecommendRateMusicIdList": []} - - async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: - """ - userRecommendSelectionMusicIdList: list[int] - """ - return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []} diff --git a/titles/mai2/festivalplus.py b/titles/mai2/festivalplus.py index 375d546..3314e34 100644 --- a/titles/mai2/festivalplus.py +++ b/titles/mai2/festivalplus.py @@ -17,22 +17,3 @@ class Mai2FestivalPlus(Mai2Festival): # hardcode lastDataVersion for CardMaker user_data["lastDataVersion"] = "1.35.00" return user_data - - async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: - user_id = data.get("userId", 0) - kind = data.get("kind", 2) - next_index = data.get("nextIndex", 0) - max_ct = data.get("maxCount", 100) - is_all = data.get("isAllFavoriteItem", False) - - """ - class userFavoriteItemList: - orderId: int - id: int - """ - return { - "userId": user_id, - "kind": kind, - "nextIndex": 0, - "userFavoriteItemList": [], - } diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index 00353d3..8ed0d88 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -1,8 +1,6 @@ from typing import Any, List, Dict from random import randint from datetime import datetime, timedelta -import pytz -import json from core.config import CoreConfig from titles.mai2.splashplus import Mai2SplashPlus @@ -14,207 +12,3 @@ class Mai2Universe(Mai2SplashPlus): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_detail(data["userId"], self.version) - if p is None: - return {} - - return { - "userName": p["userName"], - "rating": p["playerRating"], - # hardcode lastDataVersion for CardMaker - "lastDataVersion": "1.20.00", - # checks if the user is still logged in - "isLogin": False, - "isExistSellingCard": True, - } - - async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: - # user already exists, because the preview checks that already - p = await self.data.profile.get_profile_detail(data["userId"], self.version) - - cards = await self.data.card.get_user_cards(data["userId"]) - if cards is None or len(cards) == 0: - # This should never happen - self.logger.error( - f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" - ) - return {} - - # get the dict representation of the row so we can modify values - user_data = p._asdict() - - # remove the values the game doesn't want - user_data.pop("id") - user_data.pop("user") - user_data.pop("version") - - return {"userId": data["userId"], "userData": user_data} - - async def handle_cm_login_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - - async def handle_cm_logout_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - - async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: - selling_cards = await self.data.static.get_enabled_cards(self.version) - if selling_cards is None: - return {"length": 0, "sellingCardList": []} - - selling_card_list = [] - for card in selling_cards: - tmp = card._asdict() - tmp.pop("id") - tmp.pop("version") - tmp.pop("cardName") - tmp.pop("enabled") - - tmp["startDate"] = datetime.strftime( - tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["endDate"] = datetime.strftime( - tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["noticeStartDate"] = datetime.strftime( - tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["noticeEndDate"] = datetime.strftime( - tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT - ) - - selling_card_list.append(tmp) - - return {"length": len(selling_card_list), "sellingCardList": selling_card_list} - - async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = await self.data.item.get_cards(data["userId"]) - if user_cards is None: - return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} - - max_ct = data["maxCount"] - next_idx = data["nextIndex"] - start_idx = next_idx - end_idx = max_ct + start_idx - - if len(user_cards[start_idx:]) > max_ct: - next_idx += max_ct - else: - next_idx = 0 - - card_list = [] - for card in user_cards: - tmp = card._asdict() - tmp.pop("id") - tmp.pop("user") - - tmp["startDate"] = datetime.strftime( - tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["endDate"] = datetime.strftime( - tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT - ) - card_list.append(tmp) - - return { - "returnCode": 1, - "length": len(card_list[start_idx:end_idx]), - "nextIndex": next_idx, - "userCardList": card_list[start_idx:end_idx], - } - - async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: - await super().handle_get_user_item_api_request(data) - - async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: - characters = await self.data.item.get_characters(data["userId"]) - - chara_list = [] - for chara in characters: - chara_list.append( - { - "characterId": chara["characterId"], - # no clue why those values are even needed - "point": 0, - "count": 0, - "level": chara["level"], - "nextAwake": 0, - "nextAwakePercent": 0, - "favorite": False, - "awakening": chara["awakening"], - "useCount": chara["useCount"], - } - ) - - return { - "returnCode": 1, - "length": len(chara_list), - "userCharacterList": chara_list, - } - - async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: - return {"length": 0, "userPrintDetailList": []} - - async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: - user_id = data["userId"] - upsert = data["userPrintDetail"] - - # set a random card serial number - serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) - - # calculate start and end date of the card - start_date = datetime.utcnow() - end_date = datetime.utcnow() + timedelta(days=15) - - user_card = upsert["userCard"] - await self.data.item.put_card( - user_id, - user_card["cardId"], - user_card["cardTypeId"], - user_card["charaId"], - user_card["mapId"], - # add the correct start date and also the end date in 15 days - start_date, - end_date, - ) - - # get the profile extend to save the new bought card - extend = await self.data.profile.get_profile_extend(user_id, self.version) - if extend: - extend = extend._asdict() - # parse the selectedCardList - # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId) - selected_cards: List = extend["selectedCardList"] - - # if no pass is already added, add the corresponding pass - if not user_card["cardTypeId"] in selected_cards: - selected_cards.insert(0, user_card["cardTypeId"]) - - extend["selectedCardList"] = selected_cards - await self.data.profile.put_profile_extend(user_id, self.version, extend) - - # properly format userPrintDetail for the database - upsert.pop("userCard") - upsert.pop("serialId") - upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") - - await self.data.item.put_user_print_detail(user_id, serial_id, upsert) - - return { - "returnCode": 1, - "orderId": 0, - "serialId": serial_id, - "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT), - "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT), - } - - async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: - return { - "returnCode": 1, - "orderId": 0, - "serialId": data["userPrintlog"]["serialId"], - } - - async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} From f94d22ab0dc5af97d55cd4ce1713efb19e29acb7 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 8 Jun 2024 19:04:27 -0400 Subject: [PATCH 05/14] mai2: add tables for rivals and favorite music --- .../4a02e623e5e6_mai2_add_favs_rivals.py | 48 ++++++++++++++ .../81e44dd6047a_mai2_buddies_support.py | 2 +- titles/mai2/base.py | 25 ++++++-- titles/mai2/schema/item.py | 38 +++++++++++ titles/mai2/schema/profile.py | 64 +++++++++++++++++-- 5 files changed, 165 insertions(+), 12 deletions(-) create mode 100644 core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py diff --git a/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py b/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py new file mode 100644 index 0000000..d221bb6 --- /dev/null +++ b/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py @@ -0,0 +1,48 @@ +"""mai2_add_favs_rivals + +Revision ID: 4a02e623e5e6 +Revises: 8ad40a6e7be2 +Create Date: 2024-06-08 19:02:43.856395 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '4a02e623e5e6' +down_revision = '8ad40a6e7be2' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('mai2_item_favorite_music', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user', sa.Integer(), nullable=False), + sa.Column('musicId', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'musicId', name='mai2_item_favorite_music_uk'), + mysql_charset='utf8mb4' + ) + op.create_table('mai2_user_rival', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user', sa.Integer(), nullable=False), + sa.Column('rival', sa.Integer(), nullable=False), + sa.Column('show', sa.Boolean(), server_default='0', nullable=False), + sa.ForeignKeyConstraint(['rival'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'rival', name='mai2_user_rival_uk'), + mysql_charset='utf8mb4' + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('mai2_user_rival') + op.drop_table('mai2_item_favorite_music') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py b/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py index 04d2217..0a2267c 100644 --- a/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py +++ b/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py @@ -1,7 +1,7 @@ """mai2_buddies_support Revision ID: 81e44dd6047a -Revises: d8950c7ce2fc +Revises: 6a7e8277763b Create Date: 2024-03-12 19:10:37.063907 """ diff --git a/titles/mai2/base.py b/titles/mai2/base.py index ee46ae9..b2386c1 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -890,21 +890,32 @@ class Mai2Base: async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: user_id = data.get("userId", 0) - kind = data.get("kind", 2) # 1 is fav music, 2 is rival user IDs + kind = data.get("kind", 0) # 1 is fav music, 2 is rival user IDs next_index = data.get("nextIndex", 0) max_ct = data.get("maxCount", 100) # always 100 is_all = data.get("isAllFavoriteItem", False) # always false + id_list: List[Dict] = [] + + if user_id: + if kind == 1: + fav_music = await self.data.item.get_fav_music(user_id) + if fav_music: + for fav in fav_music: + id_list.append({"orderId": 0, "id": fav["musicId"]}) + if len(id_list) >= 100: # Lazy but whatever + break + + elif kind == 2: + rivals = await self.data.profile.get_rivals_game(user_id) + if rivals: + for rival in rivals: + id_list.append({"orderId": 0, "id": rival["rival"]}) - """ - class userFavoriteItemList: - orderId: int, never checked - id: int, either song ID for kind 1, or rival user ID for kind 2 - """ return { "userId": user_id, "kind": kind, "nextIndex": 0, - "userFavoriteItemList": [], + "userFavoriteItemList": id_list, } async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 9aaf592..f22cccd 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -134,6 +134,20 @@ favorite = Table( mysql_charset="utf8mb4", ) +fav_music = Table( + "mai2_item_favorite_music", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("musicId", Integer, nullable=False), + UniqueConstraint("user", "musicId", name="mai2_item_favorite_music_uk"), + mysql_charset="utf8mb4", +) + charge = Table( "mai2_item_charge", metadata, @@ -451,6 +465,30 @@ class Mai2ItemData(BaseData): return None return result.fetchall() + async def get_fav_music(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(fav_music.select(fav_music.c.user == user_id)) + if result: + return result.fetchall() + + async def add_fav_music(self, user_id: int, music_id: int) -> Optional[int]: + sql = insert(fav_music).values( + user = user_id, + musicId = music_id + ) + + conflict = sql.on_duplicate_key_do_nothing() + + result = await self.execute(conflict) + if result: + return result.lastrowid + + self.logger.error(f"Failed to add music {music_id} as favorite for user {user_id}!") + + async def remove_fav_music(self, user_id: int, music_id: int) -> None: + result = await self.execute(fav_music.delete(and_(fav_music.c.user == user_id, fav_music.c.musicId == music_id))) + if not result: + self.logger.error(f"Failed to remove music {music_id} as favorite for user {user_id}!") + async def put_card( self, user_id: int, diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 8f1d5f3..97dcaad 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -187,14 +187,14 @@ ghost = Table( Column("shopId", Integer), Column("regionCode", Integer), Column("typeId", Integer), - Column("musicId", Integer), + Column("rival", Integer), Column("difficulty", Integer), Column("version", Integer), Column("resultBitList", JSON), Column("resultNum", Integer), Column("achievement", Integer), UniqueConstraint( - "user", "version", "musicId", "difficulty", name="mai2_profile_ghost_uk" + "user", "version", "rival", "difficulty", name="mai2_profile_ghost_uk" ), mysql_charset="utf8mb4", ) @@ -209,7 +209,7 @@ extend = Table( nullable=False, ), Column("version", Integer, nullable=False), - Column("selectMusicId", Integer), + Column("selectrival", Integer), Column("selectDifficultyId", Integer), Column("categoryIndex", Integer), Column("musicIndex", Integer), @@ -239,7 +239,7 @@ option = Table( nullable=False, ), Column("version", Integer, nullable=False), - Column("selectMusicId", Integer), + Column("selectrival", Integer), Column("optionKind", Integer), Column("noteSpeed", Integer), Column("slideSpeed", Integer), @@ -491,6 +491,24 @@ consec_logins = Table( mysql_charset="utf8mb4", ) +rival = Table( + "mai2_user_rival", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column( + "rival", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("show", Boolean, nullable=False, server_default="0"), + UniqueConstraint("user", "rival", name="mai2_user_rival_uk"), + mysql_charset="utf8mb4", +) class Mai2ProfileData(BaseData): async def put_profile_detail( @@ -843,3 +861,41 @@ class Mai2ProfileData(BaseData): if result is None: return None return result.fetchone() + + async def get_rivals(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(rival.select(rival.c.user == user_id)) + if result: + return result.fetchall() + + async def get_rivals_game(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(rival.select(and_(rival.c.user == user_id, rival.c.show == True)).limit(3)) + if result: + return result.fetchall() + + async def set_rival_shown(self, user_id: int, rival_id: int, is_shown: bool) -> None: + sql = rival.update(and_(rival.c.user == user_id, rival.c.rival == rival_id)).values( + show = is_shown + ) + + result = await self.execute(sql) + if not result: + self.logger.error(f"Failed to set rival {rival_id} shown status to {is_shown} for user {user_id}") + + async def add_rival(self, user_id: int, rival_id: int) -> Optional[int]: + sql = insert(rival).values( + user = user_id, + rival = rival_id + ) + + conflict = sql.on_duplicate_key_do_nothing() + + result = await self.execute(conflict) + if result: + return result.lastrowid + + self.logger.error(f"Failed to add music {rival_id} as favorite for user {user_id}!") + + async def remove_rival(self, user_id: int, rival_id: int) -> None: + result = await self.execute(rival.delete(and_(rival.c.user == user_id, rival.c.rival == rival_id))) + if not result: + self.logger.error(f"Failed to remove rival {rival_id} for user {user_id}!") From 8e6e5ea903090021e229684d64af7c433b3c0e6d Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 8 Jun 2024 19:10:24 -0400 Subject: [PATCH 06/14] chuni: fix frontend if no chunithm profiles are loaded --- titles/chuni/templates/chuni_index.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/chuni/templates/chuni_index.jinja b/titles/chuni/templates/chuni_index.jinja index 69248e8..1854a89 100644 --- a/titles/chuni/templates/chuni_index.jinja +++ b/titles/chuni/templates/chuni_index.jinja @@ -5,7 +5,7 @@
{% include 'titles/chuni/templates/chuni_header.jinja' %} - {% if profile is defined and profile is not none and profile.id > 0 %} + {% if profile is defined and profile is not none and profile|length > 0 %}
From 79f511c83794d65d06bcd531c5c0074f52bce7b4 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 8 Jun 2024 19:18:15 -0400 Subject: [PATCH 07/14] frontend: add username login --- core/data/schema/card.py | 2 +- core/data/schema/user.py | 4 ++++ core/frontend.py | 8 ++++++-- core/templates/gate/gate.jinja | 12 ++++++------ 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/core/data/schema/card.py b/core/data/schema/card.py index c31e1b2..cd5c647 100644 --- a/core/data/schema/card.py +++ b/core/data/schema/card.py @@ -121,7 +121,7 @@ class CardData(BaseData): result = await self.execute(sql) if result is None: self.logger.warn(f"Failed to update last login time for {access_code}") - + def to_access_code(self, luid: str) -> str: """ Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string diff --git a/core/data/schema/user.py b/core/data/schema/user.py index 5f1ccf2..8c3695c 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -120,3 +120,7 @@ class UserData(BaseData): result = await self.execute(sql) return result is not None + + async def get_user_by_username(self, username: str) -> Optional[Row]: + result = await self.execute(aime_user.select(aime_user.c.username == username)) + if result: return result.fetchone() diff --git a/core/frontend.py b/core/frontend.py index bb3e9aa..b9d1604 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -308,8 +308,12 @@ class FE_Gate(FE_Base): uid = await self.data.card.get_user_id_from_card(access_code) if uid is None: - self.logger.debug(f"Failed to find user for card {access_code}") - return RedirectResponse("/gate/?e=1", 303) + user = await self.data.user.get_user_by_username(access_code) # Lookup as username + if not user: + self.logger.debug(f"Failed to find user for card/username {access_code}") + return RedirectResponse("/gate/?e=1", 303) + + uid = user['id'] user = await self.data.user.get_user(uid) if user is None: diff --git a/core/templates/gate/gate.jinja b/core/templates/gate/gate.jinja index ca3e2eb..d398cbd 100644 --- a/core/templates/gate/gate.jinja +++ b/core/templates/gate/gate.jinja @@ -15,18 +15,18 @@ -moz-appearance: textfield; } -
+
-
- +
+ +
20 Digit access code from a card registered to your account, or your account username. (NOT your username from a game!)

- + +
Leave blank if registering for the webui. Your card must have been used on a game connected to this server to register.

-
*To register for the webui, type in the access code of your card, as shown in a game, and leave the password field blank.
-
*If you have not registered a card with this server, you cannot create a webui account.
{% endblock content %} \ No newline at end of file From 123ec99a97d4f7045a082a01d9a15b2e27c6f9de Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 8 Jun 2024 20:55:41 -0400 Subject: [PATCH 08/14] mai2: fix aggressive find and replace --- titles/mai2/schema/profile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 97dcaad..d45ee16 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -187,14 +187,14 @@ ghost = Table( Column("shopId", Integer), Column("regionCode", Integer), Column("typeId", Integer), - Column("rival", Integer), + Column("musicId", Integer), Column("difficulty", Integer), Column("version", Integer), Column("resultBitList", JSON), Column("resultNum", Integer), Column("achievement", Integer), UniqueConstraint( - "user", "version", "rival", "difficulty", name="mai2_profile_ghost_uk" + "user", "version", "musicId", "difficulty", name="mai2_profile_ghost_uk" ), mysql_charset="utf8mb4", ) @@ -209,7 +209,7 @@ extend = Table( nullable=False, ), Column("version", Integer, nullable=False), - Column("selectrival", Integer), + Column("selectMusicId", Integer), Column("selectDifficultyId", Integer), Column("categoryIndex", Integer), Column("musicIndex", Integer), @@ -239,7 +239,7 @@ option = Table( nullable=False, ), Column("version", Integer, nullable=False), - Column("selectrival", Integer), + Column("selectMusicId", Integer), Column("optionKind", Integer), Column("noteSpeed", Integer), Column("slideSpeed", Integer), From eccbd1ad812a02a1e193a6186b7fbeab879554b7 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 8 Jun 2024 21:25:48 -0400 Subject: [PATCH 09/14] mai2: add rivals support --- titles/mai2/base.py | 58 +++++++++++++++++++++++++++++++++++++ titles/mai2/schema/score.py | 8 ++--- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index b2386c1..974c94c 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -942,6 +942,64 @@ class Mai2Base: "itemId": -1, } + async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + user_id = data.get("userId", 0) + rival_id = data.get("rivalId", 0) + + if not user_id or not rival_id: return {} + + rival_pf = await self.data.profile.get_profile_detail(rival_id) + if not rival_pf: return {} + + return { + "userId": user_id, + "userRivalData": { + "rivalId": rival_id, + "rivalName": rival_pf['userName'] + } + } + + async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + user_id = data.get("userId", 0) + rival_id = data.get("rivalId", 0) + next_index = data.get("nextIndex", 0) + max_ct = 100 + upper_lim = next_index + max_ct + rival_music_list: Dict[int, List] = {} + + songs = await self.data.score.get_best_scores(rival_id) + if songs is None: + self.logger.debug("handle_get_user_rival_music_api_request: get_best_scores returned None!") + return { + "userId": user_id, + "rivalId": rival_id, + "nextIndex": 0, + "userRivalMusicList": [] # musicId userRivalMusicDetailList -> level achievement deluxscoreMax + } + + num_user_songs = len(songs) + + for x in range(next_index, upper_lim): + + tmp = songs[x]._asdict() + if tmp['musicId'] in rival_music_list: + rival_music_list[tmp['musicId']].append([{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}]) + + else: + if len(rival_music_list) >= max_ct: + break + rival_music_list[tmp['musicId']] = [{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}] + + next_index = 0 if len(rival_music_list) < max_ct or num_user_songs == upper_lim else upper_lim + self.logger.info(f"Send rival {rival_id} songs {next_index}-{upper_lim} ({len(rival_music_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})") + + return { + "userId": user_id, + "rivalId": rival_id, + "nextIndex": next_index, + "userRivalMusicList": [{"musicId": x, "userRivalMusicDetailList": y} for x, y in rival_music_list.items()] + } + # CardMaker support added in Universe async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: p = await self.data.profile.get_profile_detail(data["userId"], self.version) diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index d13faae..59b61b6 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -319,16 +319,16 @@ class Mai2ScoreData(BaseData): sql = best_score.select( and_( best_score.c.user == user_id, - (best_score.c.song_id == song_id) if song_id is not None else True, + (best_score.c.musicId == song_id) if song_id is not None else True, ) - ) + ).order_by(best_score.c.musicId).order_by(best_score.c.level) else: sql = best_score_old.select( and_( best_score_old.c.user == user_id, - (best_score_old.c.song_id == song_id) if song_id is not None else True, + (best_score_old.c.musicId == song_id) if song_id is not None else True, ) - ) + ).order_by(best_score.c.musicId).order_by(best_score.c.level) result = await self.execute(sql) if result is None: From 8b03f1a4f13487779f4b6036a5ca02e950ddbf5d Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 8 Jun 2024 22:02:31 -0400 Subject: [PATCH 10/14] mai2: fix rival data load failing due to inheritance --- titles/mai2/base.py | 275 ----------------------------------------- titles/mai2/dx.py | 292 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 269 insertions(+), 298 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 974c94c..4d31213 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -929,278 +929,3 @@ class Mai2Base: userRecommendSelectionMusicIdList: list[int] """ return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []} - - async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict: - # TODO: Added in 1.41, implement this? - user_id = data["userId"] - version = data.get("version", 1041000) - user_playlog_list = data.get("userPlaylogList", []) - - return { - "userId": user_id, - "itemKind": -1, - "itemId": -1, - } - - async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: - user_id = data.get("userId", 0) - rival_id = data.get("rivalId", 0) - - if not user_id or not rival_id: return {} - - rival_pf = await self.data.profile.get_profile_detail(rival_id) - if not rival_pf: return {} - - return { - "userId": user_id, - "userRivalData": { - "rivalId": rival_id, - "rivalName": rival_pf['userName'] - } - } - - async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: - user_id = data.get("userId", 0) - rival_id = data.get("rivalId", 0) - next_index = data.get("nextIndex", 0) - max_ct = 100 - upper_lim = next_index + max_ct - rival_music_list: Dict[int, List] = {} - - songs = await self.data.score.get_best_scores(rival_id) - if songs is None: - self.logger.debug("handle_get_user_rival_music_api_request: get_best_scores returned None!") - return { - "userId": user_id, - "rivalId": rival_id, - "nextIndex": 0, - "userRivalMusicList": [] # musicId userRivalMusicDetailList -> level achievement deluxscoreMax - } - - num_user_songs = len(songs) - - for x in range(next_index, upper_lim): - - tmp = songs[x]._asdict() - if tmp['musicId'] in rival_music_list: - rival_music_list[tmp['musicId']].append([{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}]) - - else: - if len(rival_music_list) >= max_ct: - break - rival_music_list[tmp['musicId']] = [{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}] - - next_index = 0 if len(rival_music_list) < max_ct or num_user_songs == upper_lim else upper_lim - self.logger.info(f"Send rival {rival_id} songs {next_index}-{upper_lim} ({len(rival_music_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})") - - return { - "userId": user_id, - "rivalId": rival_id, - "nextIndex": next_index, - "userRivalMusicList": [{"musicId": x, "userRivalMusicDetailList": y} for x, y in rival_music_list.items()] - } - - # CardMaker support added in Universe - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_detail(data["userId"], self.version) - if p is None: - return {} - - return { - "userName": p["userName"], - "rating": p["playerRating"], - # hardcode lastDataVersion for CardMaker - "lastDataVersion": "1.20.00", # Future versiohs should replace this with the correct version - # checks if the user is still logged in - "isLogin": False, - "isExistSellingCard": True, - } - - async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: - # user already exists, because the preview checks that already - p = await self.data.profile.get_profile_detail(data["userId"], self.version) - - cards = await self.data.card.get_user_cards(data["userId"]) - if cards is None or len(cards) == 0: - # This should never happen - self.logger.error( - f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" - ) - return {} - - # get the dict representation of the row so we can modify values - user_data = p._asdict() - - # remove the values the game doesn't want - user_data.pop("id") - user_data.pop("user") - user_data.pop("version") - - return {"userId": data["userId"], "userData": user_data} - - async def handle_cm_login_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - - async def handle_cm_logout_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - - async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: - selling_cards = await self.data.static.get_enabled_cards(self.version) - if selling_cards is None: - return {"length": 0, "sellingCardList": []} - - selling_card_list = [] - for card in selling_cards: - tmp = card._asdict() - tmp.pop("id") - tmp.pop("version") - tmp.pop("cardName") - tmp.pop("enabled") - - tmp["startDate"] = datetime.strftime( - tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["endDate"] = datetime.strftime( - tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["noticeStartDate"] = datetime.strftime( - tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["noticeEndDate"] = datetime.strftime( - tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT - ) - - selling_card_list.append(tmp) - - return {"length": len(selling_card_list), "sellingCardList": selling_card_list} - - async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = await self.data.item.get_cards(data["userId"]) - if user_cards is None: - return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} - - max_ct = data["maxCount"] - next_idx = data["nextIndex"] - start_idx = next_idx - end_idx = max_ct + start_idx - - if len(user_cards[start_idx:]) > max_ct: - next_idx += max_ct - else: - next_idx = 0 - - card_list = [] - for card in user_cards: - tmp = card._asdict() - tmp.pop("id") - tmp.pop("user") - - tmp["startDate"] = datetime.strftime( - tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["endDate"] = datetime.strftime( - tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT - ) - card_list.append(tmp) - - return { - "returnCode": 1, - "length": len(card_list[start_idx:end_idx]), - "nextIndex": next_idx, - "userCardList": card_list[start_idx:end_idx], - } - - async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: - await self.handle_get_user_item_api_request(data) - - async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: - characters = await self.data.item.get_characters(data["userId"]) - - chara_list = [] - for chara in characters: - chara_list.append( - { - "characterId": chara["characterId"], - # no clue why those values are even needed - "point": 0, - "count": 0, - "level": chara["level"], - "nextAwake": 0, - "nextAwakePercent": 0, - "favorite": False, - "awakening": chara["awakening"], - "useCount": chara["useCount"], - } - ) - - return { - "returnCode": 1, - "length": len(chara_list), - "userCharacterList": chara_list, - } - - async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: - return {"length": 0, "userPrintDetailList": []} - - async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: - user_id = data["userId"] - upsert = data["userPrintDetail"] - - # set a random card serial number - serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) - - # calculate start and end date of the card - start_date = datetime.utcnow() - end_date = datetime.utcnow() + timedelta(days=15) - - user_card = upsert["userCard"] - await self.data.item.put_card( - user_id, - user_card["cardId"], - user_card["cardTypeId"], - user_card["charaId"], - user_card["mapId"], - # add the correct start date and also the end date in 15 days - start_date, - end_date, - ) - - # get the profile extend to save the new bought card - extend = await self.data.profile.get_profile_extend(user_id, self.version) - if extend: - extend = extend._asdict() - # parse the selectedCardList - # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId) - selected_cards: List = extend["selectedCardList"] - - # if no pass is already added, add the corresponding pass - if not user_card["cardTypeId"] in selected_cards: - selected_cards.insert(0, user_card["cardTypeId"]) - - extend["selectedCardList"] = selected_cards - await self.data.profile.put_profile_extend(user_id, self.version, extend) - - # properly format userPrintDetail for the database - upsert.pop("userCard") - upsert.pop("serialId") - upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") - - await self.data.item.put_user_print_detail(user_id, serial_id, upsert) - - return { - "returnCode": 1, - "orderId": 0, - "serialId": serial_id, - "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT), - "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT), - } - - async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: - return { - "returnCode": 1, - "orderId": 0, - "serialId": data["userPrintlog"]["serialId"], - } - - async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 4423824..2c7d960 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -563,33 +563,74 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "length": 0, "userRegionList": []} async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: - user_id = data["userId"] - rival_id = data["rivalId"] + user_id = data.get("userId", 0) + rival_id = data.get("rivalId", 0) - """ - class UserRivalData: - rivalId: int - rivalName: str - """ - return {"userId": user_id, "userRivalData": {}} + if not user_id or not rival_id: return {} + + rival_pf = await self.data.profile.get_profile_detail(rival_id) + if not rival_pf: return {} + + return { + "userId": user_id, + "userRivalData": { + "rivalId": rival_id, + "rivalName": rival_pf['userName'] + } + } async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + user_id = data.get("userId", 0) + rival_id = data.get("rivalId", 0) + next_index = data.get("nextIndex", 0) + max_ct = 100 + upper_lim = next_index + max_ct + rival_music_list: Dict[int, List] = {} + + songs = await self.data.score.get_best_scores(rival_id) + if songs is None: + self.logger.debug("handle_get_user_rival_music_api_request: get_best_scores returned None!") + return { + "userId": user_id, + "rivalId": rival_id, + "nextIndex": 0, + "userRivalMusicList": [] # musicId userRivalMusicDetailList -> level achievement deluxscoreMax + } + + num_user_songs = len(songs) + + for x in range(next_index, upper_lim): + + tmp = songs[x]._asdict() + if tmp['musicId'] in rival_music_list: + rival_music_list[tmp['musicId']].append([{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}]) + + else: + if len(rival_music_list) >= max_ct: + break + rival_music_list[tmp['musicId']] = [{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}] + + next_index = 0 if len(rival_music_list) < max_ct or num_user_songs == upper_lim else upper_lim + self.logger.info(f"Send rival {rival_id} songs {next_index}-{upper_lim} ({len(rival_music_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})") + + return { + "userId": user_id, + "rivalId": rival_id, + "nextIndex": next_index, + "userRivalMusicList": [{"musicId": x, "userRivalMusicDetailList": y} for x, y in rival_music_list.items()] + } + + async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict: + # TODO: Added in 1.41, implement this? user_id = data["userId"] - rival_id = data["rivalId"] - next_idx = data["nextIndex"] - rival_music_levels = data["userRivalMusicLevelList"] - - """ - class UserRivalMusicList: - class UserRivalMusicDetailList: - level: int - achievement: int - deluxscoreMax: int - - musicId: int - userRivalMusicDetailList: list[UserRivalMusicDetailList] - """ - return {"userId": user_id, "nextIndex": 0, "userRivalMusicList": []} + version = data.get("version", 1041000) + user_playlog_list = data.get("userPlaylogList", []) + + return { + "userId": user_id, + "itemKind": -1, + "itemId": -1, + } async def handle_get_user_music_api_request(self, data: Dict) -> Dict: user_id = data.get("userId", 0) @@ -636,3 +677,208 @@ class Mai2DX(Mai2Base): return ret ret['loginId'] = ret.get('loginCount', 0) return ret + + # CardMaker support added in Universe + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_detail(data["userId"], self.version) + if p is None: + return {} + + return { + "userName": p["userName"], + "rating": p["playerRating"], + # hardcode lastDataVersion for CardMaker + "lastDataVersion": "1.20.00", # Future versiohs should replace this with the correct version + # checks if the user is still logged in + "isLogin": False, + "isExistSellingCard": True, + } + + async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # user already exists, because the preview checks that already + p = await self.data.profile.get_profile_detail(data["userId"], self.version) + + cards = await self.data.card.get_user_cards(data["userId"]) + if cards is None or len(cards) == 0: + # This should never happen + self.logger.error( + f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" + ) + return {} + + # get the dict representation of the row so we can modify values + user_data = p._asdict() + + # remove the values the game doesn't want + user_data.pop("id") + user_data.pop("user") + user_data.pop("version") + + return {"userId": data["userId"], "userData": user_data} + + async def handle_cm_login_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + async def handle_cm_logout_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: + selling_cards = await self.data.static.get_enabled_cards(self.version) + if selling_cards is None: + return {"length": 0, "sellingCardList": []} + + selling_card_list = [] + for card in selling_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("version") + tmp.pop("cardName") + tmp.pop("enabled") + + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["noticeStartDate"] = datetime.strftime( + tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["noticeEndDate"] = datetime.strftime( + tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT + ) + + selling_card_list.append(tmp) + + return {"length": len(selling_card_list), "sellingCardList": selling_card_list} + + async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = await self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_cards[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = 0 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) + card_list.append(tmp) + + return { + "returnCode": 1, + "length": len(card_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx], + } + + async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + await self.handle_get_user_item_api_request(data) + + async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + characters = await self.data.item.get_characters(data["userId"]) + + chara_list = [] + for chara in characters: + chara_list.append( + { + "characterId": chara["characterId"], + # no clue why those values are even needed + "point": 0, + "count": 0, + "level": chara["level"], + "nextAwake": 0, + "nextAwakePercent": 0, + "favorite": False, + "awakening": chara["awakening"], + "useCount": chara["useCount"], + } + ) + + return { + "returnCode": 1, + "length": len(chara_list), + "userCharacterList": chara_list, + } + + async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + return {"length": 0, "userPrintDetailList": []} + + async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + upsert = data["userPrintDetail"] + + # set a random card serial number + serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) + + # calculate start and end date of the card + start_date = datetime.utcnow() + end_date = datetime.utcnow() + timedelta(days=15) + + user_card = upsert["userCard"] + await self.data.item.put_card( + user_id, + user_card["cardId"], + user_card["cardTypeId"], + user_card["charaId"], + user_card["mapId"], + # add the correct start date and also the end date in 15 days + start_date, + end_date, + ) + + # get the profile extend to save the new bought card + extend = await self.data.profile.get_profile_extend(user_id, self.version) + if extend: + extend = extend._asdict() + # parse the selectedCardList + # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId) + selected_cards: List = extend["selectedCardList"] + + # if no pass is already added, add the corresponding pass + if not user_card["cardTypeId"] in selected_cards: + selected_cards.insert(0, user_card["cardTypeId"]) + + extend["selectedCardList"] = selected_cards + await self.data.profile.put_profile_extend(user_id, self.version, extend) + + # properly format userPrintDetail for the database + upsert.pop("userCard") + upsert.pop("serialId") + upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") + + await self.data.item.put_user_print_detail(user_id, serial_id, upsert) + + return { + "returnCode": 1, + "orderId": 0, + "serialId": serial_id, + "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT), + "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT), + } + + async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": data["userPrintlog"]["serialId"], + } + + async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} From 319aea098f1dbe587f4aea15997cd1e071bc948c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 8 Jun 2024 22:19:59 -0400 Subject: [PATCH 11/14] dx: fix GetUserRivalMusicApi list index out of range --- titles/mai2/dx.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 2c7d960..3c5c319 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -600,6 +600,8 @@ class Mai2DX(Mai2Base): num_user_songs = len(songs) for x in range(next_index, upper_lim): + if x >= num_user_songs: + break tmp = songs[x]._asdict() if tmp['musicId'] in rival_music_list: From e7ddfcda2e25ca30868908bf55426958eadd6ea6 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 8 Jun 2024 22:29:49 -0400 Subject: [PATCH 12/14] mai2: oops, forgot version --- titles/mai2/dx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 3c5c319..04c9172 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -568,7 +568,7 @@ class Mai2DX(Mai2Base): if not user_id or not rival_id: return {} - rival_pf = await self.data.profile.get_profile_detail(rival_id) + rival_pf = await self.data.profile.get_profile_detail(rival_id, self.version) if not rival_pf: return {} return { From b4b8650acc846cff59092f9e95218ab50d162549 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 9 Jun 2024 03:05:57 -0400 Subject: [PATCH 13/14] mai2: add basic webui --- core/frontend.py | 16 +- titles/mai2/__init__.py | 2 + titles/mai2/frontend.py | 190 +++++++++++++++++++ titles/mai2/schema/profile.py | 16 ++ titles/mai2/schema/score.py | 20 ++ titles/mai2/templates/css/mai2_style.css | 195 ++++++++++++++++++++ titles/mai2/templates/mai2_header.jinja | 17 ++ titles/mai2/templates/mai2_index.jinja | 134 ++++++++++++++ titles/mai2/templates/mai2_playlog.jinja | 225 +++++++++++++++++++++++ 9 files changed, 813 insertions(+), 2 deletions(-) create mode 100644 titles/mai2/frontend.py create mode 100644 titles/mai2/templates/css/mai2_style.css create mode 100644 titles/mai2/templates/mai2_header.jinja create mode 100644 titles/mai2/templates/mai2_index.jinja create mode 100644 titles/mai2/templates/mai2_playlog.jinja diff --git a/core/frontend.py b/core/frontend.py index b9d1604..31a6254 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -44,12 +44,13 @@ class ShopOwner(): self.permissions = perms class UserSession(): - def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7, chunithm_ver: int = -1): + def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7, chunithm_ver: int = -1, maimai_version: int = -1): self.user_id = usr_id self.current_ip = ip self.permissions = perms self.ongeki_version = ongeki_ver self.chunithm_version = chunithm_ver + self.maimai_version = maimai_version class FrontendServlet(): def __init__(self, cfg: CoreConfig, config_dir: str) -> None: @@ -216,6 +217,7 @@ class FE_Base(): sesh.current_ip = tk['current_ip'] sesh.permissions = tk['permissions'] sesh.chunithm_version = tk['chunithm_version'] + sesh.maimai_version = tk['maimai_version'] if sesh.user_id <= 0: self.logger.error("User session failed to validate due to an invalid ID!") @@ -260,7 +262,17 @@ class FE_Base(): 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, "chunithm_version": sesh.chunithm_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, + "maimai_version": sesh.maimai_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/mai2/__init__.py b/titles/mai2/__init__.py index 74cfddf..234e864 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -2,10 +2,12 @@ from titles.mai2.index import Mai2Servlet from titles.mai2.const import Mai2Constants from titles.mai2.database import Mai2Data from titles.mai2.read import Mai2Reader +from .frontend import Mai2Frontend index = Mai2Servlet database = Mai2Data reader = Mai2Reader +frontend = Mai2Frontend game_codes = [ Mai2Constants.GAME_CODE_DX, Mai2Constants.GAME_CODE_FINALE, diff --git a/titles/mai2/frontend.py b/titles/mai2/frontend.py new file mode 100644 index 0000000..6ce8828 --- /dev/null +++ b/titles/mai2/frontend.py @@ -0,0 +1,190 @@ +from typing import List +from starlette.routing import Route, Mount +from starlette.requests import Request +from starlette.responses import Response, RedirectResponse +from os import path +import yaml +import jinja2 + +from core.frontend import FE_Base, UserSession +from core.config import CoreConfig +from .database import Mai2Data +from .config import Mai2Config +from .const import Mai2Constants + +class Mai2Frontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = Mai2Data(cfg) + self.game_cfg = Mai2Config() + if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")) + ) + self.nav_name = "maimai" + + def get_routes(self) -> List[Route]: + return [ + Route("/", self.render_GET, 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: + template = self.environment.get_template( + "titles/mai2/templates/mai2_index.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id) + profile = [] + if versions: + # maimai_version is -1 means it is not initialized yet, select a default version from existing. + if usr_sesh.maimai_version < 0: + usr_sesh.maimai_version = versions[0]['version'] + profile = await self.data.profile.get_profile_detail(usr_sesh.user_id, usr_sesh.maimai_version) + versions = [x['version'] for x in versions] + + 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=Mai2Constants.VERSION_STRING, + versions=versions, + cur_version=usr_sesh.maimai_version + ), media_type="text/html; charset=utf-8") + + if usr_sesh.maimai_version >= 0: + encoded_sesh = self.encode_session(usr_sesh) + resp.delete_cookie("DIANA_SESH") + resp.set_cookie("DIANA_SESH", encoded_sesh) + return resp + + else: + return RedirectResponse("/gate/", 303) + + async def render_GET_playlog(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/mai2/templates/mai2_playlog.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + print("wtf") + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + if usr_sesh.maimai_version < 0: + print(usr_sesh.maimai_version) + return RedirectResponse("/game/mai2/", 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(user_id, index, 20) + playlog_with_title = [] + for record in playlog: + music_chart = await self.data.static.get_music_chart(usr_sesh.maimai_version, record.musicId, record.level) + if music_chart: + difficultyNum=music_chart.chartId + difficulty=music_chart.difficulty + artist=music_chart.artist + title=music_chart.title + else: + difficultyNum=0 + difficulty=0 + artist="unknown" + title="musicid: " + str(record.musicId) + playlog_with_title.append({ + "raw": record, + "title": title, + "difficultyNum": difficultyNum, + "difficulty": difficulty, + "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) + + 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: + if o == 0x20: + new_name_full += chr(0x3000) + elif o < 0x7F and o > 0x20: + new_name_full += chr(o + 0xFEE0) + elif o <= 0x7F: + self.logger.warn(f"Invalid ascii character {o:02X}") + 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.user_id, new_name_full): + return RedirectResponse("/gate/?e=999", 303) + + return RedirectResponse("/game/mai2/?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() + maimai_version = form_data.get("version") + self.logger.info(f"version change to: {maimai_version}") + if(maimai_version.isdigit()): + usr_sesh.maimai_version=int(maimai_version) + encoded_sesh = self.encode_session(usr_sesh) + self.logger.info(f"Created session with JWT {encoded_sesh}") + resp = RedirectResponse("/game/mai2/", 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/mai2/schema/profile.py b/titles/mai2/schema/profile.py index d45ee16..1b76b07 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -511,6 +511,11 @@ rival = Table( ) class Mai2ProfileData(BaseData): + async def get_all_profile_versions(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(detail.select(detail.c.user == user_id)) + if result: + return result.fetchall() + async def put_profile_detail( self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True ) -> Optional[Row]: @@ -899,3 +904,14 @@ class Mai2ProfileData(BaseData): result = await self.execute(rival.delete(and_(rival.c.user == user_id, rival.c.rival == rival_id))) if not result: self.logger.error(f"Failed to remove rival {rival_id} for user {user_id}!") + + async def update_name(self, user_id: int, new_name: str) -> bool: + sql = detail.update(detail.c.user == user_id).values( + userName=new_name + ) + result = await self.execute(sql) + + if result is None: + self.logger.warning(f"Failed to set user {user_id} name to {new_name}") + return False + return True diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 59b61b6..d4ea5b9 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -398,3 +398,23 @@ class Mai2ScoreData(BaseData): if result is None: return None return result.fetchall() + + async def get_playlogs(self, user_id: int, idx: int = 0, limit: int = 0) -> Optional[List[Row]]: + sql = playlog.select(playlog.c.user == user_id) + + if limit: + sql = sql.limit(limit) + if idx: + sql = sql.offset(idx * limit) + + result = await self.execute(sql) + if result: + 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() diff --git a/titles/mai2/templates/css/mai2_style.css b/titles/mai2/templates/css/mai2_style.css new file mode 100644 index 0000000..4aceab8 --- /dev/null +++ b/titles/mai2/templates/css/mai2_style.css @@ -0,0 +1,195 @@ +.mai2-header { + text-align: center; +} + +ul.mai2-navi { + list-style-type: none; + padding: 0; + overflow: hidden; + background-color: #333; + text-align: center; + display: inline-block; +} + +ul.mai2-navi li { + display: inline-block; +} + +ul.mai2-navi li a { + display: block; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +ul.mai2-navi li a:hover:not(.active) { + background-color: #111; +} + +ul.mai2-navi li a.active { + background-color: #4CAF50; +} + +ul.mai2-navi li.right { + float: right; +} + +@media screen and (max-width: 600px) { + + ul.mai2-navi li.right, + ul.mai2-navi li { + float: none; + display: block; + text-align: center; + } +} + +table { + border-collapse: collapse; + border-spacing: 0; + border-collapse: separate; + overflow: hidden; + background-color: #555555; + +} + +th, td { + text-align: left; + border: none; + +} + +th { + color: white; +} + +.table-rowdistinct tr:nth-child(even) { + background-color: #303030; +} + +.table-rowdistinct tr:nth-child(odd) { + background-color: #555555; +} + +caption { + text-align: center; + color: white; + font-size: 18px; + font-weight: bold; +} + +.table-large { + margin: 16px; +} + +.table-large th, +.table-large td { + padding: 8px; +} + +.table-small { + width: 100%; + margin: 4px; +} + +.table-small th, +.table-small td { + padding: 2px; +} + +.bg-card { + background-color: #555555; +} + +.card-hover { + transition: all 0.2s ease-in-out; +} + +.card-hover:hover { + transform: scale(1.02); +} + +.basic { + color: #28a745; + font-weight: bold; +} + +.hard { + color: #ffc107; + + font-weight: bold; +} + +.expert { + color: #dc3545; + font-weight: bold; +} + +.master { + color: #dd09e8; + font-weight: bold; +} + +.ultimate { + color: #000000; + font-weight: bold; +} + +.score { + color: #ffffff; + font-weight: bold; +} + +.rainbow { + background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-weight: bold; +} + +.platinum { + color: #FFFF00; + font-weight: bold; +} + +.gold { + color: #FFFF00; + font-weight: bold; +} + +.scrolling-text { + overflow: hidden; +} + +.scrolling-text p { + white-space: nowrap; + display: inline-block; + +} + +.scrolling-text h6 { + white-space: nowrap; + display: inline-block; + +} + +.scrolling-text h5 { + white-space: nowrap; + display: inline-block; + +} + +.scrolling { + animation: scroll 10s linear infinite; +} + +@keyframes scroll { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(-100%); + } +} \ No newline at end of file diff --git a/titles/mai2/templates/mai2_header.jinja b/titles/mai2/templates/mai2_header.jinja new file mode 100644 index 0000000..4a4cb86 --- /dev/null +++ b/titles/mai2/templates/mai2_header.jinja @@ -0,0 +1,17 @@ +
+

maimai

+ +
+ \ No newline at end of file diff --git a/titles/mai2/templates/mai2_index.jinja b/titles/mai2/templates/mai2_index.jinja new file mode 100644 index 0000000..6490fdc --- /dev/null +++ b/titles/mai2/templates/mai2_index.jinja @@ -0,0 +1,134 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/mai2/templates/mai2_header.jinja' %} + {% if profile is defined and profile is not none and profile|length > 0 %} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
OVERVIEW
{{ profile.userName }} + +
version: + + {% if versions | length > 1 %} +

You have {{ versions | length }} versions.

+ {% endif %} +
Rating: + + {{ profile.playerRating }} + + + (highest: {{ profile.highestRating }}) + +
Play Counts:{{ profile.playCount }}
Last Play Date:{{ profile.lastPlayDate }}
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
SCORE
Total Delux Score:{{ profile.totalDeluxscore }}
Total Basic Delux Score:{{ profile.totalBasicDeluxscore }}
Total Advanced Delux Score:{{ profile.totalAdvancedDeluxscore }}
Total Expert Delux Score:{{ profile.totalExpertDeluxscore }}
Total Master Delux Score:{{ profile.totalMasterDeluxscore }}
Total ReMaster Delux Score:{{ profile.totalReMasterDeluxscore }}
+
+
+
+ {% if error is defined %} + {% include "core/templates/widgets/err_banner.jinja" %} + {% endif %} + {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No profile information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+ + +{% endblock content %} \ No newline at end of file diff --git a/titles/mai2/templates/mai2_playlog.jinja b/titles/mai2/templates/mai2_playlog.jinja new file mode 100644 index 0000000..3e1d5fd --- /dev/null +++ b/titles/mai2/templates/mai2_playlog.jinja @@ -0,0 +1,225 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/mai2/templates/mai2_header.jinja' %} + {% if playlog is defined and playlog is not none %} +
+

Playlog counts: {{ playlog_count }}

+ {% set rankName = ['D', 'C', 'B', 'BB', 'BBB', 'A', 'AA', 'AAA', 'S', 'S+', 'SS', 'SS+', 'SSS', 'SSS+'] %} + {% set difficultyName = ['basic', 'hard', 'expert', 'master', 'ultimate'] %} + {% for record in playlog %} +
+
+
+
+
{{ record.title }}
+
+
{{ record.artist }}
+
+
+
{{ record.raw.userPlayDate }}
+
TRACK {{ record.raw.trackNo }}
+
+
+
+
+

{{ record.raw.deluxscore }}

+

{{ rankName[record.raw.rank] }}

+
+ {{ difficultyName[record.raw.level] }}  {{ record.difficulty }} +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
CRITICAL PERFECT + Tap: {{ record.raw.tapCriticalPerfect }}
+ Hold: {{ record.raw.holdCriticalPerfect }}
+ Slide: {{ record.raw.slideCriticalPerfect }}
+ Touch: {{ record.raw.touchCriticalPerfect }}
+ Break: {{ record.raw.breakCriticalPerfect }} +
PERFECT + Tap: {{ record.raw.tapPerfect }}
+ Hold: {{ record.raw.holdPerfect }}
+ Slide: {{ record.raw.slidePerfect }}
+ Touch: {{ record.raw.touchPerfect }}
+ Break: {{ record.raw.breakPerfect }} +
GREAT + Tap: {{ record.raw.tapGreat }}
+ Hold: {{ record.raw.holdGreat }}
+ Slide: {{ record.raw.slideGreat }}
+ Touch: {{ record.raw.touchGreat }}
+ Break: {{ record.raw.breakGreat }} +
GOOD + Tap: {{ record.raw.tapGood }}
+ Hold: {{ record.raw.holdGood }}
+ Slide: {{ record.raw.slideGood }}
+ Touch: {{ record.raw.touchGood }}
+ Break: {{ record.raw.breakGood }} +
MISS + Tap: {{ record.raw.tapMiss }}
+ Hold: {{ record.raw.holdMiss }}
+ Slide: {{ record.raw.slideMiss }}
+ Touch: {{ record.raw.touchMiss }}
+ Break: {{ record.raw.breakMiss }} +
+
+
+ {%if record.raw.comboStatus == 1 %} +
FULL COMBO
+ {% endif %} + {%if record.raw.comboStatus == 2 %} +
FULL COMBO +
+ {% endif %} + {%if record.raw.comboStatus == 3 %} +
ALL PERFECT
+ {% endif %} + {%if record.raw.comboStatus == 4 %} +
ALL PERFECT +
+ {% endif %} + {%if record.raw.syncStatus == 1 %} +
FULL SYNC
+ {% endif %} + {%if record.raw.syncStatus == 2 %} +
FULL SYNC +
+ {% endif %} + {%if record.raw.syncStatus == 3 %} +
FULL SYNC DX
+ {% endif %} + {%if record.raw.syncStatus == 4 %} +
FULL SYNC DX +
+ {% endif %} + {%if record.raw.isAchieveNewRecord == 1 or record.raw.isDeluxscoreNewRecord == 1 %} +
NEW RECORD
+ {% endif %} +
+
+
+
+ {% endfor %} +
+ {% set playlog_pages = playlog_count // 20 + 1 %} + {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No Playlog information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+ + + +{% endblock content %} \ No newline at end of file From e69922d91b06e8ae5046755889999ad7defb9f19 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 9 Jun 2024 03:14:43 -0400 Subject: [PATCH 14/14] ongeki: fix frontend versions --- core/allnet.py | 2 +- core/frontend.py | 15 ++++++++------- titles/chuni/frontend.py | 4 ++-- titles/mai2/frontend.py | 6 +++--- titles/ongeki/frontend.py | 18 ++++++++++++------ 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 861a603..9eb6595 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -960,7 +960,7 @@ class DLReport: return True -cfg_dir = environ.get("DIANA_CFG_DIR", "config") +cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config") cfg: CoreConfig = CoreConfig() if path.exists(f"{cfg_dir}/core.yaml"): cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) diff --git a/core/frontend.py b/core/frontend.py index 31a6254..f15f58a 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -193,7 +193,7 @@ class FE_Base(): ), media_type="text/html; charset=utf-8") if sesh is None: - resp.delete_cookie("DIANA_SESH") + resp.delete_cookie("ARTEMIS_SESH") return resp def get_routes(self) -> List[Route]: @@ -218,6 +218,7 @@ class FE_Base(): sesh.permissions = tk['permissions'] sesh.chunithm_version = tk['chunithm_version'] sesh.maimai_version = tk['maimai_version'] + sesh.ongeki_version = tk['ongeki_version'] if sesh.user_id <= 0: self.logger.error("User session failed to validate due to an invalid ID!") @@ -243,7 +244,7 @@ class FE_Base(): return UserSession() def validate_session(self, request: Request) -> Optional[UserSession]: - sesh = request.cookies.get('DIANA_SESH', "") + sesh = request.cookies.get('ARTEMIS_SESH', "") if not sesh: return None @@ -304,7 +305,7 @@ class FE_Gate(FE_Base): error=err, sesh=vars(UserSession()), ), media_type="text/html; charset=utf-8") - resp.delete_cookie("DIANA_SESH") + resp.delete_cookie("ARTEMIS_SESH") return resp async def render_login(self, request: Request): @@ -354,7 +355,7 @@ class FE_Gate(FE_Base): usr_sesh = self.encode_session(sesh) self.logger.debug(f"Created session with JWT {usr_sesh}") resp = RedirectResponse("/user/", 303) - resp.set_cookie("DIANA_SESH", usr_sesh) + resp.set_cookie("ARTEMIS_SESH", usr_sesh) return resp @@ -393,7 +394,7 @@ class FE_Gate(FE_Base): usr_sesh = self.encode_session(sesh) self.logger.debug(f"Created session with JWT {usr_sesh}") resp = RedirectResponse("/user/", 303) - resp.set_cookie("DIANA_SESH", usr_sesh) + resp.set_cookie("ARTEMIS_SESH", usr_sesh) return resp @@ -511,7 +512,7 @@ class FE_User(FE_Base): async def render_logout(self, request: Request): resp = RedirectResponse("/gate/", 303) - resp.delete_cookie("DIANA_SESH") + resp.delete_cookie("ARTEMIS_SESH") return resp async def edit_card(self, request: Request) -> RedirectResponse: @@ -895,7 +896,7 @@ class FE_Machine(FE_Base): arcade={} ), media_type="text/html; charset=utf-8") -cfg_dir = environ.get("DIANA_CFG_DIR", "config") +cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config") cfg: CoreConfig = CoreConfig() if path.exists(f"{cfg_dir}/core.yaml"): cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) diff --git a/titles/chuni/frontend.py b/titles/chuni/frontend.py index b0fa9bc..0dbefac 100644 --- a/titles/chuni/frontend.py +++ b/titles/chuni/frontend.py @@ -68,7 +68,7 @@ class ChuniFrontend(FE_Base): if usr_sesh.chunithm_version >= 0: encoded_sesh = self.encode_session(usr_sesh) - resp.set_cookie("DIANA_SESH", encoded_sesh) + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) return resp else: @@ -240,7 +240,7 @@ class ChuniFrontend(FE_Base): 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) + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) return resp else: return RedirectResponse("/gate/", 303) \ No newline at end of file diff --git a/titles/mai2/frontend.py b/titles/mai2/frontend.py index 6ce8828..635c2fa 100644 --- a/titles/mai2/frontend.py +++ b/titles/mai2/frontend.py @@ -67,8 +67,8 @@ class Mai2Frontend(FE_Base): if usr_sesh.maimai_version >= 0: encoded_sesh = self.encode_session(usr_sesh) - resp.delete_cookie("DIANA_SESH") - resp.set_cookie("DIANA_SESH", encoded_sesh) + resp.delete_cookie("ARTEMIS_SESH") + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) return resp else: @@ -184,7 +184,7 @@ class Mai2Frontend(FE_Base): encoded_sesh = self.encode_session(usr_sesh) self.logger.info(f"Created session with JWT {encoded_sesh}") resp = RedirectResponse("/game/mai2/", 303) - resp.set_cookie("DIANA_SESH", encoded_sesh) + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) return resp else: return RedirectResponse("/gate/", 303) \ No newline at end of file diff --git a/titles/ongeki/frontend.py b/titles/ongeki/frontend.py index 1cdec03..226f318 100644 --- a/titles/ongeki/frontend.py +++ b/titles/ongeki/frontend.py @@ -31,7 +31,8 @@ class OngekiFrontend(FE_Base): def get_routes(self) -> List[Route]: return [ - Route("/", self.render_GET) + Route("/", self.render_GET), + Route("/version.change", self.render_POST, methods=['POST']) ] async def render_GET(self, request: Request) -> bytes: @@ -69,29 +70,34 @@ class OngekiFrontend(FE_Base): return RedirectResponse("/gate/", 303) async def render_POST(self, request: Request): - uri = request.uri.decode() + uri = request.url.path + frm = await request.form() usr_sesh = self.validate_session(request) if not usr_sesh: usr_sesh = UserSession() if usr_sesh.user_id > 0: if uri == "/game/ongeki/rival.add": - rival_id = request.args[b"rivalUserId"][0].decode() + rival_id = frm.get("rivalUserId") await self.data.profile.put_rival(usr_sesh.user_id, rival_id) # self.logger.info(f"{usr_sesh.user_id} added a rival") return RedirectResponse(b"/game/ongeki/", 303) elif uri == "/game/ongeki/rival.delete": - rival_id = request.args[b"rivalUserId"][0].decode() + rival_id = frm.get("rivalUserId") await self.data.profile.delete_rival(usr_sesh.user_id, rival_id) # self.logger.info(f"{response}") return RedirectResponse(b"/game/ongeki/", 303) elif uri == "/game/ongeki/version.change": - ongeki_version=request.args[b"version"][0].decode() + ongeki_version=frm.get("version") if(ongeki_version.isdigit()): usr_sesh.ongeki_version=int(ongeki_version) - return RedirectResponse("/game/ongeki/", 303) + enc = self.encode_session(usr_sesh) + resp = RedirectResponse("/game/ongeki/", 303) + resp.delete_cookie('ARTEMIS_SESH') + resp.set_cookie('ARTEMIS_SESH', enc) + return resp else: Response("Something went wrong", status_code=500)