1
0
mirror of synced 2024-11-24 14:30:09 +01:00

Merge branch 'develop' into fork_develop

This commit is contained in:
Dniel97 2023-08-16 11:02:22 +02:00
commit 8a8c0e023e
No known key found for this signature in database
GPG Key ID: 6180B3C768FB2E08
66 changed files with 1561 additions and 457 deletions

View File

@ -0,0 +1,6 @@
from .base import ADBBaseRequest, ADBBaseResponse, ADBHeader, ADBHeaderException, PortalRegStatus, LogStatus, ADBStatus
from .base import CompanyCodes, ReaderFwVer, CMD_CODE_GOODBYE, HEADER_SIZE
from .lookup import ADBLookupRequest, ADBLookupResponse, ADBLookupExResponse
from .campaign import ADBCampaignClearRequest, ADBCampaignClearResponse, ADBCampaignResponse, ADBOldCampaignRequest, ADBOldCampaignResponse
from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookup2Request, ADBFelicaLookup2Response
from .log import ADBLogExRequest, ADBLogRequest, ADBStatusLogRequest

163
core/adb_handlers/base.py Normal file
View File

@ -0,0 +1,163 @@
import struct
from construct import Struct, Int16ul, Int32ul, PaddedString
from enum import Enum
import re
from typing import Union, Final
class LogStatus(Enum):
NONE = 0
START = 1
CONTINUE = 2
END = 3
OTHER = 4
class PortalRegStatus(Enum):
NO_REG = 0
PORTAL = 1
SEGA_ID = 2
class ADBStatus(Enum):
UNKNOWN = 0
GOOD = 1
BAD_AMIE_ID = 2
ALREADY_REG = 3
BAN_SYS_USER = 4
BAN_SYS = 5
BAN_USER = 6
BAN_GEN = 7
LOCK_SYS_USER = 8
LOCK_SYS = 9
LOCK_USER = 10
class CompanyCodes(Enum):
NONE = 0
SEGA = 1
BAMCO = 2
KONAMI = 3
TAITO = 4
class ReaderFwVer(Enum): # Newer readers use a singly byte value
NONE = 0
TN32_10 = 1
TN32_12 = 2
OTHER = 9
def __str__(self) -> str:
if self == self.TN32_10:
return "TN32MSEC003S F/W Ver1.0"
elif self == self.TN32_12:
return "TN32MSEC003S F/W Ver1.2"
elif self == self.NONE:
return "Not Specified"
elif self == self.OTHER:
return "Unknown/Other"
else:
raise ValueError(f"Bad ReaderFwVer value {self.value}")
@classmethod
def from_byte(self, byte: bytes) -> Union["ReaderFwVer", int]:
try:
i = int.from_bytes(byte, 'little')
try:
return ReaderFwVer(i)
except ValueError:
return i
except TypeError:
return 0
class ADBHeaderException(Exception):
pass
HEADER_SIZE: Final[int] = 0x20
CMD_CODE_GOODBYE: Final[int] = 0x66
# everything is LE
class ADBHeader:
def __init__(self, magic: int, protocol_ver: int, cmd: int, length: int, status: int, game_id: Union[str, bytes], store_id: int, keychip_id: Union[str, bytes]) -> None:
self.magic = magic # u16
self.protocol_ver = protocol_ver # u16
self.cmd = cmd # u16
self.length = length # u16
self.status = ADBStatus(status) # u16
self.game_id = game_id # 4 char + \x00
self.store_id = store_id # u32
self.keychip_id = keychip_id# 11 char + \x00
if type(self.game_id) == bytes:
self.game_id = self.game_id.decode()
if type(self.keychip_id) == bytes:
self.keychip_id = self.keychip_id.decode()
self.game_id = self.game_id.replace("\0", "")
self.keychip_id = self.keychip_id.replace("\0", "")
if self.cmd != CMD_CODE_GOODBYE: # Games for some reason send no data with goodbye
self.validate()
@classmethod
def from_data(cls, data: bytes) -> "ADBHeader":
magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id = struct.unpack_from("<5H6sI12s", data)
head = cls(magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id)
if head.length != len(data):
raise ADBHeaderException(f"Length is incorrect! Expect {head.length}, got {len(data)}")
return head
def validate(self) -> bool:
if self.magic != 0xa13e:
raise ADBHeaderException(f"Magic {self.magic} != 0xa13e")
if self.protocol_ver < 0x1000:
raise ADBHeaderException(f"Protocol version {hex(self.protocol_ver)} is invalid!")
if re.fullmatch(r"^S[0-9A-Z]{3}[P]?$", self.game_id) is None:
raise ADBHeaderException(f"Game ID {self.game_id} is invalid!")
if self.store_id == 0:
raise ADBHeaderException(f"Store ID cannot be 0!")
if re.fullmatch(r"^A[0-9]{2}[E|X][0-9]{2}[A-HJ-NP-Z][0-9]{4}$", self.keychip_id) is None:
raise ADBHeaderException(f"Keychip ID {self.keychip_id} is invalid!")
return True
def make(self) -> bytes:
resp_struct = Struct(
"magic" / Int16ul,
"unknown" / Int16ul,
"response_code" / Int16ul,
"length" / Int16ul,
"status" / Int16ul,
"game_id" / PaddedString(6, 'utf_8'),
"store_id" / Int32ul,
"keychip_id" / PaddedString(12, 'utf_8'),
)
return resp_struct.build(dict(
magic=self.magic,
unknown=self.protocol_ver,
response_code=self.cmd,
length=self.length,
status=self.status.value,
game_id = self.game_id,
store_id = self.store_id,
keychip_id = self.keychip_id,
))
class ADBBaseRequest:
def __init__(self, data: bytes) -> None:
self.head = ADBHeader.from_data(data)
class ADBBaseResponse:
def __init__(self, code: int = 0, length: int = 0x20, status: int = 1, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888") -> None:
self.head = ADBHeader(0xa13e, 0x3087, code, length, status, game_id, store_id, keychip_id)
def append_padding(self, data: bytes):
"""Appends 0s to the end of the data until it's at the correct size"""
padding_size = self.head.length - len(data)
data += bytes(padding_size)
return data
def make(self) -> bytes:
return self.head.make()

View File

@ -0,0 +1,114 @@
from construct import Struct, Int16ul, Padding, Bytes, Int32ul, Int32sl
from .base import *
class Campaign:
def __init__(self) -> None:
self.id = 0
self.name = ""
self.announce_date = 0
self.start_date = 0
self.end_date = 0
self.distrib_start_date = 0
self.distrib_end_date = 0
def make(self) -> bytes:
name_padding = bytes(128 - len(self.name))
return Struct(
"id" / Int32ul,
"name" / Bytes(128),
"announce_date" / Int32ul,
"start_date" / Int32ul,
"end_date" / Int32ul,
"distrib_start_date" / Int32ul,
"distrib_end_date" / Int32ul,
Padding(8),
).build(dict(
id = self.id,
name = self.name.encode() + name_padding,
announce_date = self.announce_date,
start_date = self.start_date,
end_date = self.end_date,
distrib_start_date = self.distrib_start_date,
distrib_end_date = self.distrib_end_date,
))
class CampaignClear:
def __init__(self) -> None:
self.id = 0
self.entry_flag = 0
self.clear_flag = 0
def make(self) -> bytes:
return Struct(
"id" / Int32ul,
"entry_flag" / Int32ul,
"clear_flag" / Int32ul,
Padding(4),
).build(dict(
id = self.id,
entry_flag = self.entry_flag,
clear_flag = self.clear_flag,
))
class ADBCampaignResponse(ADBBaseResponse):
def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x0C, length: int = 0x200, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.campaigns = [Campaign(), Campaign(), Campaign()]
def make(self) -> bytes:
body = b""
for c in self.campaigns:
body += c.make()
self.head.length = HEADER_SIZE + len(body)
return self.head.make() + body
class ADBOldCampaignRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.campaign_id = struct.unpack_from("<I", data, 0x20)
class ADBOldCampaignResponse(ADBBaseResponse):
def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x0C, length: int = 0x30, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.info0 = 0
self.info1 = 0
self.info2 = 0
self.info3 = 0
def make(self) -> bytes:
resp_struct = Struct(
"info0" / Int32sl,
"info1" / Int32sl,
"info2" / Int32sl,
"info3" / Int32sl,
).build(
info0 = self.info0,
info1 = self.info1,
info2 = self.info2,
info3 = self.info3,
)
self.head.length = HEADER_SIZE + len(resp_struct)
return self.head.make() + resp_struct
class ADBCampaignClearRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.aime_id = struct.unpack_from("<i", data, 0x20)
class ADBCampaignClearResponse(ADBBaseResponse):
def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x0E, length: int = 0x50, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.campaign_clear_status = [CampaignClear(), CampaignClear(), CampaignClear()]
def make(self) -> bytes:
body = b""
for c in self.campaign_clear_status:
body += c.make()
self.head.length = HEADER_SIZE + len(body)
return self.head.make() + body

View File

@ -0,0 +1,72 @@
from construct import Struct, Int32sl, Padding, Int8ub, Int16sl
from typing import Union
from .base import *
class ADBFelicaLookupRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
idm, pmm = struct.unpack_from(">QQ", data, 0x20)
self.idm = hex(idm)[2:].upper()
self.pmm = hex(pmm)[2:].upper()
class ADBFelicaLookupResponse(ADBBaseResponse):
def __init__(self, access_code: str = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.access_code = access_code if access_code is not None else "00000000000000000000"
def make(self) -> bytes:
resp_struct = Struct(
"felica_idx" / Int32ul,
"access_code" / Int8ub[10],
Padding(2)
).build(dict(
felica_idx = 0,
access_code = bytes.fromhex(self.access_code)
))
self.head.length = HEADER_SIZE + len(resp_struct)
return self.head.make() + resp_struct
class ADBFelicaLookup2Request(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.random = struct.unpack_from("<16s", data, 0x20)[0]
idm, pmm = struct.unpack_from(">QQ", data, 0x30)
self.card_key_ver, self.write_ct, self.maca, company, fw_ver, self.dfc = struct.unpack_from("<16s16sQccH", data, 0x40)
self.idm = hex(idm)[2:].upper()
self.pmm = hex(pmm)[2:].upper()
self.company = CompanyCodes(int.from_bytes(company, 'little'))
self.fw_ver = ReaderFwVer.from_byte(fw_ver)
class ADBFelicaLookup2Response(ADBBaseResponse):
def __init__(self, user_id: Union[int, None] = None, access_code: Union[str, None] = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x12, length: int = 0x130, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.user_id = user_id if user_id is not None else -1
self.access_code = access_code if access_code is not None else "00000000000000000000"
self.company = CompanyCodes.SEGA
self.portal_status = PortalRegStatus.NO_REG
def make(self) -> bytes:
resp_struct = Struct(
"user_id" / Int32sl,
"relation1" / Int32sl,
"relation2" / Int32sl,
"access_code" / Int8ub[10],
"portal_status" / Int8ub,
"company_code" / Int8ub,
Padding(8),
"auth_key" / Int8ub[256],
).build(dict(
user_id = self.user_id,
relation1 = -1, # Unsupported
relation2 = -1, # Unsupported
access_code = bytes.fromhex(self.access_code),
portal_status = self.portal_status.value,
company_code = self.company.value,
auth_key = [0] * 256 # Unsupported
))
self.head.length = HEADER_SIZE + len(resp_struct)
return self.head.make() + resp_struct

23
core/adb_handlers/log.py Normal file
View File

@ -0,0 +1,23 @@
from construct import Struct, Int32sl, Padding, Int8sl
from typing import Union
from .base import *
class ADBStatusLogRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.aime_id, status = struct.unpack_from("<II", data, 0x20)
self.status = LogStatus(status)
class ADBLogRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.aime_id, status, self.user_id, self.credit_ct, self.bet_ct, self.won_ct = struct.unpack_from("<IIQiii", data, 0x20)
self.status = LogStatus(status)
class ADBLogExRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.aime_id, status, self.user_id, self.credit_ct, self.bet_ct, self.won_ct, self.local_time, \
self.tseq, self.place_id, self.num_logs = struct.unpack_from("<IIQiii4xQiII", data, 0x20)
self.status = LogStatus(status)

View File

@ -0,0 +1,70 @@
from construct import Struct, Int32sl, Padding, Int8sl
from typing import Union
from .base import *
class ADBLookupException(Exception):
pass
class ADBLookupRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.access_code = data[0x20:0x2A].hex()
company_code, fw_version, self.serial_number = struct.unpack_from("<bbI", data, 0x2A)
try:
self.company_code = CompanyCodes(company_code)
except ValueError as e:
raise ADBLookupException(f"Invalid company code - {e}")
self.fw_version = ReaderFwVer.from_byte(fw_version)
class ADBLookupResponse(ADBBaseResponse):
def __init__(self, user_id: Union[int, None], game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x06, length: int = 0x30, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.user_id = user_id if user_id is not None else -1
self.portal_reg = PortalRegStatus.NO_REG
def make(self):
resp_struct = Struct(
"user_id" / Int32sl,
"portal_reg" / Int8sl,
Padding(11)
)
body = resp_struct.build(dict(
user_id = self.user_id,
portal_reg = self.portal_reg.value
))
self.head.length = HEADER_SIZE + len(body)
return self.head.make() + body
class ADBLookupExResponse(ADBBaseResponse):
def __init__(self, user_id: Union[int, None], game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888",
code: int = 0x10, length: int = 0x130, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.user_id = user_id if user_id is not None else -1
self.portal_reg = PortalRegStatus.NO_REG
def make(self):
resp_struct = Struct(
"user_id" / Int32sl,
"portal_reg" / Int8sl,
Padding(3),
"auth_key" / Int8sl[256],
"relation1" / Int32sl,
"relation2" / Int32sl,
)
body = resp_struct.build(dict(
user_id = self.user_id,
portal_reg = self.portal_reg.value,
auth_key = [0] * 256,
relation1 = -1,
relation2 = -1
))
self.head.length = HEADER_SIZE + len(body)
return self.head.make() + body

View File

@ -2,27 +2,17 @@ from twisted.internet.protocol import Factory, Protocol
import logging, coloredlogs
from Crypto.Cipher import AES
import struct
from typing import Dict, Any
from typing import Dict, Tuple, Callable, Union
from typing_extensions import Final
from logging.handlers import TimedRotatingFileHandler
from core.config import CoreConfig
from core.data import Data
from .adb_handlers import *
class AimedbProtocol(Protocol):
AIMEDB_RESPONSE_CODES = {
"felica_lookup": 0x03,
"lookup": 0x06,
"log": 0x0A,
"campaign": 0x0C,
"touch": 0x0E,
"lookup2": 0x10,
"felica_lookup2": 0x12,
"log2": 0x14,
"hello": 0x65,
}
request_list: Dict[int, Any] = {}
request_list: Dict[int, Tuple[Callable[[bytes, int], Union[ADBBaseResponse, bytes]], int, str]] = {}
def __init__(self, core_cfg: CoreConfig) -> None:
self.logger = logging.getLogger("aimedb")
@ -32,16 +22,27 @@ class AimedbProtocol(Protocol):
self.logger.error("!!!KEY NOT SET!!!")
exit(1)
self.request_list[0x01] = self.handle_felica_lookup
self.request_list[0x04] = self.handle_lookup
self.request_list[0x05] = self.handle_register
self.request_list[0x09] = self.handle_log
self.request_list[0x0B] = self.handle_campaign
self.request_list[0x0D] = self.handle_touch
self.request_list[0x0F] = self.handle_lookup2
self.request_list[0x11] = self.handle_felica_lookup2
self.request_list[0x13] = self.handle_log2
self.request_list[0x64] = self.handle_hello
self.register_handler(0x01, 0x03, self.handle_felica_lookup, 'felica_lookup')
self.register_handler(0x02, 0x03, self.handle_felica_register, 'felica_register')
self.register_handler(0x04, 0x06, self.handle_lookup, 'lookup')
self.register_handler(0x05, 0x06, self.handle_register, 'register')
self.register_handler(0x07, 0x08, self.handle_status_log, 'status_log')
self.register_handler(0x09, 0x0A, self.handle_log, 'aime_log')
self.register_handler(0x0B, 0x0C, self.handle_campaign, 'campaign')
self.register_handler(0x0D, 0x0E, self.handle_campaign_clear, 'campaign_clear')
self.register_handler(0x0F, 0x10, self.handle_lookup_ex, 'lookup_ex')
self.register_handler(0x11, 0x12, self.handle_felica_lookup_ex, 'felica_lookup_ex')
self.register_handler(0x13, 0x14, self.handle_log_ex, 'aime_log_ex')
self.register_handler(0x64, 0x65, self.handle_hello, 'hello')
self.register_handler(0x66, 0, self.handle_goodbye, 'goodbye')
def register_handler(self, cmd: int, resp:int, handler: Callable[[bytes, int], Union[ADBBaseResponse, bytes]], name: str) -> None:
self.request_list[cmd] = (handler, resp, name)
def append_padding(self, data: bytes):
"""Appends 0s to the end of the data until it's at the correct size"""
@ -63,202 +64,226 @@ class AimedbProtocol(Protocol):
try:
decrypted = cipher.decrypt(data)
except Exception:
self.logger.error(f"Failed to decrypt {data.hex()}")
except Exception as e:
self.logger.error(f"Failed to decrypt {data.hex()} because {e}")
return None
self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}")
if not decrypted[1] == 0xA1 and not decrypted[0] == 0x3E:
self.logger.error(f"Bad magic")
return None
try:
head = ADBHeader.from_data(decrypted)
except ADBHeaderException as e:
self.logger.error(f"Error parsing ADB header: {e}")
try:
encrypted = cipher.encrypt(ADBBaseResponse().make())
self.transport.write(encrypted)
req_code = decrypted[4]
if req_code == 0x66:
self.logger.info(f"goodbye from {self.transport.getPeer().host}")
self.transport.loseConnection()
except Exception as e:
self.logger.error(f"Failed to encrypt default response because {e}")
return
try:
resp = self.request_list[req_code](decrypted)
encrypted = cipher.encrypt(resp)
self.logger.debug(f"Response {resp.hex()}")
handler, resp_code, name = self.request_list.get(head.cmd, (self.handle_default, None, 'default'))
if resp_code is None:
self.logger.warning(f"No handler for cmd {hex(head.cmd)}")
elif resp_code > 0:
self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {self.transport.getPeer().host}")
resp = handler(decrypted, resp_code)
if type(resp) == ADBBaseResponse or issubclass(type(resp), ADBBaseResponse):
resp_bytes = resp.make()
if len(resp_bytes) != resp.head.length:
resp_bytes = self.append_padding(resp_bytes)
elif type(resp) == bytes:
resp_bytes = resp
elif resp is None: # Nothing to send, probably a goodbye
return
else:
raise TypeError(f"Unsupported type returned by ADB handler for {name}: {type(resp)}")
try:
encrypted = cipher.encrypt(resp_bytes)
self.logger.debug(f"Response {resp_bytes.hex()}")
self.transport.write(encrypted)
except KeyError:
self.logger.error(f"Unknown command code {hex(req_code)}")
return None
except Exception as e:
self.logger.error(f"Failed to encrypt {resp_bytes.hex()} because {e}")
def handle_default(self, data: bytes, resp_code: int, length: int = 0x20) -> ADBBaseResponse:
req = ADBHeader.from_data(data)
return ADBBaseResponse(resp_code, length, 1, req.game_id, req.store_id, req.keychip_id)
except ValueError as e:
self.logger.error(f"Failed to encrypt {resp.hex()} because {e}")
return None
def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse:
return self.handle_default(data, resp_code)
def handle_campaign(self, data: bytes) -> bytes:
self.logger.info(f"campaign from {self.transport.getPeer().host}")
ret = struct.pack(
"<5H",
0xA13E,
0x3087,
self.AIMEDB_RESPONSE_CODES["campaign"],
0x0200,
0x0001,
)
return self.append_padding(ret)
def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse:
h = ADBHeader.from_data(data)
if h.protocol_ver >= 0x3030:
req = h
resp = ADBCampaignResponse(req.game_id, req.store_id, req.keychip_id)
def handle_hello(self, data: bytes) -> bytes:
self.logger.info(f"hello from {self.transport.getPeer().host}")
ret = struct.pack(
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["hello"], 0x0020, 0x0001
)
return self.append_padding(ret)
def handle_lookup(self, data: bytes) -> bytes:
luid = data[0x20:0x2A].hex()
user_id = self.data.card.get_user_id_from_card(access_code=luid)
if user_id is None:
user_id = -1
self.logger.info(
f"lookup from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}"
)
ret = struct.pack(
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0130, 0x0001
)
ret += bytes(0x20 - len(ret))
if user_id is None:
ret += struct.pack("<iH", -1, 0)
else:
ret += struct.pack("<l", user_id)
return self.append_padding(ret)
req = ADBOldCampaignRequest(data)
self.logger.info(f"Legacy campaign request for campaign {req.campaign_id} (protocol version {hex(h.protocol_ver)})")
resp = ADBOldCampaignResponse(req.head.game_id, req.head.store_id, req.head.keychip_id)
# We don't currently support campaigns
return resp
def handle_lookup2(self, data: bytes) -> bytes:
self.logger.info(f"lookup2")
def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse:
req = ADBLookupRequest(data)
user_id = self.data.card.get_user_id_from_card(req.access_code)
ret = bytearray(self.handle_lookup(data))
ret[4] = self.AIMEDB_RESPONSE_CODES["lookup2"]
return bytes(ret)
def handle_felica_lookup(self, data: bytes) -> bytes:
idm = data[0x20:0x28].hex()
pmm = data[0x28:0x30].hex()
access_code = self.data.card.to_access_code(idm)
ret = ADBLookupResponse(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id)
self.logger.info(
f"felica_lookup from {self.transport.getPeer().host}: idm {idm} pmm {pmm} -> access_code {access_code}"
f"access_code {req.access_code} -> user_id {ret.user_id}"
)
return ret
ret = struct.pack(
"<5H",
0xA13E,
0x3087,
self.AIMEDB_RESPONSE_CODES["felica_lookup"],
0x0030,
0x0001,
def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse:
req = ADBLookupRequest(data)
user_id = self.data.card.get_user_id_from_card(req.access_code)
ret = ADBLookupExResponse(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id)
self.logger.info(
f"access_code {req.access_code} -> user_id {ret.user_id}"
)
ret += bytes(26)
ret += bytes.fromhex(access_code)
return ret
return self.append_padding(ret)
def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes:
"""
On official, I think a card has to be registered for this to actually work, but
I'm making the executive decision to not implement that and just kick back our
faux generated access code. The real felica IDm -> access code conversion is done
on the ADB server, which we do not and will not ever have access to. Because we can
assure that all IDms will be unique, this basic 0-padded hex -> int conversion will
be fine.
"""
req = ADBFelicaLookupRequest(data)
ac = self.data.card.to_access_code(req.idm)
self.logger.info(
f"idm {req.idm} ipm {req.pmm} -> access_code {ac}"
)
return ADBFelicaLookupResponse(ac, req.head.game_id, req.head.store_id, req.head.keychip_id)
def handle_felica_lookup2(self, data: bytes) -> bytes:
idm = data[0x30:0x38].hex()
pmm = data[0x38:0x40].hex()
access_code = self.data.card.to_access_code(idm)
def handle_felica_register(self, data: bytes, resp_code: int) -> bytes:
"""
I've never seen this used.
"""
req = ADBFelicaLookupRequest(data)
ac = self.data.card.to_access_code(req.idm)
if self.config.server.allow_user_registration:
user_id = self.data.user.create_user()
if user_id is None:
self.logger.error("Failed to register user!")
user_id = -1
else:
card_id = self.data.card.create_card(user_id, ac)
if card_id is None:
self.logger.error("Failed to register card!")
user_id = -1
self.logger.info(
f"Register access code {ac} (IDm: {req.idm} PMm: {req.pmm}) -> user_id {user_id}"
)
else:
self.logger.info(
f"Registration blocked!: access code {ac} (IDm: {req.idm} PMm: {req.pmm})"
)
return ADBFelicaLookupResponse(ac, req.head.game_id, req.head.store_id, req.head.keychip_id)
def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes:
req = ADBFelicaLookup2Request(data)
access_code = self.data.card.to_access_code(req.idm)
user_id = self.data.card.get_user_id_from_card(access_code=access_code)
if user_id is None:
user_id = -1
self.logger.info(
f"felica_lookup2 from {self.transport.getPeer().host}: idm {idm} ipm {pmm} -> access_code {access_code} user_id {user_id}"
f"idm {req.idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}"
)
ret = struct.pack(
"<5H",
0xA13E,
0x3087,
self.AIMEDB_RESPONSE_CODES["felica_lookup2"],
0x0140,
0x0001,
)
ret += bytes(22)
ret += struct.pack("<lq", user_id, -1) # first -1 is ext_id, 3rd is access code
ret += bytes.fromhex(access_code)
ret += struct.pack("<l", 1)
return ADBFelicaLookup2Response(user_id, access_code, req.head.game_id, req.head.store_id, req.head.keychip_id)
return self.append_padding(ret)
def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse:
req = ADBCampaignClearRequest(data)
def handle_touch(self, data: bytes) -> bytes:
self.logger.info(f"touch from {self.transport.getPeer().host}")
ret = struct.pack(
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["touch"], 0x0050, 0x0001
)
ret += bytes(5)
ret += struct.pack("<3H", 0x6F, 0, 1)
resp = ADBCampaignClearResponse(req.head.game_id, req.head.store_id, req.head.keychip_id)
return self.append_padding(ret)
# We don't support campaign stuff
return resp
def handle_register(self, data: bytes) -> bytes:
luid = data[0x20:0x2A].hex()
def handle_register(self, data: bytes, resp_code: int) -> bytes:
req = ADBLookupRequest(data)
user_id = -1
if self.config.server.allow_user_registration:
user_id = self.data.user.create_user()
if user_id is None:
user_id = -1
self.logger.error("Failed to register user!")
user_id = -1
else:
card_id = self.data.card.create_card(user_id, luid)
card_id = self.data.card.create_card(user_id, req.access_code)
if card_id is None:
user_id = -1
self.logger.error("Failed to register card!")
user_id = -1
self.logger.info(
f"register from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}"
f"Register access code {req.access_code} -> user_id {user_id}"
)
else:
self.logger.info(
f"register from {self.transport.getPeer().host} blocked!: luid {luid}"
f"Registration blocked!: access code {req.access_code}"
)
user_id = -1
ret = struct.pack(
"<5H",
0xA13E,
0x3087,
self.AIMEDB_RESPONSE_CODES["lookup"],
0x0030,
0x0001 if user_id > -1 else 0,
)
ret += bytes(0x20 - len(ret))
ret += struct.pack("<l", user_id)
resp = ADBLookupResponse(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id)
if resp.user_id <= 0:
resp.head.status = ADBStatus.BAN_SYS # Closest we can get to a "You cannot register"
return self.append_padding(ret)
return resp
def handle_log(self, data: bytes) -> bytes:
# TODO: Save aimedb logs
self.logger.info(f"log from {self.transport.getPeer().host}")
ret = struct.pack(
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["log"], 0x0020, 0x0001
)
return self.append_padding(ret)
# TODO: Save these in some capacity, as deemed relevant
def handle_status_log(self, data: bytes, resp_code: int) -> bytes:
req = ADBStatusLogRequest(data)
self.logger.info(f"User {req.aime_id} logged {req.status.name} event")
return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id)
def handle_log2(self, data: bytes) -> bytes:
self.logger.info(f"log2 from {self.transport.getPeer().host}")
ret = struct.pack(
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["log2"], 0x0040, 0x0001
)
ret += bytes(22)
ret += struct.pack("H", 1)
def handle_log(self, data: bytes, resp_code: int) -> bytes:
req = ADBLogRequest(data)
self.logger.info(f"User {req.aime_id} logged {req.status.name} event, credit_ct: {req.credit_ct} bet_ct: {req.bet_ct} won_ct: {req.won_ct}")
return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id)
return self.append_padding(ret)
def handle_log_ex(self, data: bytes, resp_code: int) -> bytes:
req = ADBLogExRequest(data)
self.logger.info(f"User {req.aime_id} logged {req.status.name} event, credit_ct: {req.credit_ct} bet_ct: {req.bet_ct} won_ct: {req.won_ct}")
return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id)
def handle_goodbye(self, data: bytes, resp_code: int) -> None:
self.logger.info(f"goodbye from {self.transport.getPeer().host}")
self.transport.loseConnection()
return
class AimedbFactory(Factory):
protocol = AimedbProtocol

View File

@ -6,6 +6,8 @@ from datetime import datetime
import pytz
import base64
import zlib
import json
from enum import Enum
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA
from Crypto.Signature import PKCS1_v1_5
@ -18,6 +20,15 @@ from core.utils import Utils
from core.data import Data
from core.const import *
class DLIMG_TYPE(Enum):
app = 0
opt = 1
class ALLNET_STAT(Enum):
ok = 0
bad_game = -1
bad_machine = -2
bad_shop = -3
class AllnetServlet:
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
@ -97,33 +108,7 @@ class AllnetServlet:
else:
resp = AllnetPowerOnResponse()
self.logger.debug(f"Allnet request: {vars(req)}")
if req.game_id not in self.uri_registry:
if not self.config.server.is_develop:
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
self.data.base.log_event(
"allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg
)
self.logger.warn(msg)
resp.stat = -1
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
else:
self.logger.info(
f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}"
)
resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/"
resp.host = f"{self.config.title.hostname}:{self.config.title.port}"
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict))
self.logger.debug(f"Allnet response: {resp_str}")
return (resp_str + "\n").encode("utf-8")
resp.uri, resp.host = self.uri_registry[req.game_id]
self.logger.debug(f"Allnet request: {vars(req)}")
machine = self.data.arcade.get_machine(req.serial)
if machine is None and not self.config.server.allow_unregistered_serials:
@ -131,14 +116,38 @@ class AllnetServlet:
self.data.base.log_event(
"allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg
)
self.logger.warn(msg)
self.logger.warning(msg)
resp.stat = -2
resp.stat = ALLNET_STAT.bad_machine.value
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
if machine is not None:
arcade = self.data.arcade.get_arcade(machine["arcade"])
if self.config.server.check_arcade_ip:
if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip:
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})."
self.data.base.log_event(
"allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg
)
self.logger.warning(msg)
resp.stat = ALLNET_STAT.bad_shop.value
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
elif not arcade["ip"] or arcade["ip"] is None and self.config.server.strict_ip_checking:
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)."
self.data.base.log_event(
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg
)
self.logger.warning(msg)
resp.stat = ALLNET_STAT.bad_shop.value
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
country = (
arcade["country"] if machine["country"] is None else machine["country"]
)
@ -169,6 +178,33 @@ class AllnetServlet:
resp.client_timezone = (
arcade["timezone"] if arcade["timezone"] is not None else "+0900"
)
if req.game_id not in self.uri_registry:
if not self.config.server.is_develop:
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
self.data.base.log_event(
"allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg
)
self.logger.warning(msg)
resp.stat = ALLNET_STAT.bad_game.value
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
else:
self.logger.info(
f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}"
)
resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/"
resp.host = f"{self.config.title.hostname}:{self.config.title.port}"
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict))
self.logger.debug(f"Allnet response: {resp_str}")
return (resp_str + "\n").encode("utf-8")
resp.uri, resp.host = self.uri_registry[req.game_id]
int_ver = req.ver.replace(".", "")
resp.uri = resp.uri.replace("$v", int_ver)
@ -241,6 +277,7 @@ class AllnetServlet:
if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"):
self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful")
self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}")
return open(
f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb"
).read()
@ -249,10 +286,31 @@ class AllnetServlet:
return b""
def handle_dlorder_report(self, request: Request, match: Dict) -> bytes:
self.logger.info(
f"DLI Report from {Utils.get_ip_addr(request)}: {request.content.getvalue()}"
)
return b""
req_raw = request.content.getvalue()
try:
req_dict: Dict = json.loads(req_raw)
except Exception as e:
self.logger.warning(f"Failed to parse DL Report: {e}")
return "NG"
dl_data_type = DLIMG_TYPE.app
dl_data = req_dict.get("appimage", {})
if dl_data is None or not dl_data:
dl_data_type = DLIMG_TYPE.opt
dl_data = req_dict.get("optimage", {})
if dl_data is None or not dl_data:
self.logger.warning(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage")
return "NG"
dl_report_data = DLReport(dl_data, dl_data_type)
if not dl_report_data.validate():
self.logger.warning(f"Failed to parse DL Report: Invalid format - {dl_report_data.err}")
return "NG"
return "OK"
def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes:
req_data = request.content.getvalue()
@ -307,7 +365,7 @@ class AllnetServlet:
self.data.base.log_event(
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg
)
self.logger.warn(msg)
self.logger.warning(msg)
resp = BillingResponse("", "", "", "")
resp.result = "1"
@ -529,3 +587,86 @@ class AllnetRequestException(Exception):
def __init__(self, message="") -> None:
self.message = message
super().__init__(self.message)
class DLReport:
def __init__(self, data: Dict, report_type: DLIMG_TYPE) -> None:
self.serial = data.get("serial")
self.dfl = data.get("dfl")
self.wfl = data.get("wfl")
self.tsc = data.get("tsc")
self.tdsc = data.get("tdsc")
self.at = data.get("at")
self.ot = data.get("ot")
self.rt = data.get("rt")
self.as_ = data.get("as")
self.rf_state = data.get("rf_state")
self.gd = data.get("gd")
self.dav = data.get("dav")
self.wdav = data.get("wdav") # app only
self.dov = data.get("dov")
self.wdov = data.get("wdov") # app only
self.__type = report_type
self.err = ""
def validate(self) -> bool:
if self.serial is None:
self.err = "serial not provided"
return False
if self.dfl is None:
self.err = "dfl not provided"
return False
if self.wfl is None:
self.err = "wfl not provided"
return False
if self.tsc is None:
self.err = "tsc not provided"
return False
if self.tdsc is None:
self.err = "tdsc not provided"
return False
if self.at is None:
self.err = "at not provided"
return False
if self.ot is None:
self.err = "ot not provided"
return False
if self.rt is None:
self.err = "rt not provided"
return False
if self.as_ is None:
self.err = "as not provided"
return False
if self.rf_state is None:
self.err = "rf_state not provided"
return False
if self.gd is None:
self.err = "gd not provided"
return False
if self.dav is None:
self.err = "dav not provided"
return False
if self.dov is None:
self.err = "dov not provided"
return False
if (self.wdav is None or self.wdov is None) and self.__type == DLIMG_TYPE.app:
self.err = "wdav or wdov not provided in app image"
return False
if (self.wdav is not None or self.wdov is not None) and self.__type == DLIMG_TYPE.opt:
self.err = "wdav or wdov provided in opt image"
return False
return True

View File

@ -48,6 +48,18 @@ class ServerConfig:
self.__config, "core", "server", "log_dir", default="logs"
)
@property
def check_arcade_ip(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "core", "server", "check_arcade_ip", default=False
)
@property
def strict_ip_checking(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "core", "server", "strict_ip_checking", default=False
)
class TitleConfig:
def __init__(self, parent_config: "CoreConfig") -> None:

View File

@ -15,7 +15,7 @@ from core.utils import Utils
class Data:
current_schema_version = 4
current_schema_version = 6
engine = None
session = None
user = None
@ -163,7 +163,7 @@ class Data:
version = mod.current_schema_version
else:
self.logger.warn(
self.logger.warning(
f"current_schema_version not found for {folder}"
)
@ -171,7 +171,7 @@ class Data:
version = self.current_schema_version
if version is None:
self.logger.warn(
self.logger.warning(
f"Could not determine latest version for {game}, please specify --version"
)
@ -254,7 +254,7 @@ class Data:
self.logger.error(f"Failed to create card for owner with id {user_id}")
return
self.logger.warn(
self.logger.warning(
f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!"
)
@ -269,7 +269,7 @@ class Data:
return
if not should_force:
self.logger.warn(
self.logger.warning(
f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag."
f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
)
@ -307,7 +307,7 @@ class Data:
def autoupgrade(self) -> None:
all_game_versions = self.base.get_all_schema_vers()
if all_game_versions is None:
self.logger.warn("Failed to get schema versions")
self.logger.warning("Failed to get schema versions")
return
all_games = Utils.get_all_titles()

View File

@ -1,9 +1,10 @@
from typing import Optional, Dict
from sqlalchemy import Table, Column
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, and_, or_
from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint
from sqlalchemy.types import Integer, String, Boolean
from sqlalchemy.types import Integer, String, Boolean, JSON
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from sqlalchemy.engine import Row
import re
from core.data.schema.base import BaseData, metadata
@ -21,6 +22,7 @@ arcade = Table(
Column("city", String(255)),
Column("region_id", Integer),
Column("timezone", String(255)),
Column("ip", String(39)),
mysql_charset="utf8mb4",
)
@ -40,6 +42,9 @@ machine = Table(
Column("timezone", String(255)),
Column("ota_enable", Boolean),
Column("is_cab", Boolean),
Column("memo", String(255)),
Column("is_cab", Boolean),
Column("data", JSON),
mysql_charset="utf8mb4",
)
@ -65,7 +70,7 @@ arcade_owner = Table(
class ArcadeData(BaseData):
def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]:
def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]:
if serial is not None:
serial = serial.replace("-", "")
if len(serial) == 11:
@ -130,12 +135,19 @@ class ArcadeData(BaseData):
f"Failed to update board id for machine {machine_id} -> {boardid}"
)
def get_arcade(self, id: int) -> Optional[Dict]:
def get_arcade(self, id: int) -> Optional[Row]:
sql = arcade.select(arcade.c.id == id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_arcade_machines(self, id: int) -> Optional[List[Row]]:
sql = machine.select(machine.c.arcade == id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_arcade(
self,
@ -165,7 +177,21 @@ class ArcadeData(BaseData):
return None
return result.lastrowid
def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]:
def get_arcades_managed_by_user(self, user_id: int) -> Optional[List[Row]]:
sql = select(arcade).join(arcade_owner, arcade_owner.c.arcade == arcade.c.id).where(arcade_owner.c.user == user_id)
result = self.execute(sql)
if result is None:
return False
return result.fetchall()
def get_manager_permissions(self, user_id: int, arcade_id: int) -> Optional[int]:
sql = select(arcade_owner.c.permissions).where(and_(arcade_owner.c.user == user_id, arcade_owner.c.arcade == arcade_id))
result = self.execute(sql)
if result is None:
return False
return result.fetchone()
def get_arcade_owners(self, arcade_id: int) -> Optional[Row]:
sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id)
result = self.execute(sql)
@ -187,33 +213,14 @@ class ArcadeData(BaseData):
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
def validate_keychip_format(self, serial: str) -> bool:
serial = serial.replace("-", "")
if len(serial) != 11 or len(serial) != 15:
self.logger.error(
f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})"
)
if re.fullmatch(r"^A[0-9]{2}[E|X][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None:
return False
platform_code = serial[:4]
platform_rev = serial[4:6]
const_a = serial[6]
num = serial[7:11]
append = serial[11:15]
if re.match("A[7|6]\d[E|X][0|1][0|1|2]A\d{4,8}", serial) is None:
self.logger.error(f"Serial validate failed: {serial} failed regex")
return False
if len(append) != 0 or len(append) != 4:
self.logger.error(
f"Serial validate failed: {serial} had malformed append {append}"
)
return False
if len(num) != 4:
self.logger.error(
f"Serial validate failed: {serial} had malformed number {num}"
)
return False
return True
def find_arcade_by_name(self, name: str) -> List[Row]:
sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%")))
result = self.execute(sql)
if result is None:
return False
return result.fetchall()

View File

@ -107,3 +107,17 @@ class UserData(BaseData):
if result is None:
return None
return result.fetchall()
def find_user_by_email(self, email: str) -> Row:
sql = select(aime_user).where(aime_user.c.email == email)
result = self.execute(sql)
if result is None:
return False
return result.fetchone()
def find_user_by_username(self, username: str) -> List[Row]:
sql = aime_user.select(aime_user.c.username.like(f"%{username}%"))
result = self.execute(sql)
if result is None:
return False
return result.fetchall()

View File

@ -0,0 +1,3 @@
ALTER TABLE machine DROP COLUMN memo;
ALTER TABLE machine DROP COLUMN is_blacklisted;
ALTER TABLE machine DROP COLUMN `data`;

View File

@ -0,0 +1 @@
ALTER TABLE arcade DROP COLUMN 'ip';

View File

@ -0,0 +1,3 @@
ALTER TABLE machine ADD memo varchar(255) NULL;
ALTER TABLE machine ADD is_blacklisted tinyint(1) NULL;
ALTER TABLE machine ADD `data` longtext NULL;

View File

@ -0,0 +1 @@
ALTER TABLE arcade ADD ip varchar(39) NULL;

View File

@ -9,6 +9,9 @@ from zope.interface import Interface, Attribute, implementer
from twisted.python.components import registerAdapter
import jinja2
import bcrypt
import re
from enum import Enum
from urllib import parse
from core import CoreConfig, Utils
from core.data import Data
@ -19,6 +22,13 @@ class IUserSession(Interface):
current_ip = Attribute("User's current ip address")
permissions = Attribute("User's permission level")
class PermissionOffset(Enum):
USER = 0 # Regular user
USERMOD = 1 # Can moderate other users
ACMOD = 2 # Can add arcades and cabs
SYSADMIN = 3 # Can change settings
# 4 - 6 reserved for future use
OWNER = 7 # Can do anything
@implementer(IUserSession)
class UserSession(object):
@ -80,6 +90,9 @@ class FrontendServlet(resource.Resource):
self.environment.globals["game_list"] = self.game_list
self.putChild(b"gate", FE_Gate(cfg, self.environment))
self.putChild(b"user", FE_User(cfg, self.environment))
self.putChild(b"sys", FE_System(cfg, self.environment))
self.putChild(b"arcade", FE_Arcade(cfg, self.environment))
self.putChild(b"cab", FE_Machine(cfg, self.environment))
self.putChild(b"game", fe_game)
self.logger.info(
@ -154,6 +167,7 @@ class FE_Gate(FE_Base):
passwd = None
uid = self.data.card.get_user_id_from_card(access_code)
user = self.data.user.get_user(uid)
if uid is None:
return redirectTo(b"/gate?e=1", request)
@ -175,6 +189,7 @@ class FE_Gate(FE_Base):
usr_sesh = IUserSession(sesh)
usr_sesh.userId = uid
usr_sesh.current_ip = ip
usr_sesh.permissions = user['permissions']
return redirectTo(b"/user", request)
@ -192,7 +207,7 @@ class FE_Gate(FE_Base):
hashed = bcrypt.hashpw(passwd, salt)
result = self.data.user.create_user(
uid, username, email, hashed.decode(), 1
uid, username, email.lower(), hashed.decode(), 1
)
if result is None:
return redirectTo(b"/gate?e=3", request)
@ -210,17 +225,29 @@ class FE_Gate(FE_Base):
return redirectTo(b"/gate?e=2", request)
ac = request.args[b"ac"][0].decode()
card = self.data.card.get_card_by_access_code(ac)
if card is None:
return redirectTo(b"/gate?e=1", request)
user = self.data.user.get_user(card['user'])
if user is None:
self.logger.warning(f"Card {ac} exists with no/invalid associated user ID {card['user']}")
return redirectTo(b"/gate?e=0", request)
if user['password'] is not None:
return redirectTo(b"/gate?e=1", request)
template = self.environment.get_template("core/frontend/gate/create.jinja")
return template.render(
title=f"{self.core_config.server.name} | Create User",
code=ac,
sesh={"userId": 0},
sesh={"userId": 0, "permissions": 0},
).encode("utf-16")
class FE_User(FE_Base):
def render_GET(self, request: Request):
uri = request.uri.decode()
template = self.environment.get_template("core/frontend/user/index.jinja")
sesh: Session = request.getSession()
@ -228,9 +255,26 @@ class FE_User(FE_Base):
if usr_sesh.userId == 0:
return redirectTo(b"/gate", request)
cards = self.data.card.get_user_cards(usr_sesh.userId)
user = self.data.user.get_user(usr_sesh.userId)
m = re.match("\/user\/(\d*)", uri)
if m is not None:
usrid = m.group(1)
if usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value or not usrid == usr_sesh.userId:
return redirectTo(b"/user", request)
else:
usrid = usr_sesh.userId
user = self.data.user.get_user(usrid)
if user is None:
return redirectTo(b"/user", request)
cards = self.data.card.get_user_cards(usrid)
arcades = self.data.arcade.get_arcades_managed_by_user(usrid)
card_data = []
arcade_data = []
for c in cards:
if c['is_locked']:
status = 'Locked'
@ -240,9 +284,104 @@ class FE_User(FE_Base):
status = 'Active'
card_data.append({'access_code': c['access_code'], 'status': status})
for a in arcades:
arcade_data.append({'id': a['id'], 'name': a['name']})
return template.render(
title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh), cards=card_data, username=user['username']
title=f"{self.core_config.server.name} | Account",
sesh=vars(usr_sesh),
cards=card_data,
username=user['username'],
arcades=arcade_data
).encode("utf-16")
def render_POST(self, request: Request):
pass
class FE_System(FE_Base):
def render_GET(self, request: Request):
uri = request.uri.decode()
template = self.environment.get_template("core/frontend/sys/index.jinja")
usrlist = []
aclist = []
cablist = []
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
if usr_sesh.userId == 0 or usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value:
return redirectTo(b"/gate", request)
if uri.startswith("/sys/lookup.user?"):
uri_parse = parse.parse_qs(uri.replace("/sys/lookup.user?", "")) # lop off the first bit
uid_search = uri_parse.get("usrId")
email_search = uri_parse.get("usrEmail")
uname_search = uri_parse.get("usrName")
if uid_search is not None:
u = self.data.user.get_user(uid_search[0])
if u is not None:
usrlist.append(u._asdict())
elif email_search is not None:
u = self.data.user.find_user_by_email(email_search[0])
if u is not None:
usrlist.append(u._asdict())
elif uname_search is not None:
ul = self.data.user.find_user_by_username(uname_search[0])
for u in ul:
usrlist.append(u._asdict())
elif uri.startswith("/sys/lookup.arcade?"):
uri_parse = parse.parse_qs(uri.replace("/sys/lookup.arcade?", "")) # lop off the first bit
ac_id_search = uri_parse.get("arcadeId")
ac_name_search = uri_parse.get("arcadeName")
ac_user_search = uri_parse.get("arcadeUser")
if ac_id_search is not None:
u = self.data.arcade.get_arcade(ac_id_search[0])
if u is not None:
aclist.append(u._asdict())
elif ac_name_search is not None:
ul = self.data.arcade.find_arcade_by_name(ac_name_search[0])
for u in ul:
aclist.append(u._asdict())
elif ac_user_search is not None:
ul = self.data.arcade.get_arcades_managed_by_user(ac_user_search[0])
for u in ul:
aclist.append(u._asdict())
elif uri.startswith("/sys/lookup.cab?"):
uri_parse = parse.parse_qs(uri.replace("/sys/lookup.cab?", "")) # lop off the first bit
cab_id_search = uri_parse.get("cabId")
cab_serial_search = uri_parse.get("cabSerial")
cab_acid_search = uri_parse.get("cabAcId")
if cab_id_search is not None:
u = self.data.arcade.get_machine(id=cab_id_search[0])
if u is not None:
cablist.append(u._asdict())
elif cab_serial_search is not None:
u = self.data.arcade.get_machine(serial=cab_serial_search[0])
if u is not None:
cablist.append(u._asdict())
elif cab_acid_search is not None:
ul = self.data.arcade.get_arcade_machines(cab_acid_search[0])
for u in ul:
cablist.append(u._asdict())
return template.render(
title=f"{self.core_config.server.name} | System",
sesh=vars(usr_sesh),
usrlist=usrlist,
aclist=aclist,
cablist=cablist,
).encode("utf-16")
@ -257,3 +396,54 @@ class FE_Game(FE_Base):
def render_GET(self, request: Request) -> bytes:
return redirectTo(b"/user", request)
class FE_Arcade(FE_Base):
def render_GET(self, request: Request):
uri = request.uri.decode()
template = self.environment.get_template("core/frontend/arcade/index.jinja")
managed = []
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
if usr_sesh.userId == 0:
return redirectTo(b"/gate", request)
m = re.match("\/arcade\/(\d*)", uri)
if m is not None:
arcadeid = m.group(1)
perms = self.data.arcade.get_manager_permissions(usr_sesh.userId, arcadeid)
arcade = self.data.arcade.get_arcade(arcadeid)
if perms is None:
perms = 0
else:
return redirectTo(b"/user", request)
return template.render(
title=f"{self.core_config.server.name} | Arcade",
sesh=vars(usr_sesh),
error=0,
perms=perms,
arcade=arcade._asdict()
).encode("utf-16")
class FE_Machine(FE_Base):
def render_GET(self, request: Request):
uri = request.uri.decode()
template = self.environment.get_template("core/frontend/machine/index.jinja")
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
if usr_sesh.userId == 0:
return redirectTo(b"/gate", request)
return template.render(
title=f"{self.core_config.server.name} | Machine",
sesh=vars(usr_sesh),
arcade={},
error=0,
).encode("utf-16")

View File

@ -0,0 +1,4 @@
{% extends "core/frontend/index.jinja" %}
{% block content %}
<h1>{{ arcade.name }}</h1>
{% endblock content %}

View File

@ -0,0 +1,5 @@
{% extends "core/frontend/index.jinja" %}
{% block content %}
{% include "core/frontend/widgets/err_banner.jinja" %}
<h1>Machine Management</h1>
{% endblock content %}

View File

@ -0,0 +1,98 @@
{% extends "core/frontend/index.jinja" %}
{% block content %}
<h1>System Management</h1>
<div class="row" id="rowForm">
{% if sesh.permissions >= 2 %}
<div class="col-sm-6" style="max-width: 25%;">
<form id="usrLookup" name="usrLookup" action="/sys/lookup.user" class="form-inline">
<h3>User Search</h3>
<div class="form-group">
<label for="usrEmail">Email address</label>
<input type="email" class="form-control" id="usrEmail" name="usrEmail" aria-describedby="emailHelp">
</div>
OR
<div class="form-group">
<label for="usrName">Username</label>
<input type="text" class="form-control" id="usrName" name="usrName">
</div>
OR
<div class="form-group">
<label for="usrId">User ID</label>
<input type="number" class="form-control" id="usrId" name="usrId">
</div>
<br />
<button type="submit" class="btn btn-primary">Search</button>
</form>
</div>
{% endif %}
{% if sesh.permissions >= 4 %}
<div class="col-sm-6" style="max-width: 25%;">
<form id="arcadeLookup" name="arcadeLookup" action="/sys/lookup.arcade" class="form-inline" >
<h3>Arcade Search</h3>
<div class="form-group">
<label for="arcadeName">Arcade Name</label>
<input type="text" class="form-control" id="arcadeName" name="arcadeName">
</div>
OR
<div class="form-group">
<label for="arcadeId">Arcade ID</label>
<input type="number" class="form-control" id="arcadeId" name="arcadeId">
</div>
OR
<div class="form-group">
<label for="arcadeUser">Owner User ID</label>
<input type="number" class="form-control" id="arcadeUser" name="arcadeUser">
</div>
<br />
<button type="submit" class="btn btn-primary">Search</button>
</form>
</div>
<div class="col-sm-6" style="max-width: 25%;">
<form id="cabLookup" name="cabLookup" action="/sys/lookup.cab" class="form-inline" >
<h3>Machine Search</h3>
<div class="form-group">
<label for="cabSerial">Machine Serial</label>
<input type="text" class="form-control" id="cabSerial" name="cabSerial">
</div>
OR
<div class="form-group">
<label for="cabId">Machine ID</label>
<input type="number" class="form-control" id="cabId" name="cabId">
</div>
OR
<div class="form-group">
<label for="cabAcId">Arcade ID</label>
<input type="number" class="form-control" id="cabAcId" name="cabAcId">
</div>
<br />
<button type="submit" class="btn btn-primary">Search</button>
</form>
</div>
{% endif %}
</div>
<div class="row" id="rowResult" style="margin: 10px;">
{% if sesh.permissions >= 2 %}
<div id="userSearchResult" class="col-sm-6" style="max-width: 25%;">
{% for usr in usrlist %}
<pre><a href=/user/{{ usr.id }}>{{ usr.id }} | {{ usr.username }}</a></pre>
{% endfor %}
</div>
{% endif %}
{% if sesh.permissions >= 4 %}
<div id="arcadeSearchResult" class="col-sm-6" style="max-width: 25%;">
{% for ac in aclist %}
<pre><a href=/arcade/{{ ac.id }}>{{ ac.id }} | {{ ac.name }}</a></pre>
{% endfor %}
</div
><div id="cabSearchResult" class="col-sm-6" style="max-width: 25%;">
{% for cab in cablist %}
<a href=/cab/{{ cab.id }}><pre>{{ cab.id }} | {{ cab.game if cab.game is defined else "ANY " }} | {{ cab.serial }}</pre></a>
{% endfor %}
</div>
{% endif %}
</div>
<div class="row" id="rowAdd">
</div>
{% endblock content %}

View File

@ -2,11 +2,21 @@
{% block content %}
<h1>Management for {{ username }}</h1>
<h2>Cards <button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#card_add">Add</button></h2>
<ul>
<ul style="font-size: 20px;">
{% for c in cards %}
<li>{{ c.access_code }}: {{ c.status }} <button class="btn-danger btn">Delete</button></li>
<li>{{ c.access_code }}: {{ c.status }}&nbsp;{% if c.status == 'Active'%}<button class="btn-warning btn">Lock</button>{% elif c.status == 'Locked' %}<button class="btn-warning btn">Unlock</button>{% endif %}&nbsp;<button class="btn-danger btn">Delete</button></li>
{% endfor %}
</ul>
{% if arcades is defined %}
<h2>Arcades</h2>
<ul style="font-size: 20px;">
{% for a in arcades %}
<li><a href=/arcade/{{ a.id }}>{{ a.name }}</a></li>
{% endfor %}
</ul>
{% endif %}
<div class="modal fade" id="card_add" tabindex="-1" aria-labelledby="card_add_label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">

View File

@ -7,6 +7,10 @@ Card not registered, or wrong password
Missing or malformed access code
{% elif error == 3 %}
Failed to create user
{% elif error == 4 %}
Arcade not found
{% elif error == 5 %}
Machine not found
{% else %}
An unknown error occoured
{% endif %}

View File

@ -9,6 +9,9 @@
</div>
</div>
<div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;">
{% if sesh is defined and sesh["permissions"] >= 2 %}
<a href="/sys"><button class="btn btn-primary">System</button></a>
{% endif %}
{% if sesh is defined and sesh["userId"] > 0 %}
<a href="/user"><button class="btn btn-primary">Account</button></a>
{% else %}

View File

@ -64,7 +64,7 @@ class MuchaServlet:
self.logger.debug(f"Mucha request {vars(req)}")
if req.gameCd not in self.mucha_registry:
self.logger.warn(f"Unknown gameCd {req.gameCd}")
self.logger.warning(f"Unknown gameCd {req.gameCd}")
return b"RESULTS=000"
# TODO: Decrypt S/N
@ -99,7 +99,7 @@ class MuchaServlet:
self.logger.debug(f"Mucha request {vars(req)}")
if req.gameCd not in self.mucha_registry:
self.logger.warn(f"Unknown gameCd {req.gameCd}")
self.logger.warning(f"Unknown gameCd {req.gameCd}")
return b"RESULTS=000"
resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}")
@ -279,3 +279,67 @@ class MuchaDownloadStateRequest:
self.boardId = request.get("boardId", "")
self.placeId = request.get("placeId", "")
self.storeRouterIp = request.get("storeRouterIp", "")
class MuchaDownloadErrorRequest:
def __init__(self, request: Dict) -> None:
self.gameCd = request.get("gameCd", "")
self.updateVer = request.get("updateVer", "")
self.serialNum = request.get("serialNum", "")
self.downloadUrl = request.get("downloadUrl", "")
self.errCd = request.get("errCd", "")
self.errMessage = request.get("errMessage", "")
self.boardId = request.get("boardId", "")
self.placeId = request.get("placeId", "")
self.storeRouterIp = request.get("storeRouterIp", "")
class MuchaRegiAuthRequest:
def __init__(self, request: Dict) -> None:
self.gameCd = request.get("gameCd", "")
self.serialNum = request.get("serialNum", "") # Encrypted
self.countryCd = request.get("countryCd", "")
self.registrationCd = request.get("registrationCd", "")
self.sendDate = request.get("sendDate", "")
self.useToken = request.get("useToken", "")
self.allToken = request.get("allToken", "")
self.placeId = request.get("placeId", "")
self.storeRouterIp = request.get("storeRouterIp", "")
class MuchaRegiAuthResponse:
def __init__(self) -> None:
self.RESULTS = "001" # 001 = success, 099, 098, 097 = fail, others = fail
self.ALL_TOKEN = "0" # Encrypted
self.ADD_TOKEN = "0" # Encrypted
class MuchaTokenStateRequest:
def __init__(self, request: Dict) -> None:
self.gameCd = request.get("gameCd", "")
self.serialNum = request.get("serialNum", "")
self.countryCd = request.get("countryCd", "")
self.useToken = request.get("useToken", "")
self.allToken = request.get("allToken", "")
self.placeId = request.get("placeId", "")
self.storeRouterIp = request.get("storeRouterIp", "")
class MuchaTokenStateResponse:
def __init__(self) -> None:
self.RESULTS = "001"
class MuchaTokenMarginStateRequest:
def __init__(self, request: Dict) -> None:
self.gameCd = request.get("gameCd", "")
self.serialNum = request.get("serialNum", "")
self.countryCd = request.get("countryCd", "")
self.placeId = request.get("placeId", "")
self.limitLowerToken = request.get("limitLowerToken", 0)
self.limitUpperToken = request.get("limitUpperToken", 0)
self.settlementMonth = request.get("settlementMonth", 0)
class MuchaTokenMarginStateResponse:
def __init__(self) -> None:
self.RESULTS = "001"
self.LIMIT_LOWER_TOKEN = 0
self.LIMIT_UPPER_TOKEN = 0
self.LAST_SETTLEMENT_MONTH = 0
self.LAST_LIMIT_LOWER_TOKEN = 0
self.LAST_LIMIT_UPPER_TOKEN = 0
self.SETTLEMENT_MONTH = 0

View File

@ -62,7 +62,7 @@ class TitleServlet:
self.title_registry[code] = handler_cls
else:
self.logger.warn(f"Game {folder} has no get_allnet_info")
self.logger.warning(f"Game {folder} has no get_allnet_info")
else:
self.logger.error(f"{folder} missing game_code or index in __init__.py")
@ -74,13 +74,13 @@ class TitleServlet:
def render_GET(self, request: Request, endpoints: dict) -> bytes:
code = endpoints["game"]
if code not in self.title_registry:
self.logger.warn(f"Unknown game code {code}")
self.logger.warning(f"Unknown game code {code}")
request.setResponseCode(404)
return b""
index = self.title_registry[code]
if not hasattr(index, "render_GET"):
self.logger.warn(f"{code} does not dispatch GET")
self.logger.warning(f"{code} does not dispatch GET")
request.setResponseCode(405)
return b""
@ -89,13 +89,13 @@ class TitleServlet:
def render_POST(self, request: Request, endpoints: dict) -> bytes:
code = endpoints["game"]
if code not in self.title_registry:
self.logger.warn(f"Unknown game code {code}")
self.logger.warning(f"Unknown game code {code}")
request.setResponseCode(404)
return b""
index = self.title_registry[code]
if not hasattr(index, "render_POST"):
self.logger.warn(f"{code} does not dispatch POST")
self.logger.warning(f"{code} does not dispatch POST")
request.setResponseCode(405)
return b""

View File

@ -56,10 +56,10 @@ if __name__ == "__main__":
elif args.action == "upgrade" or args.action == "rollback":
if args.version is None:
data.logger.warn("No version set, upgrading to latest")
data.logger.warning("No version set, upgrading to latest")
if args.game is None:
data.logger.warn("No game set, upgrading core schema")
data.logger.warning("No game set, upgrading core schema")
data.migrate_database(
"CORE",
int(args.version) if args.version is not None else None,

View File

@ -6,6 +6,8 @@ server:
is_develop: True
threading: False
log_dir: "logs"
check_arcade_ip: False
strict_ip_checking: False
title:
loglevel: "info"

View File

@ -36,7 +36,7 @@ class HttpDispatcher(resource.Resource):
self.map_post.connect(
"allnet_downloadorder_report",
"/dl/report",
"/report-api/Report",
controller="allnet",
action="handle_dlorder_report",
conditions=dict(method=["POST"]),
@ -99,6 +99,7 @@ class HttpDispatcher(resource.Resource):
conditions=dict(method=["POST"]),
)
# Maintain compatability
self.map_post.connect(
"mucha_boardauth",
"/mucha/boardauth.do",
@ -121,6 +122,28 @@ class HttpDispatcher(resource.Resource):
conditions=dict(method=["POST"]),
)
self.map_post.connect(
"mucha_boardauth",
"/mucha_front/boardauth.do",
controller="mucha",
action="handle_boardauth",
conditions=dict(method=["POST"]),
)
self.map_post.connect(
"mucha_updatacheck",
"/mucha_front/updatacheck.do",
controller="mucha",
action="handle_updatecheck",
conditions=dict(method=["POST"]),
)
self.map_post.connect(
"mucha_dlstate",
"/mucha_front/downloadstate.do",
controller="mucha",
action="handle_dlstate",
conditions=dict(method=["POST"]),
)
self.map_get.connect(
"title_get",
"/{game}/{version}/{endpoint:.*?}",
@ -193,11 +216,11 @@ class HttpDispatcher(resource.Resource):
return ret
elif ret is None:
self.logger.warn(f"None returned by controller for {request.uri.decode()} endpoint")
self.logger.warning(f"None returned by controller for {request.uri.decode()} endpoint")
return b""
else:
self.logger.warn(f"Unknown data type returned by controller for {request.uri.decode()} endpoint")
self.logger.warning(f"Unknown data type returned by controller for {request.uri.decode()} endpoint")
return b""

View File

@ -73,7 +73,7 @@ class ChuniBase:
# skip the current bonus preset if no boni were found
if all_login_boni is None or len(all_login_boni) < 1:
self.logger.warn(
self.logger.warning(
f"No bonus entries found for bonus preset {preset['presetId']}"
)
continue
@ -149,7 +149,7 @@ class ChuniBase:
game_events = self.data.static.get_enabled_events(self.version)
if game_events is None or len(game_events) == 0:
self.logger.warn("No enabled events, did you run the reader?")
self.logger.warning("No enabled events, did you run the reader?")
return {
"type": data["type"],
"length": 0,

View File

@ -67,7 +67,7 @@ class ChuniReader(BaseReader):
if result is not None:
self.logger.info(f"Inserted login bonus preset {id}")
else:
self.logger.warn(f"Failed to insert login bonus preset {id}")
self.logger.warning(f"Failed to insert login bonus preset {id}")
for bonus in xml_root.find("infos").findall("LoginBonusDataInfo"):
for name in bonus.findall("loginBonusName"):
@ -113,7 +113,7 @@ class ChuniReader(BaseReader):
if result is not None:
self.logger.info(f"Inserted login bonus {bonus_id}")
else:
self.logger.warn(
self.logger.warning(
f"Failed to insert login bonus {bonus_id}"
)
@ -138,7 +138,7 @@ class ChuniReader(BaseReader):
if result is not None:
self.logger.info(f"Inserted event {id}")
else:
self.logger.warn(f"Failed to insert event {id}")
self.logger.warning(f"Failed to insert event {id}")
def read_music(self, music_dir: str) -> None:
for root, dirs, files in walk(music_dir):
@ -200,7 +200,7 @@ class ChuniReader(BaseReader):
f"Inserted music {song_id} chart {chart_id}"
)
else:
self.logger.warn(
self.logger.warning(
f"Failed to insert music {song_id} chart {chart_id}"
)
@ -232,7 +232,7 @@ class ChuniReader(BaseReader):
if result is not None:
self.logger.info(f"Inserted charge {id}")
else:
self.logger.warn(f"Failed to insert charge {id}")
self.logger.warning(f"Failed to insert charge {id}")
def read_avatar(self, avatar_dir: str) -> None:
for root, dirs, files in walk(avatar_dir):
@ -259,4 +259,4 @@ class ChuniReader(BaseReader):
if result is not None:
self.logger.info(f"Inserted avatarAccessory {id}")
else:
self.logger.warn(f"Failed to insert avatarAccessory {id}")
self.logger.warning(f"Failed to insert avatarAccessory {id}")

View File

@ -530,7 +530,7 @@ class ChuniItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_user_gacha: Failed to insert! aime_id: {aime_id}")
self.logger.warning(f"put_user_gacha: Failed to insert! aime_id: {aime_id}")
return None
return result.lastrowid
@ -572,7 +572,7 @@ class ChuniItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_user_print_state: Failed to insert! aime_id: {aime_id}"
)
return None
@ -589,7 +589,7 @@ class ChuniItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_user_print_detail: Failed to insert! aime_id: {aime_id}"
)
return None

View File

@ -410,7 +410,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_data: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_profile_data: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -452,7 +452,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile_data_ex: Failed to update! aime_id: {aime_id}"
)
return None
@ -479,7 +479,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile_option: Failed to update! aime_id: {aime_id}"
)
return None
@ -503,7 +503,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile_option_ex: Failed to update! aime_id: {aime_id}"
)
return None
@ -527,7 +527,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile_recent_rating: Failed to update! aime_id: {aime_id}"
)
return None
@ -552,7 +552,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile_activity: Failed to update! aime_id: {aime_id}"
)
return None
@ -578,7 +578,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile_charge: Failed to update! aime_id: {aime_id}"
)
return None

View File

@ -302,14 +302,14 @@ class ChuniStaticData(BaseData):
result = self.execute(sql)
if result is None:
self.logger.warn(
self.logger.warning(
f"update_event: failed to update event! version: {version}, event_id: {event_id}, enabled: {enabled}"
)
return None
event = self.get_event(version, event_id)
if event is None:
self.logger.warn(
self.logger.warning(
f"update_event: failed to fetch event {event_id} after updating"
)
return None
@ -506,7 +506,7 @@ class ChuniStaticData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert gacha! gacha_id {gacha_id}")
self.logger.warning(f"Failed to insert gacha! gacha_id {gacha_id}")
return None
return result.lastrowid
@ -541,7 +541,7 @@ class ChuniStaticData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert gacha card! gacha_id {gacha_id}")
self.logger.warning(f"Failed to insert gacha card! gacha_id {gacha_id}")
return None
return result.lastrowid
@ -577,7 +577,7 @@ class ChuniStaticData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert card! card_id {card_id}")
self.logger.warning(f"Failed to insert card! card_id {card_id}")
return None
return result.lastrowid

View File

@ -68,7 +68,7 @@ class CardMakerReader(BaseReader):
read_csv = getattr(CardMakerReader, func)
read_csv(self, f"{self.bin_dir}/MU3/{file}")
else:
self.logger.warn(
self.logger.warning(
f"Couldn't find {file} file in {self.bin_dir}, skipping"
)

View File

@ -52,7 +52,7 @@ class CxbBase:
self.logger.info(f"Login user {data['login']['authid']}")
return {"token": data["login"]["authid"], "uid": data["login"]["authid"]}
self.logger.warn(f"User {data['login']['authid']} does not have a profile")
self.logger.warning(f"User {data['login']['authid']} does not have a profile")
return {}
def task_generateCoupon(index, data1):

View File

@ -123,13 +123,13 @@ class CxbServlet(resource.Resource):
)
except Exception as f:
self.logger.warn(
self.logger.warning(
f"Error decoding json: {e} / {f} - {req_url} - {req_bytes}"
)
return b""
if req_json == {}:
self.logger.warn(f"Empty json request to {req_url}")
self.logger.warning(f"Empty json request to {req_url}")
return b""
cmd = url_split[len(url_split) - 1]
@ -140,7 +140,7 @@ class CxbServlet(resource.Resource):
not type(req_json["dldate"]) is dict
or "filetype" not in req_json["dldate"]
):
self.logger.warn(f"Malformed dldate request: {req_url} {req_json}")
self.logger.warning(f"Malformed dldate request: {req_url} {req_json}")
return b""
filetype = req_json["dldate"]["filetype"]

View File

@ -33,7 +33,7 @@ class CxbReader(BaseReader):
pull_bin_ram = True
if not path.exists(f"{self.bin_dir}"):
self.logger.warn(f"Couldn't find csv file in {self.bin_dir}, skipping")
self.logger.warning(f"Couldn't find csv file in {self.bin_dir}, skipping")
pull_bin_ram = False
if pull_bin_ram:
@ -124,4 +124,4 @@ class CxbReader(BaseReader):
int(row["easy"].replace("Easy ", "").replace("N/A", "0")),
)
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.warning(f"Couldn't read csv file in {self.bin_dir}, skipping")

View File

@ -3,6 +3,7 @@ from typing import Any, List, Dict
import logging
import json
import urllib
from threading import Thread
from core.config import CoreConfig
from titles.diva.config import DivaConfig
@ -663,50 +664,66 @@ class DivaBase:
return pv_result
def task_generateScoreData(self, data: Dict, pd_by_pv_id, song):
if int(song) > 0:
# the request do not send a edition so just perform a query best score and ranking for each edition.
# 0=ORIGINAL, 1=EXTRA
pd_db_song_0 = self.data.score.get_best_user_score(
data["pd_id"], int(song), data["difficulty"], edition=0
)
pd_db_song_1 = self.data.score.get_best_user_score(
data["pd_id"], int(song), data["difficulty"], edition=1
)
pd_db_ranking_0, pd_db_ranking_1 = None, None
if pd_db_song_0:
pd_db_ranking_0 = self.data.score.get_global_ranking(
data["pd_id"], int(song), data["difficulty"], edition=0
)
if pd_db_song_1:
pd_db_ranking_1 = self.data.score.get_global_ranking(
data["pd_id"], int(song), data["difficulty"], edition=1
)
pd_db_customize = self.data.pv_customize.get_pv_customize(
data["pd_id"], int(song)
)
# generate the pv_result string with the ORIGINAL edition and the EXTRA edition appended
pv_result = self._get_pv_pd_result(
int(song), pd_db_song_0, pd_db_ranking_0, pd_db_customize, edition=0
)
pv_result += "," + self._get_pv_pd_result(
int(song), pd_db_song_1, pd_db_ranking_1, pd_db_customize, edition=1
)
self.logger.debug(f"pv_result = {pv_result}")
pd_by_pv_id.append(urllib.parse.quote(pv_result))
else:
pd_by_pv_id.append(urllib.parse.quote(f"{song}***"))
pd_by_pv_id.append(",")
def handle_get_pv_pd_request(self, data: Dict) -> Dict:
song_id = data["pd_pv_id_lst"].split(",")
pv = ""
threads = []
pd_by_pv_id = []
for song in song_id:
if int(song) > 0:
# the request do not send a edition so just perform a query best score and ranking for each edition.
# 0=ORIGINAL, 1=EXTRA
pd_db_song_0 = self.data.score.get_best_user_score(
data["pd_id"], int(song), data["difficulty"], edition=0
)
pd_db_song_1 = self.data.score.get_best_user_score(
data["pd_id"], int(song), data["difficulty"], edition=1
)
thread_ScoreData = Thread(target=self.task_generateScoreData(data, pd_by_pv_id, song))
threads.append(thread_ScoreData)
pd_db_ranking_0, pd_db_ranking_1 = None, None
if pd_db_song_0:
pd_db_ranking_0 = self.data.score.get_global_ranking(
data["pd_id"], int(song), data["difficulty"], edition=0
)
for x in threads:
x.start()
if pd_db_song_1:
pd_db_ranking_1 = self.data.score.get_global_ranking(
data["pd_id"], int(song), data["difficulty"], edition=1
)
for x in threads:
x.join()
pd_db_customize = self.data.pv_customize.get_pv_customize(
data["pd_id"], int(song)
)
# generate the pv_result string with the ORIGINAL edition and the EXTRA edition appended
pv_result = self._get_pv_pd_result(
int(song), pd_db_song_0, pd_db_ranking_0, pd_db_customize, edition=0
)
pv_result += "," + self._get_pv_pd_result(
int(song), pd_db_song_1, pd_db_ranking_1, pd_db_customize, edition=1
)
self.logger.debug(f"pv_result = {pv_result}")
pv += urllib.parse.quote(pv_result)
else:
pv += urllib.parse.quote(f"{song}***")
pv += ","
for x in pd_by_pv_id:
pv += x
response = ""
response += f"&pd_by_pv_id={pv[:-1]}"

View File

@ -34,18 +34,18 @@ class DivaReader(BaseReader):
pull_opt_rom = True
if not path.exists(f"{self.bin_dir}/ram"):
self.logger.warn(f"Couldn't find ram folder in {self.bin_dir}, skipping")
self.logger.warning(f"Couldn't find ram folder in {self.bin_dir}, skipping")
pull_bin_ram = False
if not path.exists(f"{self.bin_dir}/rom"):
self.logger.warn(f"Couldn't find rom folder in {self.bin_dir}, skipping")
self.logger.warning(f"Couldn't find rom folder in {self.bin_dir}, skipping")
pull_bin_rom = False
if self.opt_dir is not None:
opt_dirs = self.get_data_directories(self.opt_dir)
else:
pull_opt_rom = False
self.logger.warn("No option directory specified, skipping")
self.logger.warning("No option directory specified, skipping")
if pull_bin_ram:
self.read_ram(f"{self.bin_dir}/ram")
@ -139,7 +139,7 @@ class DivaReader(BaseReader):
else:
continue
else:
self.logger.warn(f"Databank folder not found in {ram_root_dir}, skipping")
self.logger.warning(f"Databank folder not found in {ram_root_dir}, skipping")
def read_rom(self, rom_root_dir: str) -> None:
self.logger.info(f"Read ROM from {rom_root_dir}")
@ -150,7 +150,7 @@ class DivaReader(BaseReader):
elif path.exists(f"{rom_root_dir}/pv_db.txt"):
file_path = f"{rom_root_dir}/pv_db.txt"
else:
self.logger.warn(
self.logger.warning(
f"Cannot find pv_db.txt or mdata_pv_db.txt in {rom_root_dir}, skipping"
)
return

View File

@ -114,7 +114,7 @@ class IDZUserDBProtocol(Protocol):
elif self.version == 230:
self.version_internal = IDZConstants.VER_IDZ_230
else:
self.logger.warn(f"Bad version v{self.version}")
self.logger.warning(f"Bad version v{self.version}")
self.version = None
self.version_internal = None
@ -142,7 +142,7 @@ class IDZUserDBProtocol(Protocol):
self.version_internal
].get(cmd, None)
if handler_cls is None:
self.logger.warn(f"No handler for v{self.version} {hex(cmd)} cmd")
self.logger.warning(f"No handler for v{self.version} {hex(cmd)} cmd")
handler_cls = IDZHandlerBase
handler = handler_cls(self.core_config, self.game_config, self.version_internal)

View File

@ -57,7 +57,7 @@ class Mai2Base:
events = self.data.static.get_enabled_events(self.version)
events_lst = []
if events is None or not events:
self.logger.warn("No enabled events, did you run the reader?")
self.logger.warning("No enabled events, did you run the reader?")
return {"type": data["type"], "length": 0, "gameEventList": []}
for event in events:
@ -741,7 +741,7 @@ class Mai2Base:
music_detail_list = []
if user_id <= 0:
self.logger.warn("handle_get_user_music_api_request: Could not find userid in data, or userId is 0")
self.logger.warning("handle_get_user_music_api_request: Could not find userid in data, or userId is 0")
return {}
songs = self.data.score.get_best_scores(user_id, is_dx=False)
@ -794,46 +794,46 @@ class Mai2Base:
upload_date = photo.get("uploadDate", "")
if order_id < 0 or user_id <= 0 or div_num < 0 or div_len <= 0 or not div_data or playlog_id < 0 or track_num <= 0 or not upload_date:
self.logger.warn(f"Malformed photo upload request")
self.logger.warning(f"Malformed photo upload request")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
if order_id == 0 and div_num > 0:
self.logger.warn(f"Failed to set orderId properly (still 0 after first chunk)")
self.logger.warning(f"Failed to set orderId properly (still 0 after first chunk)")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
if div_num == 0 and order_id > 0:
self.logger.warn(f"First chuck re-send, Ignore")
self.logger.warning(f"First chuck re-send, Ignore")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
if div_num >= div_len:
self.logger.warn(f"Sent extra chunks ({div_num} >= {div_len})")
self.logger.warning(f"Sent extra chunks ({div_num} >= {div_len})")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
if div_len >= 100:
self.logger.warn(f"Photo too large ({div_len} * 10240 = {div_len * 10240} bytes)")
self.logger.warning(f"Photo too large ({div_len} * 10240 = {div_len * 10240} bytes)")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
ret_code = order_id + 1
photo_chunk = b64decode(div_data)
if len(photo_chunk) > 10240 or (len(photo_chunk) < 10240 and div_num + 1 != div_len):
self.logger.warn(f"Incorrect data size after decoding (Expected 10240, got {len(photo_chunk)})")
self.logger.warning(f"Incorrect data size after decoding (Expected 10240, got {len(photo_chunk)})")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
out_name = f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}"
if not path.exists(f"{out_name}.bin") and div_num != 0:
self.logger.warn(f"Out of order photo upload (div_num {div_num})")
self.logger.warning(f"Out of order photo upload (div_num {div_num})")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
if path.exists(f"{out_name}.bin") and div_num == 0:
self.logger.warn(f"Duplicate file upload")
self.logger.warning(f"Duplicate file upload")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
elif path.exists(f"{out_name}.bin"):
fstats = stat(f"{out_name}.bin")
if fstats.st_size != 10240 * div_num:
self.logger.warn(f"Out of order photo upload (trying to upload div {div_num}, expected div {fstats.st_size / 10240} for file sized {fstats.st_size} bytes)")
self.logger.warning(f"Out of order photo upload (trying to upload div {div_num}, expected div {fstats.st_size / 10240} for file sized {fstats.st_size} bytes)")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
try:

View File

@ -545,21 +545,38 @@ class Mai2DX(Mai2Base):
return {"userId": data["userId"], "length": 0, "userRegionList": []}
def handle_get_user_music_api_request(self, data: Dict) -> Dict:
songs = self.data.score.get_best_scores(data["userId"])
user_id = data.get("userId", 0)
next_index = data.get("nextIndex", 0)
max_ct = data.get("maxCount", 50)
upper_lim = next_index + max_ct
music_detail_list = []
next_index = 0
if songs is not None:
for song in songs:
tmp = song._asdict()
tmp.pop("id")
tmp.pop("user")
music_detail_list.append(tmp)
if user_id <= 0:
self.logger.warning("handle_get_user_music_api_request: Could not find userid in data, or userId is 0")
return {}
songs = self.data.score.get_best_scores(user_id)
if songs is None:
self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!")
return {
"userId": data["userId"],
"nextIndex": 0,
"userMusicList": [],
}
if len(music_detail_list) == data["maxCount"]:
next_index = data["maxCount"] + data["nextIndex"]
break
num_user_songs = len(songs)
for x in range(next_index, upper_lim):
if num_user_songs <= x:
break
tmp = songs[x]._asdict()
tmp.pop("id")
tmp.pop("user")
music_detail_list.append(tmp)
next_index = 0 if len(music_detail_list) < max_ct or num_user_songs == upper_lim else upper_lim
self.logger.info(f"Send songs {next_index}-{upper_lim} ({len(music_detail_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})")
return {
"userId": data["userId"],
"nextIndex": next_index,

View File

@ -181,11 +181,14 @@ class Mai2Servlet:
elif version >= 197: # Finale
internal_ver = Mai2Constants.VER_MAIMAI_FINALE
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
# doing encrypted. The likelyhood of false positives is low but
# technically not 0
self.logger.error("Encryption not supported at this time")
if request.getHeader('Mai-Encoding') is not None or request.getHeader('X-Mai-Encoding') is not None:
# The has is some flavor of MD5 of the endpoint with a constant bolted onto the end of it.
# See cake.dll's Obfuscator function for details. Hopefully most DLL edits will remove
# these two(?) headers to not cause issues, but given the general quality of SEGA data...
enc_ver = request.getHeader('Mai-Encoding')
if enc_ver is None:
enc_ver = request.getHeader('X-Mai-Encoding')
self.logger.debug(f"Encryption v{enc_ver} - User-Agent: {request.getHeader('User-Agent')}")
try:
unzip = zlib.decompress(req_raw)

View File

@ -85,7 +85,7 @@ class Mai2Reader(BaseReader):
def load_table_raw(self, dir: str, file: str, key: Optional[bytes]) -> Optional[List[Dict[str, str]]]:
if not os.path.exists(f"{dir}/{file}"):
self.logger.warn(f"file {file} does not exist in directory {dir}, skipping")
self.logger.warning(f"file {file} does not exist in directory {dir}, skipping")
return
self.logger.info(f"Load table {file} from {dir}")
@ -100,7 +100,7 @@ class Mai2Reader(BaseReader):
f_data = f.read()[0x10:]
if f_data is None or not f_data:
self.logger.warn(f"file {dir} could not be read, skipping")
self.logger.warning(f"file {dir} could not be read, skipping")
return
f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16)[0x12:] # lop off the junk at the beginning
@ -127,13 +127,13 @@ class Mai2Reader(BaseReader):
try:
struct_def.append(x[x.rindex(" ") + 2: -1])
except ValueError:
self.logger.warn(f"rindex failed on line {x}")
self.logger.warning(f"rindex failed on line {x}")
if is_struct:
self.logger.warn("Struct not formatted properly")
self.logger.warning("Struct not formatted properly")
if not struct_def:
self.logger.warn("Struct def not found")
self.logger.warning("Struct def not found")
name = file[:file.index(".")]
if "_" in name:
@ -148,7 +148,7 @@ class Mai2Reader(BaseReader):
continue
if not line_match.group(1) == name.upper():
self.logger.warn(f"Strange regex match for line {x} -> {line_match}")
self.logger.warning(f"Strange regex match for line {x} -> {line_match}")
continue
vals = line_match.group(2)

View File

@ -204,7 +204,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_item: failed to insert item! user_id: {user_id}, item_kind: {item_kind}, item_id: {item_id}"
)
return None
@ -261,7 +261,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_login_bonus: failed to insert item! user_id: {user_id}, bonus_id: {bonus_id}, point: {point}"
)
return None
@ -312,7 +312,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_map: failed to insert item! user_id: {user_id}, map_id: {map_id}, distance: {distance}"
)
return None
@ -341,7 +341,7 @@ class Mai2ItemData(BaseData):
conflict = sql.on_duplicate_key_update(**char_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_character_: failed to insert item! user_id: {user_id}"
)
return None
@ -371,7 +371,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_character: failed to insert item! user_id: {user_id}, character_id: {character_id}, level: {level}"
)
return None
@ -414,7 +414,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_friend_season_ranking: failed to insert",
f"friend_season_ranking! aime_id: {aime_id}",
)
@ -432,7 +432,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_favorite: failed to insert item! user_id: {user_id}, kind: {kind}"
)
return None
@ -477,7 +477,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_card: failed to insert card! user_id: {user_id}, kind: {card_kind}"
)
return None
@ -516,7 +516,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_card: failed to insert charge! user_id: {user_id}, chargeId: {charge_id}"
)
return None
@ -541,7 +541,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_user_print_detail: Failed to insert! aime_id: {aime_id}"
)
return None

View File

@ -488,7 +488,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile: Failed to create profile! user_id {user_id} is_dx {is_dx}"
)
return None
@ -525,7 +525,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_ghost: failed to update! {user_id}")
self.logger.warning(f"put_profile_ghost: failed to update! {user_id}")
return None
return result.lastrowid
@ -552,7 +552,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_extend: failed to update! {user_id}")
self.logger.warning(f"put_profile_extend: failed to update! {user_id}")
return None
return result.lastrowid
@ -582,7 +582,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_option: failed to update! {user_id} is_dx {is_dx}")
self.logger.warning(f"put_profile_option: failed to update! {user_id} is_dx {is_dx}")
return None
return result.lastrowid
@ -616,7 +616,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_rating: failed to update! {user_id}")
self.logger.warning(f"put_profile_rating: failed to update! {user_id}")
return None
return result.lastrowid
@ -643,7 +643,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_region: failed to update! {user_id}")
self.logger.warning(f"put_region: failed to update! {user_id}")
return None
return result.lastrowid
@ -668,7 +668,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile_activity: failed to update! user_id: {user_id}"
)
return None
@ -698,7 +698,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_web_option: failed to update! user_id: {user_id}"
)
return None
@ -720,7 +720,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_grade_status: failed to update! user_id: {user_id}"
)
return None
@ -742,7 +742,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_boss_list: failed to update! user_id: {user_id}"
)
return None
@ -763,7 +763,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_recent_rating: failed to update! user_id: {user_id}"
)
return None

View File

@ -161,7 +161,7 @@ class Mai2StaticData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert song {song_id} chart {chart_id}")
self.logger.warning(f"Failed to insert song {song_id} chart {chart_id}")
return None
return result.lastrowid
@ -187,7 +187,7 @@ class Mai2StaticData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert charge {ticket_id} type {ticket_type}")
self.logger.warning(f"Failed to insert charge {ticket_id} type {ticket_type}")
return None
return result.lastrowid
@ -237,7 +237,7 @@ class Mai2StaticData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert card {card_id}")
self.logger.warning(f"Failed to insert card {card_id}")
return None
return result.lastrowid

View File

@ -326,7 +326,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_card: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_card: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -346,7 +346,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_character: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_character: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -366,7 +366,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_deck: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_deck: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -394,7 +394,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_boss: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_boss: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -406,7 +406,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_story: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_story: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -426,7 +426,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_chapter: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_chapter: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -446,7 +446,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_item: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_item: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -479,7 +479,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_music_item: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_music_item: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -499,7 +499,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_login_bonus: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_login_bonus: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -521,7 +521,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_mission_point: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_mission_point: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -541,7 +541,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_event_point: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_event_point: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -561,7 +561,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_scenerio: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_scenerio: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -581,7 +581,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_trade_item: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_trade_item: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -601,7 +601,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_event_music: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_event_music: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -621,7 +621,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_tech_event: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_tech_event: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -651,7 +651,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_memorychapter: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_memorychapter: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -694,7 +694,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_user_gacha: Failed to insert! aime_id: {aime_id}")
self.logger.warning(f"put_user_gacha: Failed to insert! aime_id: {aime_id}")
return None
return result.lastrowid
@ -709,7 +709,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_user_print_detail: Failed to insert! aime_id: {aime_id}"
)
return None

View File

@ -63,7 +63,7 @@ class OngekiLogData(BaseData):
result = self.execute(sql)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_gp_log: Failed to insert GP log! aime_id: {aime_id} kind {kind} pattern {pattern} current_gp {current_gp}"
)
return result.lastrowid

View File

@ -364,7 +364,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_data: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_profile_data: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -376,7 +376,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile_options: Failed to update! aime_id: {aime_id}"
)
return None
@ -393,7 +393,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile_recent_rating: failed to update recent rating! aime_id {aime_id}"
)
return None
@ -415,7 +415,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile_rating_log: failed to update rating log! aime_id {aime_id} data_version {data_version} highest_rating {highest_rating}"
)
return None
@ -449,7 +449,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile_activity: failed to put activity! aime_id {aime_id} kind {kind} activity_id {activity_id}"
)
return None
@ -466,7 +466,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_profile_region: failed to update! aime_id {aime_id} region {region}"
)
return None
@ -480,7 +480,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_best_score: Failed to add score! aime_id: {aime_id}")
self.logger.warning(f"put_best_score: Failed to add score! aime_id: {aime_id}")
return None
return result.lastrowid
@ -492,7 +492,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_kop: Failed to add score! aime_id: {aime_id}")
self.logger.warning(f"put_kop: Failed to add score! aime_id: {aime_id}")
return None
return result.lastrowid
@ -503,7 +503,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_rival: failed to update! aime_id: {aime_id}, rival_id: {rival_id}"
)
return None

View File

@ -139,7 +139,7 @@ class OngekiScoreData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_tech_count: Failed to update! aime_id: {aime_id}")
self.logger.warning(f"put_tech_count: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
@ -164,7 +164,7 @@ class OngekiScoreData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_best_score: Failed to add score! aime_id: {aime_id}")
self.logger.warning(f"put_best_score: Failed to add score! aime_id: {aime_id}")
return None
return result.lastrowid
@ -175,6 +175,6 @@ class OngekiScoreData(BaseData):
result = self.execute(sql)
if result is None:
self.logger.warn(f"put_playlog: Failed to add playlog! aime_id: {aime_id}")
self.logger.warning(f"put_playlog: Failed to add playlog! aime_id: {aime_id}")
return None
return result.lastrowid

View File

@ -105,7 +105,7 @@ class OngekiStaticData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert card! card_id {card_id}")
self.logger.warning(f"Failed to insert card! card_id {card_id}")
return None
return result.lastrowid
@ -180,7 +180,7 @@ class OngekiStaticData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert gacha! gacha_id {gacha_id}")
self.logger.warning(f"Failed to insert gacha! gacha_id {gacha_id}")
return None
return result.lastrowid
@ -215,7 +215,7 @@ class OngekiStaticData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert gacha card! gacha_id {gacha_id}")
self.logger.warning(f"Failed to insert gacha card! gacha_id {gacha_id}")
return None
return result.lastrowid
@ -243,7 +243,7 @@ class OngekiStaticData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert event! event_id {event_id}")
self.logger.warning(f"Failed to insert event! event_id {event_id}")
return None
return result.lastrowid
@ -304,7 +304,7 @@ class OngekiStaticData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"Failed to insert chart! song_id: {song_id}, chart_id: {chart_id}"
)
return None

View File

@ -15,6 +15,7 @@ class PokkenConstants:
AI = 2
LAN = 3
WAN = 4
TUTORIAL_3 = 7
class BATTLE_RESULT(Enum):
WIN = 1

View File

@ -112,7 +112,7 @@ class PokkenServlet(resource.Resource):
try:
pokken_request.ParseFromString(content)
except DecodeError as e:
self.logger.warn(f"{e} {content}")
self.logger.warning(f"{e} {content}")
return b""
endpoint = jackal_pb2.MessageType.DESCRIPTOR.values_by_number[
@ -123,7 +123,7 @@ class PokkenServlet(resource.Resource):
handler = getattr(self.base, f"handle_{endpoint}", None)
if handler is None:
self.logger.warn(f"No handler found for message type {endpoint}")
self.logger.warning(f"No handler found for message type {endpoint}")
return self.base.handle_noop(pokken_request)
self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}")
@ -157,7 +157,7 @@ class PokkenServlet(resource.Resource):
None,
)
if handler is None:
self.logger.warn(
self.logger.warning(
f"No handler found for message type {json_content['call']}"
)
return json.dumps(self.base.handle_matching_noop()).encode()

View File

@ -39,8 +39,12 @@ class PokkenItemData(BaseData):
type=item_type,
)
result = self.execute(sql)
conflict = sql.on_duplicate_key_update(
content=content,
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert reward for user {user_id}: {category}-{content}-{item_type}")
self.logger.warning(f"Failed to insert reward for user {user_id}: {category}-{content}-{item_type}")
return None
return result.lastrowid

View File

@ -259,7 +259,7 @@ class PokkenProfileData(BaseData):
illustration_book_no=illust_no,
bp_point_atk=atk,
bp_point_res=res,
bp_point_defe=defe,
bp_point_def=defe,
bp_point_sp=sp,
)
@ -267,13 +267,13 @@ class PokkenProfileData(BaseData):
illustration_book_no=illust_no,
bp_point_atk=atk,
bp_point_res=res,
bp_point_defe=defe,
bp_point_def=defe,
bp_point_sp=sp,
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert pokemon ID {pokemon_id} for user {user_id}")
self.logger.warning(f"Failed to insert pokemon ID {pokemon_id} for user {user_id}")
return None
return result.lastrowid
@ -289,7 +289,7 @@ class PokkenProfileData(BaseData):
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}")
self.logger.warning(f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}")
def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]:
pass
@ -319,7 +319,7 @@ class PokkenProfileData(BaseData):
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to record match stats for user {user_id}'s pokemon {pokemon_id} (type {match_type.name} | result {match_result.name})")
self.logger.warning(f"Failed to record match stats for user {user_id}'s pokemon {pokemon_id} (type {match_type.name} | result {match_result.name})")
def put_stats(
self,
@ -345,9 +345,13 @@ class PokkenProfileData(BaseData):
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to update stats for user {user_id}")
self.logger.warning(f"Failed to update stats for user {user_id}")
def update_support_team(self, user_id: int, support_id: int, support1: int = 4294967295, support2: int = 4294967295) -> None:
def update_support_team(self, user_id: int, support_id: int, support1: int = None, support2: int = None) -> None:
if support1 == 4294967295:
support1 = None
if support2 == 4294967295:
support2 = None
sql = update(profile).where(profile.c.user==user_id).values(
support_set_1_1=support1 if support_id == 1 else profile.c.support_set_1_1,
support_set_1_2=support2 if support_id == 1 else profile.c.support_set_1_2,
@ -359,4 +363,4 @@ class PokkenProfileData(BaseData):
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to update support team {support_id} for user {user_id}")
self.logger.warning(f"Failed to update support team {support_id} for user {user_id}")

View File

@ -33,7 +33,7 @@ class SaoReader(BaseReader):
pull_bin_ram = True
if not path.exists(f"{self.bin_dir}"):
self.logger.warn(f"Couldn't find csv file in {self.bin_dir}, skipping")
self.logger.warning(f"Couldn't find csv file in {self.bin_dir}, skipping")
pull_bin_ram = False
if pull_bin_ram:
@ -66,7 +66,7 @@ class SaoReader(BaseReader):
except Exception as err:
print(err)
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.warning(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.info("Now reading HeroLog.csv")
try:
@ -100,7 +100,7 @@ class SaoReader(BaseReader):
except Exception as err:
print(err)
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.warning(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.info("Now reading Equipment.csv")
try:
@ -132,7 +132,7 @@ class SaoReader(BaseReader):
except Exception as err:
print(err)
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.warning(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.info("Now reading Item.csv")
try:
@ -162,7 +162,7 @@ class SaoReader(BaseReader):
except Exception as err:
print(err)
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.warning(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.info("Now reading SupportLog.csv")
try:
@ -194,7 +194,7 @@ class SaoReader(BaseReader):
except Exception as err:
print(err)
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.warning(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.info("Now reading Title.csv")
try:
@ -227,7 +227,7 @@ class SaoReader(BaseReader):
elif len(titleId) < 6: # current server code cannot have multiple lengths for the id
continue
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.warning(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.info("Now reading RareDropTable.csv")
try:
@ -251,4 +251,4 @@ class SaoReader(BaseReader):
except Exception as err:
print(err)
except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")
self.logger.warning(f"Couldn't read csv file in {self.bin_dir}, skipping")

View File

@ -192,7 +192,7 @@ class WaccaBase:
else:
profile = self.data.profile.get_profile(req.userId)
if profile is None:
self.logger.warn(
self.logger.warning(
f"Unknown user id {req.userId} attempted login from {req.chipId}"
)
return resp.make()
@ -282,7 +282,7 @@ class WaccaBase:
profile = self.data.profile.get_profile(req.userId)
if profile is None:
self.logger.warn(f"Unknown profile {req.userId}")
self.logger.warning(f"Unknown profile {req.userId}")
return resp.make()
self.logger.info(f"Get detail for profile {req.userId}")
@ -709,7 +709,7 @@ class WaccaBase:
profile = self.data.profile.get_profile(req.profileId)
if profile is None:
self.logger.warn(
self.logger.warning(
f"handle_user_music_update_request: No profile for game_id {req.profileId}"
)
return resp.make()
@ -1003,7 +1003,7 @@ class WaccaBase:
profile = self.data.profile.get_profile(req.profileId)
if profile is None:
self.logger.warn(
self.logger.warning(
f"handle_user_vip_get_request no profile with ID {req.profileId}"
)
return BaseResponse().make()

View File

@ -146,7 +146,7 @@ class WaccaServlet:
self.logger.debug(req_json)
if not hasattr(self.versions[internal_ver], func_to_find):
self.logger.warn(
self.logger.warning(
f"{req_json['appVersion']} has no handler for {func_to_find}"
)
resp = BaseResponse().make()

View File

@ -157,7 +157,7 @@ class WaccaLily(WaccaS):
else:
profile = self.data.profile.get_profile(req.userId)
if profile is None:
self.logger.warn(
self.logger.warning(
f"Unknown user id {req.userId} attempted login from {req.chipId}"
)
return resp.make()
@ -198,7 +198,7 @@ class WaccaLily(WaccaS):
profile = self.data.profile.get_profile(req.userId)
if profile is None:
self.logger.warn(f"Unknown profile {req.userId}")
self.logger.warning(f"Unknown profile {req.userId}")
return resp.make()
self.logger.info(f"Get detail for profile {req.userId}")

View File

@ -41,7 +41,7 @@ class WaccaReader(BaseReader):
def read_music(self, base_dir: str, table: str) -> None:
if not self.check_valid_pair(base_dir, table):
self.logger.warn(
self.logger.warning(
f"Cannot find {table} uasset/uexp pair at {base_dir}, music will not be read"
)
return

View File

@ -58,7 +58,7 @@ class WaccaReverse(WaccaLilyR):
profile = self.data.profile.get_profile(req.userId)
if profile is None:
self.logger.warn(f"Unknown profile {req.userId}")
self.logger.warning(f"Unknown profile {req.userId}")
return resp.make()
self.logger.info(f"Get detail for profile {req.userId}")

View File

@ -169,7 +169,7 @@ class WaccaItemData(BaseData):
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to delete ticket id {id}")
self.logger.warning(f"Failed to delete ticket id {id}")
return None
def get_trophies(self, user_id: int, season: int = None) -> Optional[List[Row]]:

View File

@ -218,7 +218,7 @@ class WaccaProfileData(BaseData):
result = self.execute(sql)
if result is None:
self.logger.warn(
self.logger.warning(
f"update_profile_dan: Failed to update! profile {profile_id}"
)
return None

View File

@ -294,7 +294,7 @@ class WaccaScoreData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(
self.logger.warning(
f"put_stageup: failed to update! user_id: {user_id} version: {version} stage_id: {stage_id}"
)
return None

View File

@ -63,7 +63,7 @@ class WaccaStaticData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert music {song_id} chart {chart_id}")
self.logger.warning(f"Failed to insert music {song_id} chart {chart_id}")
return None
return result.lastrowid