Merge branch 'develop' into diva_configire_festa

This commit is contained in:
= 2024-06-30 12:26:53 +02:00
commit eb275062a1
12 changed files with 335 additions and 51 deletions

View File

@ -3,13 +3,28 @@ Documenting updates to ARTEMiS, to be updated every time the master branch is pu
## 20240630 ## 20240630
### DIVA ### DIVA
+ Added configurable festa options + Added configurable festa options'
## 20240629
### CHUNITHM
+ Add team points
## 20240628
### maimai
+ Add present support
## 20240627
### SAO
+ Fix ghost items, character and player XP, EX Bonuses, unlocks, and much much more
## 20240620 ## 20240620
### CHUNITHM ### CHUNITHM
+ CHUNITHM LUMINOUS support + CHUNITHM LUMINOUS support
## 20240616 ## 20240616
### CHUNITHM
+ Support network encryption for Export/International versions
### DIVA ### DIVA
+ Working frontend with name and level strings edit and playlog + Working frontend with name and level strings edit and playlog

View File

@ -0,0 +1,41 @@
"""mai2_presents
Revision ID: 5ea363686347
Revises: 680789dabab3
Create Date: 2024-06-28 14:49:07.666879
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '5ea363686347'
down_revision = '680789dabab3'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('mai2_item_present',
sa.Column('id', sa.BIGINT(), nullable=False),
sa.Column('version', sa.INTEGER(), nullable=True),
sa.Column('user', sa.Integer(), nullable=True),
sa.Column('itemKind', sa.INTEGER(), nullable=False),
sa.Column('itemId', sa.INTEGER(), nullable=False),
sa.Column('stock', sa.INTEGER(), server_default='1', nullable=False),
sa.Column('startDate', sa.TIMESTAMP(), nullable=True),
sa.Column('endDate', sa.TIMESTAMP(), nullable=True),
sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('version', 'user', 'itemKind', 'itemId', name='mai2_item_present_uk'),
mysql_charset='utf8mb4'
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('mai2_item_present')
# ### end Alembic commands ###

View File

@ -64,6 +64,12 @@ def upgrade():
op.add_column('sao_hero_log_data', sa.Column('property4_value1', sa.INTEGER(), server_default='0', nullable=False)) op.add_column('sao_hero_log_data', sa.Column('property4_value1', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property4_value2', sa.INTEGER(), server_default='0', nullable=False)) op.add_column('sao_hero_log_data', sa.Column('property4_value2', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('converted_card_num', sa.INTEGER(), server_default='0', nullable=False)) op.add_column('sao_hero_log_data', sa.Column('converted_card_num', sa.INTEGER(), server_default='0', nullable=False))
op.alter_column('sao_hero_log_data', 'main_weapon',
existing_type=mysql.INTEGER(),
nullable=True)
op.alter_column('sao_hero_log_data', 'sub_equipment',
existing_type=mysql.INTEGER(),
nullable=True)
op.alter_column('sao_hero_log_data', 'skill_slot1_skill_id', op.alter_column('sao_hero_log_data', 'skill_slot1_skill_id',
existing_type=mysql.INTEGER(), existing_type=mysql.INTEGER(),
type_=sa.BIGINT(), type_=sa.BIGINT(),
@ -235,12 +241,10 @@ def downgrade():
type_=mysql.INTEGER(), type_=mysql.INTEGER(),
nullable=False) nullable=False)
op.alter_column('sao_hero_log_data', 'sub_equipment', op.alter_column('sao_hero_log_data', 'sub_equipment',
existing_type=sa.BIGINT(), existing_type=mysql.INTEGER(),
type_=mysql.INTEGER(),
nullable=False) nullable=False)
op.alter_column('sao_hero_log_data', 'main_weapon', op.alter_column('sao_hero_log_data', 'main_weapon',
existing_type=sa.BIGINT(), existing_type=mysql.INTEGER(),
type_=mysql.INTEGER(),
nullable=False) nullable=False)
op.drop_column('sao_hero_log_data', 'converted_card_num') op.drop_column('sao_hero_log_data', 'converted_card_num')
op.drop_column('sao_hero_log_data', 'property4_value2') op.drop_column('sao_hero_log_data', 'property4_value2')

View File

@ -0,0 +1,28 @@
"""chuni_team_points
Revision ID: 745448d83696
Revises: 5ea363686347
Create Date: 2024-06-29 00:05:22.479187
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '745448d83696'
down_revision = '5ea363686347'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('chuni_profile_team', sa.Column('userTeamPoint', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('chuni_profile_team', 'userTeamPoint')
# ### end Alembic commands ###

View File

@ -88,13 +88,19 @@ Config file is located in `config/chuni.yaml`.
| `crypto` | This option is used to enable the TLS Encryption | | `crypto` | This option is used to enable the TLS Encryption |
**If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:** If you would like to use network encryption, add the keys to the `keys` section under `crypto`, where the key
is the version ID for Japanese (SDHD) versions and `"{versionID}_int"` for Export (SDGS) versions, and the value
is an array containing `[key, iv, salt, iter_count]` in order.
`iter_count` is optional for all Japanese (SDHD) versions but may be required for some Export (SDGS) versions.
You will receive an error in the logs if it needs to be specified.
```yaml ```yaml
crypto: crypto:
encrypted_only: False encrypted_only: False
keys: keys:
13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] 13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
"13_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000", 42]
``` ```
### Database upgrade ### Database upgrade
@ -176,6 +182,14 @@ Config file is located in `config/cxb.yaml`.
## maimai DX ## maimai DX
### Presents
Presents are items given to the user when they login, with a little animation (for example, the KOP song was given to the finalists as a present). To add a present, you must insert it into the `mai2_item_present` table. In that table, a NULL version means any version, a NULL user means any user, a NULL start date means always open, and a NULL end date means it never expires. Below is a list of presents one might wish to add:
| Game Version | Item ID | Item Kind | Item Description | Present Description |
|--------------|---------|-----------|-------------------------------------------------|------------------------------------------------|
| BUDDiES (21) | 409505 | Icon (3) | 旅行スタンプ(月面基地) (Travel Stamp - Moon Base) | Officially obtained on the webui with a serial |
| | | | | number, for project raputa |
### Versions ### Versions
| Game Code | Version ID | Version Name | | Game Code | Version ID | Version Name |

View File

@ -720,9 +720,14 @@ class ChuniBase:
team_id = 65535 team_id = 65535
team_name = self.game_cfg.team.team_name team_name = self.game_cfg.team.team_name
team_rank = 0 team_rank = 0
team_user_point = 0
# Get user profile # Get user profile
profile = await self.data.profile.get_profile_data(data["userId"], self.version) profile = await self.data.profile.get_profile_data(data["userId"], self.version)
if profile is None:
return {"userId": data["userId"], "teamId": 0}
if profile and profile["teamId"]: if profile and profile["teamId"]:
# Get team by id # Get team by id
team = await self.data.profile.get_team_by_id(profile["teamId"]) team = await self.data.profile.get_team_by_id(profile["teamId"])
@ -731,7 +736,12 @@ class ChuniBase:
team_id = team["id"] team_id = team["id"]
team_name = team["teamName"] team_name = team["teamName"]
team_rank = await self.data.profile.get_team_rank(team["id"]) team_rank = await self.data.profile.get_team_rank(team["id"])
team_point = team["teamPoint"]
if team["userTeamPoint"] is not None and team["userTeamPoint"] != "":
user_team_point_data = json.loads(team["userTeamPoint"])
for user_point_data in user_team_point_data:
if user_point_data["user"] == data["userId"]:
team_user_point = int(user_point_data["userPoint"])
# Don't return anything if no team name has been defined for defaults and there is no team set for the player # Don't return anything if no team name has been defined for defaults and there is no team set for the player
if not profile["teamId"] and team_name == "": if not profile["teamId"] and team_name == "":
return {"userId": data["userId"], "teamId": 0} return {"userId": data["userId"], "teamId": 0}
@ -741,11 +751,12 @@ class ChuniBase:
"teamId": team_id, "teamId": team_id,
"teamRank": team_rank, "teamRank": team_rank,
"teamName": team_name, "teamName": team_name,
"assaultTimeRate": 1, # TODO: Figure out assaultTime, which might be team point boost?
"userTeamPoint": { "userTeamPoint": {
"userId": data["userId"], "userId": data["userId"],
"teamId": team_id, "teamId": team_id,
"orderId": 1, "orderId": 0,
"teamPoint": 1, "teamPoint": team_user_point,
"aggrDate": data["playDate"], "aggrDate": data["playDate"],
}, },
} }

View File

@ -41,7 +41,7 @@ class ChuniServlet(BaseServlet):
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
super().__init__(core_cfg, cfg_dir) super().__init__(core_cfg, cfg_dir)
self.game_cfg = ChuniConfig() self.game_cfg = ChuniConfig()
self.hash_table: Dict[Dict[str, str]] = {} self.hash_table: Dict[str, Dict[str, str]] = {}
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"): if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
self.game_cfg.update( self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}")) yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))
@ -92,32 +92,60 @@ class ChuniServlet(BaseServlet):
) )
self.logger.inited = True self.logger.inited = True
known_iter_counts = {
ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: 67,
ChuniConstants.VER_CHUNITHM_PARADISE: 44,
f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 25,
ChuniConstants.VER_CHUNITHM_NEW: 54,
f"{ChuniConstants.VER_CHUNITHM_NEW}_int": 49,
ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25,
ChuniConstants.VER_CHUNITHM_SUN: 70,
ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36,
ChuniConstants.VER_CHUNITHM_LUMINOUS: 8,
}
for version, keys in self.game_cfg.crypto.keys.items(): for version, keys in self.game_cfg.crypto.keys.items():
if len(keys) < 3: if len(keys) < 3:
continue continue
self.hash_table[version] = {} if isinstance(version, int):
version_idx = version
else:
version_idx = int(version.split("_")[0])
salt = bytes.fromhex(keys[2])
if len(keys) >= 4:
iter_count = keys[3]
elif (iter_count := known_iter_counts.get(version)) is None:
self.logger.error(
"Number of iteration rounds for version %s is not known, but it is not specified in the config",
version,
)
continue
self.hash_table[version] = {}
method_list = [ method_list = [
method method
for method in dir(self.versions[version]) for method in dir(self.versions[version_idx])
if not method.startswith("__") if not method.startswith("__")
] ]
for method in method_list: for method in method_list:
method_fixed = inflection.camelize(method)[6:-7] method_fixed = inflection.camelize(method)[6:-7]
# number of iterations was changed to 70 in SUN and then to 36
if version == ChuniConstants.VER_CHUNITHM_LUMINOUS: # This only applies for CHUNITHM NEW International and later for some reason.
iter_count = 8 # CHUNITHM SUPERSTAR (PLUS) did not add "Exp" to the endpoint when hashing.
elif version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: if (
iter_count = 36 isinstance(version, str)
elif version == ChuniConstants.VER_CHUNITHM_SUN: and version.endswith("_int")
iter_count = 70 and version_idx >= ChuniConstants.VER_CHUNITHM_NEW
else: ):
iter_count = 44 method_fixed += "C3Exp"
hash = PBKDF2( hash = PBKDF2(
method_fixed, method_fixed,
bytes.fromhex(keys[2]), salt,
128, 128,
count=iter_count, count=iter_count,
hmac_hash_module=SHA1, hmac_hash_module=SHA1,
@ -127,7 +155,7 @@ class ChuniServlet(BaseServlet):
self.hash_table[version][hashed_name] = method_fixed self.hash_table[version][hashed_name] = method_fixed
self.logger.debug( self.logger.debug(
f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}" f"Hashed v{version} method {method_fixed} with {salt} to get {hashed_name}"
) )
@classmethod @classmethod
@ -220,31 +248,39 @@ class ChuniServlet(BaseServlet):
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
# If we get a 32 character long hex string, it's a hash and we're # If we get a 32 character long hex string, it's a hash and we're
# doing encrypted. The likelyhood of false positives is low but # doing encrypted. The likelihood of false positives is low but
# technically not 0 # technically not 0
if game_code == "SDGS":
crypto_cfg_key = f"{internal_ver}_int"
hash_table_key = f"{internal_ver}_int"
else:
crypto_cfg_key = internal_ver
hash_table_key = internal_ver
if internal_ver < ChuniConstants.VER_CHUNITHM_NEW: if internal_ver < ChuniConstants.VER_CHUNITHM_NEW:
endpoint = request.headers.get("User-Agent").split("#")[0] endpoint = request.headers.get("User-Agent").split("#")[0]
else: else:
if internal_ver not in self.hash_table: if hash_table_key not in self.hash_table:
self.logger.error( self.logger.error(
f"v{version} does not support encryption or no keys entered" f"v{version} does not support encryption or no keys entered"
) )
return Response(zlib.compress(b'{"stat": "0"}')) return Response(zlib.compress(b'{"stat": "0"}'))
elif endpoint.lower() not in self.hash_table[internal_ver]: elif endpoint.lower() not in self.hash_table[hash_table_key]:
self.logger.error( self.logger.error(
f"No hash found for v{version} endpoint {endpoint}" f"No hash found for v{version} endpoint {endpoint}"
) )
return Response(zlib.compress(b'{"stat": "0"}')) return Response(zlib.compress(b'{"stat": "0"}'))
endpoint = self.hash_table[internal_ver][endpoint.lower()] endpoint = self.hash_table[hash_table_key][endpoint.lower()]
try: try:
crypt = AES.new( crypt = AES.new(
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]),
AES.MODE_CBC, AES.MODE_CBC,
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]),
) )
req_raw = crypt.decrypt(req_raw) req_raw = crypt.decrypt(req_raw)

View File

@ -1,3 +1,4 @@
import json
from typing import Dict, List, Optional from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, and_ from sqlalchemy import Table, Column, UniqueConstraint, and_
from sqlalchemy.types import Integer, String, Boolean, JSON, BigInteger from sqlalchemy.types import Integer, String, Boolean, JSON, BigInteger
@ -389,6 +390,7 @@ team = Table(
Column("id", Integer, primary_key=True, nullable=False), Column("id", Integer, primary_key=True, nullable=False),
Column("teamName", String(255)), Column("teamName", String(255)),
Column("teamPoint", Integer), Column("teamPoint", Integer),
Column("userTeamPoint", JSON),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@ -705,12 +707,36 @@ class ChuniProfileData(BaseData):
# Return the rank if found, or a default rank otherwise # Return the rank if found, or a default rank otherwise
return rank if rank is not None else 0 return rank if rank is not None else 0
# RIP scaled team ranking. Gone, but forgotten async def update_team(self, team_id: int, team_data: Dict, user_id: str, user_point_delta: int) -> bool:
# def get_team_rank_scaled(self, team_id: int) -> int: # Update the team data
async def update_team(self, team_id: int, team_data: Dict) -> bool:
team_data["id"] = team_id team_data["id"] = team_id
existing_team = self.get_team_by_id(team_id)
if existing_team is None or "userTeamPoint" not in existing_team:
self.logger.warn(
f"update_team: Failed to update team! team id: {team_id}. Existing team data not found."
)
return False
user_team_point_data = []
if existing_team["userTeamPoint"] is not None and existing_team["userTeamPoint"] != "":
user_team_point_data = json.loads(existing_team["userTeamPoint"])
updated = False
# Try to find the user in the existing data and update their points
for user_point_data in user_team_point_data:
if user_point_data["user"] == user_id:
user_point_data["userPoint"] = str(int(user_point_delta))
updated = True
break
# If the user was not found, add them to the data with the new points
if not updated:
user_team_point_data.append({"user": user_id, "userPoint": str(user_point_delta)})
# Update the team's userTeamPoint field in the team data
team_data["userTeamPoint"] = json.dumps(user_team_point_data)
# Update the team in the database
sql = insert(team).values(**team_data) sql = insert(team).values(**team_data)
conflict = sql.on_duplicate_key_update(**team_data) conflict = sql.on_duplicate_key_update(**team_data)
@ -722,6 +748,7 @@ class ChuniProfileData(BaseData):
) )
return False return False
return True return True
async def get_rival(self, rival_id: int) -> Optional[Row]: async def get_rival(self, rival_id: int) -> Optional[Row]:
sql = select(profile).where(profile.c.user == rival_id) sql = select(profile).where(profile.c.user == rival_id)
result = await self.execute(sql) result = await self.execute(sql)

View File

@ -471,7 +471,27 @@ class Mai2Base:
} }
async def handle_get_user_present_api_request(self, data: Dict) -> Dict: async def handle_get_user_present_api_request(self, data: Dict) -> Dict:
return { "userId": data.get("userId", 0), "length": 0, "userPresentList": []} items: List[Dict[str, Any]] = []
user_pres_list = await self.data.item.get_presents_by_version_user(self.version, data["userId"])
if user_pres_list:
self.logger.debug(f"Found {len(user_pres_list)} possible presents")
for present in user_pres_list:
if (present['startDate'] and present['startDate'].timestamp() > datetime.now().timestamp()):
self.logger.debug(f"Present {present['id']} distribution hasn't started yet (begins {present['startDate']})")
continue # present period hasn't started yet, move onto the next one
if (present['endDate'] and present['endDate'].timestamp() < datetime.now().timestamp()):
self.logger.warn(f"Present {present['id']} ended on {present['endDate']} and should be removed")
continue # present period ended, move onto the next one
test = await self.data.item.get_item(data["userId"], present['itemKind'], present['itemId'])
if not test: # Don't send presents for items the user already has
pres_id = present['itemKind'] * 1000000
pres_id += present['itemId']
items.append({"itemId": pres_id, "itemKind": 4, "stock": present['stock'], "isValid": True})
self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present")
return { "userId": data.get("userId", 0), "length": len(items), "userPresentList": items}
async def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict: async def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict:
return {} return {}

View File

@ -196,10 +196,17 @@ class Mai2DX(Mai2Base):
if "userItemList" in upsert and len(upsert["userItemList"]) > 0: if "userItemList" in upsert and len(upsert["userItemList"]) > 0:
for item in upsert["userItemList"]: for item in upsert["userItemList"]:
if item["itemKind"] == 4:
item_id = item["itemId"] % 1000000
item_kind = item["itemId"] // 1000000
else:
item_id = item["itemId"]
item_kind = item["itemKind"]
await self.data.item.put_item( await self.data.item.put_item(
user_id, user_id,
int(item["itemKind"]), item_kind,
item["itemId"], item_id,
item["stock"], item["stock"],
item["isValid"], item["isValid"],
) )
@ -325,18 +332,39 @@ class Mai2DX(Mai2Base):
} }
async def handle_get_user_item_api_request(self, data: Dict) -> Dict: async def handle_get_user_item_api_request(self, data: Dict) -> Dict:
kind = int(data["nextIndex"] / 10000000000) kind = data["nextIndex"] // 10000000000
next_idx = int(data["nextIndex"] % 10000000000) next_idx = data["nextIndex"] % 10000000000
user_item_list = await self.data.item.get_items(data["userId"], kind)
items: List[Dict[str, Any]] = [] items: List[Dict[str, Any]] = []
for i in range(next_idx, len(user_item_list)):
tmp = user_item_list[i]._asdict() if kind == 4: # presents
tmp.pop("user") user_pres_list = await self.data.item.get_presents_by_version_user(self.version, data["userId"])
tmp.pop("id") if user_pres_list:
items.append(tmp) self.logger.debug(f"Found {len(user_pres_list)} possible presents")
if len(items) >= int(data["maxCount"]): for present in user_pres_list:
break if (present['startDate'] and present['startDate'].timestamp() > datetime.now().timestamp()):
self.logger.debug(f"Present {present['id']} distribution hasn't started yet (begins {present['startDate']})")
continue # present period hasn't started yet, move onto the next one
if (present['endDate'] and present['endDate'].timestamp() < datetime.now().timestamp()):
self.logger.warn(f"Present {present['id']} ended on {present['endDate']} and should be removed")
continue # present period ended, move onto the next one
test = await self.data.item.get_item(data["userId"], present['itemKind'], present['itemId'])
if not test: # Don't send presents for items the user already has
pres_id = present['itemKind'] * 1000000
pres_id += present['itemId']
items.append({"itemId": pres_id, "itemKind": 4, "stock": present['stock'], "isValid": True})
self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present")
else:
user_item_list = await self.data.item.get_items(data["userId"], kind)
for i in range(next_idx, len(user_item_list)):
tmp = user_item_list[i]._asdict()
tmp.pop("user")
tmp.pop("id")
items.append(tmp)
if len(items) >= int(data["maxCount"]):
break
xout = kind * 10000000000 + next_idx + len(items) xout = kind * 10000000000 + next_idx + len(items)

View File

@ -2,8 +2,8 @@ from core.data.schema import BaseData, metadata
from datetime import datetime from datetime import datetime
from typing import Optional, Dict, List from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, or_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BIGINT, INTEGER
from sqlalchemy.schema import ForeignKey from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
@ -198,6 +198,20 @@ print_detail = Table(
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
present = Table(
"mai2_item_present",
metadata,
Column('id', BIGINT, primary_key=True, nullable=False),
Column('version', INTEGER),
Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("itemKind", INTEGER, nullable=False),
Column("itemId", INTEGER, nullable=False),
Column("stock", INTEGER, nullable=False, server_default="1"),
Column("startDate", TIMESTAMP),
Column("endDate", TIMESTAMP),
UniqueConstraint("version", "user", "itemKind", "itemId", name="mai2_item_present_uk"),
mysql_charset="utf8mb4",
)
class Mai2ItemData(BaseData): class Mai2ItemData(BaseData):
async def put_item( async def put_item(
@ -476,7 +490,7 @@ class Mai2ItemData(BaseData):
musicId = music_id musicId = music_id
) )
conflict = sql.on_duplicate_key_do_nothing() conflict = sql.on_duplicate_key_update(musicId = music_id)
result = await self.execute(conflict) result = await self.execute(conflict)
if result: if result:
@ -586,3 +600,49 @@ class Mai2ItemData(BaseData):
) )
return None return None
return result.lastrowid return result.lastrowid
async def put_present(self, item_kind: int, item_id: int, version: int = None, user_id: int = None, start_date: datetime = None, end_date: datetime = None) -> Optional[int]:
sql = insert(present).values(
version = version,
user = user_id,
itemKind = item_kind,
itemId = item_id,
startDate = start_date,
endDate = end_date
)
conflict = sql.on_duplicate_key_update(
startDate = start_date,
endDate = end_date
)
result = await self.execute(conflict)
if result:
return result.lastrowid
self.logger.error(f"Failed to add present item {item_id}!")
async def get_presents_by_user(self, user_id: int = None) -> Optional[List[Row]]:
result = await self.execute(present.select(or_(present.c.user == user_id, present.c.user is None)))
if result:
return result.fetchall()
async def get_presents_by_version(self, ver: int = None) -> Optional[List[Row]]:
result = await self.execute(present.select(or_(present.c.version == ver, present.c.version is None)))
if result:
return result.fetchall()
async def get_presents_by_version_user(self, ver: int = None, user_id: int = None) -> Optional[List[Row]]:
result = await self.execute(present.select(
and_(
or_(present.c.user == user_id, present.c.user == None),
or_(present.c.version == ver, present.c.version == None)
)
))
if result:
return result.fetchall()
async def get_present_by_id(self, present_id: int) -> Optional[Row]:
result = await self.execute(present.select(present.c.id == present_id))
if result:
return result.fetchone()

View File

@ -892,7 +892,7 @@ class Mai2ProfileData(BaseData):
rival = rival_id rival = rival_id
) )
conflict = sql.on_duplicate_key_do_nothing() conflict = sql.on_duplicate_key_update(rival = rival_id)
result = await self.execute(conflict) result = await self.execute(conflict)
if result: if result: