Merge branch 'develop'
This commit is contained in:
commit
b8b93a8d51
169 files changed
+11982
-3731
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
# Contributing to ARTEMiS
|
||||||
|
If you would like to contribute to artemis, either by adding features, games, or fixing bugs, you can do so by forking the repo and submitting a pull request [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls). Please make sure, if you're submitting a PR for a game or game version, that you're following the n-0/y-1 guidelines, or it will be rejected.
|
||||||
|
|
||||||
|
## Adding games
|
||||||
|
Guide WIP
|
||||||
|
|
||||||
|
## Adding game versions
|
||||||
|
Guide WIP
|
||||||
+100
-37
@@ -8,17 +8,18 @@ from logging.handlers import TimedRotatingFileHandler
|
|||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from core.data import Data
|
from core.data import Data
|
||||||
|
|
||||||
|
|
||||||
class AimedbProtocol(Protocol):
|
class AimedbProtocol(Protocol):
|
||||||
AIMEDB_RESPONSE_CODES = {
|
AIMEDB_RESPONSE_CODES = {
|
||||||
"felica_lookup": 0x03,
|
"felica_lookup": 0x03,
|
||||||
"lookup": 0x06,
|
"lookup": 0x06,
|
||||||
"log": 0x0a,
|
"log": 0x0A,
|
||||||
"campaign": 0x0c,
|
"campaign": 0x0C,
|
||||||
"touch": 0x0e,
|
"touch": 0x0E,
|
||||||
"lookup2": 0x10,
|
"lookup2": 0x10,
|
||||||
"felica_lookup2": 0x12,
|
"felica_lookup2": 0x12,
|
||||||
"log2": 0x14,
|
"log2": 0x14,
|
||||||
"hello": 0x65
|
"hello": 0x65,
|
||||||
}
|
}
|
||||||
|
|
||||||
request_list: Dict[int, Any] = {}
|
request_list: Dict[int, Any] = {}
|
||||||
@@ -35,9 +36,9 @@ class AimedbProtocol(Protocol):
|
|||||||
self.request_list[0x04] = self.handle_lookup
|
self.request_list[0x04] = self.handle_lookup
|
||||||
self.request_list[0x05] = self.handle_register
|
self.request_list[0x05] = self.handle_register
|
||||||
self.request_list[0x09] = self.handle_log
|
self.request_list[0x09] = self.handle_log
|
||||||
self.request_list[0x0b] = self.handle_campaign
|
self.request_list[0x0B] = self.handle_campaign
|
||||||
self.request_list[0x0d] = self.handle_touch
|
self.request_list[0x0D] = self.handle_touch
|
||||||
self.request_list[0x0f] = self.handle_lookup2
|
self.request_list[0x0F] = self.handle_lookup2
|
||||||
self.request_list[0x11] = self.handle_felica_lookup2
|
self.request_list[0x11] = self.handle_felica_lookup2
|
||||||
self.request_list[0x13] = self.handle_log2
|
self.request_list[0x13] = self.handle_log2
|
||||||
self.request_list[0x64] = self.handle_hello
|
self.request_list[0x64] = self.handle_hello
|
||||||
@@ -53,7 +54,9 @@ class AimedbProtocol(Protocol):
|
|||||||
self.logger.debug(f"{self.transport.getPeer().host} Connected")
|
self.logger.debug(f"{self.transport.getPeer().host} Connected")
|
||||||
|
|
||||||
def connectionLost(self, reason) -> None:
|
def connectionLost(self, reason) -> None:
|
||||||
self.logger.debug(f"{self.transport.getPeer().host} Disconnected - {reason.value}")
|
self.logger.debug(
|
||||||
|
f"{self.transport.getPeer().host} Disconnected - {reason.value}"
|
||||||
|
)
|
||||||
|
|
||||||
def dataReceived(self, data: bytes) -> None:
|
def dataReceived(self, data: bytes) -> None:
|
||||||
cipher = AES.new(self.config.aimedb.key.encode(), AES.MODE_ECB)
|
cipher = AES.new(self.config.aimedb.key.encode(), AES.MODE_ECB)
|
||||||
@@ -66,7 +69,7 @@ class AimedbProtocol(Protocol):
|
|||||||
|
|
||||||
self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}")
|
self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}")
|
||||||
|
|
||||||
if not decrypted[1] == 0xa1 and not decrypted[0] == 0x3e:
|
if not decrypted[1] == 0xA1 and not decrypted[0] == 0x3E:
|
||||||
self.logger.error(f"Bad magic")
|
self.logger.error(f"Bad magic")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -93,27 +96,43 @@ class AimedbProtocol(Protocol):
|
|||||||
|
|
||||||
def handle_campaign(self, data: bytes) -> bytes:
|
def handle_campaign(self, data: bytes) -> bytes:
|
||||||
self.logger.info(f"campaign from {self.transport.getPeer().host}")
|
self.logger.info(f"campaign from {self.transport.getPeer().host}")
|
||||||
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["campaign"], 0x0200, 0x0001)
|
ret = struct.pack(
|
||||||
|
"<5H",
|
||||||
|
0xA13E,
|
||||||
|
0x3087,
|
||||||
|
self.AIMEDB_RESPONSE_CODES["campaign"],
|
||||||
|
0x0200,
|
||||||
|
0x0001,
|
||||||
|
)
|
||||||
return self.append_padding(ret)
|
return self.append_padding(ret)
|
||||||
|
|
||||||
def handle_hello(self, data: bytes) -> bytes:
|
def handle_hello(self, data: bytes) -> bytes:
|
||||||
self.logger.info(f"hello from {self.transport.getPeer().host}")
|
self.logger.info(f"hello from {self.transport.getPeer().host}")
|
||||||
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["hello"], 0x0020, 0x0001)
|
ret = struct.pack(
|
||||||
|
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["hello"], 0x0020, 0x0001
|
||||||
|
)
|
||||||
return self.append_padding(ret)
|
return self.append_padding(ret)
|
||||||
|
|
||||||
def handle_lookup(self, data: bytes) -> bytes:
|
def handle_lookup(self, data: bytes) -> bytes:
|
||||||
luid = data[0x20: 0x2a].hex()
|
luid = data[0x20:0x2A].hex()
|
||||||
user_id = self.data.card.get_user_id_from_card(access_code=luid)
|
user_id = self.data.card.get_user_id_from_card(access_code=luid)
|
||||||
|
|
||||||
if user_id is None: user_id = -1
|
if user_id is None:
|
||||||
|
user_id = -1
|
||||||
|
|
||||||
self.logger.info(f"lookup from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}")
|
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 = struct.pack(
|
||||||
|
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0130, 0x0001
|
||||||
|
)
|
||||||
ret += bytes(0x20 - len(ret))
|
ret += bytes(0x20 - len(ret))
|
||||||
|
|
||||||
if user_id is None: ret += struct.pack("<iH", -1, 0)
|
if user_id is None:
|
||||||
else: ret += struct.pack("<l", user_id)
|
ret += struct.pack("<iH", -1, 0)
|
||||||
|
else:
|
||||||
|
ret += struct.pack("<l", user_id)
|
||||||
return self.append_padding(ret)
|
return self.append_padding(ret)
|
||||||
|
|
||||||
def handle_lookup2(self, data: bytes) -> bytes:
|
def handle_lookup2(self, data: bytes) -> bytes:
|
||||||
@@ -125,30 +144,49 @@ class AimedbProtocol(Protocol):
|
|||||||
return bytes(ret)
|
return bytes(ret)
|
||||||
|
|
||||||
def handle_felica_lookup(self, data: bytes) -> bytes:
|
def handle_felica_lookup(self, data: bytes) -> bytes:
|
||||||
idm = data[0x20: 0x28].hex()
|
idm = data[0x20:0x28].hex()
|
||||||
pmm = data[0x28: 0x30].hex()
|
pmm = data[0x28:0x30].hex()
|
||||||
access_code = self.data.card.to_access_code(idm)
|
access_code = self.data.card.to_access_code(idm)
|
||||||
self.logger.info(f"felica_lookup from {self.transport.getPeer().host}: idm {idm} pmm {pmm} -> access_code {access_code}")
|
self.logger.info(
|
||||||
|
f"felica_lookup from {self.transport.getPeer().host}: idm {idm} pmm {pmm} -> access_code {access_code}"
|
||||||
|
)
|
||||||
|
|
||||||
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["felica_lookup"], 0x0030, 0x0001)
|
ret = struct.pack(
|
||||||
|
"<5H",
|
||||||
|
0xA13E,
|
||||||
|
0x3087,
|
||||||
|
self.AIMEDB_RESPONSE_CODES["felica_lookup"],
|
||||||
|
0x0030,
|
||||||
|
0x0001,
|
||||||
|
)
|
||||||
ret += bytes(26)
|
ret += bytes(26)
|
||||||
ret += bytes.fromhex(access_code)
|
ret += bytes.fromhex(access_code)
|
||||||
|
|
||||||
return self.append_padding(ret)
|
return self.append_padding(ret)
|
||||||
|
|
||||||
def handle_felica_lookup2(self, data: bytes) -> bytes:
|
def handle_felica_lookup2(self, data: bytes) -> bytes:
|
||||||
idm = data[0x30: 0x38].hex()
|
idm = data[0x30:0x38].hex()
|
||||||
pmm = data[0x38: 0x40].hex()
|
pmm = data[0x38:0x40].hex()
|
||||||
access_code = self.data.card.to_access_code(idm)
|
access_code = self.data.card.to_access_code(idm)
|
||||||
user_id = self.data.card.get_user_id_from_card(access_code=access_code)
|
user_id = self.data.card.get_user_id_from_card(access_code=access_code)
|
||||||
|
|
||||||
if user_id is None: user_id = -1
|
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}")
|
self.logger.info(
|
||||||
|
f"felica_lookup2 from {self.transport.getPeer().host}: idm {idm} ipm {pmm} -> access_code {access_code} user_id {user_id}"
|
||||||
|
)
|
||||||
|
|
||||||
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["felica_lookup2"], 0x0140, 0x0001)
|
ret = struct.pack(
|
||||||
|
"<5H",
|
||||||
|
0xA13E,
|
||||||
|
0x3087,
|
||||||
|
self.AIMEDB_RESPONSE_CODES["felica_lookup2"],
|
||||||
|
0x0140,
|
||||||
|
0x0001,
|
||||||
|
)
|
||||||
ret += bytes(22)
|
ret += bytes(22)
|
||||||
ret += struct.pack("<lq", user_id, -1) # first -1 is ext_id, 3rd is access code
|
ret += struct.pack("<lq", user_id, -1) # first -1 is ext_id, 3rd is access code
|
||||||
ret += bytes.fromhex(access_code)
|
ret += bytes.fromhex(access_code)
|
||||||
ret += struct.pack("<l", 1)
|
ret += struct.pack("<l", 1)
|
||||||
|
|
||||||
@@ -156,14 +194,16 @@ class AimedbProtocol(Protocol):
|
|||||||
|
|
||||||
def handle_touch(self, data: bytes) -> bytes:
|
def handle_touch(self, data: bytes) -> bytes:
|
||||||
self.logger.info(f"touch from {self.transport.getPeer().host}")
|
self.logger.info(f"touch from {self.transport.getPeer().host}")
|
||||||
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["touch"], 0x0050, 0x0001)
|
ret = struct.pack(
|
||||||
|
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["touch"], 0x0050, 0x0001
|
||||||
|
)
|
||||||
ret += bytes(5)
|
ret += bytes(5)
|
||||||
ret += struct.pack("<3H", 0x6f, 0, 1)
|
ret += struct.pack("<3H", 0x6F, 0, 1)
|
||||||
|
|
||||||
return self.append_padding(ret)
|
return self.append_padding(ret)
|
||||||
|
|
||||||
def handle_register(self, data: bytes) -> bytes:
|
def handle_register(self, data: bytes) -> bytes:
|
||||||
luid = data[0x20: 0x2a].hex()
|
luid = data[0x20:0x2A].hex()
|
||||||
if self.config.server.allow_user_registration:
|
if self.config.server.allow_user_registration:
|
||||||
user_id = self.data.user.create_user()
|
user_id = self.data.user.create_user()
|
||||||
|
|
||||||
@@ -178,13 +218,24 @@ class AimedbProtocol(Protocol):
|
|||||||
user_id = -1
|
user_id = -1
|
||||||
self.logger.error("Failed to register card!")
|
self.logger.error("Failed to register card!")
|
||||||
|
|
||||||
self.logger.info(f"register from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}")
|
self.logger.info(
|
||||||
|
f"register from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}"
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.logger.info(f"register from {self.transport.getPeer().host} blocked!: luid {luid}")
|
self.logger.info(
|
||||||
|
f"register from {self.transport.getPeer().host} blocked!: luid {luid}"
|
||||||
|
)
|
||||||
user_id = -1
|
user_id = -1
|
||||||
|
|
||||||
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0030, 0x0001 if user_id > -1 else 0)
|
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 += bytes(0x20 - len(ret))
|
||||||
ret += struct.pack("<l", user_id)
|
ret += struct.pack("<l", user_id)
|
||||||
|
|
||||||
@@ -193,26 +244,36 @@ class AimedbProtocol(Protocol):
|
|||||||
def handle_log(self, data: bytes) -> bytes:
|
def handle_log(self, data: bytes) -> bytes:
|
||||||
# TODO: Save aimedb logs
|
# TODO: Save aimedb logs
|
||||||
self.logger.info(f"log from {self.transport.getPeer().host}")
|
self.logger.info(f"log from {self.transport.getPeer().host}")
|
||||||
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["log"], 0x0020, 0x0001)
|
ret = struct.pack(
|
||||||
|
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["log"], 0x0020, 0x0001
|
||||||
|
)
|
||||||
return self.append_padding(ret)
|
return self.append_padding(ret)
|
||||||
|
|
||||||
def handle_log2(self, data: bytes) -> bytes:
|
def handle_log2(self, data: bytes) -> bytes:
|
||||||
self.logger.info(f"log2 from {self.transport.getPeer().host}")
|
self.logger.info(f"log2 from {self.transport.getPeer().host}")
|
||||||
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["log2"], 0x0040, 0x0001)
|
ret = struct.pack(
|
||||||
|
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["log2"], 0x0040, 0x0001
|
||||||
|
)
|
||||||
ret += bytes(22)
|
ret += bytes(22)
|
||||||
ret += struct.pack("H", 1)
|
ret += struct.pack("H", 1)
|
||||||
|
|
||||||
return self.append_padding(ret)
|
return self.append_padding(ret)
|
||||||
|
|
||||||
|
|
||||||
class AimedbFactory(Factory):
|
class AimedbFactory(Factory):
|
||||||
protocol = AimedbProtocol
|
protocol = AimedbProtocol
|
||||||
|
|
||||||
def __init__(self, cfg: CoreConfig) -> None:
|
def __init__(self, cfg: CoreConfig) -> None:
|
||||||
self.config = cfg
|
self.config = cfg
|
||||||
log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s"
|
log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s"
|
||||||
log_fmt = logging.Formatter(log_fmt_str)
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
self.logger = logging.getLogger("aimedb")
|
self.logger = logging.getLogger("aimedb")
|
||||||
|
|
||||||
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "aimedb"), when="d", backupCount=10)
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(self.config.server.log_dir, "aimedb"),
|
||||||
|
when="d",
|
||||||
|
backupCount=10,
|
||||||
|
)
|
||||||
fileHandler.setFormatter(log_fmt)
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
consoleHandler = logging.StreamHandler()
|
consoleHandler = logging.StreamHandler()
|
||||||
@@ -222,7 +283,9 @@ class AimedbFactory(Factory):
|
|||||||
self.logger.addHandler(consoleHandler)
|
self.logger.addHandler(consoleHandler)
|
||||||
|
|
||||||
self.logger.setLevel(self.config.aimedb.loglevel)
|
self.logger.setLevel(self.config.aimedb.loglevel)
|
||||||
coloredlogs.install(level=cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str)
|
coloredlogs.install(
|
||||||
|
level=cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||||
|
)
|
||||||
|
|
||||||
if self.config.aimedb.key == "":
|
if self.config.aimedb.key == "":
|
||||||
self.logger.error("Please set 'key' field in your config file.")
|
self.logger.error("Please set 'key' field in your config file.")
|
||||||
|
|||||||
+121
-101
@@ -12,10 +12,11 @@ from Crypto.Signature import PKCS1_v1_5
|
|||||||
from time import strptime
|
from time import strptime
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from core.data import Data
|
|
||||||
from core.utils import Utils
|
from core.utils import Utils
|
||||||
|
from core.data import Data
|
||||||
from core.const import *
|
from core.const import *
|
||||||
|
|
||||||
|
|
||||||
class AllnetServlet:
|
class AllnetServlet:
|
||||||
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -29,7 +30,11 @@ class AllnetServlet:
|
|||||||
log_fmt_str = "[%(asctime)s] Allnet | %(levelname)s | %(message)s"
|
log_fmt_str = "[%(asctime)s] Allnet | %(levelname)s | %(message)s"
|
||||||
log_fmt = logging.Formatter(log_fmt_str)
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
|
|
||||||
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "allnet"), when="d", backupCount=10)
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(self.config.server.log_dir, "allnet"),
|
||||||
|
when="d",
|
||||||
|
backupCount=10,
|
||||||
|
)
|
||||||
fileHandler.setFormatter(log_fmt)
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
consoleHandler = logging.StreamHandler()
|
consoleHandler = logging.StreamHandler()
|
||||||
@@ -39,7 +44,9 @@ class AllnetServlet:
|
|||||||
self.logger.addHandler(consoleHandler)
|
self.logger.addHandler(consoleHandler)
|
||||||
|
|
||||||
self.logger.setLevel(core_cfg.allnet.loglevel)
|
self.logger.setLevel(core_cfg.allnet.loglevel)
|
||||||
coloredlogs.install(level=core_cfg.allnet.loglevel, logger=self.logger, fmt=log_fmt_str)
|
coloredlogs.install(
|
||||||
|
level=core_cfg.allnet.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||||
|
)
|
||||||
self.logger.initialized = True
|
self.logger.initialized = True
|
||||||
|
|
||||||
plugins = Utils.get_all_titles()
|
plugins = Utils.get_all_titles()
|
||||||
@@ -48,53 +55,21 @@ class AllnetServlet:
|
|||||||
self.logger.error("No games detected!")
|
self.logger.error("No games detected!")
|
||||||
|
|
||||||
for _, mod in plugins.items():
|
for _, mod in plugins.items():
|
||||||
for code in mod.game_codes:
|
if hasattr(mod.index, "get_allnet_info"):
|
||||||
if hasattr(mod, "use_default_title") and mod.use_default_title:
|
for code in mod.game_codes:
|
||||||
if hasattr(mod, "include_protocol") and mod.include_protocol:
|
enabled, uri, host = mod.index.get_allnet_info(
|
||||||
if hasattr(mod, "title_secure") and mod.title_secure:
|
code, self.config, self.config_folder
|
||||||
uri = "https://"
|
)
|
||||||
|
|
||||||
else:
|
if enabled:
|
||||||
uri = "http://"
|
self.uri_registry[code] = (uri, host)
|
||||||
|
|
||||||
else:
|
self.logger.info(
|
||||||
uri = ""
|
f"Serving {len(self.uri_registry)} game codes port {core_cfg.allnet.port}"
|
||||||
|
)
|
||||||
if core_cfg.server.is_develop:
|
|
||||||
uri += f"{core_cfg.title.hostname}:{core_cfg.title.port}"
|
|
||||||
|
|
||||||
else:
|
|
||||||
uri += f"{core_cfg.title.hostname}"
|
|
||||||
|
|
||||||
uri += f"/{code}/$v"
|
|
||||||
|
|
||||||
if hasattr(mod, "trailing_slash") and mod.trailing_slash:
|
|
||||||
uri += "/"
|
|
||||||
|
|
||||||
else:
|
|
||||||
if hasattr(mod, "uri"):
|
|
||||||
uri = mod.uri
|
|
||||||
else:
|
|
||||||
uri = ""
|
|
||||||
|
|
||||||
if hasattr(mod, "host"):
|
|
||||||
host = mod.host
|
|
||||||
|
|
||||||
elif hasattr(mod, "use_default_host") and mod.use_default_host:
|
|
||||||
if core_cfg.server.is_develop:
|
|
||||||
host = f"{core_cfg.title.hostname}:{core_cfg.title.port}"
|
|
||||||
|
|
||||||
else:
|
|
||||||
host = f"{core_cfg.title.hostname}"
|
|
||||||
|
|
||||||
else:
|
|
||||||
host = ""
|
|
||||||
|
|
||||||
self.uri_registry[code] = (uri, host)
|
|
||||||
self.logger.info(f"Allnet serving {len(self.uri_registry)} games on port {core_cfg.allnet.port}")
|
|
||||||
|
|
||||||
def handle_poweron(self, request: Request, _: Dict):
|
def handle_poweron(self, request: Request, _: Dict):
|
||||||
request_ip = request.getClientAddress().host
|
request_ip = Utils.get_ip_addr(request)
|
||||||
try:
|
try:
|
||||||
req_dict = self.allnet_req_to_dict(request.content.getvalue())
|
req_dict = self.allnet_req_to_dict(request.content.getvalue())
|
||||||
if req_dict is None:
|
if req_dict is None:
|
||||||
@@ -103,15 +78,17 @@ class AllnetServlet:
|
|||||||
req = AllnetPowerOnRequest(req_dict[0])
|
req = AllnetPowerOnRequest(req_dict[0])
|
||||||
# Validate the request. Currently we only validate the fields we plan on using
|
# Validate the request. Currently we only validate the fields we plan on using
|
||||||
|
|
||||||
if not req.game_id or not req.ver or not req.token or not req.serial or not req.ip:
|
if not req.game_id or not req.ver or not req.serial or not req.ip:
|
||||||
raise AllnetRequestException(f"Bad auth request params from {request_ip} - {vars(req)}")
|
raise AllnetRequestException(
|
||||||
|
f"Bad auth request params from {request_ip} - {vars(req)}"
|
||||||
|
)
|
||||||
|
|
||||||
except AllnetRequestException as e:
|
except AllnetRequestException as e:
|
||||||
if e.message != "":
|
if e.message != "":
|
||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
if req.format_ver == 3:
|
if req.format_ver == "3":
|
||||||
resp = AllnetPowerOnResponse3(req.token)
|
resp = AllnetPowerOnResponse3(req.token)
|
||||||
else:
|
else:
|
||||||
resp = AllnetPowerOnResponse2()
|
resp = AllnetPowerOnResponse2()
|
||||||
@@ -119,7 +96,9 @@ class AllnetServlet:
|
|||||||
self.logger.debug(f"Allnet request: {vars(req)}")
|
self.logger.debug(f"Allnet request: {vars(req)}")
|
||||||
if req.game_id not in self.uri_registry:
|
if req.game_id not in self.uri_registry:
|
||||||
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
|
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.data.base.log_event(
|
||||||
|
"allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg
|
||||||
|
)
|
||||||
self.logger.warn(msg)
|
self.logger.warn(msg)
|
||||||
|
|
||||||
resp.stat = 0
|
resp.stat = 0
|
||||||
@@ -130,7 +109,9 @@ class AllnetServlet:
|
|||||||
machine = self.data.arcade.get_machine(req.serial)
|
machine = self.data.arcade.get_machine(req.serial)
|
||||||
if machine is None and not self.config.server.allow_unregistered_serials:
|
if machine is None and not self.config.server.allow_unregistered_serials:
|
||||||
msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}."
|
msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}."
|
||||||
self.data.base.log_event("allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg)
|
self.data.base.log_event(
|
||||||
|
"allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg
|
||||||
|
)
|
||||||
self.logger.warn(msg)
|
self.logger.warn(msg)
|
||||||
|
|
||||||
resp.stat = 0
|
resp.stat = 0
|
||||||
@@ -138,7 +119,9 @@ class AllnetServlet:
|
|||||||
|
|
||||||
if machine is not None:
|
if machine is not None:
|
||||||
arcade = self.data.arcade.get_arcade(machine["arcade"])
|
arcade = self.data.arcade.get_arcade(machine["arcade"])
|
||||||
country = arcade["country"] if machine["country"] is None else machine["country"]
|
country = (
|
||||||
|
arcade["country"] if machine["country"] is None else machine["country"]
|
||||||
|
)
|
||||||
if country is None:
|
if country is None:
|
||||||
country = AllnetCountryCode.JAPAN.value
|
country = AllnetCountryCode.JAPAN.value
|
||||||
|
|
||||||
@@ -147,11 +130,25 @@ class AllnetServlet:
|
|||||||
resp.allnet_id = machine["id"]
|
resp.allnet_id = machine["id"]
|
||||||
resp.name = arcade["name"] if arcade["name"] is not None else ""
|
resp.name = arcade["name"] if arcade["name"] is not None else ""
|
||||||
resp.nickname = arcade["nickname"] if arcade["nickname"] is not None else ""
|
resp.nickname = arcade["nickname"] if arcade["nickname"] is not None else ""
|
||||||
resp.region0 = arcade["region_id"] if arcade["region_id"] is not None else AllnetJapanRegionId.AICHI.value
|
resp.region0 = (
|
||||||
resp.region_name0 = arcade["country"] if arcade["country"] is not None else AllnetCountryCode.JAPAN.value
|
arcade["region_id"]
|
||||||
resp.region_name1 = arcade["state"] if arcade["state"] is not None else AllnetJapanRegionId.AICHI.name
|
if arcade["region_id"] is not None
|
||||||
|
else AllnetJapanRegionId.AICHI.value
|
||||||
|
)
|
||||||
|
resp.region_name0 = (
|
||||||
|
arcade["country"]
|
||||||
|
if arcade["country"] is not None
|
||||||
|
else AllnetCountryCode.JAPAN.value
|
||||||
|
)
|
||||||
|
resp.region_name1 = (
|
||||||
|
arcade["state"]
|
||||||
|
if arcade["state"] is not None
|
||||||
|
else AllnetJapanRegionId.AICHI.name
|
||||||
|
)
|
||||||
resp.region_name2 = arcade["city"] if arcade["city"] is not None else ""
|
resp.region_name2 = arcade["city"] if arcade["city"] is not None else ""
|
||||||
resp.client_timezone = arcade["timezone"] if arcade["timezone"] is not None else "+0900"
|
resp.client_timezone = (
|
||||||
|
arcade["timezone"] if arcade["timezone"] is not None else "+0900"
|
||||||
|
)
|
||||||
|
|
||||||
int_ver = req.ver.replace(".", "")
|
int_ver = req.ver.replace(".", "")
|
||||||
resp.uri = resp.uri.replace("$v", int_ver)
|
resp.uri = resp.uri.replace("$v", int_ver)
|
||||||
@@ -165,7 +162,7 @@ class AllnetServlet:
|
|||||||
return self.dict_to_http_form_string([vars(resp)]).encode("utf-8")
|
return self.dict_to_http_form_string([vars(resp)]).encode("utf-8")
|
||||||
|
|
||||||
def handle_dlorder(self, request: Request, _: Dict):
|
def handle_dlorder(self, request: Request, _: Dict):
|
||||||
request_ip = request.getClientAddress().host
|
request_ip = Utils.get_ip_addr(request)
|
||||||
try:
|
try:
|
||||||
req_dict = self.allnet_req_to_dict(request.content.getvalue())
|
req_dict = self.allnet_req_to_dict(request.content.getvalue())
|
||||||
if req_dict is None:
|
if req_dict is None:
|
||||||
@@ -175,30 +172,34 @@ class AllnetServlet:
|
|||||||
# Validate the request. Currently we only validate the fields we plan on using
|
# Validate the request. Currently we only validate the fields we plan on using
|
||||||
|
|
||||||
if not req.game_id or not req.ver or not req.serial:
|
if not req.game_id or not req.ver or not req.serial:
|
||||||
raise AllnetRequestException(f"Bad download request params from {request_ip} - {vars(req)}")
|
raise AllnetRequestException(
|
||||||
|
f"Bad download request params from {request_ip} - {vars(req)}"
|
||||||
|
)
|
||||||
|
|
||||||
except AllnetRequestException as e:
|
except AllnetRequestException as e:
|
||||||
if e.message != "":
|
if e.message != "":
|
||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
|
self.logger.info(f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}")
|
||||||
resp = AllnetDownloadOrderResponse()
|
resp = AllnetDownloadOrderResponse()
|
||||||
|
|
||||||
if not self.config.allnet.allow_online_updates:
|
if not self.config.allnet.allow_online_updates:
|
||||||
return self.dict_to_http_form_string([vars(resp)])
|
return self.dict_to_http_form_string([vars(resp)])
|
||||||
|
|
||||||
else: # TODO: Actual dlorder response
|
else: # TODO: Actual dlorder response
|
||||||
return self.dict_to_http_form_string([vars(resp)])
|
return self.dict_to_http_form_string([vars(resp)])
|
||||||
|
|
||||||
def handle_billing_request(self, request: Request, _: Dict):
|
def handle_billing_request(self, request: Request, _: Dict):
|
||||||
req_dict = self.billing_req_to_dict(request.content.getvalue())
|
req_dict = self.billing_req_to_dict(request.content.getvalue())
|
||||||
request_ip = request.getClientAddress()
|
request_ip = Utils.get_ip_addr(request)
|
||||||
if req_dict is None:
|
if req_dict is None:
|
||||||
self.logger.error(f"Failed to parse request {request.content.getvalue()}")
|
self.logger.error(f"Failed to parse request {request.content.getvalue()}")
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
self.logger.debug(f"request {req_dict}")
|
self.logger.debug(f"request {req_dict}")
|
||||||
|
|
||||||
rsa = RSA.import_key(open(self.config.billing.signing_key, 'rb').read())
|
rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read())
|
||||||
signer = PKCS1_v1_5.new(rsa)
|
signer = PKCS1_v1_5.new(rsa)
|
||||||
digest = SHA.new()
|
digest = SHA.new()
|
||||||
|
|
||||||
@@ -214,17 +215,21 @@ class AllnetServlet:
|
|||||||
machine = self.data.arcade.get_machine(kc_serial)
|
machine = self.data.arcade.get_machine(kc_serial)
|
||||||
if machine is None and not self.config.server.allow_unregistered_serials:
|
if machine is None and not self.config.server.allow_unregistered_serials:
|
||||||
msg = f"Unrecognised serial {kc_serial} attempted billing checkin from {request_ip} for game {kc_game}."
|
msg = f"Unrecognised serial {kc_serial} attempted billing checkin from {request_ip} for game {kc_game}."
|
||||||
self.data.base.log_event("allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg)
|
self.data.base.log_event(
|
||||||
|
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg
|
||||||
|
)
|
||||||
self.logger.warn(msg)
|
self.logger.warn(msg)
|
||||||
|
|
||||||
resp = BillingResponse("", "", "", "")
|
resp = BillingResponse("", "", "", "")
|
||||||
resp.result = "1"
|
resp.result = "1"
|
||||||
return self.dict_to_http_form_string([vars(resp)])
|
return self.dict_to_http_form_string([vars(resp)])
|
||||||
|
|
||||||
msg = f"Billing checkin from {request.getClientIP()}: game {kc_game} keychip {kc_serial} playcount " \
|
msg = (
|
||||||
|
f"Billing checkin from {request_ip}: game {kc_game} keychip {kc_serial} playcount "
|
||||||
f"{kc_playcount} billing_type {kc_billigtype} nearfull {kc_nearfull} playlimit {kc_playlimit}"
|
f"{kc_playcount} billing_type {kc_billigtype} nearfull {kc_nearfull} playlimit {kc_playlimit}"
|
||||||
|
)
|
||||||
self.logger.info(msg)
|
self.logger.info(msg)
|
||||||
self.data.base.log_event('billing', 'BILLING_CHECKIN_OK', logging.INFO, msg)
|
self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, msg)
|
||||||
|
|
||||||
while kc_playcount > kc_playlimit:
|
while kc_playcount > kc_playlimit:
|
||||||
kc_playlimit += 1024
|
kc_playlimit += 1024
|
||||||
@@ -233,11 +238,11 @@ class AllnetServlet:
|
|||||||
playlimit = kc_playlimit
|
playlimit = kc_playlimit
|
||||||
nearfull = kc_nearfull + (kc_billigtype * 0x00010000)
|
nearfull = kc_nearfull + (kc_billigtype * 0x00010000)
|
||||||
|
|
||||||
digest.update(playlimit.to_bytes(4, 'little') + kc_serial_bytes)
|
digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes)
|
||||||
playlimit_sig = signer.sign(digest).hex()
|
playlimit_sig = signer.sign(digest).hex()
|
||||||
|
|
||||||
digest = SHA.new()
|
digest = SHA.new()
|
||||||
digest.update(nearfull.to_bytes(4, 'little') + kc_serial_bytes)
|
digest.update(nearfull.to_bytes(4, "little") + kc_serial_bytes)
|
||||||
nearfull_sig = signer.sign(digest).hex()
|
nearfull_sig = signer.sign(digest).hex()
|
||||||
|
|
||||||
# TODO: playhistory
|
# TODO: playhistory
|
||||||
@@ -252,17 +257,17 @@ class AllnetServlet:
|
|||||||
return resp_str.encode("utf-8")
|
return resp_str.encode("utf-8")
|
||||||
|
|
||||||
def handle_naomitest(self, request: Request, _: Dict) -> bytes:
|
def handle_naomitest(self, request: Request, _: Dict) -> bytes:
|
||||||
self.logger.info(f"Ping from {request.getClientAddress().host}")
|
self.logger.info(f"Ping from {Utils.get_ip_addr(request)}")
|
||||||
return b"naomi ok"
|
return b"naomi ok"
|
||||||
|
|
||||||
def kvp_to_dict(self, kvp: List[str]) -> List[Dict[str, Any]]:
|
def kvp_to_dict(self, kvp: List[str]) -> List[Dict[str, Any]]:
|
||||||
ret: List[Dict[str, Any]] = []
|
ret: List[Dict[str, Any]] = []
|
||||||
for x in kvp:
|
for x in kvp:
|
||||||
items = x.split('&')
|
items = x.split("&")
|
||||||
tmp = {}
|
tmp = {}
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
kvp = item.split('=')
|
kvp = item.split("=")
|
||||||
if len(kvp) == 2:
|
if len(kvp) == 2:
|
||||||
tmp[kvp[0]] = kvp[1]
|
tmp[kvp[0]] = kvp[1]
|
||||||
|
|
||||||
@@ -277,7 +282,7 @@ class AllnetServlet:
|
|||||||
try:
|
try:
|
||||||
decomp = zlib.decompressobj(-zlib.MAX_WBITS)
|
decomp = zlib.decompressobj(-zlib.MAX_WBITS)
|
||||||
unzipped = decomp.decompress(data)
|
unzipped = decomp.decompress(data)
|
||||||
sections = unzipped.decode('ascii').split('\r\n')
|
sections = unzipped.decode("ascii").split("\r\n")
|
||||||
|
|
||||||
return self.kvp_to_dict(sections)
|
return self.kvp_to_dict(sections)
|
||||||
|
|
||||||
@@ -292,7 +297,7 @@ class AllnetServlet:
|
|||||||
try:
|
try:
|
||||||
zipped = base64.b64decode(data)
|
zipped = base64.b64decode(data)
|
||||||
unzipped = zlib.decompress(zipped)
|
unzipped = zlib.decompress(zipped)
|
||||||
sections = unzipped.decode('utf-8').split('\r\n')
|
sections = unzipped.decode("utf-8").split("\r\n")
|
||||||
|
|
||||||
return self.kvp_to_dict(sections)
|
return self.kvp_to_dict(sections)
|
||||||
|
|
||||||
@@ -300,14 +305,19 @@ class AllnetServlet:
|
|||||||
self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}")
|
self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def dict_to_http_form_string(self, data:List[Dict[str, Any]], crlf: bool = False, trailing_newline: bool = True) -> Optional[str]:
|
def dict_to_http_form_string(
|
||||||
|
self,
|
||||||
|
data: List[Dict[str, Any]],
|
||||||
|
crlf: bool = False,
|
||||||
|
trailing_newline: bool = True,
|
||||||
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Takes a python dictionary and parses it into an allnet response string
|
Takes a python dictionary and parses it into an allnet response string
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
urlencode = ""
|
urlencode = ""
|
||||||
for item in data:
|
for item in data:
|
||||||
for k,v in item.items():
|
for k, v in item.items():
|
||||||
urlencode += f"{k}={v}&"
|
urlencode += f"{k}={v}&"
|
||||||
|
|
||||||
if crlf:
|
if crlf:
|
||||||
@@ -327,26 +337,24 @@ class AllnetServlet:
|
|||||||
self.logger.error(f"dict_to_http_form_string: {e} while parsing {data}")
|
self.logger.error(f"dict_to_http_form_string: {e} while parsing {data}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
class AllnetPowerOnRequest():
|
|
||||||
|
class AllnetPowerOnRequest:
|
||||||
def __init__(self, req: Dict) -> None:
|
def __init__(self, req: Dict) -> None:
|
||||||
if req is None:
|
if req is None:
|
||||||
raise AllnetRequestException("Request processing failed")
|
raise AllnetRequestException("Request processing failed")
|
||||||
self.game_id: str = req["game_id"] if "game_id" in req else ""
|
self.game_id: str = req.get("game_id", "")
|
||||||
self.ver: str = req["ver"] if "ver" in req else ""
|
self.ver: str = req.get("ver", "")
|
||||||
self.serial: str = req["serial"] if "serial" in req else ""
|
self.serial: str = req.get("serial", "")
|
||||||
self.ip: str = req["ip"] if "ip" in req else ""
|
self.ip: str = req.get("ip", "")
|
||||||
self.firm_ver: str = req["firm_ver"] if "firm_ver" in req else ""
|
self.firm_ver: str = req.get("firm_ver", "")
|
||||||
self.boot_ver: str = req["boot_ver"] if "boot_ver" in req else ""
|
self.boot_ver: str = req.get("boot_ver", "")
|
||||||
self.encode: str = req["encode"] if "encode" in req else ""
|
self.encode: str = req.get("encode", "")
|
||||||
|
self.hops = int(req.get("hops", "0"))
|
||||||
|
self.format_ver = req.get("format_ver", "2")
|
||||||
|
self.token = int(req.get("token", "0"))
|
||||||
|
|
||||||
try:
|
|
||||||
self.hops = int(req["hops"]) if "hops" in req else 0
|
|
||||||
self.format_ver = int(req["format_ver"]) if "format_ver" in req else 2
|
|
||||||
self.token = int(req["token"]) if "token" in req else 0
|
|
||||||
except ValueError as e:
|
|
||||||
raise AllnetRequestException(f"Failed to parse int: {e}")
|
|
||||||
|
|
||||||
class AllnetPowerOnResponse3():
|
class AllnetPowerOnResponse3:
|
||||||
def __init__(self, token) -> None:
|
def __init__(self, token) -> None:
|
||||||
self.stat = 1
|
self.stat = 1
|
||||||
self.uri = ""
|
self.uri = ""
|
||||||
@@ -362,12 +370,15 @@ class AllnetPowerOnResponse3():
|
|||||||
self.country = "JPN"
|
self.country = "JPN"
|
||||||
self.allnet_id = "123"
|
self.allnet_id = "123"
|
||||||
self.client_timezone = "+0900"
|
self.client_timezone = "+0900"
|
||||||
self.utc_time = datetime.now(tz=pytz.timezone('UTC')).strftime("%Y-%m-%dT%H:%M:%SZ")
|
self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime(
|
||||||
self.setting = ""
|
"%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
)
|
||||||
|
self.setting = "1"
|
||||||
self.res_ver = "3"
|
self.res_ver = "3"
|
||||||
self.token = str(token)
|
self.token = str(token)
|
||||||
|
|
||||||
class AllnetPowerOnResponse2():
|
|
||||||
|
class AllnetPowerOnResponse2:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.stat = 1
|
self.stat = 1
|
||||||
self.uri = ""
|
self.uri = ""
|
||||||
@@ -391,23 +402,31 @@ class AllnetPowerOnResponse2():
|
|||||||
self.timezone = "+0900"
|
self.timezone = "+0900"
|
||||||
self.res_class = "PowerOnResponseV2"
|
self.res_class = "PowerOnResponseV2"
|
||||||
|
|
||||||
class AllnetDownloadOrderRequest():
|
|
||||||
def __init__(self, req: Dict) -> None:
|
|
||||||
self.game_id = req["game_id"] if "game_id" in req else ""
|
|
||||||
self.ver = req["ver"] if "ver" in req else ""
|
|
||||||
self.serial = req["serial"] if "serial" in req else ""
|
|
||||||
self.encode = req["encode"] if "encode" in req else ""
|
|
||||||
|
|
||||||
class AllnetDownloadOrderResponse():
|
class AllnetDownloadOrderRequest:
|
||||||
|
def __init__(self, req: Dict) -> None:
|
||||||
|
self.game_id = req.get("game_id", "")
|
||||||
|
self.ver = req.get("ver", "")
|
||||||
|
self.serial = req.get("serial", "")
|
||||||
|
self.encode = req.get("encode", "")
|
||||||
|
|
||||||
|
|
||||||
|
class AllnetDownloadOrderResponse:
|
||||||
def __init__(self, stat: int = 1, serial: str = "", uri: str = "null") -> None:
|
def __init__(self, stat: int = 1, serial: str = "", uri: str = "null") -> None:
|
||||||
self.stat = stat
|
self.stat = stat
|
||||||
self.serial = serial
|
self.serial = serial
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
|
|
||||||
class BillingResponse():
|
|
||||||
def __init__(self, playlimit: str = "", playlimit_sig: str = "", nearfull: str = "", nearfull_sig: str = "",
|
|
||||||
playhistory: str = "000000/0:000000/0:000000/0") -> None:
|
|
||||||
|
|
||||||
|
class BillingResponse:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
playlimit: str = "",
|
||||||
|
playlimit_sig: str = "",
|
||||||
|
nearfull: str = "",
|
||||||
|
nearfull_sig: str = "",
|
||||||
|
playhistory: str = "000000/0:000000/0:000000/0",
|
||||||
|
) -> None:
|
||||||
self.result = "0"
|
self.result = "0"
|
||||||
self.waitime = "100"
|
self.waitime = "100"
|
||||||
self.linelimit = "1"
|
self.linelimit = "1"
|
||||||
@@ -423,6 +442,7 @@ class BillingResponse():
|
|||||||
# playhistory -> YYYYMM/C:...
|
# playhistory -> YYYYMM/C:...
|
||||||
# YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period
|
# YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period
|
||||||
|
|
||||||
|
|
||||||
class AllnetRequestException(Exception):
|
class AllnetRequestException(Exception):
|
||||||
def __init__(self, message="") -> None:
|
def __init__(self, message="") -> None:
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|||||||
+137
-52
@@ -1,33 +1,47 @@
|
|||||||
import logging, os
|
import logging, os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class ServerConfig:
|
class ServerConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
self.__config = parent_config
|
self.__config = parent_config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def listen_address(self) -> str:
|
def listen_address(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'server', 'listen_address', default='127.0.0.1')
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "server", "listen_address", default="127.0.0.1"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def allow_user_registration(self) -> bool:
|
def allow_user_registration(self) -> bool:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'server', 'allow_user_registration', default=True)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "server", "allow_user_registration", default=True
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def allow_unregistered_serials(self) -> bool:
|
def allow_unregistered_serials(self) -> bool:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'server', 'allow_unregistered_serials', default=True)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "server", "allow_unregistered_serials", default=True
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'server', 'name', default="ARTEMiS")
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "server", "name", default="ARTEMiS"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_develop(self) -> bool:
|
def is_develop(self) -> bool:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'server', 'is_develop', default=True)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "server", "is_develop", default=True
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log_dir(self) -> str:
|
def log_dir(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'server', 'log_dir', default='logs')
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "server", "log_dir", default="logs"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TitleConfig:
|
class TitleConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
@@ -35,15 +49,24 @@ class TitleConfig:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def loglevel(self) -> int:
|
def loglevel(self) -> int:
|
||||||
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'title', 'loglevel', default="info"))
|
return CoreConfig.str_to_loglevel(
|
||||||
|
CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "title", "loglevel", default="info"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hostname(self) -> str:
|
def hostname(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'title', 'hostname', default="localhost")
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "title", "hostname", default="localhost"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
def port(self) -> int:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'title', 'port', default=8080)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "title", "port", default=8080
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseConfig:
|
class DatabaseConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
@@ -51,43 +74,70 @@ class DatabaseConfig:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def host(self) -> str:
|
def host(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'host', default="localhost")
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "database", "host", default="localhost"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def username(self) -> str:
|
def username(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'username', default='aime')
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "database", "username", default="aime"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def password(self) -> str:
|
def password(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'password', default='aime')
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "database", "password", default="aime"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'name', default='aime')
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "database", "name", default="aime"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
def port(self) -> int:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'port', default=3306)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "database", "port", default=3306
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def protocol(self) -> str:
|
def protocol(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'type', default="mysql")
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "database", "type", default="mysql"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sha2_password(self) -> bool:
|
def sha2_password(self) -> bool:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'sha2_password', default=False)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "database", "sha2_password", default=False
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loglevel(self) -> int:
|
def loglevel(self) -> int:
|
||||||
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'database', 'loglevel', default="info"))
|
return CoreConfig.str_to_loglevel(
|
||||||
|
CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "database", "loglevel", default="info"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_table_autoincrement_start(self) -> int:
|
def user_table_autoincrement_start(self) -> int:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'user_table_autoincrement_start', default=10000)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config,
|
||||||
|
"core",
|
||||||
|
"database",
|
||||||
|
"user_table_autoincrement_start",
|
||||||
|
default=10000,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def memcached_host(self) -> str:
|
def memcached_host(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'memcached_host', default="localhost")
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "database", "memcached_host", default="localhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FrontendConfig:
|
class FrontendConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
@@ -95,15 +145,24 @@ class FrontendConfig:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def enable(self) -> int:
|
def enable(self) -> int:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'enable', default=False)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "frontend", "enable", default=False
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
def port(self) -> int:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'port', default=8090)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "frontend", "port", default=8090
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loglevel(self) -> int:
|
def loglevel(self) -> int:
|
||||||
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'loglevel', default="info"))
|
return CoreConfig.str_to_loglevel(
|
||||||
|
CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "frontend", "loglevel", default="info"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AllnetConfig:
|
class AllnetConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
@@ -111,15 +170,24 @@ class AllnetConfig:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def loglevel(self) -> int:
|
def loglevel(self) -> int:
|
||||||
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'loglevel', default="info"))
|
return CoreConfig.str_to_loglevel(
|
||||||
|
CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "allnet", "loglevel", default="info"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
def port(self) -> int:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'port', default=80)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "allnet", "port", default=80
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def allow_online_updates(self) -> int:
|
def allow_online_updates(self) -> int:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'allow_online_updates', default=False)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "allnet", "allow_online_updates", default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BillingConfig:
|
class BillingConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
@@ -127,19 +195,28 @@ class BillingConfig:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
def port(self) -> int:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'port', default=8443)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "billing", "port", default=8443
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ssl_key(self) -> str:
|
def ssl_key(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'ssl_key', default="cert/server.key")
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "billing", "ssl_key", default="cert/server.key"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ssl_cert(self) -> str:
|
def ssl_cert(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'ssl_cert', default="cert/server.pem")
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "billing", "ssl_cert", default="cert/server.pem"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def signing_key(self) -> str:
|
def signing_key(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'signing_key', default="cert/billing.key")
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "billing", "signing_key", default="cert/billing.key"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AimedbConfig:
|
class AimedbConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
@@ -147,15 +224,24 @@ class AimedbConfig:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def loglevel(self) -> int:
|
def loglevel(self) -> int:
|
||||||
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'loglevel', default="info"))
|
return CoreConfig.str_to_loglevel(
|
||||||
|
CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "aimedb", "loglevel", default="info"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
def port(self) -> int:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'port', default=22345)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "aimedb", "port", default=22345
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def key(self) -> str:
|
def key(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'key', default="")
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "aimedb", "key", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MuchaConfig:
|
class MuchaConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
@@ -163,27 +249,24 @@ class MuchaConfig:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def enable(self) -> int:
|
def enable(self) -> int:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'enable', default=False)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "mucha", "enable", default=False
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loglevel(self) -> int:
|
def loglevel(self) -> int:
|
||||||
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'loglevel', default="info"))
|
return CoreConfig.str_to_loglevel(
|
||||||
|
CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "mucha", "loglevel", default="info"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hostname(self) -> str:
|
def hostname(self) -> str:
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'hostname', default="localhost")
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "mucha", "hostname", default="localhost"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def port(self) -> int:
|
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'port', default=8444)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ssl_cert(self) -> str:
|
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'ssl_cert', default="cert/server.pem")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def signing_key(self) -> str:
|
|
||||||
return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'signing_key', default="cert/billing.key")
|
|
||||||
|
|
||||||
class CoreConfig(dict):
|
class CoreConfig(dict):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@@ -200,20 +283,22 @@ class CoreConfig(dict):
|
|||||||
def str_to_loglevel(cls, level_str: str):
|
def str_to_loglevel(cls, level_str: str):
|
||||||
if level_str.lower() == "error":
|
if level_str.lower() == "error":
|
||||||
return logging.ERROR
|
return logging.ERROR
|
||||||
elif level_str.lower().startswith("warn"): # Fits warn or warning
|
elif level_str.lower().startswith("warn"): # Fits warn or warning
|
||||||
return logging.WARN
|
return logging.WARN
|
||||||
elif level_str.lower() == "debug":
|
elif level_str.lower() == "debug":
|
||||||
return logging.DEBUG
|
return logging.DEBUG
|
||||||
else:
|
else:
|
||||||
return logging.INFO
|
return logging.INFO
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_config_field(cls, __config: dict, module, *path: str, default: Any = "") -> Any:
|
def get_config_field(
|
||||||
envKey = f'CFG_{module}_'
|
cls, __config: dict, module, *path: str, default: Any = ""
|
||||||
|
) -> Any:
|
||||||
|
envKey = f"CFG_{module}_"
|
||||||
for arg in path:
|
for arg in path:
|
||||||
envKey += arg + '_'
|
envKey += arg + "_"
|
||||||
|
|
||||||
if envKey.endswith('_'):
|
if envKey.endswith("_"):
|
||||||
envKey = envKey[:-1]
|
envKey = envKey[:-1]
|
||||||
|
|
||||||
if envKey in os.environ:
|
if envKey in os.environ:
|
||||||
|
|||||||
+8
-3
@@ -1,6 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
class MainboardPlatformCodes():
|
|
||||||
|
class MainboardPlatformCodes:
|
||||||
RINGEDGE = "AALE"
|
RINGEDGE = "AALE"
|
||||||
RINGWIDE = "AAML"
|
RINGWIDE = "AAML"
|
||||||
NU = "AAVE"
|
NU = "AAVE"
|
||||||
@@ -8,7 +9,8 @@ class MainboardPlatformCodes():
|
|||||||
ALLS_UX = "ACAE"
|
ALLS_UX = "ACAE"
|
||||||
ALLS_HX = "ACAX"
|
ALLS_HX = "ACAX"
|
||||||
|
|
||||||
class MainboardRevisions():
|
|
||||||
|
class MainboardRevisions:
|
||||||
RINGEDGE = 1
|
RINGEDGE = 1
|
||||||
RINGEDGE2 = 2
|
RINGEDGE2 = 2
|
||||||
|
|
||||||
@@ -26,12 +28,14 @@ class MainboardRevisions():
|
|||||||
ALLS_UX2 = 2
|
ALLS_UX2 = 2
|
||||||
ALLS_HX2 = 12
|
ALLS_HX2 = 12
|
||||||
|
|
||||||
class KeychipPlatformsCodes():
|
|
||||||
|
class KeychipPlatformsCodes:
|
||||||
RING = "A72E"
|
RING = "A72E"
|
||||||
NU = ("A60E", "A60E", "A60E")
|
NU = ("A60E", "A60E", "A60E")
|
||||||
NUSX = ("A61X", "A69X")
|
NUSX = ("A61X", "A69X")
|
||||||
ALLS = "A63E"
|
ALLS = "A63E"
|
||||||
|
|
||||||
|
|
||||||
class AllnetCountryCode(Enum):
|
class AllnetCountryCode(Enum):
|
||||||
JAPAN = "JPN"
|
JAPAN = "JPN"
|
||||||
UNITED_STATES = "USA"
|
UNITED_STATES = "USA"
|
||||||
@@ -41,6 +45,7 @@ class AllnetCountryCode(Enum):
|
|||||||
TAIWAN = "TWN"
|
TAIWAN = "TWN"
|
||||||
CHINA = "CHN"
|
CHINA = "CHN"
|
||||||
|
|
||||||
|
|
||||||
class AllnetJapanRegionId(Enum):
|
class AllnetJapanRegionId(Enum):
|
||||||
NONE = 0
|
NONE = 0
|
||||||
AICHI = 1
|
AICHI = 1
|
||||||
|
|||||||
+6
-4
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -6,15 +5,17 @@ import pickle
|
|||||||
import logging
|
import logging
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
|
|
||||||
cfg:CoreConfig = None # type: ignore
|
cfg: CoreConfig = None # type: ignore
|
||||||
# Make memcache optional
|
# Make memcache optional
|
||||||
try:
|
try:
|
||||||
import pylibmc # type: ignore
|
import pylibmc # type: ignore
|
||||||
|
|
||||||
has_mc = True
|
has_mc = True
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
has_mc = False
|
has_mc = False
|
||||||
|
|
||||||
def cached(lifetime: int=10, extra_key: Any=None) -> Callable:
|
|
||||||
|
def cached(lifetime: int = 10, extra_key: Any = None) -> Callable:
|
||||||
def _cached(func: Callable) -> Callable:
|
def _cached(func: Callable) -> Callable:
|
||||||
if has_mc:
|
if has_mc:
|
||||||
hostname = "127.0.0.1"
|
hostname = "127.0.0.1"
|
||||||
@@ -26,7 +27,6 @@ def cached(lifetime: int=10, extra_key: Any=None) -> Callable:
|
|||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||||
if lifetime is not None:
|
if lifetime is not None:
|
||||||
|
|
||||||
# Hash function args
|
# Hash function args
|
||||||
items = kwargs.items()
|
items = kwargs.items()
|
||||||
hashable_args = (args[1:], sorted(list(items)))
|
hashable_args = (args[1:], sorted(list(items)))
|
||||||
@@ -55,7 +55,9 @@ def cached(lifetime: int=10, extra_key: Any=None) -> Callable:
|
|||||||
memcache.set(cache_key, result, lifetime)
|
memcache.set(cache_key, result, lifetime)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|||||||
+168
-29
@@ -1,18 +1,19 @@
|
|||||||
import logging, coloredlogs
|
import logging, coloredlogs
|
||||||
from typing import Any, Dict, List
|
from typing import Optional
|
||||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
from datetime import datetime
|
import importlib, os
|
||||||
import importlib, os, json
|
import secrets, string
|
||||||
|
import bcrypt
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from core.data.schema import *
|
from core.data.schema import *
|
||||||
from core.utils import Utils
|
from core.utils import Utils
|
||||||
|
|
||||||
|
|
||||||
class Data:
|
class Data:
|
||||||
def __init__(self, cfg: CoreConfig) -> None:
|
def __init__(self, cfg: CoreConfig) -> None:
|
||||||
self.config = cfg
|
self.config = cfg
|
||||||
@@ -31,16 +32,20 @@ class Data:
|
|||||||
self.arcade = ArcadeData(self.config, self.session)
|
self.arcade = ArcadeData(self.config, self.session)
|
||||||
self.card = CardData(self.config, self.session)
|
self.card = CardData(self.config, self.session)
|
||||||
self.base = BaseData(self.config, self.session)
|
self.base = BaseData(self.config, self.session)
|
||||||
self.schema_ver_latest = 2
|
self.schema_ver_latest = 4
|
||||||
|
|
||||||
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
|
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
|
||||||
log_fmt = logging.Formatter(log_fmt_str)
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
self.logger = logging.getLogger("database")
|
self.logger = logging.getLogger("database")
|
||||||
|
|
||||||
# Prevent the logger from adding handlers multiple times
|
# Prevent the logger from adding handlers multiple times
|
||||||
if not getattr(self.logger, 'handler_set', None):
|
if not getattr(self.logger, "handler_set", None):
|
||||||
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "db"), encoding="utf-8",
|
fileHandler = TimedRotatingFileHandler(
|
||||||
when="d", backupCount=10)
|
"{0}/{1}.log".format(self.config.server.log_dir, "db"),
|
||||||
|
encoding="utf-8",
|
||||||
|
when="d",
|
||||||
|
backupCount=10,
|
||||||
|
)
|
||||||
fileHandler.setFormatter(log_fmt)
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
consoleHandler = logging.StreamHandler()
|
consoleHandler = logging.StreamHandler()
|
||||||
@@ -50,8 +55,10 @@ class Data:
|
|||||||
self.logger.addHandler(consoleHandler)
|
self.logger.addHandler(consoleHandler)
|
||||||
|
|
||||||
self.logger.setLevel(self.config.database.loglevel)
|
self.logger.setLevel(self.config.database.loglevel)
|
||||||
coloredlogs.install(cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str)
|
coloredlogs.install(
|
||||||
self.logger.handler_set = True # type: ignore
|
cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||||
|
)
|
||||||
|
self.logger.handler_set = True # type: ignore
|
||||||
|
|
||||||
def create_database(self):
|
def create_database(self):
|
||||||
self.logger.info("Creating databases...")
|
self.logger.info("Creating databases...")
|
||||||
@@ -64,19 +71,28 @@ class Data:
|
|||||||
games = Utils.get_all_titles()
|
games = Utils.get_all_titles()
|
||||||
for game_dir, game_mod in games.items():
|
for game_dir, game_mod in games.items():
|
||||||
try:
|
try:
|
||||||
title_db = game_mod.database(self.config)
|
if hasattr(game_mod, "database") and hasattr(game_mod, "current_schema_version"):
|
||||||
|
game_mod.database(self.config)
|
||||||
metadata.create_all(self.__engine.connect())
|
metadata.create_all(self.__engine.connect())
|
||||||
|
|
||||||
self.base.set_schema_ver(game_mod.current_schema_version, game_mod.game_codes[0])
|
self.base.set_schema_ver(
|
||||||
|
game_mod.current_schema_version, game_mod.game_codes[0]
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Could not load database schema from {game_dir} - {e}")
|
self.logger.warning(
|
||||||
|
f"Could not load database schema from {game_dir} - {e}"
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info(f"Setting base_schema_ver to {self.schema_ver_latest}")
|
self.logger.info(f"Setting base_schema_ver to {self.schema_ver_latest}")
|
||||||
self.base.set_schema_ver(self.schema_ver_latest)
|
self.base.set_schema_ver(self.schema_ver_latest)
|
||||||
|
|
||||||
self.logger.info(f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}")
|
self.logger.info(
|
||||||
self.user.reset_autoincrement(self.config.database.user_table_autoincrement_start)
|
f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}"
|
||||||
|
)
|
||||||
|
self.user.reset_autoincrement(
|
||||||
|
self.config.database.user_table_autoincrement_start
|
||||||
|
)
|
||||||
|
|
||||||
def recreate_database(self):
|
def recreate_database(self):
|
||||||
self.logger.info("Dropping all databases...")
|
self.logger.info("Dropping all databases...")
|
||||||
@@ -94,14 +110,19 @@ class Data:
|
|||||||
mod = importlib.import_module(f"titles.{dir}")
|
mod = importlib.import_module(f"titles.{dir}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
title_db = mod.database(self.config)
|
if hasattr(mod, "database"):
|
||||||
|
mod.database(self.config)
|
||||||
metadata.drop_all(self.__engine.connect())
|
metadata.drop_all(self.__engine.connect())
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Could not load database schema from {dir} - {e}")
|
self.logger.warning(
|
||||||
|
f"Could not load database schema from {dir} - {e}"
|
||||||
|
)
|
||||||
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
self.logger.warning(f"Failed to load database schema dir {dir} - {e}")
|
self.logger.warning(
|
||||||
|
f"Failed to load database schema dir {dir} - {e}"
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
self.base.execute("SET FOREIGN_KEY_CHECKS=1")
|
self.base.execute("SET FOREIGN_KEY_CHECKS=1")
|
||||||
@@ -113,24 +134,60 @@ class Data:
|
|||||||
sql = ""
|
sql = ""
|
||||||
|
|
||||||
if old_ver is None:
|
if old_ver is None:
|
||||||
self.logger.error(f"Schema for game {game} does not exist, did you run the creation script?")
|
self.logger.error(
|
||||||
|
f"Schema for game {game} does not exist, did you run the creation script?"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if old_ver == version:
|
if old_ver == version:
|
||||||
self.logger.info(f"Schema for game {game} is already version {old_ver}, nothing to do")
|
self.logger.info(
|
||||||
|
f"Schema for game {game} is already version {old_ver}, nothing to do"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not os.path.exists(f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql"):
|
if action == "upgrade":
|
||||||
self.logger.error(f"Could not find {action} script {game.upper()}_{version}_{action}.sql in core/data/schema/versions folder")
|
for x in range(old_ver, version):
|
||||||
return
|
if not os.path.exists(
|
||||||
|
f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql"
|
||||||
|
):
|
||||||
|
self.logger.error(
|
||||||
|
f"Could not find {action} script {game.upper()}_{x + 1}_{action}.sql in core/data/schema/versions folder"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
with open(f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql", "r", encoding="utf-8") as f:
|
with open(
|
||||||
sql = f.read()
|
f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql",
|
||||||
|
"r",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as f:
|
||||||
|
sql = f.read()
|
||||||
|
|
||||||
result = self.base.execute(sql)
|
result = self.base.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.error("Error execuing sql script!")
|
self.logger.error("Error execuing sql script!")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
else:
|
||||||
|
for x in range(old_ver, version, -1):
|
||||||
|
if not os.path.exists(
|
||||||
|
f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql"
|
||||||
|
):
|
||||||
|
self.logger.error(
|
||||||
|
f"Could not find {action} script {game.upper()}_{x - 1}_{action}.sql in core/data/schema/versions folder"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(
|
||||||
|
f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql",
|
||||||
|
"r",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as f:
|
||||||
|
sql = f.read()
|
||||||
|
|
||||||
|
result = self.base.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
self.logger.error("Error execuing sql script!")
|
||||||
|
return None
|
||||||
|
|
||||||
result = self.base.set_schema_ver(version, game)
|
result = self.base.set_schema_ver(version, game)
|
||||||
if result is None:
|
if result is None:
|
||||||
@@ -138,3 +195,85 @@ class Data:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
self.logger.info(f"Successfully migrated {game} to schema version {version}")
|
self.logger.info(f"Successfully migrated {game} to schema version {version}")
|
||||||
|
|
||||||
|
def create_owner(self, email: Optional[str] = None) -> None:
|
||||||
|
pw = "".join(
|
||||||
|
secrets.choice(string.ascii_letters + string.digits) for i in range(20)
|
||||||
|
)
|
||||||
|
hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
|
||||||
|
|
||||||
|
user_id = self.user.create_user(email=email, permission=255, password=hash)
|
||||||
|
if user_id is None:
|
||||||
|
self.logger.error(f"Failed to create owner with email {email}")
|
||||||
|
return
|
||||||
|
|
||||||
|
card_id = self.card.create_card(user_id, "00000000000000000000")
|
||||||
|
if card_id is None:
|
||||||
|
self.logger.error(f"Failed to create card for owner with id {user_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.warn(
|
||||||
|
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!"
|
||||||
|
)
|
||||||
|
|
||||||
|
def migrate_card(self, old_ac: str, new_ac: str, should_force: bool) -> None:
|
||||||
|
if old_ac == new_ac:
|
||||||
|
self.logger.error("Both access codes are the same!")
|
||||||
|
return
|
||||||
|
|
||||||
|
new_card = self.card.get_card_by_access_code(new_ac)
|
||||||
|
if new_card is None:
|
||||||
|
self.card.update_access_code(old_ac, new_ac)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not should_force:
|
||||||
|
self.logger.warn(
|
||||||
|
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}."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
f"All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
|
||||||
|
)
|
||||||
|
self.card.delete_card(new_card["id"])
|
||||||
|
self.card.update_access_code(old_ac, new_ac)
|
||||||
|
|
||||||
|
hanging_user = self.user.get_user(new_card["user"])
|
||||||
|
if hanging_user["password"] is None:
|
||||||
|
self.logger.info(f"Delete hanging user {hanging_user['id']}")
|
||||||
|
self.user.delete_user(hanging_user["id"])
|
||||||
|
|
||||||
|
def delete_hanging_users(self) -> None:
|
||||||
|
"""
|
||||||
|
Finds and deletes users that have not registered for the webui that have no cards assocated with them.
|
||||||
|
"""
|
||||||
|
unreg_users = self.user.get_unregistered_users()
|
||||||
|
if unreg_users is None:
|
||||||
|
self.logger.error("Error occoured finding unregistered users")
|
||||||
|
|
||||||
|
for user in unreg_users:
|
||||||
|
cards = self.card.get_user_cards(user["id"])
|
||||||
|
if cards is None:
|
||||||
|
self.logger.error(f"Error getting cards for user {user['id']}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not cards:
|
||||||
|
self.logger.info(f"Delete hanging user {user['id']}")
|
||||||
|
self.user.delete_user(user["id"])
|
||||||
|
|
||||||
|
def autoupgrade(self) -> None:
|
||||||
|
all_games = self.base.get_all_schema_vers()
|
||||||
|
if all_games is None:
|
||||||
|
self.logger.warn("Failed to get schema versions")
|
||||||
|
|
||||||
|
for x in all_games:
|
||||||
|
game = x["game"].upper()
|
||||||
|
update_ver = 1
|
||||||
|
for y in range(2, 100):
|
||||||
|
if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"):
|
||||||
|
update_ver = y
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.migrate_database(game, update_ver, "upgrade")
|
||||||
+105
-46
@@ -21,34 +21,49 @@ arcade = Table(
|
|||||||
Column("city", String(255)),
|
Column("city", String(255)),
|
||||||
Column("region_id", Integer),
|
Column("region_id", Integer),
|
||||||
Column("timezone", String(255)),
|
Column("timezone", String(255)),
|
||||||
mysql_charset='utf8mb4'
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
machine = Table(
|
machine = Table(
|
||||||
"machine",
|
"machine",
|
||||||
metadata,
|
metadata,
|
||||||
Column("id", Integer, primary_key=True, nullable=False),
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
Column("arcade", ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"), nullable=False),
|
Column(
|
||||||
|
"arcade",
|
||||||
|
ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
Column("serial", String(15), nullable=False),
|
Column("serial", String(15), nullable=False),
|
||||||
Column("board", String(15)),
|
Column("board", String(15)),
|
||||||
Column("game", String(4)),
|
Column("game", String(4)),
|
||||||
Column("country", String(3)), # overwrites if not null
|
Column("country", String(3)), # overwrites if not null
|
||||||
Column("timezone", String(255)),
|
Column("timezone", String(255)),
|
||||||
Column("ota_enable", Boolean),
|
Column("ota_enable", Boolean),
|
||||||
Column("is_cab", Boolean),
|
Column("is_cab", Boolean),
|
||||||
mysql_charset='utf8mb4'
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
arcade_owner = Table(
|
arcade_owner = Table(
|
||||||
'arcade_owner',
|
"arcade_owner",
|
||||||
metadata,
|
metadata,
|
||||||
Column('user', Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
|
Column(
|
||||||
Column('arcade', Integer, ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"), nullable=False),
|
"user",
|
||||||
Column('permissions', Integer, nullable=False),
|
Integer,
|
||||||
PrimaryKeyConstraint('user', 'arcade', name='arcade_owner_pk'),
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||||
mysql_charset='utf8mb4'
|
nullable=False,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
"arcade",
|
||||||
|
Integer,
|
||||||
|
ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
Column("permissions", Integer, nullable=False),
|
||||||
|
PrimaryKeyConstraint("user", "arcade", name="arcade_owner_pk"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ArcadeData(BaseData):
|
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[Dict]:
|
||||||
if serial is not None:
|
if serial is not None:
|
||||||
@@ -71,72 +86,112 @@ class ArcadeData(BaseData):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
if result is None: return None
|
if result is None:
|
||||||
|
return None
|
||||||
return result.fetchone()
|
return result.fetchone()
|
||||||
|
|
||||||
def put_machine(self, arcade_id: int, serial: str = "", board: str = None, game: str = None, is_cab: bool = False) -> Optional[int]:
|
def put_machine(
|
||||||
|
self,
|
||||||
|
arcade_id: int,
|
||||||
|
serial: str = "",
|
||||||
|
board: str = None,
|
||||||
|
game: str = None,
|
||||||
|
is_cab: bool = False,
|
||||||
|
) -> Optional[int]:
|
||||||
if arcade_id:
|
if arcade_id:
|
||||||
self.logger.error(f"{__name__ }: Need arcade id!")
|
self.logger.error(f"{__name__ }: Need arcade id!")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
sql = machine.insert().values(arcade = arcade_id, keychip = serial, board = board, game = game, is_cab = is_cab)
|
sql = machine.insert().values(
|
||||||
|
arcade=arcade_id, keychip=serial, board=board, game=game, is_cab=is_cab
|
||||||
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
if result is None: return None
|
if result is None:
|
||||||
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def set_machine_serial(self, machine_id: int, serial: str) -> None:
|
def set_machine_serial(self, machine_id: int, serial: str) -> None:
|
||||||
result = self.execute(machine.update(machine.c.id == machine_id).values(keychip = serial))
|
result = self.execute(
|
||||||
|
machine.update(machine.c.id == machine_id).values(keychip=serial)
|
||||||
|
)
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.error(f"Failed to update serial for machine {machine_id} -> {serial}")
|
self.logger.error(
|
||||||
|
f"Failed to update serial for machine {machine_id} -> {serial}"
|
||||||
|
)
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def set_machine_boardid(self, machine_id: int, boardid: str) -> None:
|
def set_machine_boardid(self, machine_id: int, boardid: str) -> None:
|
||||||
result = self.execute(machine.update(machine.c.id == machine_id).values(board = boardid))
|
result = self.execute(
|
||||||
|
machine.update(machine.c.id == machine_id).values(board=boardid)
|
||||||
|
)
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.error(f"Failed to update board id for machine {machine_id} -> {boardid}")
|
self.logger.error(
|
||||||
|
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[Dict]:
|
||||||
sql = arcade.select(arcade.c.id == id)
|
sql = arcade.select(arcade.c.id == id)
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
if result is None: return None
|
if result is None:
|
||||||
|
return None
|
||||||
return result.fetchone()
|
return result.fetchone()
|
||||||
|
|
||||||
def put_arcade(self, name: str, nickname: str = None, country: str = "JPN", country_id: int = 1,
|
def put_arcade(
|
||||||
state: str = "", city: str = "", regional_id: int = 1) -> Optional[int]:
|
self,
|
||||||
if nickname is None: nickname = name
|
name: str,
|
||||||
|
nickname: str = None,
|
||||||
|
country: str = "JPN",
|
||||||
|
country_id: int = 1,
|
||||||
|
state: str = "",
|
||||||
|
city: str = "",
|
||||||
|
regional_id: int = 1,
|
||||||
|
) -> Optional[int]:
|
||||||
|
if nickname is None:
|
||||||
|
nickname = name
|
||||||
|
|
||||||
sql = arcade.insert().values(name = name, nickname = nickname, country = country, country_id = country_id,
|
sql = arcade.insert().values(
|
||||||
state = state, city = city, regional_id = regional_id)
|
name=name,
|
||||||
|
nickname=nickname,
|
||||||
result = self.execute(sql)
|
country=country,
|
||||||
if result is None: return None
|
country_id=country_id,
|
||||||
return result.lastrowid
|
state=state,
|
||||||
|
city=city,
|
||||||
def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]:
|
regional_id=regional_id,
|
||||||
sql = select(arcade_owner).where(arcade_owner.c.arcade==arcade_id)
|
|
||||||
|
|
||||||
result = self.execute(sql)
|
|
||||||
if result is None: return None
|
|
||||||
return result.fetchall()
|
|
||||||
|
|
||||||
def add_arcade_owner(self, arcade_id: int, user_id: int) -> None:
|
|
||||||
sql = insert(arcade_owner).values(
|
|
||||||
arcade=arcade_id,
|
|
||||||
user=user_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
if result is None: return None
|
if result is None:
|
||||||
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def format_serial(self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152) -> str:
|
def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]:
|
||||||
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
|
sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchall()
|
||||||
|
|
||||||
|
def add_arcade_owner(self, arcade_id: int, user_id: int) -> None:
|
||||||
|
sql = insert(arcade_owner).values(arcade=arcade_id, user=user_id)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.lastrowid
|
||||||
|
|
||||||
|
def format_serial(
|
||||||
|
self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152
|
||||||
|
) -> str:
|
||||||
|
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:
|
def validate_keychip_format(self, serial: str) -> bool:
|
||||||
serial = serial.replace("-", "")
|
serial = serial.replace("-", "")
|
||||||
if len(serial) != 11 or len(serial) != 15:
|
if len(serial) != 11 or len(serial) != 15:
|
||||||
self.logger.error(f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})")
|
self.logger.error(
|
||||||
|
f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
platform_code = serial[:4]
|
platform_code = serial[:4]
|
||||||
@@ -150,11 +205,15 @@ class ArcadeData(BaseData):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if len(append) != 0 or len(append) != 4:
|
if len(append) != 0 or len(append) != 4:
|
||||||
self.logger.error(f"Serial validate failed: {serial} had malformed append {append}")
|
self.logger.error(
|
||||||
|
f"Serial validate failed: {serial} had malformed append {append}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if len(num) != 4:
|
if len(num) != 4:
|
||||||
self.logger.error(f"Serial validate failed: {serial} had malformed number {num}")
|
self.logger.error(
|
||||||
|
f"Serial validate failed: {serial} had malformed number {num}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
+42
-13
@@ -2,6 +2,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from random import randrange
|
from random import randrange
|
||||||
from typing import Any, Optional, Dict, List
|
from typing import Any, Optional, Dict, List
|
||||||
|
from sqlalchemy.engine import Row
|
||||||
from sqlalchemy.engine.cursor import CursorResult
|
from sqlalchemy.engine.cursor import CursorResult
|
||||||
from sqlalchemy.engine.base import Connection
|
from sqlalchemy.engine.base import Connection
|
||||||
from sqlalchemy.sql import text, func, select
|
from sqlalchemy.sql import text, func, select
|
||||||
@@ -19,7 +20,7 @@ schema_ver = Table(
|
|||||||
metadata,
|
metadata,
|
||||||
Column("game", String(4), primary_key=True, nullable=False),
|
Column("game", String(4), primary_key=True, nullable=False),
|
||||||
Column("version", Integer, nullable=False, server_default="1"),
|
Column("version", Integer, nullable=False, server_default="1"),
|
||||||
mysql_charset='utf8mb4'
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
event_log = Table(
|
event_log = Table(
|
||||||
@@ -29,18 +30,20 @@ event_log = Table(
|
|||||||
Column("system", String(255), nullable=False),
|
Column("system", String(255), nullable=False),
|
||||||
Column("type", String(255), nullable=False),
|
Column("type", String(255), nullable=False),
|
||||||
Column("severity", Integer, nullable=False),
|
Column("severity", Integer, nullable=False),
|
||||||
|
Column("message", String(1000), nullable=False),
|
||||||
Column("details", JSON, nullable=False),
|
Column("details", JSON, nullable=False),
|
||||||
Column("when_logged", TIMESTAMP, nullable=False, server_default=func.now()),
|
Column("when_logged", TIMESTAMP, nullable=False, server_default=func.now()),
|
||||||
mysql_charset='utf8mb4'
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
class BaseData():
|
|
||||||
|
class BaseData:
|
||||||
def __init__(self, cfg: CoreConfig, conn: Connection) -> None:
|
def __init__(self, cfg: CoreConfig, conn: Connection) -> None:
|
||||||
self.config = cfg
|
self.config = cfg
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
self.logger = logging.getLogger("database")
|
self.logger = logging.getLogger("database")
|
||||||
|
|
||||||
def execute(self, sql: str, opts: Dict[str, Any]={}) -> Optional[CursorResult]:
|
def execute(self, sql: str, opts: Dict[str, Any] = {}) -> Optional[CursorResult]:
|
||||||
res = None
|
res = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -79,30 +82,55 @@ class BaseData():
|
|||||||
"""
|
"""
|
||||||
return randrange(10000, 9999999)
|
return randrange(10000, 9999999)
|
||||||
|
|
||||||
|
def get_all_schema_vers(self) -> Optional[List[Row]]:
|
||||||
|
sql = select(schema_ver)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchall()
|
||||||
|
|
||||||
def get_schema_ver(self, game: str) -> Optional[int]:
|
def get_schema_ver(self, game: str) -> Optional[int]:
|
||||||
sql = select(schema_ver).where(schema_ver.c.game == game)
|
sql = select(schema_ver).where(schema_ver.c.game == game)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchone()["version"]
|
|
||||||
|
row = result.fetchone()
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return row["version"]
|
||||||
|
|
||||||
def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
|
def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
|
||||||
sql = insert(schema_ver).values(game = game, version = ver)
|
sql = insert(schema_ver).values(game=game, version=ver)
|
||||||
conflict = sql.on_duplicate_key_update(version = ver)
|
conflict = sql.on_duplicate_key_update(version=ver)
|
||||||
|
|
||||||
result = self.execute(conflict)
|
result = self.execute(conflict)
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.error(f"Failed to update schema version for game {game} (v{ver})")
|
self.logger.error(
|
||||||
|
f"Failed to update schema version for game {game} (v{ver})"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def log_event(self, system: str, type: str, severity: int, details: Dict) -> Optional[int]:
|
def log_event(
|
||||||
sql = event_log.insert().values(system = system, type = type, severity = severity, details = json.dumps(details))
|
self, system: str, type: str, severity: int, message: str, details: Dict = {}
|
||||||
|
) -> Optional[int]:
|
||||||
|
sql = event_log.insert().values(
|
||||||
|
system=system,
|
||||||
|
type=type,
|
||||||
|
severity=severity,
|
||||||
|
message=message,
|
||||||
|
details=json.dumps(details),
|
||||||
|
)
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.error(f"{__name__}: Failed to insert event into event log! system = {system}, type = {type}, severity = {severity}, details = {details}")
|
self.logger.error(
|
||||||
|
f"{__name__}: Failed to insert event into event log! system = {system}, type = {type}, severity = {severity}, message = {message}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
@@ -111,11 +139,12 @@ class BaseData():
|
|||||||
sql = event_log.select().limit(entries).all()
|
sql = event_log.select().limit(entries).all()
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
|
|
||||||
if result is None: return None
|
if result is None:
|
||||||
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def fix_bools(self, data: Dict) -> Dict:
|
def fix_bools(self, data: Dict) -> Dict:
|
||||||
for k,v in data.items():
|
for k, v in data.items():
|
||||||
if type(v) == str and v.lower() == "true":
|
if type(v) == str and v.lower() == "true":
|
||||||
data[k] = True
|
data[k] = True
|
||||||
elif type(v) == str and v.lower() == "false":
|
elif type(v) == str and v.lower() == "false":
|
||||||
|
|||||||
+52
-15
@@ -3,55 +3,92 @@ from sqlalchemy import Table, Column, UniqueConstraint
|
|||||||
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP
|
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP
|
||||||
from sqlalchemy.sql.schema import ForeignKey
|
from sqlalchemy.sql.schema import ForeignKey
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
from sqlalchemy.engine import Row
|
||||||
|
|
||||||
from core.data.schema.base import BaseData, metadata
|
from core.data.schema.base import BaseData, metadata
|
||||||
|
|
||||||
aime_card = Table(
|
aime_card = Table(
|
||||||
'aime_card',
|
"aime_card",
|
||||||
metadata,
|
metadata,
|
||||||
Column("id", Integer, primary_key=True, nullable=False),
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
|
Column(
|
||||||
|
"user",
|
||||||
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
Column("access_code", String(20)),
|
Column("access_code", String(20)),
|
||||||
Column("created_date", TIMESTAMP, server_default=func.now()),
|
Column("created_date", TIMESTAMP, server_default=func.now()),
|
||||||
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
|
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
|
||||||
Column("is_locked", Boolean, server_default="0"),
|
Column("is_locked", Boolean, server_default="0"),
|
||||||
Column("is_banned", Boolean, server_default="0"),
|
Column("is_banned", Boolean, server_default="0"),
|
||||||
UniqueConstraint("user", "access_code", name="aime_card_uk"),
|
UniqueConstraint("user", "access_code", name="aime_card_uk"),
|
||||||
mysql_charset='utf8mb4'
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CardData(BaseData):
|
class CardData(BaseData):
|
||||||
|
def get_card_by_access_code(self, access_code: str) -> Optional[Row]:
|
||||||
|
sql = aime_card.select(aime_card.c.access_code == access_code)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchone()
|
||||||
|
|
||||||
|
def get_card_by_id(self, card_id: int) -> Optional[Row]:
|
||||||
|
sql = aime_card.select(aime_card.c.id == card_id)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchone()
|
||||||
|
|
||||||
|
def update_access_code(self, old_ac: str, new_ac: str) -> None:
|
||||||
|
sql = aime_card.update(aime_card.c.access_code == old_ac).values(
|
||||||
|
access_code=new_ac
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
self.logger.error(
|
||||||
|
f"Failed to change card access code from {old_ac} to {new_ac}"
|
||||||
|
)
|
||||||
|
|
||||||
def get_user_id_from_card(self, access_code: str) -> Optional[int]:
|
def get_user_id_from_card(self, access_code: str) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
Given a 20 digit access code as a string, get the user id associated with that card
|
Given a 20 digit access code as a string, get the user id associated with that card
|
||||||
"""
|
"""
|
||||||
sql = aime_card.select(aime_card.c.access_code == access_code)
|
card = self.get_card_by_access_code(access_code)
|
||||||
|
if card is None:
|
||||||
result = self.execute(sql)
|
return None
|
||||||
if result is None: return None
|
|
||||||
|
|
||||||
card = result.fetchone()
|
|
||||||
if card is None: return None
|
|
||||||
|
|
||||||
return int(card["user"])
|
return int(card["user"])
|
||||||
|
|
||||||
def get_user_cards(self, aime_id: int) -> Optional[List[Dict]]:
|
def delete_card(self, card_id: int) -> None:
|
||||||
|
sql = aime_card.delete(aime_card.c.id == card_id)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
self.logger.error(f"Failed to delete card with id {card_id}")
|
||||||
|
|
||||||
|
def get_user_cards(self, aime_id: int) -> Optional[List[Row]]:
|
||||||
"""
|
"""
|
||||||
Returns all cards owned by a user
|
Returns all cards owned by a user
|
||||||
"""
|
"""
|
||||||
sql = aime_card.select(aime_card.c.user == aime_id)
|
sql = aime_card.select(aime_card.c.user == aime_id)
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
if result is None: return None
|
if result is None:
|
||||||
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
|
|
||||||
def create_card(self, user_id: int, access_code: str) -> Optional[int]:
|
def create_card(self, user_id: int, access_code: str) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
Given a aime_user id and a 20 digit access code as a string, create a card and return the ID if successful
|
Given a aime_user id and a 20 digit access code as a string, create a card and return the ID if successful
|
||||||
"""
|
"""
|
||||||
sql = aime_card.insert().values(user=user_id, access_code=access_code)
|
sql = aime_card.insert().values(user=user_id, access_code=access_code)
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
if result is None: return None
|
if result is None:
|
||||||
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def to_access_code(self, luid: str) -> str:
|
def to_access_code(self, luid: str) -> str:
|
||||||
@@ -64,4 +101,4 @@ class CardData(BaseData):
|
|||||||
"""
|
"""
|
||||||
Given a 20 digit access code as a string, return the 16 hex character luid
|
Given a 20 digit access code as a string, return the 16 hex character luid
|
||||||
"""
|
"""
|
||||||
return f'{int(access_code):0{16}x}'
|
return f"{int(access_code):0{16}x}"
|
||||||
+53
-72
@@ -1,14 +1,12 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, Optional
|
from typing import Optional, List
|
||||||
from sqlalchemy import Table, Column, and_
|
from sqlalchemy import Table, Column
|
||||||
from sqlalchemy.types import Integer, String, TIMESTAMP
|
from sqlalchemy.types import Integer, String, TIMESTAMP
|
||||||
from sqlalchemy.sql.schema import ForeignKey
|
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from sqlalchemy.dialects.mysql import insert
|
from sqlalchemy.dialects.mysql import insert
|
||||||
from sqlalchemy.sql import func, select, Delete
|
from sqlalchemy.sql import func, select
|
||||||
from uuid import uuid4
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from sqlalchemy.engine import Row
|
from sqlalchemy.engine import Row
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
from core.data.schema.base import BaseData, metadata
|
from core.data.schema.base import BaseData, metadata
|
||||||
|
|
||||||
@@ -23,36 +21,31 @@ aime_user = Table(
|
|||||||
Column("created_date", TIMESTAMP, server_default=func.now()),
|
Column("created_date", TIMESTAMP, server_default=func.now()),
|
||||||
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
|
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
|
||||||
Column("suspend_expire_time", TIMESTAMP),
|
Column("suspend_expire_time", TIMESTAMP),
|
||||||
mysql_charset='utf8mb4'
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
frontend_session = Table(
|
|
||||||
"frontend_session",
|
|
||||||
metadata,
|
|
||||||
Column("id", Integer, primary_key=True, unique=True),
|
|
||||||
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
|
|
||||||
Column("ip", String(15)),
|
|
||||||
Column('session_cookie', String(32), nullable=False, unique=True),
|
|
||||||
Column("expires", TIMESTAMP, nullable=False),
|
|
||||||
mysql_charset='utf8mb4'
|
|
||||||
)
|
|
||||||
|
|
||||||
class PermissionBits(Enum):
|
class PermissionBits(Enum):
|
||||||
PermUser = 1
|
PermUser = 1
|
||||||
PermMod = 2
|
PermMod = 2
|
||||||
PermSysAdmin = 4
|
PermSysAdmin = 4
|
||||||
|
|
||||||
class UserData(BaseData):
|
|
||||||
def create_user(self, id: int = None, username: str = None, email: str = None, password: str = None, permission: int = 1) -> Optional[int]:
|
|
||||||
if email is None:
|
|
||||||
permission = 1
|
|
||||||
|
|
||||||
|
class UserData(BaseData):
|
||||||
|
def create_user(
|
||||||
|
self,
|
||||||
|
id: int = None,
|
||||||
|
username: str = None,
|
||||||
|
email: str = None,
|
||||||
|
password: str = None,
|
||||||
|
permission: int = 1,
|
||||||
|
) -> Optional[int]:
|
||||||
if id is None:
|
if id is None:
|
||||||
sql = insert(aime_user).values(
|
sql = insert(aime_user).values(
|
||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
password=password,
|
password=password,
|
||||||
permissions=permission
|
permissions=permission,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sql = insert(aime_user).values(
|
sql = insert(aime_user).values(
|
||||||
@@ -60,66 +53,54 @@ class UserData(BaseData):
|
|||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
password=password,
|
password=password,
|
||||||
permissions=permission
|
permissions=permission,
|
||||||
)
|
)
|
||||||
|
|
||||||
conflict = sql.on_duplicate_key_update(
|
conflict = sql.on_duplicate_key_update(
|
||||||
username=username,
|
username=username, email=email, password=password, permissions=permission
|
||||||
email=email,
|
|
||||||
password=password,
|
|
||||||
permissions=permission
|
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(conflict)
|
result = self.execute(conflict)
|
||||||
if result is None: return None
|
|
||||||
return result.lastrowid
|
|
||||||
|
|
||||||
def login(self, user_id: int, passwd: bytes = None, ip: str = "0.0.0.0") -> Optional[str]:
|
|
||||||
sql = select(aime_user).where(and_(aime_user.c.id == user_id, aime_user.c.password == passwd))
|
|
||||||
|
|
||||||
result = self.execute(sql)
|
|
||||||
if result is None: return None
|
|
||||||
|
|
||||||
usr = result.fetchone()
|
|
||||||
if usr is None: return None
|
|
||||||
|
|
||||||
return self.create_session(user_id, ip)
|
|
||||||
|
|
||||||
def check_session(self, cookie: str, ip: str = "0.0.0.0") -> Optional[Row]:
|
|
||||||
sql = select(frontend_session).where(
|
|
||||||
and_(
|
|
||||||
frontend_session.c.session_cookie == cookie,
|
|
||||||
frontend_session.c.ip == ip
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
result = self.execute(sql)
|
|
||||||
if result is None: return None
|
|
||||||
return result.fetchone()
|
|
||||||
|
|
||||||
def delete_session(self, session_id: int) -> bool:
|
|
||||||
sql = Delete(frontend_session).where(frontend_session.c.id == session_id)
|
|
||||||
|
|
||||||
result = self.execute(sql)
|
|
||||||
if result is None: return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def create_session(self, user_id: int, ip: str = "0.0.0.0", expires: datetime = datetime.now() + timedelta(days=1)) -> Optional[str]:
|
|
||||||
cookie = uuid4().hex
|
|
||||||
|
|
||||||
sql = insert(frontend_session).values(
|
|
||||||
user = user_id,
|
|
||||||
ip = ip,
|
|
||||||
session_cookie = cookie,
|
|
||||||
expires = expires
|
|
||||||
)
|
|
||||||
|
|
||||||
result = self.execute(sql)
|
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return cookie
|
return result.lastrowid
|
||||||
|
|
||||||
|
def get_user(self, user_id: int) -> Optional[Row]:
|
||||||
|
sql = select(aime_user).where(aime_user.c.id == user_id)
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return False
|
||||||
|
return result.fetchone()
|
||||||
|
|
||||||
|
def check_password(self, user_id: int, passwd: bytes = None) -> bool:
|
||||||
|
usr = self.get_user(user_id)
|
||||||
|
if usr is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if usr["password"] is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return bcrypt.checkpw(passwd, usr["password"].encode())
|
||||||
|
|
||||||
def reset_autoincrement(self, ai_value: int) -> None:
|
def reset_autoincrement(self, ai_value: int) -> None:
|
||||||
# ALTER TABLE isn't in sqlalchemy so we do this the ugly way
|
# ALTER TABLE isn't in sqlalchemy so we do this the ugly way
|
||||||
sql = f"ALTER TABLE aime_user AUTO_INCREMENT={ai_value}"
|
sql = f"ALTER TABLE aime_user AUTO_INCREMENT={ai_value}"
|
||||||
self.execute(sql)
|
self.execute(sql)
|
||||||
|
|
||||||
|
def delete_user(self, user_id: int) -> None:
|
||||||
|
sql = aime_user.delete(aime_user.c.id == user_id)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
self.logger.error(f"Failed to delete user with id {user_id}")
|
||||||
|
|
||||||
|
def get_unregistered_users(self) -> List[Row]:
|
||||||
|
"""
|
||||||
|
Returns a list of users who have not registered with the webui. They may or may not have cards.
|
||||||
|
"""
|
||||||
|
sql = select(aime_user).where(aime_user.c.password == None)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchall()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `event_log` DROP COLUMN `message`;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE `frontend_session` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`user` int(11) NOT NULL,
|
||||||
|
`ip` varchar(15) DEFAULT NULL,
|
||||||
|
`session_cookie` varchar(32) NOT NULL,
|
||||||
|
`expires` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id` (`id`),
|
||||||
|
UNIQUE KEY `session_cookie` (`session_cookie`),
|
||||||
|
KEY `user` (`user`),
|
||||||
|
CONSTRAINT `frontend_session_ibfk_1` FOREIGN KEY (`user`) REFERENCES `aime_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `event_log` ADD COLUMN `message` VARCHAR(1000) NOT NULL AFTER `severity`;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE `frontend_session`;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
SET FOREIGN_KEY_CHECKS=0;
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
ALTER TABLE diva_score DROP COLUMN edition;
|
|
||||||
ALTER TABLE diva_playlog DROP COLUMN edition;
|
|
||||||
|
|
||||||
ALTER TABLE diva_score DROP FOREIGN KEY diva_score_ibfk_1;
|
ALTER TABLE diva_score DROP FOREIGN KEY diva_score_ibfk_1;
|
||||||
ALTER TABLE diva_score DROP CONSTRAINT diva_score_uk;
|
ALTER TABLE diva_score DROP CONSTRAINT diva_score_uk;
|
||||||
ALTER TABLE diva_score ADD CONSTRAINT diva_score_uk UNIQUE (user, pv_id, difficulty);
|
ALTER TABLE diva_score ADD CONSTRAINT diva_score_uk UNIQUE (user, pv_id, difficulty);
|
||||||
ALTER TABLE diva_score ADD CONSTRAINT diva_score_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE;
|
ALTER TABLE diva_score ADD CONSTRAINT diva_score_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE diva_score DROP COLUMN edition;
|
||||||
|
ALTER TABLE diva_playlog DROP COLUMN edition;
|
||||||
SET FOREIGN_KEY_CHECKS=1;
|
SET FOREIGN_KEY_CHECKS=1;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE ongeki_profile_data DROP COLUMN lastEmoneyCredit;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE ongeki_profile_data ADD COLUMN lastEmoneyCredit INTEGER DEFAULT 0;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
ALTER TABLE mai2_item_card
|
||||||
|
CHANGE COLUMN cardId card_id INT NOT NULL AFTER user,
|
||||||
|
CHANGE COLUMN cardTypeId card_kind INT NOT NULL,
|
||||||
|
CHANGE COLUMN charaId chara_id INT NOT NULL,
|
||||||
|
CHANGE COLUMN mapId map_id INT NOT NULL,
|
||||||
|
CHANGE COLUMN startDate start_date TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00',
|
||||||
|
CHANGE COLUMN endDate end_date TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00';
|
||||||
|
|
||||||
|
ALTER TABLE mai2_item_item
|
||||||
|
CHANGE COLUMN itemId item_id INT NOT NULL AFTER user,
|
||||||
|
CHANGE COLUMN itemKind item_kind INT NOT NULL,
|
||||||
|
CHANGE COLUMN isValid is_valid TINYINT(1) NOT NULL DEFAULT '1';
|
||||||
|
|
||||||
|
ALTER TABLE mai2_item_character
|
||||||
|
CHANGE COLUMN characterId character_id INT NOT NULL,
|
||||||
|
CHANGE COLUMN useCount use_count INT NOT NULL DEFAULT '0';
|
||||||
|
|
||||||
|
ALTER TABLE mai2_item_charge
|
||||||
|
CHANGE COLUMN chargeId charge_id INT NOT NULL,
|
||||||
|
CHANGE COLUMN purchaseDate purchase_date TIMESTAMP NOT NULL,
|
||||||
|
CHANGE COLUMN validDate valid_date TIMESTAMP NOT NULL;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
ALTER TABLE mai2_item_card
|
||||||
|
CHANGE COLUMN card_id cardId INT NOT NULL AFTER user,
|
||||||
|
CHANGE COLUMN card_kind cardTypeId INT NOT NULL,
|
||||||
|
CHANGE COLUMN chara_id charaId INT NOT NULL,
|
||||||
|
CHANGE COLUMN map_id mapId INT NOT NULL,
|
||||||
|
CHANGE COLUMN start_date startDate TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00',
|
||||||
|
CHANGE COLUMN end_date endDate TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00';
|
||||||
|
|
||||||
|
ALTER TABLE mai2_item_item
|
||||||
|
CHANGE COLUMN item_id itemId INT NOT NULL AFTER user,
|
||||||
|
CHANGE COLUMN item_kind itemKind INT NOT NULL,
|
||||||
|
CHANGE COLUMN is_valid isValid TINYINT(1) NOT NULL DEFAULT '1';
|
||||||
|
|
||||||
|
ALTER TABLE mai2_item_character
|
||||||
|
CHANGE COLUMN character_id characterId INT NOT NULL,
|
||||||
|
CHANGE COLUMN use_count useCount INT NOT NULL DEFAULT '0';
|
||||||
|
|
||||||
|
ALTER TABLE mai2_item_charge
|
||||||
|
CHANGE COLUMN charge_id chargeId INT NOT NULL,
|
||||||
|
CHANGE COLUMN purchase_date purchaseDate TIMESTAMP NOT NULL,
|
||||||
|
CHANGE COLUMN valid_date validDate TIMESTAMP NOT NULL;
|
||||||
+97
-35
@@ -4,17 +4,34 @@ from twisted.web import resource
|
|||||||
from twisted.web.util import redirectTo
|
from twisted.web.util import redirectTo
|
||||||
from twisted.web.http import Request
|
from twisted.web.http import Request
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
from twisted.web.server import Session
|
||||||
|
from zope.interface import Interface, Attribute, implementer
|
||||||
|
from twisted.python.components import registerAdapter
|
||||||
import jinja2
|
import jinja2
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core import CoreConfig, Utils
|
||||||
from core.data import Data
|
from core.data import Data
|
||||||
from core.utils import Utils
|
|
||||||
|
|
||||||
|
class IUserSession(Interface):
|
||||||
|
userId = Attribute("User's ID")
|
||||||
|
current_ip = Attribute("User's current ip address")
|
||||||
|
permissions = Attribute("User's permission level")
|
||||||
|
|
||||||
|
|
||||||
|
@implementer(IUserSession)
|
||||||
|
class UserSession(object):
|
||||||
|
def __init__(self, session):
|
||||||
|
self.userId = 0
|
||||||
|
self.current_ip = "0.0.0.0"
|
||||||
|
self.permissions = 0
|
||||||
|
|
||||||
|
|
||||||
class FrontendServlet(resource.Resource):
|
class FrontendServlet(resource.Resource):
|
||||||
def getChild(self, name: bytes, request: Request):
|
def getChild(self, name: bytes, request: Request):
|
||||||
self.logger.debug(f"{request.getClientIP()} -> {name.decode()}")
|
self.logger.debug(f"{Utils.get_ip_addr(request)} -> {name.decode()}")
|
||||||
if name == b'':
|
if name == b"":
|
||||||
return self
|
return self
|
||||||
return resource.Resource.getChild(self, name, request)
|
return resource.Resource.getChild(self, name, request)
|
||||||
|
|
||||||
@@ -27,7 +44,11 @@ class FrontendServlet(resource.Resource):
|
|||||||
self.game_list: List[Dict[str, str]] = []
|
self.game_list: List[Dict[str, str]] = []
|
||||||
self.children: Dict[str, Any] = {}
|
self.children: Dict[str, Any] = {}
|
||||||
|
|
||||||
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "frontend"), when="d", backupCount=10)
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(self.config.server.log_dir, "frontend"),
|
||||||
|
when="d",
|
||||||
|
backupCount=10,
|
||||||
|
)
|
||||||
fileHandler.setFormatter(log_fmt)
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
consoleHandler = logging.StreamHandler()
|
consoleHandler = logging.StreamHandler()
|
||||||
@@ -37,7 +58,10 @@ class FrontendServlet(resource.Resource):
|
|||||||
self.logger.addHandler(consoleHandler)
|
self.logger.addHandler(consoleHandler)
|
||||||
|
|
||||||
self.logger.setLevel(cfg.frontend.loglevel)
|
self.logger.setLevel(cfg.frontend.loglevel)
|
||||||
coloredlogs.install(level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str)
|
coloredlogs.install(
|
||||||
|
level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||||
|
)
|
||||||
|
registerAdapter(UserSession, Session, IUserSession)
|
||||||
|
|
||||||
fe_game = FE_Game(cfg, self.environment)
|
fe_game = FE_Game(cfg, self.environment)
|
||||||
games = Utils.get_all_titles()
|
games = Utils.get_all_titles()
|
||||||
@@ -55,12 +79,20 @@ class FrontendServlet(resource.Resource):
|
|||||||
self.putChild(b"user", FE_User(cfg, self.environment))
|
self.putChild(b"user", FE_User(cfg, self.environment))
|
||||||
self.putChild(b"game", fe_game)
|
self.putChild(b"game", fe_game)
|
||||||
|
|
||||||
self.logger.info(f"Ready on port {self.config.frontend.port} serving {len(fe_game.children)} games")
|
self.logger.info(
|
||||||
|
f"Ready on port {self.config.frontend.port} serving {len(fe_game.children)} games"
|
||||||
|
)
|
||||||
|
|
||||||
def render_GET(self, request):
|
def render_GET(self, request):
|
||||||
self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}")
|
self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}")
|
||||||
template = self.environment.get_template("core/frontend/index.jinja")
|
template = self.environment.get_template("core/frontend/index.jinja")
|
||||||
return template.render(server_name=self.config.server.name, title=self.config.server.name, game_list=self.game_list).encode("utf-16")
|
return template.render(
|
||||||
|
server_name=self.config.server.name,
|
||||||
|
title=self.config.server.name,
|
||||||
|
game_list=self.game_list,
|
||||||
|
sesh=vars(IUserSession(request.getSession())),
|
||||||
|
).encode("utf-16")
|
||||||
|
|
||||||
|
|
||||||
class FE_Base(resource.Resource):
|
class FE_Base(resource.Resource):
|
||||||
"""
|
"""
|
||||||
@@ -68,39 +100,53 @@ class FE_Base(resource.Resource):
|
|||||||
Initializes the environment, data, logger, config, and sets isLeaf to true
|
Initializes the environment, data, logger, config, and sets isLeaf to true
|
||||||
It is expected that game implementations of this class overwrite many of these
|
It is expected that game implementations of this class overwrite many of these
|
||||||
"""
|
"""
|
||||||
|
|
||||||
isLeaf = True
|
isLeaf = True
|
||||||
|
|
||||||
def __init__(self, cfg: CoreConfig, environment: jinja2.Environment) -> None:
|
def __init__(self, cfg: CoreConfig, environment: jinja2.Environment) -> None:
|
||||||
self.core_config = cfg
|
self.core_config = cfg
|
||||||
self.data = Data(cfg)
|
self.data = Data(cfg)
|
||||||
self.logger = logging.getLogger('frontend')
|
self.logger = logging.getLogger("frontend")
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
self.nav_name = "nav_name"
|
self.nav_name = "nav_name"
|
||||||
|
|
||||||
|
|
||||||
class FE_Gate(FE_Base):
|
class FE_Gate(FE_Base):
|
||||||
def render_GET(self, request: Request):
|
def render_GET(self, request: Request):
|
||||||
self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}")
|
self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}")
|
||||||
uri: str = request.uri.decode()
|
uri: str = request.uri.decode()
|
||||||
|
|
||||||
|
sesh = request.getSession()
|
||||||
|
usr_sesh = IUserSession(sesh)
|
||||||
|
if usr_sesh.userId > 0:
|
||||||
|
return redirectTo(b"/user", request)
|
||||||
|
|
||||||
if uri.startswith("/gate/create"):
|
if uri.startswith("/gate/create"):
|
||||||
return self.create_user(request)
|
return self.create_user(request)
|
||||||
|
|
||||||
if b'e' in request.args:
|
if b"e" in request.args:
|
||||||
try:
|
try:
|
||||||
err = int(request.args[b'e'][0].decode())
|
err = int(request.args[b"e"][0].decode())
|
||||||
except:
|
except:
|
||||||
err = 0
|
err = 0
|
||||||
|
|
||||||
else: err = 0
|
else:
|
||||||
|
err = 0
|
||||||
|
|
||||||
template = self.environment.get_template("core/frontend/gate/gate.jinja")
|
template = self.environment.get_template("core/frontend/gate/gate.jinja")
|
||||||
return template.render(title=f"{self.core_config.server.name} | Login Gate", error=err).encode("utf-16")
|
return template.render(
|
||||||
|
title=f"{self.core_config.server.name} | Login Gate",
|
||||||
|
error=err,
|
||||||
|
sesh=vars(usr_sesh),
|
||||||
|
).encode("utf-16")
|
||||||
|
|
||||||
def render_POST(self, request: Request):
|
def render_POST(self, request: Request):
|
||||||
uri = request.uri.decode()
|
uri = request.uri.decode()
|
||||||
ip = request.getClientAddress().host
|
ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if uri == "/gate/gate.login":
|
if uri == "/gate/gate.login":
|
||||||
access_code: str = request.args[b"access_code"][0].decode()
|
access_code: str = request.args[b"access_code"][0].decode()
|
||||||
passwd: str = request.args[b"passwd"][0]
|
passwd: bytes = request.args[b"passwd"][0]
|
||||||
if passwd == b"":
|
if passwd == b"":
|
||||||
passwd = None
|
passwd = None
|
||||||
|
|
||||||
@@ -109,20 +155,24 @@ class FE_Gate(FE_Base):
|
|||||||
return redirectTo(b"/gate?e=1", request)
|
return redirectTo(b"/gate?e=1", request)
|
||||||
|
|
||||||
if passwd is None:
|
if passwd is None:
|
||||||
sesh = self.data.user.login(uid, ip=ip)
|
sesh = self.data.user.check_password(uid)
|
||||||
|
|
||||||
if sesh is not None:
|
if sesh is not None:
|
||||||
return redirectTo(f"/gate/create?ac={access_code}".encode(), request)
|
return redirectTo(
|
||||||
|
f"/gate/create?ac={access_code}".encode(), request
|
||||||
|
)
|
||||||
return redirectTo(b"/gate?e=1", request)
|
return redirectTo(b"/gate?e=1", request)
|
||||||
|
|
||||||
salt = bcrypt.gensalt()
|
if not self.data.user.check_password(uid, passwd):
|
||||||
hashed = bcrypt.hashpw(passwd, salt)
|
|
||||||
sesh = self.data.user.login(uid, hashed, ip)
|
|
||||||
|
|
||||||
if sesh is None:
|
|
||||||
return redirectTo(b"/gate?e=1", request)
|
return redirectTo(b"/gate?e=1", request)
|
||||||
|
|
||||||
request.addCookie('session', sesh)
|
self.logger.info(f"Successful login of user {uid} at {ip}")
|
||||||
|
|
||||||
|
sesh = request.getSession()
|
||||||
|
usr_sesh = IUserSession(sesh)
|
||||||
|
usr_sesh.userId = uid
|
||||||
|
usr_sesh.current_ip = ip
|
||||||
|
|
||||||
return redirectTo(b"/user", request)
|
return redirectTo(b"/user", request)
|
||||||
|
|
||||||
elif uri == "/gate/gate.create":
|
elif uri == "/gate/gate.create":
|
||||||
@@ -138,14 +188,14 @@ class FE_Gate(FE_Base):
|
|||||||
salt = bcrypt.gensalt()
|
salt = bcrypt.gensalt()
|
||||||
hashed = bcrypt.hashpw(passwd, salt)
|
hashed = bcrypt.hashpw(passwd, salt)
|
||||||
|
|
||||||
result = self.data.user.create_user(uid, username, email, hashed.decode(), 1)
|
result = self.data.user.create_user(
|
||||||
|
uid, username, email, hashed.decode(), 1
|
||||||
|
)
|
||||||
if result is None:
|
if result is None:
|
||||||
return redirectTo(b"/gate?e=3", request)
|
return redirectTo(b"/gate?e=3", request)
|
||||||
|
|
||||||
sesh = self.data.user.login(uid, hashed, ip)
|
if not self.data.user.check_password(uid, passwd.encode()):
|
||||||
if sesh is None:
|
|
||||||
return redirectTo(b"/gate", request)
|
return redirectTo(b"/gate", request)
|
||||||
request.addCookie('session', sesh)
|
|
||||||
|
|
||||||
return redirectTo(b"/user", request)
|
return redirectTo(b"/user", request)
|
||||||
|
|
||||||
@@ -153,27 +203,39 @@ class FE_Gate(FE_Base):
|
|||||||
return b""
|
return b""
|
||||||
|
|
||||||
def create_user(self, request: Request):
|
def create_user(self, request: Request):
|
||||||
if b'ac' not in request.args or len(request.args[b'ac'][0].decode()) != 20:
|
if b"ac" not in request.args or len(request.args[b"ac"][0].decode()) != 20:
|
||||||
return redirectTo(b"/gate?e=2", request)
|
return redirectTo(b"/gate?e=2", request)
|
||||||
|
|
||||||
ac = request.args[b'ac'][0].decode()
|
ac = request.args[b"ac"][0].decode()
|
||||||
|
|
||||||
template = self.environment.get_template("core/frontend/gate/create.jinja")
|
template = self.environment.get_template("core/frontend/gate/create.jinja")
|
||||||
return template.render(title=f"{self.core_config.server.name} | Create User", code=ac).encode("utf-16")
|
return template.render(
|
||||||
|
title=f"{self.core_config.server.name} | Create User",
|
||||||
|
code=ac,
|
||||||
|
sesh={"userId": 0},
|
||||||
|
).encode("utf-16")
|
||||||
|
|
||||||
|
|
||||||
class FE_User(FE_Base):
|
class FE_User(FE_Base):
|
||||||
def render_GET(self, request: Request):
|
def render_GET(self, request: Request):
|
||||||
template = self.environment.get_template("core/frontend/user/index.jinja")
|
template = self.environment.get_template("core/frontend/user/index.jinja")
|
||||||
return template.render().encode("utf-16")
|
|
||||||
if b'session' not in request.cookies:
|
sesh: Session = request.getSession()
|
||||||
|
usr_sesh = IUserSession(sesh)
|
||||||
|
if usr_sesh.userId == 0:
|
||||||
return redirectTo(b"/gate", request)
|
return redirectTo(b"/gate", request)
|
||||||
|
|
||||||
|
return template.render(
|
||||||
|
title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh)
|
||||||
|
).encode("utf-16")
|
||||||
|
|
||||||
|
|
||||||
class FE_Game(FE_Base):
|
class FE_Game(FE_Base):
|
||||||
isLeaf = False
|
isLeaf = False
|
||||||
children: Dict[str, Any] = {}
|
children: Dict[str, Any] = {}
|
||||||
|
|
||||||
def getChild(self, name: bytes, request: Request):
|
def getChild(self, name: bytes, request: Request):
|
||||||
if name == b'':
|
if name == b"":
|
||||||
return self
|
return self
|
||||||
return resource.Resource.getChild(self, name, request)
|
return resource.Resource.getChild(self, name, request)
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,23 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Gate</h1>
|
<h1>Gate</h1>
|
||||||
{% include "core/frontend/widgets/err_banner.jinja" %}
|
{% include "core/frontend/widgets/err_banner.jinja" %}
|
||||||
|
<style>
|
||||||
|
/* Chrome, Safari, Edge, Opera */
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
input[type=number] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<form id="login" style="max-width: 240px; min-width: 10%;" action="/gate/gate.login" method="post">
|
<form id="login" style="max-width: 240px; min-width: 10%;" action="/gate/gate.login" method="post">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="access_code">Card Access Code</label><br>
|
<label for="access_code">Card Access Code</label><br>
|
||||||
<input form="login" class="form-control" name="access_code" id="access_code" type="text" placeholder="00000000000000000000" maxlength="20" required>
|
<input form="login" class="form-control" name="access_code" id="access_code" type="number" placeholder="00000000000000000000" maxlength="20" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="passwd">Password</label><br>
|
<label for="passwd">Password</label><br>
|
||||||
@@ -14,4 +27,6 @@
|
|||||||
<p></p>
|
<p></p>
|
||||||
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" form="login" type="submit" value="Login">
|
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" form="login" type="submit" value="Login">
|
||||||
</form>
|
</form>
|
||||||
|
<h6>*To register for the webui, type in the access code of your card, as shown in a game, and leave the password field blank.</h6>
|
||||||
|
<h6>*If you have not registered a card with this server, you cannot create a webui account.</h6>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
@@ -9,5 +9,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;">
|
<div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;">
|
||||||
|
{% if sesh is defined and sesh["userId"] > 0 %}
|
||||||
|
<a href="/user"><button class="btn btn-primary">Account</button></a>
|
||||||
|
{% else %}
|
||||||
<a href="/gate"><button class="btn btn-primary">Gate</button></a>
|
<a href="/gate"><button class="btn btn-primary">Gate</button></a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
+99
-43
@@ -1,4 +1,4 @@
|
|||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional, List
|
||||||
import logging, coloredlogs
|
import logging, coloredlogs
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
from twisted.web import resource
|
from twisted.web import resource
|
||||||
@@ -6,17 +6,25 @@ from twisted.web.http import Request
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core import CoreConfig
|
||||||
|
from core.utils import Utils
|
||||||
|
|
||||||
|
|
||||||
class MuchaServlet:
|
class MuchaServlet:
|
||||||
def __init__(self, cfg: CoreConfig) -> None:
|
def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
self.config = cfg
|
self.config = cfg
|
||||||
|
self.config_dir = cfg_dir
|
||||||
|
self.mucha_registry: List[str] = []
|
||||||
|
|
||||||
self.logger = logging.getLogger('mucha')
|
self.logger = logging.getLogger("mucha")
|
||||||
log_fmt_str = "[%(asctime)s] Mucha | %(levelname)s | %(message)s"
|
log_fmt_str = "[%(asctime)s] Mucha | %(levelname)s | %(message)s"
|
||||||
log_fmt = logging.Formatter(log_fmt_str)
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
|
|
||||||
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "mucha"), when="d", backupCount=10)
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(self.config.server.log_dir, "mucha"),
|
||||||
|
when="d",
|
||||||
|
backupCount=10,
|
||||||
|
)
|
||||||
fileHandler.setFormatter(log_fmt)
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
consoleHandler = logging.StreamHandler()
|
consoleHandler = logging.StreamHandler()
|
||||||
@@ -28,19 +36,45 @@ class MuchaServlet:
|
|||||||
self.logger.setLevel(logging.INFO)
|
self.logger.setLevel(logging.INFO)
|
||||||
coloredlogs.install(level=logging.INFO, logger=self.logger, fmt=log_fmt_str)
|
coloredlogs.install(level=logging.INFO, logger=self.logger, fmt=log_fmt_str)
|
||||||
|
|
||||||
|
all_titles = Utils.get_all_titles()
|
||||||
|
|
||||||
|
for _, mod in all_titles.items():
|
||||||
|
if hasattr(mod, "index") and hasattr(mod.index, "get_mucha_info"):
|
||||||
|
enabled, game_cd = mod.index.get_mucha_info(
|
||||||
|
self.config, self.config_dir
|
||||||
|
)
|
||||||
|
if enabled:
|
||||||
|
self.mucha_registry.append(game_cd)
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
f"Serving {len(self.mucha_registry)} games"
|
||||||
|
)
|
||||||
|
|
||||||
def handle_boardauth(self, request: Request, _: Dict) -> bytes:
|
def handle_boardauth(self, request: Request, _: Dict) -> bytes:
|
||||||
req_dict = self.mucha_preprocess(request.content.getvalue())
|
req_dict = self.mucha_preprocess(request.content.getvalue())
|
||||||
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if req_dict is None:
|
if req_dict is None:
|
||||||
self.logger.error(f"Error processing mucha request {request.content.getvalue()}")
|
self.logger.error(
|
||||||
|
f"Error processing mucha request {request.content.getvalue()}"
|
||||||
|
)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
req = MuchaAuthRequest(req_dict)
|
req = MuchaAuthRequest(req_dict)
|
||||||
self.logger.debug(f"Mucha request {vars(req)}")
|
self.logger.debug(f"Mucha request {vars(req)}")
|
||||||
|
self.logger.info(
|
||||||
|
f"Boardauth request from {client_ip} for {req.gameVer}"
|
||||||
|
)
|
||||||
|
|
||||||
if self.config.server.is_develop:
|
if req.gameCd not in self.mucha_registry:
|
||||||
resp = MuchaAuthResponse(mucha_url=f"{self.config.mucha.hostname}:{self.config.mucha.port}")
|
self.logger.warn(f"Unknown gameCd {req.gameCd}")
|
||||||
else:
|
return b""
|
||||||
resp = MuchaAuthResponse(mucha_url=f"{self.config.mucha.hostname}")
|
|
||||||
|
# TODO: Decrypt S/N
|
||||||
|
|
||||||
|
resp = MuchaAuthResponse(
|
||||||
|
f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}"
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.debug(f"Mucha response {vars(resp)}")
|
self.logger.debug(f"Mucha response {vars(resp)}")
|
||||||
|
|
||||||
@@ -48,17 +82,25 @@ class MuchaServlet:
|
|||||||
|
|
||||||
def handle_updatecheck(self, request: Request, _: Dict) -> bytes:
|
def handle_updatecheck(self, request: Request, _: Dict) -> bytes:
|
||||||
req_dict = self.mucha_preprocess(request.content.getvalue())
|
req_dict = self.mucha_preprocess(request.content.getvalue())
|
||||||
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if req_dict is None:
|
if req_dict is None:
|
||||||
self.logger.error(f"Error processing mucha request {request.content.getvalue()}")
|
self.logger.error(
|
||||||
|
f"Error processing mucha request {request.content.getvalue()}"
|
||||||
|
)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
req = MuchaUpdateRequest(req_dict)
|
req = MuchaUpdateRequest(req_dict)
|
||||||
self.logger.debug(f"Mucha request {vars(req)}")
|
self.logger.debug(f"Mucha request {vars(req)}")
|
||||||
|
self.logger.info(
|
||||||
|
f"Updatecheck request from {client_ip} for {req.gameVer}"
|
||||||
|
)
|
||||||
|
|
||||||
if self.config.server.is_develop:
|
if req.gameCd not in self.mucha_registry:
|
||||||
resp = MuchaUpdateResponse(mucha_url=f"{self.config.mucha.hostname}:{self.config.mucha.port}")
|
self.logger.warn(f"Unknown gameCd {req.gameCd}")
|
||||||
else:
|
return b""
|
||||||
resp = MuchaUpdateResponse(mucha_url=f"{self.config.mucha.hostname}")
|
|
||||||
|
resp = MuchaUpdateResponseStub(req.gameVer)
|
||||||
|
|
||||||
self.logger.debug(f"Mucha response {vars(resp)}")
|
self.logger.debug(f"Mucha response {vars(resp)}")
|
||||||
|
|
||||||
@@ -68,8 +110,8 @@ class MuchaServlet:
|
|||||||
try:
|
try:
|
||||||
ret: Dict[str, Any] = {}
|
ret: Dict[str, Any] = {}
|
||||||
|
|
||||||
for x in data.decode().split('&'):
|
for x in data.decode().split("&"):
|
||||||
kvp = x.split('=')
|
kvp = x.split("=")
|
||||||
if len(kvp) == 2:
|
if len(kvp) == 2:
|
||||||
ret[kvp[0]] = kvp[1]
|
ret[kvp[0]] = kvp[1]
|
||||||
|
|
||||||
@@ -82,7 +124,7 @@ class MuchaServlet:
|
|||||||
def mucha_postprocess(self, data: dict) -> Optional[bytes]:
|
def mucha_postprocess(self, data: dict) -> Optional[bytes]:
|
||||||
try:
|
try:
|
||||||
urlencode = ""
|
urlencode = ""
|
||||||
for k,v in data.items():
|
for k, v in data.items():
|
||||||
urlencode += f"{k}={v}&"
|
urlencode += f"{k}={v}&"
|
||||||
|
|
||||||
return urlencode.encode()
|
return urlencode.encode()
|
||||||
@@ -91,22 +133,26 @@ class MuchaServlet:
|
|||||||
self.logger.error("Error processing mucha response")
|
self.logger.error("Error processing mucha response")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
class MuchaAuthRequest():
|
|
||||||
def __init__(self, request: Dict) -> None:
|
|
||||||
self.gameVer = "" if "gameVer" not in request else request["gameVer"]
|
|
||||||
self.sendDate = "" if "sendDate" not in request else request["sendDate"]
|
|
||||||
self.serialNum = "" if "serialNum" not in request else request["serialNum"]
|
|
||||||
self.gameCd = "" if "gameCd" not in request else request["gameCd"]
|
|
||||||
self.boardType = "" if "boardType" not in request else request["boardType"]
|
|
||||||
self.boardId = "" if "boardId" not in request else request["boardId"]
|
|
||||||
self.placeId = "" if "placeId" not in request else request["placeId"]
|
|
||||||
self.storeRouterIp = "" if "storeRouterIp" not in request else request["storeRouterIp"]
|
|
||||||
self.countryCd = "" if "countryCd" not in request else request["countryCd"]
|
|
||||||
self.useToken = "" if "useToken" not in request else request["useToken"]
|
|
||||||
self.allToken = "" if "allToken" not in request else request["allToken"]
|
|
||||||
|
|
||||||
class MuchaAuthResponse():
|
class MuchaAuthRequest:
|
||||||
def __init__(self, mucha_url: str = "localhost") -> None:
|
def __init__(self, request: Dict) -> None:
|
||||||
|
# gameCd + boardType + countryCd + version
|
||||||
|
self.gameVer = request.get("gameVer", "")
|
||||||
|
self.sendDate = request.get("sendDate", "") # %Y%m%d
|
||||||
|
self.serialNum = request.get("serialNum", "")
|
||||||
|
self.gameCd = request.get("gameCd", "")
|
||||||
|
self.boardType = request.get("boardType", "")
|
||||||
|
self.boardId = request.get("boardId", "")
|
||||||
|
self.mac = request.get("mac", "")
|
||||||
|
self.placeId = request.get("placeId", "")
|
||||||
|
self.storeRouterIp = request.get("storeRouterIp", "")
|
||||||
|
self.countryCd = request.get("countryCd", "")
|
||||||
|
self.useToken = request.get("useToken", "")
|
||||||
|
self.allToken = request.get("allToken", "")
|
||||||
|
|
||||||
|
|
||||||
|
class MuchaAuthResponse:
|
||||||
|
def __init__(self, mucha_url: str) -> None:
|
||||||
self.RESULTS = "001"
|
self.RESULTS = "001"
|
||||||
self.AUTH_INTERVAL = "86400"
|
self.AUTH_INTERVAL = "86400"
|
||||||
self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M")
|
self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M")
|
||||||
@@ -149,17 +195,19 @@ class MuchaAuthResponse():
|
|||||||
self.DONGLE_FLG = "1"
|
self.DONGLE_FLG = "1"
|
||||||
self.FORCE_BOOT = "0"
|
self.FORCE_BOOT = "0"
|
||||||
|
|
||||||
class MuchaUpdateRequest():
|
|
||||||
def __init__(self, request: Dict) -> None:
|
|
||||||
self.gameVer = "" if "gameVer" not in request else request["gameVer"]
|
|
||||||
self.gameCd = "" if "gameCd" not in request else request["gameCd"]
|
|
||||||
self.serialNum = "" if "serialNum" not in request else request["serialNum"]
|
|
||||||
self.countryCd = "" if "countryCd" not in request else request["countryCd"]
|
|
||||||
self.placeId = "" if "placeId" not in request else request["placeId"]
|
|
||||||
self.storeRouterIp = "" if "storeRouterIp" not in request else request["storeRouterIp"]
|
|
||||||
|
|
||||||
class MuchaUpdateResponse():
|
class MuchaUpdateRequest:
|
||||||
def __init__(self, game_ver: str = "PKFN0JPN01.01", mucha_url: str = "localhost") -> None:
|
def __init__(self, request: Dict) -> None:
|
||||||
|
self.gameVer = request.get("gameVer", "")
|
||||||
|
self.gameCd = request.get("gameCd", "")
|
||||||
|
self.serialNum = request.get("serialNum", "")
|
||||||
|
self.countryCd = request.get("countryCd", "")
|
||||||
|
self.placeId = request.get("placeId", "")
|
||||||
|
self.storeRouterIp = request.get("storeRouterIp", "")
|
||||||
|
|
||||||
|
|
||||||
|
class MuchaUpdateResponse:
|
||||||
|
def __init__(self, game_ver: str, mucha_url: str) -> None:
|
||||||
self.RESULTS = "001"
|
self.RESULTS = "001"
|
||||||
self.UPDATE_VER_1 = game_ver
|
self.UPDATE_VER_1 = game_ver
|
||||||
self.UPDATE_URL_1 = f"https://{mucha_url}/updUrl1/"
|
self.UPDATE_URL_1 = f"https://{mucha_url}/updUrl1/"
|
||||||
@@ -171,3 +219,11 @@ class MuchaUpdateResponse():
|
|||||||
self.COM_SIZE_1 = "0"
|
self.COM_SIZE_1 = "0"
|
||||||
self.COM_TIME_1 = "0"
|
self.COM_TIME_1 = "0"
|
||||||
self.LAN_INFO_SIZE_1 = "0"
|
self.LAN_INFO_SIZE_1 = "0"
|
||||||
|
self.USER_ID = ""
|
||||||
|
self.PASSWORD = ""
|
||||||
|
|
||||||
|
|
||||||
|
class MuchaUpdateResponseStub:
|
||||||
|
def __init__(self, game_ver: str) -> None:
|
||||||
|
self.RESULTS = "001"
|
||||||
|
self.UPDATE_VER_1 = game_ver
|
||||||
+34
-10
@@ -7,7 +7,8 @@ from core.config import CoreConfig
|
|||||||
from core.data import Data
|
from core.data import Data
|
||||||
from core.utils import Utils
|
from core.utils import Utils
|
||||||
|
|
||||||
class TitleServlet():
|
|
||||||
|
class TitleServlet:
|
||||||
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.config = core_cfg
|
self.config = core_cfg
|
||||||
@@ -20,7 +21,11 @@ class TitleServlet():
|
|||||||
log_fmt_str = "[%(asctime)s] Title | %(levelname)s | %(message)s"
|
log_fmt_str = "[%(asctime)s] Title | %(levelname)s | %(message)s"
|
||||||
log_fmt = logging.Formatter(log_fmt_str)
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
|
|
||||||
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "title"), when="d", backupCount=10)
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(self.config.server.log_dir, "title"),
|
||||||
|
when="d",
|
||||||
|
backupCount=10,
|
||||||
|
)
|
||||||
fileHandler.setFormatter(log_fmt)
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
consoleHandler = logging.StreamHandler()
|
consoleHandler = logging.StreamHandler()
|
||||||
@@ -30,24 +35,41 @@ class TitleServlet():
|
|||||||
self.logger.addHandler(consoleHandler)
|
self.logger.addHandler(consoleHandler)
|
||||||
|
|
||||||
self.logger.setLevel(core_cfg.title.loglevel)
|
self.logger.setLevel(core_cfg.title.loglevel)
|
||||||
coloredlogs.install(level=core_cfg.title.loglevel, logger=self.logger, fmt=log_fmt_str)
|
coloredlogs.install(
|
||||||
|
level=core_cfg.title.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||||
|
)
|
||||||
self.logger.initialized = True
|
self.logger.initialized = True
|
||||||
|
|
||||||
plugins = Utils.get_all_titles()
|
plugins = Utils.get_all_titles()
|
||||||
|
|
||||||
for folder, mod in plugins.items():
|
for folder, mod in plugins.items():
|
||||||
if hasattr(mod, "game_codes") and hasattr(mod, "index"):
|
if hasattr(mod, "game_codes") and hasattr(mod, "index"):
|
||||||
handler_cls = mod.index(self.config, self.config_folder)
|
should_call_setup = True
|
||||||
if hasattr(handler_cls, "setup"):
|
|
||||||
handler_cls.setup()
|
|
||||||
|
|
||||||
for code in mod.game_codes:
|
if hasattr(mod.index, "get_allnet_info"):
|
||||||
self.title_registry[code] = handler_cls
|
for code in mod.game_codes:
|
||||||
|
enabled, _, _ = mod.index.get_allnet_info(
|
||||||
|
code, self.config, self.config_folder
|
||||||
|
)
|
||||||
|
|
||||||
|
if enabled:
|
||||||
|
handler_cls = mod.index(self.config, self.config_folder)
|
||||||
|
|
||||||
|
if hasattr(handler_cls, "setup") and should_call_setup:
|
||||||
|
handler_cls.setup()
|
||||||
|
should_call_setup = False
|
||||||
|
|
||||||
|
self.title_registry[code] = handler_cls
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.warn(f"Game {folder} has no get_allnet_info")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.logger.error(f"{folder} missing game_code or index in __init__.py")
|
self.logger.error(f"{folder} missing game_code or index in __init__.py")
|
||||||
|
|
||||||
self.logger.info(f"Serving {len(self.title_registry)} game codes on port {core_cfg.title.port}")
|
self.logger.info(
|
||||||
|
f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.title.port) if core_cfg.title.port > 0 else ''}"
|
||||||
|
)
|
||||||
|
|
||||||
def render_GET(self, request: Request, endpoints: dict) -> bytes:
|
def render_GET(self, request: Request, endpoints: dict) -> bytes:
|
||||||
code = endpoints["game"]
|
code = endpoints["game"]
|
||||||
@@ -77,4 +99,6 @@ class TitleServlet():
|
|||||||
request.setResponseCode(405)
|
request.setResponseCode(405)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
return index.render_POST(request, int(endpoints["version"]), endpoints["endpoint"])
|
return index.render_POST(
|
||||||
|
request, int(endpoints["version"]), endpoints["endpoint"]
|
||||||
|
)
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
from twisted.web.http import Request
|
||||||
import logging
|
import logging
|
||||||
import importlib
|
import importlib
|
||||||
from os import walk
|
from os import walk
|
||||||
|
|
||||||
|
|
||||||
class Utils:
|
class Utils:
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_titles(cls) -> Dict[str, ModuleType]:
|
def get_all_titles(cls) -> Dict[str, ModuleType]:
|
||||||
@@ -20,3 +22,7 @@ class Utils:
|
|||||||
logging.getLogger("core").error(f"get_all_titles: {dir} - {e}")
|
logging.getLogger("core").error(f"get_all_titles: {dir} - {e}")
|
||||||
raise
|
raise
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_ip_addr(cls, req: Request) -> str:
|
||||||
|
return req.getAllHeaders()[b"x-forwarded-for"].decode() if b"x-forwarded-for" in req.getAllHeaders() else req.getClientAddress().host
|
||||||
+50
-7
@@ -2,19 +2,50 @@ import yaml
|
|||||||
import argparse
|
import argparse
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from core.data import Data
|
from core.data import Data
|
||||||
|
from os import path, mkdir, access, W_OK
|
||||||
|
|
||||||
if __name__=='__main__':
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Database utilities")
|
parser = argparse.ArgumentParser(description="Database utilities")
|
||||||
parser.add_argument("--config", "-c", type=str, help="Config folder to use", default="config")
|
parser.add_argument(
|
||||||
parser.add_argument("--version", "-v", type=str, help="Version of the database to upgrade/rollback to")
|
"--config", "-c", type=str, help="Config folder to use", default="config"
|
||||||
parser.add_argument("--game", "-g", type=str, help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE")
|
)
|
||||||
parser.add_argument("action", type=str, help="DB Action, create, recreate, upgrade, or rollback")
|
parser.add_argument(
|
||||||
|
"--version",
|
||||||
|
"-v",
|
||||||
|
type=str,
|
||||||
|
help="Version of the database to upgrade/rollback to",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--game",
|
||||||
|
"-g",
|
||||||
|
type=str,
|
||||||
|
help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE",
|
||||||
|
)
|
||||||
|
parser.add_argument("--email", "-e", type=str, help="Email for the new user")
|
||||||
|
parser.add_argument("--old_ac", "-o", type=str, help="Access code to transfer from")
|
||||||
|
parser.add_argument("--new_ac", "-n", type=str, help="Access code to transfer to")
|
||||||
|
parser.add_argument("--force", "-f", type=bool, help="Force the action to happen")
|
||||||
|
parser.add_argument(
|
||||||
|
"action", type=str, help="DB Action, create, recreate, upgrade, or rollback"
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
cfg = CoreConfig()
|
cfg = CoreConfig()
|
||||||
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
|
if path.exists(f"{args.config}/core.yaml"):
|
||||||
|
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
|
||||||
|
|
||||||
|
if not path.exists(cfg.server.log_dir):
|
||||||
|
mkdir(cfg.server.log_dir)
|
||||||
|
|
||||||
|
if not access(cfg.server.log_dir, W_OK):
|
||||||
|
print(
|
||||||
|
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
data = Data(cfg)
|
data = Data(cfg)
|
||||||
|
|
||||||
|
|
||||||
if args.action == "create":
|
if args.action == "create":
|
||||||
data.create_database()
|
data.create_database()
|
||||||
|
|
||||||
@@ -28,9 +59,21 @@ if __name__=='__main__':
|
|||||||
|
|
||||||
if args.game is None:
|
if args.game is None:
|
||||||
data.logger.info("No game set, upgrading core schema")
|
data.logger.info("No game set, upgrading core schema")
|
||||||
data.migrate_database("CORE", int(args.version))
|
data.migrate_database("CORE", int(args.version), args.action)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
data.migrate_database(args.game, int(args.version), args.action)
|
data.migrate_database(args.game, int(args.version), args.action)
|
||||||
|
|
||||||
|
elif args.action == "autoupgrade":
|
||||||
|
data.autoupgrade()
|
||||||
|
|
||||||
|
elif args.action == "create-owner":
|
||||||
|
data.create_owner(args.email)
|
||||||
|
|
||||||
|
elif args.action == "migrate-card":
|
||||||
|
data.migrate_card(args.old_ac, args.new_ac, args.force)
|
||||||
|
|
||||||
|
elif args.action == "cleanup":
|
||||||
|
data.delete_hanging_users()
|
||||||
|
|
||||||
data.logger.info("Done")
|
data.logger.info("Done")
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
# ARTEMiS Games Documentation
|
||||||
|
|
||||||
|
Below are all supported games with supported version ids in order to use
|
||||||
|
the corresponding importer and database upgrades.
|
||||||
|
|
||||||
|
**Important: The described database upgrades are only required if you are using an old database schema, f.e. still
|
||||||
|
using the megaime database. Clean installations always create the latest database structure!**
|
||||||
|
|
||||||
|
# Table of content
|
||||||
|
|
||||||
|
- [Supported Games](#supported-games)
|
||||||
|
- [Chunithm](#chunithm)
|
||||||
|
- [crossbeats REV.](#crossbeats-rev)
|
||||||
|
- [maimai DX](#maimai-dx)
|
||||||
|
- [O.N.G.E.K.I.](#o-n-g-e-k-i)
|
||||||
|
- [Card Maker](#card-maker)
|
||||||
|
- [WACCA](#wacca)
|
||||||
|
|
||||||
|
|
||||||
|
# Supported Games
|
||||||
|
|
||||||
|
Games listed below have been tested and confirmed working.
|
||||||
|
|
||||||
|
## Chunithm
|
||||||
|
|
||||||
|
### SDBT
|
||||||
|
|
||||||
|
| Version ID | Version Name |
|
||||||
|
|------------|--------------------|
|
||||||
|
| 0 | Chunithm |
|
||||||
|
| 1 | Chunithm+ |
|
||||||
|
| 2 | Chunithm Air |
|
||||||
|
| 3 | Chunithm Air + |
|
||||||
|
| 4 | Chunithm Star |
|
||||||
|
| 5 | Chunithm Star + |
|
||||||
|
| 6 | Chunithm Amazon |
|
||||||
|
| 7 | Chunithm Amazon + |
|
||||||
|
| 8 | Chunithm Crystal |
|
||||||
|
| 9 | Chunithm Crystal + |
|
||||||
|
| 10 | Chunithm Paradise |
|
||||||
|
|
||||||
|
### SDHD/SDBT
|
||||||
|
|
||||||
|
| Version ID | Version Name |
|
||||||
|
|------------|-----------------|
|
||||||
|
| 11 | Chunithm New!! |
|
||||||
|
| 12 | Chunithm New!!+ |
|
||||||
|
|
||||||
|
|
||||||
|
### Importer
|
||||||
|
|
||||||
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python read.py --series SDBT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
||||||
|
```
|
||||||
|
|
||||||
|
The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories.
|
||||||
|
|
||||||
|
### Database upgrade
|
||||||
|
|
||||||
|
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
|
||||||
|
which version is the latest, f.e. `SDBT_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to
|
||||||
|
perform all previous updates as well:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python dbutils.py --game SDBT --version 2 upgrade
|
||||||
|
python dbutils.py --game SDBT --version 3 upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## crossbeats REV.
|
||||||
|
|
||||||
|
### SDCA
|
||||||
|
|
||||||
|
| Version ID | Version Name |
|
||||||
|
|------------|------------------------------------|
|
||||||
|
| 0 | crossbeats REV. |
|
||||||
|
| 1 | crossbeats REV. SUNRISE |
|
||||||
|
| 2 | crossbeats REV. SUNRISE S2 |
|
||||||
|
| 3 | crossbeats REV. SUNRISE S2 Omnimix |
|
||||||
|
|
||||||
|
### Importer
|
||||||
|
|
||||||
|
In order to use the importer you need to use the provided `Export.csv` file:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python read.py --series SDCA --version <version ID> --binfolder titles/cxb/data
|
||||||
|
```
|
||||||
|
|
||||||
|
The importer for crossbeats REV. will import Music.
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
Config file is located in `config/cxb.yaml`.
|
||||||
|
|
||||||
|
| Option | Info |
|
||||||
|
|------------------------|------------------------------------------------------------|
|
||||||
|
| `hostname` | Requires a proper `hostname` (not localhost!) to run |
|
||||||
|
| `ssl_enable` | Enables/Disables the use of the `ssl_cert` and `ssl_key` |
|
||||||
|
| `port` | Set your unsecure port number |
|
||||||
|
| `port_secure` | Set your secure/SSL port number |
|
||||||
|
| `ssl_cert`, `ssl_key` | Enter your SSL certificate (requires not self signed cert) |
|
||||||
|
|
||||||
|
|
||||||
|
## maimai DX
|
||||||
|
|
||||||
|
### SDEZ
|
||||||
|
|
||||||
|
| Version ID | Version Name |
|
||||||
|
|------------|-------------------------|
|
||||||
|
| 0 | maimai DX |
|
||||||
|
| 1 | maimai DX PLUS |
|
||||||
|
| 2 | maimai DX Splash |
|
||||||
|
| 3 | maimai DX Splash PLUS |
|
||||||
|
| 4 | maimai DX Universe |
|
||||||
|
| 5 | maimai DX Universe PLUS |
|
||||||
|
|
||||||
|
### Importer
|
||||||
|
|
||||||
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python read.py --series SDEZ --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
||||||
|
```
|
||||||
|
|
||||||
|
The importer for maimai DX will import Events, Music and Tickets.
|
||||||
|
|
||||||
|
**NOTE: It is required to use the importer because the game will
|
||||||
|
crash without it!**
|
||||||
|
|
||||||
|
### Database upgrade
|
||||||
|
|
||||||
|
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEZ_2_upgrade.sql`. In order to upgrade to version 2 in this case you need to perform all previous updates as well:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python dbutils.py --game SDEZ --version 2 upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hatsune Miku Project Diva
|
||||||
|
|
||||||
|
### SBZV
|
||||||
|
|
||||||
|
| Version ID | Version Name |
|
||||||
|
|------------|---------------------------------|
|
||||||
|
| 0 | Project Diva Arcade |
|
||||||
|
| 1 | Project Diva Arcade Future Tone |
|
||||||
|
|
||||||
|
|
||||||
|
### Importer
|
||||||
|
|
||||||
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python read.py --series SBZV --version <version ID> --binfolder /path/to/game/data/diva --optfolder /path/to/game/data/diva/mdata
|
||||||
|
```
|
||||||
|
|
||||||
|
The importer for Project Diva Arcade will all required data in order to use
|
||||||
|
the Shop, Modules and Customizations.
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
Config file is located in `config/diva.yaml`.
|
||||||
|
|
||||||
|
| Option | Info |
|
||||||
|
|----------------------|-------------------------------------------------------------------------------------------------|
|
||||||
|
| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased |
|
||||||
|
| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased |
|
||||||
|
|
||||||
|
|
||||||
|
### Database upgrade
|
||||||
|
|
||||||
|
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
|
||||||
|
which version is the latest, f.e. `SBZV_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to
|
||||||
|
perform all previous updates as well:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python dbutils.py --game SBZV --version 2 upgrade
|
||||||
|
python dbutils.py --game SBZV --version 3 upgrade
|
||||||
|
python dbutils.py --game SBZV --version 4 upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## O.N.G.E.K.I.
|
||||||
|
|
||||||
|
### SDDT
|
||||||
|
|
||||||
|
| Version ID | Version Name |
|
||||||
|
|------------|----------------------------|
|
||||||
|
| 0 | O.N.G.E.K.I. |
|
||||||
|
| 1 | O.N.G.E.K.I. + |
|
||||||
|
| 2 | O.N.G.E.K.I. Summer |
|
||||||
|
| 3 | O.N.G.E.K.I. Summer + |
|
||||||
|
| 4 | O.N.G.E.K.I. Red |
|
||||||
|
| 5 | O.N.G.E.K.I. Red + |
|
||||||
|
| 6 | O.N.G.E.K.I. Bright |
|
||||||
|
| 7 | O.N.G.E.K.I. Bright Memory |
|
||||||
|
|
||||||
|
|
||||||
|
### Importer
|
||||||
|
|
||||||
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python read.py --series SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
||||||
|
```
|
||||||
|
|
||||||
|
The importer for O.N.G.E.K.I. will all all Cards, Music and Events.
|
||||||
|
|
||||||
|
**NOTE: The Importer is required for Card Maker.**
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
Config file is located in `config/ongeki.yaml`.
|
||||||
|
|
||||||
|
| Option | Info |
|
||||||
|
|------------------|----------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `enabled_gachas` | Enter all gacha IDs for Card Maker to work, other than default may not work due to missing cards added to them |
|
||||||
|
|
||||||
|
Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignored on lower versions.
|
||||||
|
|
||||||
|
### Database upgrade
|
||||||
|
|
||||||
|
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
|
||||||
|
which version is the latest, f.e. `SDDT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to
|
||||||
|
perform all previous updates as well:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python dbutils.py --game SDDT --version 2 upgrade
|
||||||
|
python dbutils.py --game SDDT --version 3 upgrade
|
||||||
|
python dbutils.py --game SDDT --version 4 upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## Card Maker
|
||||||
|
|
||||||
|
### SDED
|
||||||
|
|
||||||
|
| Version ID | Version Name |
|
||||||
|
|------------|-----------------|
|
||||||
|
| 0 | Card Maker 1.34 |
|
||||||
|
| 1 | Card Maker 1.35 |
|
||||||
|
|
||||||
|
|
||||||
|
### Support status
|
||||||
|
|
||||||
|
* Card Maker 1.34:
|
||||||
|
* Chunithm New!!: Yes
|
||||||
|
* maimai DX Universe: Yes
|
||||||
|
* O.N.G.E.K.I. Bright: Yes
|
||||||
|
|
||||||
|
* Card Maker 1.35:
|
||||||
|
* Chunithm New!!+: Yes
|
||||||
|
* maimai DX Universe PLUS: Yes
|
||||||
|
* O.N.G.E.K.I. Bright Memory: Yes
|
||||||
|
|
||||||
|
|
||||||
|
### Importer
|
||||||
|
|
||||||
|
In order to use the importer you need to use the provided `.csv` files (which are required for O.N.G.E.K.I.) and the
|
||||||
|
option folders:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python read.py --series SDED --version <version ID> --binfolder titles/cm/cm_data --optfolder /path/to/cardmaker/option/folder
|
||||||
|
```
|
||||||
|
|
||||||
|
**If you haven't already executed the O.N.G.E.K.I. importer, make sure you import all cards!**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python read.py --series SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
||||||
|
```
|
||||||
|
|
||||||
|
Also make sure to import all maimai and Chunithm data as well:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python read.py --series SDED --version <version ID> --binfolder /path/to/cardmaker/CardMaker_Data
|
||||||
|
```
|
||||||
|
|
||||||
|
The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai/Chunithm) and the hardcoded
|
||||||
|
Cards for each Gacha (O.N.G.E.K.I. only).
|
||||||
|
|
||||||
|
**NOTE: Without executing the importer Card Maker WILL NOT work!**
|
||||||
|
|
||||||
|
|
||||||
|
### O.N.G.E.K.I. Gachas
|
||||||
|
|
||||||
|
Gacha "無料ガチャ" can only pull from the free cards with the following probabilities: 94%: R, 5% SR and 1% chance of
|
||||||
|
getting an SSR card
|
||||||
|
|
||||||
|
Gacha "無料ガチャ(SR確定)" can only pull from free SR cards with prob: 92% SR and 8% chance of getting an SSR card
|
||||||
|
|
||||||
|
Gacha "レギュラーガチャ" can pull from every card added to ongeki_static_cards with the following prob: 77% R, 20% SR
|
||||||
|
and 3% chance of getting an SSR card
|
||||||
|
|
||||||
|
All other (limited) gachas can pull from every card added to ongeki_static_cards but with the promoted cards
|
||||||
|
(click on the green button under the banner) having a 10 times higher chance to get pulled
|
||||||
|
|
||||||
|
### Chunithm Gachas
|
||||||
|
|
||||||
|
All cards in Chunithm (basically just the characters) have the same rarity to it just pulls randomly from all cards
|
||||||
|
from a given gacha but made sure you cannot pull the same card twice in the same 5 times gacha roll.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
Card Maker 1.34 will only load an O.N.G.E.K.I. Bright profile (1.30). Card Maker 1.35 will only load an O.N.G.E.K.I.
|
||||||
|
Bright Memory profile (1.35).
|
||||||
|
The gachas inside the `ongeki.yaml` will make sure only the right gacha ids for the right CM version will be loaded.
|
||||||
|
Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded for CM 1.35.
|
||||||
|
|
||||||
|
**NOTE: There is currently no way to load/use the (printed) maimai DX cards!**
|
||||||
|
|
||||||
|
## WACCA
|
||||||
|
|
||||||
|
### SDFE
|
||||||
|
|
||||||
|
| Version ID | Version Name |
|
||||||
|
|------------|---------------|
|
||||||
|
| 0 | WACCA |
|
||||||
|
| 1 | WACCA S |
|
||||||
|
| 2 | WACCA Lily |
|
||||||
|
| 3 | WACCA Lily R |
|
||||||
|
| 4 | WACCA Reverse |
|
||||||
|
|
||||||
|
|
||||||
|
### Importer
|
||||||
|
|
||||||
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python read.py --series SDFE --version <version ID> --binfolder /path/to/game/WindowsNoEditor/Mercury/Content
|
||||||
|
```
|
||||||
|
|
||||||
|
The importer for WACCA will import all Music data.
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
Config file is located in `config/wacca.yaml`.
|
||||||
|
|
||||||
|
| Option | Info |
|
||||||
|
|--------------------|-----------------------------------------------------------------------------|
|
||||||
|
| `always_vip` | Enables/Disables VIP, if disabled it needs to be purchased manually in game |
|
||||||
|
| `infinite_tickets` | Always set the "unlock expert" tickets to 5 |
|
||||||
|
| `infinite_wp` | Sets the user WP to `999999` |
|
||||||
|
| `enabled_gates` | Enter all gate IDs which should be enabled in game |
|
||||||
|
|
||||||
|
|
||||||
|
### Database upgrade
|
||||||
|
|
||||||
|
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDFE_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python dbutils.py --game SDFE --version 2 upgrade
|
||||||
|
python dbutils.py --game SDFE --version 3 upgrade
|
||||||
|
```
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
server:
|
||||||
|
enable: True
|
||||||
|
loglevel: "info"
|
||||||
@@ -48,6 +48,3 @@ mucha:
|
|||||||
enable: False
|
enable: False
|
||||||
hostname: "localhost"
|
hostname: "localhost"
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
port: 8444
|
|
||||||
ssl_key: "cert/server.key"
|
|
||||||
ssl_cert: "cert/server.pem"
|
|
||||||
@@ -4,6 +4,8 @@ server {
|
|||||||
server_name naominet.jp;
|
server_name naominet.jp;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass_request_headers on;
|
||||||
proxy_pass http://localhost:8000/;
|
proxy_pass http://localhost:8000/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +16,8 @@ server {
|
|||||||
server_name your.hostname.here;
|
server_name your.hostname.here;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass_request_headers on;
|
||||||
proxy_pass http://localhost:8080/;
|
proxy_pass http://localhost:8080/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,6 +79,8 @@ server {
|
|||||||
ssl_prefer_server_ciphers off;
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass_request_headers on;
|
||||||
proxy_pass http://localhost:8080/;
|
proxy_pass http://localhost:8080/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,6 +101,8 @@ server {
|
|||||||
ssl_prefer_server_ciphers off;
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass_request_headers on;
|
||||||
proxy_pass http://localhost:8080/SDBT/104/;
|
proxy_pass http://localhost:8080/SDBT/104/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,6 +139,8 @@ server {
|
|||||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass_request_headers on;
|
||||||
proxy_pass http://localhost:8090/;
|
proxy_pass http://localhost:8090/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,31 @@
|
|||||||
server:
|
server:
|
||||||
enable: True
|
enable: True
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
|
|
||||||
|
gachas:
|
||||||
|
enabled_gachas:
|
||||||
|
- 1011
|
||||||
|
- 1012
|
||||||
|
- 1043
|
||||||
|
- 1067
|
||||||
|
- 1068
|
||||||
|
- 1069
|
||||||
|
- 1070
|
||||||
|
- 1071
|
||||||
|
- 1072
|
||||||
|
- 1073
|
||||||
|
- 1074
|
||||||
|
- 1075
|
||||||
|
- 1076
|
||||||
|
- 1077
|
||||||
|
- 1081
|
||||||
|
- 1085
|
||||||
|
- 1089
|
||||||
|
- 1104
|
||||||
|
- 1111
|
||||||
|
- 1135
|
||||||
|
# can be used for Card Maker 1.35 and up, else will be ignored
|
||||||
|
- 1149
|
||||||
|
- 1156
|
||||||
|
- 1163
|
||||||
|
- 1164
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
server:
|
server:
|
||||||
|
hostname: "localhost"
|
||||||
enable: True
|
enable: True
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
port: 9000
|
port: 9000
|
||||||
port_matching: 9001
|
port_stun: 9001
|
||||||
ssl_cert: cert/pokken.crt
|
port_turn: 9002
|
||||||
ssl_key: cert/pokken.key
|
port_admission: 9003
|
||||||
@@ -12,6 +12,7 @@ from twisted.internet import reactor, endpoints
|
|||||||
from twisted.web.http import Request
|
from twisted.web.http import Request
|
||||||
from routes import Mapper
|
from routes import Mapper
|
||||||
|
|
||||||
|
|
||||||
class HttpDispatcher(resource.Resource):
|
class HttpDispatcher(resource.Resource):
|
||||||
def __init__(self, cfg: CoreConfig, config_dir: str):
|
def __init__(self, cfg: CoreConfig, config_dir: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -23,24 +24,84 @@ class HttpDispatcher(resource.Resource):
|
|||||||
|
|
||||||
self.allnet = AllnetServlet(cfg, config_dir)
|
self.allnet = AllnetServlet(cfg, config_dir)
|
||||||
self.title = TitleServlet(cfg, config_dir)
|
self.title = TitleServlet(cfg, config_dir)
|
||||||
self.mucha = MuchaServlet(cfg)
|
self.mucha = MuchaServlet(cfg, config_dir)
|
||||||
|
|
||||||
self.map_post.connect('allnet_ping', '/naomitest.html', controller="allnet", action='handle_naomitest', conditions=dict(method=['GET']))
|
self.map_post.connect(
|
||||||
self.map_post.connect('allnet_poweron', '/sys/servlet/PowerOn', controller="allnet", action='handle_poweron', conditions=dict(method=['POST']))
|
"allnet_ping",
|
||||||
self.map_post.connect('allnet_downloadorder', '/sys/servlet/DownloadOrder', controller="allnet", action='handle_dlorder', conditions=dict(method=['POST']))
|
"/naomitest.html",
|
||||||
self.map_post.connect('allnet_billing', '/request', controller="allnet", action='handle_billing_request', conditions=dict(method=['POST']))
|
controller="allnet",
|
||||||
self.map_post.connect('allnet_billing', '/request/', controller="allnet", action='handle_billing_request', conditions=dict(method=['POST']))
|
action="handle_naomitest",
|
||||||
|
conditions=dict(method=["GET"]),
|
||||||
|
)
|
||||||
|
self.map_post.connect(
|
||||||
|
"allnet_poweron",
|
||||||
|
"/sys/servlet/PowerOn",
|
||||||
|
controller="allnet",
|
||||||
|
action="handle_poweron",
|
||||||
|
conditions=dict(method=["POST"]),
|
||||||
|
)
|
||||||
|
self.map_post.connect(
|
||||||
|
"allnet_downloadorder",
|
||||||
|
"/sys/servlet/DownloadOrder",
|
||||||
|
controller="allnet",
|
||||||
|
action="handle_dlorder",
|
||||||
|
conditions=dict(method=["POST"]),
|
||||||
|
)
|
||||||
|
self.map_post.connect(
|
||||||
|
"allnet_billing",
|
||||||
|
"/request",
|
||||||
|
controller="allnet",
|
||||||
|
action="handle_billing_request",
|
||||||
|
conditions=dict(method=["POST"]),
|
||||||
|
)
|
||||||
|
self.map_post.connect(
|
||||||
|
"allnet_billing",
|
||||||
|
"/request/",
|
||||||
|
controller="allnet",
|
||||||
|
action="handle_billing_request",
|
||||||
|
conditions=dict(method=["POST"]),
|
||||||
|
)
|
||||||
|
|
||||||
self.map_post.connect('mucha_boardauth', '/mucha/boardauth.do', controller="mucha", action='handle_boardauth', conditions=dict(method=['POST']))
|
self.map_post.connect(
|
||||||
self.map_post.connect('mucha_updatacheck', '/mucha/updatacheck.do', controller="mucha", action='handle_updatacheck', conditions=dict(method=['POST']))
|
"mucha_boardauth",
|
||||||
|
"/mucha/boardauth.do",
|
||||||
|
controller="mucha",
|
||||||
|
action="handle_boardauth",
|
||||||
|
conditions=dict(method=["POST"]),
|
||||||
|
)
|
||||||
|
self.map_post.connect(
|
||||||
|
"mucha_updatacheck",
|
||||||
|
"/mucha/updatacheck.do",
|
||||||
|
controller="mucha",
|
||||||
|
action="handle_updatecheck",
|
||||||
|
conditions=dict(method=["POST"]),
|
||||||
|
)
|
||||||
|
|
||||||
self.map_get.connect("title_get", "/{game}/{version}/{endpoint:.*?}", controller="title", action="render_GET", conditions=dict(method=['GET']), requirements=dict(game=R"S..."))
|
self.map_get.connect(
|
||||||
self.map_post.connect("title_post", "/{game}/{version}/{endpoint:.*?}", controller="title", action="render_POST", conditions=dict(method=['POST']), requirements=dict(game=R"S..."))
|
"title_get",
|
||||||
|
"/{game}/{version}/{endpoint:.*?}",
|
||||||
|
controller="title",
|
||||||
|
action="render_GET",
|
||||||
|
conditions=dict(method=["GET"]),
|
||||||
|
requirements=dict(game=R"S..."),
|
||||||
|
)
|
||||||
|
self.map_post.connect(
|
||||||
|
"title_post",
|
||||||
|
"/{game}/{version}/{endpoint:.*?}",
|
||||||
|
controller="title",
|
||||||
|
action="render_POST",
|
||||||
|
conditions=dict(method=["POST"]),
|
||||||
|
requirements=dict(game=R"S..."),
|
||||||
|
)
|
||||||
|
|
||||||
def render_GET(self, request: Request) -> bytes:
|
def render_GET(self, request: Request) -> bytes:
|
||||||
test = self.map_get.match(request.uri.decode())
|
test = self.map_get.match(request.uri.decode())
|
||||||
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if test is None:
|
if test is None:
|
||||||
self.logger.debug(f"Unknown GET endpoint {request.uri.decode()} from {request.getClientAddress().host} to port {request.getHost().port}")
|
self.logger.debug(
|
||||||
|
f"Unknown GET endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}"
|
||||||
|
)
|
||||||
request.setResponseCode(404)
|
request.setResponseCode(404)
|
||||||
return b"Endpoint not found."
|
return b"Endpoint not found."
|
||||||
|
|
||||||
@@ -48,8 +109,12 @@ class HttpDispatcher(resource.Resource):
|
|||||||
|
|
||||||
def render_POST(self, request: Request) -> bytes:
|
def render_POST(self, request: Request) -> bytes:
|
||||||
test = self.map_post.match(request.uri.decode())
|
test = self.map_post.match(request.uri.decode())
|
||||||
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if test is None:
|
if test is None:
|
||||||
self.logger.debug(f"Unknown POST endpoint {request.uri.decode()} from {request.getClientAddress().host} to port {request.getHost().port}")
|
self.logger.debug(
|
||||||
|
f"Unknown POST endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}"
|
||||||
|
)
|
||||||
request.setResponseCode(404)
|
request.setResponseCode(404)
|
||||||
return b"Endpoint not found."
|
return b"Endpoint not found."
|
||||||
|
|
||||||
@@ -58,13 +123,17 @@ class HttpDispatcher(resource.Resource):
|
|||||||
def dispatch(self, matcher: Dict, request: Request) -> bytes:
|
def dispatch(self, matcher: Dict, request: Request) -> bytes:
|
||||||
controller = getattr(self, matcher["controller"], None)
|
controller = getattr(self, matcher["controller"], None)
|
||||||
if controller is None:
|
if controller is None:
|
||||||
self.logger.error(f"Controller {matcher['controller']} not found via endpoint {request.uri.decode()}")
|
self.logger.error(
|
||||||
|
f"Controller {matcher['controller']} not found via endpoint {request.uri.decode()}"
|
||||||
|
)
|
||||||
request.setResponseCode(404)
|
request.setResponseCode(404)
|
||||||
return b"Endpoint not found."
|
return b"Endpoint not found."
|
||||||
|
|
||||||
handler = getattr(controller, matcher["action"], None)
|
handler = getattr(controller, matcher["action"], None)
|
||||||
if handler is None:
|
if handler is None:
|
||||||
self.logger.error(f"Action {matcher['action']} not found in controller {matcher['controller']} via endpoint {request.uri.decode()}")
|
self.logger.error(
|
||||||
|
f"Action {matcher['action']} not found in controller {matcher['controller']} via endpoint {request.uri.decode()}"
|
||||||
|
)
|
||||||
request.setResponseCode(404)
|
request.setResponseCode(404)
|
||||||
return b"Endpoint not found."
|
return b"Endpoint not found."
|
||||||
|
|
||||||
@@ -80,23 +149,40 @@ class HttpDispatcher(resource.Resource):
|
|||||||
else:
|
else:
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="ARTEMiS main entry point")
|
parser = argparse.ArgumentParser(description="ARTEMiS main entry point")
|
||||||
parser.add_argument("--config", "-c", type=str, default="config", help="Configuration folder")
|
parser.add_argument(
|
||||||
|
"--config", "-c", type=str, default="config", help="Configuration folder"
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if not path.exists(f"{args.config}/core.yaml"):
|
if not path.exists(f"{args.config}/core.yaml"):
|
||||||
print(f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml.\nDid you copy the example folder?")
|
print(
|
||||||
|
f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml.\nDid you copy the example folder?"
|
||||||
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
cfg: CoreConfig = CoreConfig()
|
cfg: CoreConfig = CoreConfig()
|
||||||
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
|
if path.exists(f"{args.config}/core.yaml"):
|
||||||
|
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
|
||||||
|
|
||||||
|
if not path.exists(cfg.server.log_dir):
|
||||||
|
mkdir(cfg.server.log_dir)
|
||||||
|
|
||||||
|
if not access(cfg.server.log_dir, W_OK):
|
||||||
|
print(
|
||||||
|
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
logger = logging.getLogger("core")
|
logger = logging.getLogger("core")
|
||||||
log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s"
|
log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s"
|
||||||
log_fmt = logging.Formatter(log_fmt_str)
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
|
|
||||||
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10)
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10
|
||||||
|
)
|
||||||
fileHandler.setFormatter(log_fmt)
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
consoleHandler = logging.StreamHandler()
|
consoleHandler = logging.StreamHandler()
|
||||||
@@ -109,41 +195,48 @@ if __name__ == "__main__":
|
|||||||
logger.setLevel(log_lv)
|
logger.setLevel(log_lv)
|
||||||
coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str)
|
coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str)
|
||||||
|
|
||||||
if not path.exists(cfg.server.log_dir):
|
|
||||||
mkdir(cfg.server.log_dir)
|
|
||||||
|
|
||||||
if not access(cfg.server.log_dir, W_OK):
|
|
||||||
logger.error(f"Log directory {cfg.server.log_dir} NOT writable, please check permissions")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if not cfg.aimedb.key:
|
if not cfg.aimedb.key:
|
||||||
logger.error("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!")
|
logger.error("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
logger.info(f"ARTEMiS starting in {'develop' if cfg.server.is_develop else 'production'} mode")
|
logger.info(
|
||||||
|
f"ARTEMiS starting in {'develop' if cfg.server.is_develop else 'production'} mode"
|
||||||
|
)
|
||||||
|
|
||||||
allnet_server_str = f"tcp:{cfg.allnet.port}:interface={cfg.server.listen_address}"
|
allnet_server_str = f"tcp:{cfg.allnet.port}:interface={cfg.server.listen_address}"
|
||||||
title_server_str = f"tcp:{cfg.title.port}:interface={cfg.server.listen_address}"
|
title_server_str = f"tcp:{cfg.title.port}:interface={cfg.server.listen_address}"
|
||||||
adb_server_str = f"tcp:{cfg.aimedb.port}:interface={cfg.server.listen_address}"
|
adb_server_str = f"tcp:{cfg.aimedb.port}:interface={cfg.server.listen_address}"
|
||||||
frontend_server_str = f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}"
|
frontend_server_str = (
|
||||||
|
f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}"
|
||||||
|
)
|
||||||
|
|
||||||
billing_server_str = f"tcp:{cfg.billing.port}:interface={cfg.server.listen_address}"
|
billing_server_str = f"tcp:{cfg.billing.port}:interface={cfg.server.listen_address}"
|
||||||
if cfg.server.is_develop:
|
if cfg.server.is_develop:
|
||||||
billing_server_str = f"ssl:{cfg.billing.port}:interface={cfg.server.listen_address}"\
|
billing_server_str = (
|
||||||
|
f"ssl:{cfg.billing.port}:interface={cfg.server.listen_address}"
|
||||||
f":privateKey={cfg.billing.ssl_key}:certKey={cfg.billing.ssl_cert}"
|
f":privateKey={cfg.billing.ssl_key}:certKey={cfg.billing.ssl_cert}"
|
||||||
|
)
|
||||||
|
|
||||||
dispatcher = HttpDispatcher(cfg, args.config)
|
dispatcher = HttpDispatcher(cfg, args.config)
|
||||||
|
|
||||||
endpoints.serverFromString(reactor, allnet_server_str).listen(server.Site(dispatcher))
|
endpoints.serverFromString(reactor, allnet_server_str).listen(
|
||||||
|
server.Site(dispatcher)
|
||||||
|
)
|
||||||
endpoints.serverFromString(reactor, adb_server_str).listen(AimedbFactory(cfg))
|
endpoints.serverFromString(reactor, adb_server_str).listen(AimedbFactory(cfg))
|
||||||
|
|
||||||
if cfg.frontend.enable:
|
if cfg.frontend.enable:
|
||||||
endpoints.serverFromString(reactor, frontend_server_str).listen(server.Site(FrontendServlet(cfg, args.config)))
|
endpoints.serverFromString(reactor, frontend_server_str).listen(
|
||||||
|
server.Site(FrontendServlet(cfg, args.config))
|
||||||
|
)
|
||||||
|
|
||||||
if cfg.billing.port > 0:
|
if cfg.billing.port > 0:
|
||||||
endpoints.serverFromString(reactor, billing_server_str).listen(server.Site(dispatcher))
|
endpoints.serverFromString(reactor, billing_server_str).listen(
|
||||||
|
server.Site(dispatcher)
|
||||||
|
)
|
||||||
|
|
||||||
if cfg.title.port > 0:
|
if cfg.title.port > 0:
|
||||||
endpoints.serverFromString(reactor, title_server_str).listen(server.Site(dispatcher))
|
endpoints.serverFromString(reactor, title_server_str).listen(
|
||||||
|
server.Site(dispatcher)
|
||||||
|
)
|
||||||
|
|
||||||
reactor.run() # type: ignore
|
reactor.run() # type: ignore
|
||||||
@@ -3,17 +3,25 @@ import argparse
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
import importlib
|
from os import path
|
||||||
import logging, coloredlogs
|
import logging
|
||||||
|
import coloredlogs
|
||||||
|
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from core import CoreConfig
|
from core import CoreConfig, Utils
|
||||||
from core.utils import Utils
|
|
||||||
|
|
||||||
class BaseReader():
|
|
||||||
def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None:
|
class BaseReader:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config: CoreConfig,
|
||||||
|
version: int,
|
||||||
|
bin_dir: Optional[str],
|
||||||
|
opt_dir: Optional[str],
|
||||||
|
extra: Optional[str],
|
||||||
|
) -> None:
|
||||||
self.logger = logging.getLogger("reader")
|
self.logger = logging.getLogger("reader")
|
||||||
self.config = config
|
self.config = config
|
||||||
self.bin_dir = bin_dir
|
self.bin_dir = bin_dir
|
||||||
@@ -21,47 +29,47 @@ class BaseReader():
|
|||||||
self.version = version
|
self.version = version
|
||||||
self.extra = extra
|
self.extra = extra
|
||||||
|
|
||||||
|
|
||||||
def get_data_directories(self, directory: str) -> List[str]:
|
def get_data_directories(self, directory: str) -> List[str]:
|
||||||
ret: List[str] = []
|
ret: List[str] = []
|
||||||
|
|
||||||
for root, dirs, files in os.walk(directory):
|
for root, dirs, files in os.walk(directory):
|
||||||
for dir in dirs:
|
for dir in dirs:
|
||||||
if re.fullmatch("[A-Z0-9]{4,4}", dir) is not None:
|
if re.fullmatch("[A-Z0-9]{4,4}", dir) is not None:
|
||||||
ret.append(f"{root}/{dir}")
|
ret.append(f"{root}/{dir}")
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description='Import Game Information')
|
parser = argparse.ArgumentParser(description="Import Game Information")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--series',
|
"--series",
|
||||||
action='store',
|
action="store",
|
||||||
type=str,
|
type=str,
|
||||||
required=True,
|
required=True,
|
||||||
help='The game series we are importing.',
|
help="The game series we are importing.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--version',
|
"--version",
|
||||||
dest='version',
|
dest="version",
|
||||||
action='store',
|
action="store",
|
||||||
type=int,
|
type=int,
|
||||||
required=True,
|
required=True,
|
||||||
help='The game version we are importing.',
|
help="The game version we are importing.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--binfolder',
|
"--binfolder",
|
||||||
dest='bin',
|
dest="bin",
|
||||||
action='store',
|
action="store",
|
||||||
type=str,
|
type=str,
|
||||||
help='Folder containing A000 base data',
|
help="Folder containing A000 base data",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--optfolder',
|
"--optfolder",
|
||||||
dest='opt',
|
dest="opt",
|
||||||
action='store',
|
action="store",
|
||||||
type=str,
|
type=str,
|
||||||
help='Folder containing Option data folders',
|
help="Folder containing Option data folders",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--config",
|
"--config",
|
||||||
@@ -79,13 +87,16 @@ if __name__ == "__main__":
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
config = CoreConfig()
|
config = CoreConfig()
|
||||||
config.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
|
if path.exists(f"{args.config}/core.yaml"):
|
||||||
|
config.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
|
||||||
|
|
||||||
log_fmt_str = "[%(asctime)s] Reader | %(levelname)s | %(message)s"
|
log_fmt_str = "[%(asctime)s] Reader | %(levelname)s | %(message)s"
|
||||||
log_fmt = logging.Formatter(log_fmt_str)
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
logger = logging.getLogger("reader")
|
logger = logging.getLogger("reader")
|
||||||
|
|
||||||
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(config.server.log_dir, "reader"), when="d", backupCount=10)
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(config.server.log_dir, "reader"), when="d", backupCount=10
|
||||||
|
)
|
||||||
fileHandler.setFormatter(log_fmt)
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
consoleHandler = logging.StreamHandler()
|
consoleHandler = logging.StreamHandler()
|
||||||
@@ -124,7 +135,8 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
for dir, mod in titles.items():
|
for dir, mod in titles.items():
|
||||||
if args.series in mod.game_codes:
|
if args.series in mod.game_codes:
|
||||||
handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra)
|
handler = mod.reader(config, args.version,
|
||||||
|
bin_arg, opt_arg, args.extra)
|
||||||
handler.read()
|
handler.read()
|
||||||
|
|
||||||
logger.info("Done")
|
logger.info("Done")
|
||||||
@@ -15,6 +15,10 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
|||||||
+ Hatsune Miku Arcade
|
+ Hatsune Miku Arcade
|
||||||
+ All versions
|
+ All versions
|
||||||
|
|
||||||
|
+ Card Maker
|
||||||
|
+ 1.34.xx
|
||||||
|
+ 1.35.xx
|
||||||
|
|
||||||
+ Ongeki
|
+ Ongeki
|
||||||
+ All versions up to Bright Memory
|
+ All versions up to Bright Memory
|
||||||
|
|
||||||
@@ -32,5 +36,8 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
|||||||
## Setup guides
|
## Setup guides
|
||||||
Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md) and [ubuntu](docs/INSTALL_UBUNTU.md) to setup and run the server.
|
Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md) and [ubuntu](docs/INSTALL_UBUNTU.md) to setup and run the server.
|
||||||
|
|
||||||
|
## Game specific information
|
||||||
|
Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades.
|
||||||
|
|
||||||
## Production guide
|
## Production guide
|
||||||
See the [production guide](docs/prod.md) for running a production server.
|
See the [production guide](docs/prod.md) for running a production server.
|
||||||
@@ -6,13 +6,5 @@ from titles.chuni.read import ChuniReader
|
|||||||
index = ChuniServlet
|
index = ChuniServlet
|
||||||
database = ChuniData
|
database = ChuniData
|
||||||
reader = ChuniReader
|
reader = ChuniReader
|
||||||
|
|
||||||
use_default_title = True
|
|
||||||
include_protocol = True
|
|
||||||
title_secure = False
|
|
||||||
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW]
|
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW]
|
||||||
trailing_slash = True
|
|
||||||
use_default_host = False
|
|
||||||
host = ""
|
|
||||||
|
|
||||||
current_schema_version = 1
|
current_schema_version = 1
|
||||||
+2
-1
@@ -5,12 +5,13 @@ from titles.chuni.base import ChuniBase
|
|||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
|
||||||
|
|
||||||
class ChuniAir(ChuniBase):
|
class ChuniAir(ChuniBase):
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_AIR
|
self.version = ChuniConstants.VER_CHUNITHM_AIR
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["dataVersion"] = "1.10.00"
|
ret["gameSetting"]["dataVersion"] = "1.10.00"
|
||||||
return ret
|
return ret
|
||||||
@@ -5,12 +5,13 @@ from titles.chuni.base import ChuniBase
|
|||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
|
||||||
|
|
||||||
class ChuniAirPlus(ChuniBase):
|
class ChuniAirPlus(ChuniBase):
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_AIR_PLUS
|
self.version = ChuniConstants.VER_CHUNITHM_AIR_PLUS
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["dataVersion"] = "1.15.00"
|
ret["gameSetting"]["dataVersion"] = "1.15.00"
|
||||||
return ret
|
return ret
|
||||||
@@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase
|
|||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
|
||||||
|
|
||||||
class ChuniAmazon(ChuniBase):
|
class ChuniAmazon(ChuniBase):
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_AMAZON
|
self.version = ChuniConstants.VER_CHUNITHM_AMAZON
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["dataVersion"] = "1.30.00"
|
ret["gameSetting"]["dataVersion"] = "1.30.00"
|
||||||
return ret
|
return ret
|
||||||
@@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase
|
|||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
|
||||||
|
|
||||||
class ChuniAmazonPlus(ChuniBase):
|
class ChuniAmazonPlus(ChuniBase):
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
|
self.version = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["dataVersion"] = "1.35.00"
|
ret["gameSetting"]["dataVersion"] = "1.35.00"
|
||||||
return ret
|
return ret
|
||||||
+142
-105
@@ -11,7 +11,8 @@ from titles.chuni.const import ChuniConstants
|
|||||||
from titles.chuni.database import ChuniData
|
from titles.chuni.database import ChuniData
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
|
||||||
class ChuniBase():
|
|
||||||
|
class ChuniBase:
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
self.core_cfg = core_cfg
|
self.core_cfg = core_cfg
|
||||||
self.game_cfg = game_cfg
|
self.game_cfg = game_cfg
|
||||||
@@ -22,36 +23,46 @@ class ChuniBase():
|
|||||||
self.version = ChuniConstants.VER_CHUNITHM
|
self.version = ChuniConstants.VER_CHUNITHM
|
||||||
|
|
||||||
def handle_game_login_api_request(self, data: Dict) -> Dict:
|
def handle_game_login_api_request(self, data: Dict) -> Dict:
|
||||||
#self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]})
|
# self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]})
|
||||||
return { "returnCode": 1 }
|
return {"returnCode": 1}
|
||||||
|
|
||||||
def handle_game_logout_api_request(self, data: Dict) -> Dict:
|
def handle_game_logout_api_request(self, data: Dict) -> Dict:
|
||||||
#self.data.base.log_event("chuni", "logout", logging.INFO, {"version": self.version, "user": data["userId"]})
|
# self.data.base.log_event("chuni", "logout", logging.INFO, {"version": self.version, "user": data["userId"]})
|
||||||
return { "returnCode": 1 }
|
return {"returnCode": 1}
|
||||||
|
|
||||||
def handle_get_game_charge_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_charge_api_request(self, data: Dict) -> Dict:
|
||||||
game_charge_list = self.data.static.get_enabled_charges(self.version)
|
game_charge_list = self.data.static.get_enabled_charges(self.version)
|
||||||
|
|
||||||
|
if game_charge_list is None or len(game_charge_list) == 0:
|
||||||
|
return {"length": 0, "gameChargeList": []}
|
||||||
|
|
||||||
charges = []
|
charges = []
|
||||||
for x in range(len(game_charge_list)):
|
for x in range(len(game_charge_list)):
|
||||||
charges.append({
|
charges.append(
|
||||||
"orderId": x,
|
{
|
||||||
"chargeId": game_charge_list[x]["chargeId"],
|
"orderId": x,
|
||||||
"price": 1,
|
"chargeId": game_charge_list[x]["chargeId"],
|
||||||
"startDate": "2017-12-05 07:00:00.0",
|
"price": 1,
|
||||||
"endDate": "2099-12-31 00:00:00.0",
|
"startDate": "2017-12-05 07:00:00.0",
|
||||||
"salePrice": 1,
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
"saleStartDate": "2017-12-05 07:00:00.0",
|
"salePrice": 1,
|
||||||
"saleEndDate": "2099-12-31 00:00:00.0"
|
"saleStartDate": "2017-12-05 07:00:00.0",
|
||||||
})
|
"saleEndDate": "2099-12-31 00:00:00.0",
|
||||||
return {
|
}
|
||||||
"length": len(charges),
|
)
|
||||||
"gameChargeList": charges
|
return {"length": len(charges), "gameChargeList": charges}
|
||||||
}
|
|
||||||
|
|
||||||
def handle_get_game_event_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_event_api_request(self, data: Dict) -> Dict:
|
||||||
game_events = self.data.static.get_enabled_events(self.version)
|
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?")
|
||||||
|
return {
|
||||||
|
"type": data["type"],
|
||||||
|
"length": 0,
|
||||||
|
"gameEventList": [],
|
||||||
|
}
|
||||||
|
|
||||||
event_list = []
|
event_list = []
|
||||||
for evt_row in game_events:
|
for evt_row in game_events:
|
||||||
tmp = {}
|
tmp = {}
|
||||||
@@ -64,24 +75,28 @@ class ChuniBase():
|
|||||||
return {
|
return {
|
||||||
"type": data["type"],
|
"type": data["type"],
|
||||||
"length": len(event_list),
|
"length": len(event_list),
|
||||||
"gameEventList": event_list
|
"gameEventList": event_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_game_idlist_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_idlist_api_request(self, data: Dict) -> Dict:
|
||||||
return { "type": data["type"], "length": 0, "gameIdlistList": [] }
|
return {"type": data["type"], "length": 0, "gameIdlistList": []}
|
||||||
|
|
||||||
def handle_get_game_message_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_message_api_request(self, data: Dict) -> Dict:
|
||||||
return { "type": data["type"], "length": "0", "gameMessageList": [] }
|
return {"type": data["type"], "length": "0", "gameMessageList": []}
|
||||||
|
|
||||||
def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
|
||||||
return { "type": data["type"], "gameRankingList": [] }
|
return {"type": data["type"], "gameRankingList": []}
|
||||||
|
|
||||||
def handle_get_game_sale_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_sale_api_request(self, data: Dict) -> Dict:
|
||||||
return { "type": data["type"], "length": 0, "gameSaleList": [] }
|
return {"type": data["type"], "length": 0, "gameSaleList": []}
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
reboot_start = datetime.strftime(datetime.now() - timedelta(hours=4), self.date_time_format)
|
reboot_start = datetime.strftime(
|
||||||
reboot_end = datetime.strftime(datetime.now() - timedelta(hours=3), self.date_time_format)
|
datetime.now() - timedelta(hours=4), self.date_time_format
|
||||||
|
)
|
||||||
|
reboot_end = datetime.strftime(
|
||||||
|
datetime.now() - timedelta(hours=3), self.date_time_format
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"gameSetting": {
|
"gameSetting": {
|
||||||
"dataVersion": "1.00.00",
|
"dataVersion": "1.00.00",
|
||||||
@@ -94,12 +109,14 @@ class ChuniBase():
|
|||||||
"maxCountItem": 300,
|
"maxCountItem": 300,
|
||||||
"maxCountMusic": 300,
|
"maxCountMusic": 300,
|
||||||
},
|
},
|
||||||
"isDumpUpload": "false",
|
"isDumpUpload": "false",
|
||||||
"isAou": "false",
|
"isAou": "false",
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_activity_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_activity_api_request(self, data: Dict) -> Dict:
|
||||||
user_activity_list = self.data.profile.get_profile_activity(data["userId"], data["kind"])
|
user_activity_list = self.data.profile.get_profile_activity(
|
||||||
|
data["userId"], data["kind"]
|
||||||
|
)
|
||||||
|
|
||||||
activity_list = []
|
activity_list = []
|
||||||
|
|
||||||
@@ -111,15 +128,16 @@ class ChuniBase():
|
|||||||
activity_list.append(tmp)
|
activity_list.append(tmp)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(activity_list),
|
"length": len(activity_list),
|
||||||
"kind": data["kind"],
|
"kind": data["kind"],
|
||||||
"userActivityList": activity_list
|
"userActivityList": activity_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_character_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||||
characters = self.data.item.get_characters(data["userId"])
|
characters = self.data.item.get_characters(data["userId"])
|
||||||
if characters is None: return {}
|
if characters is None:
|
||||||
|
return {}
|
||||||
next_idx = -1
|
next_idx = -1
|
||||||
|
|
||||||
characterList = []
|
characterList = []
|
||||||
@@ -132,14 +150,16 @@ class ChuniBase():
|
|||||||
if len(characterList) >= int(data["maxCount"]):
|
if len(characterList) >= int(data["maxCount"]):
|
||||||
break
|
break
|
||||||
|
|
||||||
if len(characterList) >= int(data["maxCount"]) and len(characters) > int(data["maxCount"]) + int(data["nextIndex"]):
|
if len(characterList) >= int(data["maxCount"]) and len(characters) > int(
|
||||||
|
data["maxCount"]
|
||||||
|
) + int(data["nextIndex"]):
|
||||||
next_idx = int(data["maxCount"]) + int(data["nextIndex"]) + 1
|
next_idx = int(data["maxCount"]) + int(data["nextIndex"]) + 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(characterList),
|
"length": len(characterList),
|
||||||
"nextIndex": next_idx,
|
"nextIndex": next_idx,
|
||||||
"userCharacterList": characterList
|
"userCharacterList": characterList,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
|
||||||
@@ -155,7 +175,7 @@ class ChuniBase():
|
|||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(charge_list),
|
"length": len(charge_list),
|
||||||
"userChargeList": charge_list
|
"userChargeList": charge_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_course_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_course_api_request(self, data: Dict) -> Dict:
|
||||||
@@ -165,7 +185,7 @@ class ChuniBase():
|
|||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"nextIndex": -1,
|
"nextIndex": -1,
|
||||||
"userCourseList": []
|
"userCourseList": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
course_list = []
|
course_list = []
|
||||||
@@ -190,40 +210,37 @@ class ChuniBase():
|
|||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(course_list),
|
"length": len(course_list),
|
||||||
"nextIndex": next_idx,
|
"nextIndex": next_idx,
|
||||||
"userCourseList": course_list
|
"userCourseList": course_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_data_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||||
p = self.data.profile.get_profile_data(data["userId"], self.version)
|
p = self.data.profile.get_profile_data(data["userId"], self.version)
|
||||||
if p is None: return {}
|
if p is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
profile = p._asdict()
|
profile = p._asdict()
|
||||||
profile.pop("id")
|
profile.pop("id")
|
||||||
profile.pop("user")
|
profile.pop("user")
|
||||||
profile.pop("version")
|
profile.pop("version")
|
||||||
|
|
||||||
return {
|
return {"userId": data["userId"], "userData": profile}
|
||||||
"userId": data["userId"],
|
|
||||||
"userData": profile
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict:
|
||||||
p = self.data.profile.get_profile_data_ex(data["userId"], self.version)
|
p = self.data.profile.get_profile_data_ex(data["userId"], self.version)
|
||||||
if p is None: return {}
|
if p is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
profile = p._asdict()
|
profile = p._asdict()
|
||||||
profile.pop("id")
|
profile.pop("id")
|
||||||
profile.pop("user")
|
profile.pop("user")
|
||||||
profile.pop("version")
|
profile.pop("version")
|
||||||
|
|
||||||
return {
|
return {"userId": data["userId"], "userDataEx": profile}
|
||||||
"userId": data["userId"],
|
|
||||||
"userDataEx": profile
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_get_user_duel_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_duel_api_request(self, data: Dict) -> Dict:
|
||||||
user_duel_list = self.data.item.get_duels(data["userId"])
|
user_duel_list = self.data.item.get_duels(data["userId"])
|
||||||
if user_duel_list is None: return {}
|
if user_duel_list is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
duel_list = []
|
duel_list = []
|
||||||
for duel in user_duel_list:
|
for duel in user_duel_list:
|
||||||
@@ -235,7 +252,7 @@ class ChuniBase():
|
|||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(duel_list),
|
"length": len(duel_list),
|
||||||
"userDuelList": duel_list
|
"userDuelList": duel_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
|
||||||
@@ -244,7 +261,7 @@ class ChuniBase():
|
|||||||
"length": 0,
|
"length": 0,
|
||||||
"kind": data["kind"],
|
"kind": data["kind"],
|
||||||
"nextIndex": -1,
|
"nextIndex": -1,
|
||||||
"userFavoriteItemList": []
|
"userFavoriteItemList": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict:
|
||||||
@@ -252,11 +269,7 @@ class ChuniBase():
|
|||||||
This is handled via the webui, which we don't have right now
|
This is handled via the webui, which we don't have right now
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return {
|
return {"userId": data["userId"], "length": 0, "userFavoriteMusicList": []}
|
||||||
"userId": data["userId"],
|
|
||||||
"length": 0,
|
|
||||||
"userFavoriteMusicList": []
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_get_user_item_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||||
kind = int(int(data["nextIndex"]) / 10000000000)
|
kind = int(int(data["nextIndex"]) / 10000000000)
|
||||||
@@ -264,7 +277,12 @@ class ChuniBase():
|
|||||||
user_item_list = self.data.item.get_items(data["userId"], kind)
|
user_item_list = self.data.item.get_items(data["userId"], kind)
|
||||||
|
|
||||||
if user_item_list is None or len(user_item_list) == 0:
|
if user_item_list is None or len(user_item_list) == 0:
|
||||||
return {"userId": data["userId"], "nextIndex": -1, "itemKind": kind, "userItemList": []}
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"nextIndex": -1,
|
||||||
|
"itemKind": kind,
|
||||||
|
"userItemList": [],
|
||||||
|
}
|
||||||
|
|
||||||
items: list[Dict[str, Any]] = []
|
items: list[Dict[str, Any]] = []
|
||||||
for i in range(next_idx, len(user_item_list)):
|
for i in range(next_idx, len(user_item_list)):
|
||||||
@@ -277,10 +295,18 @@ class ChuniBase():
|
|||||||
|
|
||||||
xout = kind * 10000000000 + next_idx + len(items)
|
xout = kind * 10000000000 + next_idx + len(items)
|
||||||
|
|
||||||
if len(items) < int(data["maxCount"]): nextIndex = 0
|
if len(items) < int(data["maxCount"]):
|
||||||
else: nextIndex = xout
|
nextIndex = 0
|
||||||
|
else:
|
||||||
|
nextIndex = xout
|
||||||
|
|
||||||
return {"userId": data["userId"], "nextIndex": nextIndex, "itemKind": kind, "length": len(items), "userItemList": items}
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"nextIndex": nextIndex,
|
||||||
|
"itemKind": kind,
|
||||||
|
"length": len(items),
|
||||||
|
"userItemList": items,
|
||||||
|
}
|
||||||
|
|
||||||
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
@@ -291,23 +317,24 @@ class ChuniBase():
|
|||||||
"length": 2,
|
"length": 2,
|
||||||
"userLoginBonusList": [
|
"userLoginBonusList": [
|
||||||
{
|
{
|
||||||
"presetId": '10',
|
"presetId": "10",
|
||||||
"bonusCount": '0',
|
"bonusCount": "0",
|
||||||
"lastUpdateDate": "1970-01-01 09:00:00",
|
"lastUpdateDate": "1970-01-01 09:00:00",
|
||||||
"isWatched": "true"
|
"isWatched": "true",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"presetId": '20',
|
"presetId": "20",
|
||||||
"bonusCount": '0',
|
"bonusCount": "0",
|
||||||
"lastUpdateDate": "1970-01-01 09:00:00",
|
"lastUpdateDate": "1970-01-01 09:00:00",
|
||||||
"isWatched": "true"
|
"isWatched": "true",
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
|
||||||
user_map_list = self.data.item.get_maps(data["userId"])
|
user_map_list = self.data.item.get_maps(data["userId"])
|
||||||
if user_map_list is None: return {}
|
if user_map_list is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
map_list = []
|
map_list = []
|
||||||
for map in user_map_list:
|
for map in user_map_list:
|
||||||
@@ -319,7 +346,7 @@ class ChuniBase():
|
|||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(map_list),
|
"length": len(map_list),
|
||||||
"userMapList": map_list
|
"userMapList": map_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_music_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_music_api_request(self, data: Dict) -> Dict:
|
||||||
@@ -329,7 +356,7 @@ class ChuniBase():
|
|||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"nextIndex": -1,
|
"nextIndex": -1,
|
||||||
"userMusicList": [] #240
|
"userMusicList": [], # 240
|
||||||
}
|
}
|
||||||
song_list = []
|
song_list = []
|
||||||
next_idx = int(data["nextIndex"])
|
next_idx = int(data["nextIndex"])
|
||||||
@@ -348,10 +375,7 @@ class ChuniBase():
|
|||||||
song["length"] = len(song["userMusicDetailList"])
|
song["length"] = len(song["userMusicDetailList"])
|
||||||
|
|
||||||
if not found:
|
if not found:
|
||||||
song_list.append({
|
song_list.append({"length": 1, "userMusicDetailList": [tmp]})
|
||||||
"length": 1,
|
|
||||||
"userMusicDetailList": [tmp]
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(song_list) >= max_ct:
|
if len(song_list) >= max_ct:
|
||||||
break
|
break
|
||||||
@@ -365,7 +389,7 @@ class ChuniBase():
|
|||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(song_list),
|
"length": len(song_list),
|
||||||
"nextIndex": next_idx,
|
"nextIndex": next_idx,
|
||||||
"userMusicList": song_list #240
|
"userMusicList": song_list, # 240
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_option_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_option_api_request(self, data: Dict) -> Dict:
|
||||||
@@ -375,10 +399,7 @@ class ChuniBase():
|
|||||||
option.pop("id")
|
option.pop("id")
|
||||||
option.pop("user")
|
option.pop("user")
|
||||||
|
|
||||||
return {
|
return {"userId": data["userId"], "userGameOption": option}
|
||||||
"userId": data["userId"],
|
|
||||||
"userGameOption": option
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict:
|
||||||
p = self.data.profile.get_profile_option_ex(data["userId"])
|
p = self.data.profile.get_profile_option_ex(data["userId"])
|
||||||
@@ -387,18 +408,18 @@ class ChuniBase():
|
|||||||
option.pop("id")
|
option.pop("id")
|
||||||
option.pop("user")
|
option.pop("user")
|
||||||
|
|
||||||
return {
|
return {"userId": data["userId"], "userGameOptionEx": option}
|
||||||
"userId": data["userId"],
|
|
||||||
"userGameOptionEx": option
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_wtf8(self, src):
|
def read_wtf8(self, src):
|
||||||
return bytes([ord(c) for c in src]).decode("utf-8")
|
return bytes([ord(c) for c in src]).decode("utf-8")
|
||||||
|
|
||||||
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
profile = self.data.profile.get_profile_preview(data["userId"], self.version)
|
profile = self.data.profile.get_profile_preview(data["userId"], self.version)
|
||||||
if profile is None: return None
|
if profile is None:
|
||||||
profile_character = self.data.item.get_character(data["userId"], profile["characterId"])
|
return None
|
||||||
|
profile_character = self.data.item.get_character(
|
||||||
|
data["userId"], profile["characterId"]
|
||||||
|
)
|
||||||
|
|
||||||
if profile_character is None:
|
if profile_character is None:
|
||||||
chara = {}
|
chara = {}
|
||||||
@@ -459,10 +480,7 @@ class ChuniBase():
|
|||||||
|
|
||||||
def handle_get_user_team_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_team_api_request(self, data: Dict) -> Dict:
|
||||||
# TODO: Team
|
# TODO: Team
|
||||||
return {
|
return {"userId": data["userId"], "teamId": 0}
|
||||||
"userId": data["userId"],
|
|
||||||
"teamId": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict:
|
||||||
return {
|
return {
|
||||||
@@ -486,18 +504,29 @@ class ChuniBase():
|
|||||||
|
|
||||||
if "userData" in upsert:
|
if "userData" in upsert:
|
||||||
try:
|
try:
|
||||||
upsert["userData"][0]["userName"] = self.read_wtf8(upsert["userData"][0]["userName"])
|
upsert["userData"][0]["userName"] = self.read_wtf8(
|
||||||
except: pass
|
upsert["userData"][0]["userName"]
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
self.data.profile.put_profile_data(user_id, self.version, upsert["userData"][0])
|
self.data.profile.put_profile_data(
|
||||||
|
user_id, self.version, upsert["userData"][0]
|
||||||
|
)
|
||||||
if "userDataEx" in upsert:
|
if "userDataEx" in upsert:
|
||||||
self.data.profile.put_profile_data_ex(user_id, self.version, upsert["userDataEx"][0])
|
self.data.profile.put_profile_data_ex(
|
||||||
|
user_id, self.version, upsert["userDataEx"][0]
|
||||||
|
)
|
||||||
if "userGameOption" in upsert:
|
if "userGameOption" in upsert:
|
||||||
self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0])
|
self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0])
|
||||||
if "userGameOptionEx" in upsert:
|
if "userGameOptionEx" in upsert:
|
||||||
self.data.profile.put_profile_option_ex(user_id, upsert["userGameOptionEx"][0])
|
self.data.profile.put_profile_option_ex(
|
||||||
|
user_id, upsert["userGameOptionEx"][0]
|
||||||
|
)
|
||||||
if "userRecentRatingList" in upsert:
|
if "userRecentRatingList" in upsert:
|
||||||
self.data.profile.put_profile_recent_rating(user_id, upsert["userRecentRatingList"])
|
self.data.profile.put_profile_recent_rating(
|
||||||
|
user_id, upsert["userRecentRatingList"]
|
||||||
|
)
|
||||||
|
|
||||||
if "userCharacterList" in upsert:
|
if "userCharacterList" in upsert:
|
||||||
for character in upsert["userCharacterList"]:
|
for character in upsert["userCharacterList"]:
|
||||||
@@ -551,22 +580,30 @@ class ChuniBase():
|
|||||||
for emoney in upsert["userEmoneyList"]:
|
for emoney in upsert["userEmoneyList"]:
|
||||||
self.data.profile.put_profile_emoney(user_id, emoney)
|
self.data.profile.put_profile_emoney(user_id, emoney)
|
||||||
|
|
||||||
return { "returnCode": "1" }
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
|
||||||
return { "returnCode": "1" }
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
||||||
return { "returnCode": "1" }
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict:
|
||||||
return { "returnCode": "1" }
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_client_error_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_client_error_api_request(self, data: Dict) -> Dict:
|
||||||
return { "returnCode": "1" }
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
|
||||||
return { "returnCode": "1" }
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict:
|
||||||
return { "returnCode": "1" }
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
|
def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict:
|
||||||
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"userNetBattleData": {
|
||||||
|
"recentNBSelectMusicList": []
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-6
@@ -1,19 +1,27 @@
|
|||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
class ChuniServerConfig():
|
|
||||||
|
class ChuniServerConfig:
|
||||||
def __init__(self, parent_config: "ChuniConfig") -> None:
|
def __init__(self, parent_config: "ChuniConfig") -> None:
|
||||||
self.__config = parent_config
|
self.__config = parent_config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enable(self) -> bool:
|
def enable(self) -> bool:
|
||||||
return CoreConfig.get_config_field(self.__config, 'chuni', 'server', 'enable', default=True)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "server", "enable", default=True
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loglevel(self) -> int:
|
def loglevel(self) -> int:
|
||||||
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'chuni', 'server', 'loglevel', default="info"))
|
return CoreConfig.str_to_loglevel(
|
||||||
|
CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "server", "loglevel", default="info"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
class ChuniCryptoConfig():
|
|
||||||
|
class ChuniCryptoConfig:
|
||||||
def __init__(self, parent_config: "ChuniConfig") -> None:
|
def __init__(self, parent_config: "ChuniConfig") -> None:
|
||||||
self.__config = parent_config
|
self.__config = parent_config
|
||||||
|
|
||||||
@@ -24,11 +32,16 @@ class ChuniCryptoConfig():
|
|||||||
internal_version: [key, iv]
|
internal_version: [key, iv]
|
||||||
all values are hex strings
|
all values are hex strings
|
||||||
"""
|
"""
|
||||||
return CoreConfig.get_config_field(self.__config, 'chuni', 'crypto', 'keys', default={})
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "crypto", "keys", default={}
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def encrypted_only(self) -> bool:
|
def encrypted_only(self) -> bool:
|
||||||
return CoreConfig.get_config_field(self.__config, 'chuni', 'crypto', 'encrypted_only', default=False)
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "crypto", "encrypted_only", default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChuniConfig(dict):
|
class ChuniConfig(dict):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|||||||
+18
-3
@@ -1,7 +1,9 @@
|
|||||||
class ChuniConstants():
|
class ChuniConstants:
|
||||||
GAME_CODE = "SDBT"
|
GAME_CODE = "SDBT"
|
||||||
GAME_CODE_NEW = "SDHD"
|
GAME_CODE_NEW = "SDHD"
|
||||||
|
|
||||||
|
CONFIG_NAME = "chuni.yaml"
|
||||||
|
|
||||||
VER_CHUNITHM = 0
|
VER_CHUNITHM = 0
|
||||||
VER_CHUNITHM_PLUS = 1
|
VER_CHUNITHM_PLUS = 1
|
||||||
VER_CHUNITHM_AIR = 2
|
VER_CHUNITHM_AIR = 2
|
||||||
@@ -16,8 +18,21 @@ class ChuniConstants():
|
|||||||
VER_CHUNITHM_NEW = 11
|
VER_CHUNITHM_NEW = 11
|
||||||
VER_CHUNITHM_NEW_PLUS = 12
|
VER_CHUNITHM_NEW_PLUS = 12
|
||||||
|
|
||||||
VERSION_NAMES = ["Chunithm", "Chunithm+", "Chunithm Air", "Chunithm Air+", "Chunithm Star", "Chunithm Star+", "Chunithm Amazon",
|
VERSION_NAMES = [
|
||||||
"Chunithm Amazon+", "Chunithm Crystal", "Chunithm Crystal+", "Chunithm Paradise", "Chunithm New!!", "Chunithm New!!+"]
|
"Chunithm",
|
||||||
|
"Chunithm+",
|
||||||
|
"Chunithm Air",
|
||||||
|
"Chunithm Air+",
|
||||||
|
"Chunithm Star",
|
||||||
|
"Chunithm Star+",
|
||||||
|
"Chunithm Amazon",
|
||||||
|
"Chunithm Amazon+",
|
||||||
|
"Chunithm Crystal",
|
||||||
|
"Chunithm Crystal+",
|
||||||
|
"Chunithm Paradise",
|
||||||
|
"Chunithm New!!",
|
||||||
|
"Chunithm New!!+",
|
||||||
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def game_ver_to_string(cls, ver: int):
|
def game_ver_to_string(cls, ver: int):
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase
|
|||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
|
||||||
|
|
||||||
class ChuniCrystal(ChuniBase):
|
class ChuniCrystal(ChuniBase):
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL
|
self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["dataVersion"] = "1.40.00"
|
ret["gameSetting"]["dataVersion"] = "1.40.00"
|
||||||
return ret
|
return ret
|
||||||
@@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase
|
|||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
|
||||||
|
|
||||||
class ChuniCrystalPlus(ChuniBase):
|
class ChuniCrystalPlus(ChuniBase):
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["dataVersion"] = "1.45.00"
|
ret["gameSetting"]["dataVersion"] = "1.45.00"
|
||||||
return ret
|
return ret
|
||||||
@@ -2,6 +2,7 @@ from core.data import Data
|
|||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from titles.chuni.schema import *
|
from titles.chuni.schema import *
|
||||||
|
|
||||||
|
|
||||||
class ChuniData(Data):
|
class ChuniData(Data):
|
||||||
def __init__(self, cfg: CoreConfig) -> None:
|
def __init__(self, cfg: CoreConfig) -> None:
|
||||||
super().__init__(cfg)
|
super().__init__(cfg)
|
||||||
|
|||||||
+134
-55
@@ -8,8 +8,12 @@ import inflection
|
|||||||
import string
|
import string
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
from Crypto.Util.Padding import pad
|
from Crypto.Util.Padding import pad
|
||||||
|
from Crypto.Protocol.KDF import PBKDF2
|
||||||
|
from Crypto.Hash import SHA1
|
||||||
|
from os import path
|
||||||
|
from typing import Tuple, Dict
|
||||||
|
|
||||||
from core import CoreConfig
|
from core import CoreConfig, Utils
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.base import ChuniBase
|
from titles.chuni.base import ChuniBase
|
||||||
@@ -26,26 +30,31 @@ from titles.chuni.paradise import ChuniParadise
|
|||||||
from titles.chuni.new import ChuniNew
|
from titles.chuni.new import ChuniNew
|
||||||
from titles.chuni.newplus import ChuniNewPlus
|
from titles.chuni.newplus import ChuniNewPlus
|
||||||
|
|
||||||
class ChuniServlet():
|
|
||||||
|
class ChuniServlet:
|
||||||
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
self.core_cfg = core_cfg
|
self.core_cfg = core_cfg
|
||||||
self.game_cfg = ChuniConfig()
|
self.game_cfg = ChuniConfig()
|
||||||
self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/chuni.yaml")))
|
self.hash_table: Dict[Dict[str, str]] = {}
|
||||||
|
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
||||||
|
self.game_cfg.update(
|
||||||
|
yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))
|
||||||
|
)
|
||||||
|
|
||||||
self.versions = [
|
self.versions = [
|
||||||
ChuniBase(core_cfg, self.game_cfg),
|
ChuniBase,
|
||||||
ChuniPlus(core_cfg, self.game_cfg),
|
ChuniPlus,
|
||||||
ChuniAir(core_cfg, self.game_cfg),
|
ChuniAir,
|
||||||
ChuniAirPlus(core_cfg, self.game_cfg),
|
ChuniAirPlus,
|
||||||
ChuniStar(core_cfg, self.game_cfg),
|
ChuniStar,
|
||||||
ChuniStarPlus(core_cfg, self.game_cfg),
|
ChuniStarPlus,
|
||||||
ChuniAmazon(core_cfg, self.game_cfg),
|
ChuniAmazon,
|
||||||
ChuniAmazonPlus(core_cfg, self.game_cfg),
|
ChuniAmazonPlus,
|
||||||
ChuniCrystal(core_cfg, self.game_cfg),
|
ChuniCrystal,
|
||||||
ChuniCrystalPlus(core_cfg, self.game_cfg),
|
ChuniCrystalPlus,
|
||||||
ChuniParadise(core_cfg, self.game_cfg),
|
ChuniParadise,
|
||||||
ChuniNew(core_cfg, self.game_cfg),
|
ChuniNew,
|
||||||
ChuniNewPlus(core_cfg, self.game_cfg),
|
ChuniNewPlus,
|
||||||
]
|
]
|
||||||
|
|
||||||
self.logger = logging.getLogger("chuni")
|
self.logger = logging.getLogger("chuni")
|
||||||
@@ -53,8 +62,12 @@ class ChuniServlet():
|
|||||||
if not hasattr(self.logger, "inited"):
|
if not hasattr(self.logger, "inited"):
|
||||||
log_fmt_str = "[%(asctime)s] Chunithm | %(levelname)s | %(message)s"
|
log_fmt_str = "[%(asctime)s] Chunithm | %(levelname)s | %(message)s"
|
||||||
log_fmt = logging.Formatter(log_fmt_str)
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "chuni"), encoding='utf8',
|
fileHandler = TimedRotatingFileHandler(
|
||||||
when="d", backupCount=10)
|
"{0}/{1}.log".format(self.core_cfg.server.log_dir, "chuni"),
|
||||||
|
encoding="utf8",
|
||||||
|
when="d",
|
||||||
|
backupCount=10,
|
||||||
|
)
|
||||||
|
|
||||||
fileHandler.setFormatter(log_fmt)
|
fileHandler.setFormatter(log_fmt)
|
||||||
|
|
||||||
@@ -65,95 +78,161 @@ class ChuniServlet():
|
|||||||
self.logger.addHandler(consoleHandler)
|
self.logger.addHandler(consoleHandler)
|
||||||
|
|
||||||
self.logger.setLevel(self.game_cfg.server.loglevel)
|
self.logger.setLevel(self.game_cfg.server.loglevel)
|
||||||
coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str)
|
coloredlogs.install(
|
||||||
|
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||||
|
)
|
||||||
self.logger.inited = True
|
self.logger.inited = True
|
||||||
|
|
||||||
|
for version, keys in self.game_cfg.crypto.keys.items():
|
||||||
|
if len(keys) < 3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.hash_table[version] = {}
|
||||||
|
|
||||||
|
method_list = [method for method in dir(self.versions[version]) if not method.startswith('__')]
|
||||||
|
for method in method_list:
|
||||||
|
method_fixed = inflection.camelize(method)[6:-7]
|
||||||
|
hash = PBKDF2(method_fixed, bytes.fromhex(keys[2]), 128, count=44, hmac_hash_module=SHA1)
|
||||||
|
|
||||||
|
self.hash_table[version][hash.hex()] = method_fixed
|
||||||
|
|
||||||
|
self.logger.debug(f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_allnet_info(
|
||||||
|
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
||||||
|
) -> Tuple[bool, str, str]:
|
||||||
|
game_cfg = ChuniConfig()
|
||||||
|
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
||||||
|
game_cfg.update(
|
||||||
|
yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))
|
||||||
|
)
|
||||||
|
|
||||||
|
if not game_cfg.server.enable:
|
||||||
|
return (False, "", "")
|
||||||
|
|
||||||
|
if core_cfg.server.is_develop:
|
||||||
|
return (
|
||||||
|
True,
|
||||||
|
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "")
|
||||||
|
|
||||||
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
|
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
|
||||||
|
if url_path.lower() == "ping":
|
||||||
|
return zlib.compress(b'{"returnCode": "1"}')
|
||||||
|
|
||||||
req_raw = request.content.getvalue()
|
req_raw = request.content.getvalue()
|
||||||
url_split = url_path.split("/")
|
url_split = url_path.split("/")
|
||||||
encrtped = False
|
encrtped = False
|
||||||
internal_ver = 0
|
internal_ver = 0
|
||||||
endpoint = url_split[len(url_split) - 1]
|
endpoint = url_split[len(url_split) - 1]
|
||||||
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if version < 105: # 1.0
|
if version < 105: # 1.0
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM
|
internal_ver = ChuniConstants.VER_CHUNITHM
|
||||||
elif version >= 105 and version < 110: # Plus
|
elif version >= 105 and version < 110: # Plus
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_PLUS
|
||||||
elif version >= 110 and version < 115: # Air
|
elif version >= 110 and version < 115: # Air
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_AIR
|
internal_ver = ChuniConstants.VER_CHUNITHM_AIR
|
||||||
elif version >= 115 and version < 120: # Air Plus
|
elif version >= 115 and version < 120: # Air Plus
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS
|
||||||
elif version >= 120 and version < 125: # Star
|
elif version >= 120 and version < 125: # Star
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_STAR
|
internal_ver = ChuniConstants.VER_CHUNITHM_STAR
|
||||||
elif version >= 125 and version < 130: # Star Plus
|
elif version >= 125 and version < 130: # Star Plus
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS
|
||||||
elif version >= 130 and version < 135: # Amazon
|
elif version >= 130 and version < 135: # Amazon
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON
|
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON
|
||||||
elif version >= 135 and version < 140: # Amazon Plus
|
elif version >= 135 and version < 140: # Amazon Plus
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
|
||||||
elif version >= 140 and version < 145: # Crystal
|
elif version >= 140 and version < 145: # Crystal
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL
|
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL
|
||||||
elif version >= 145 and version < 150: # Crystal Plus
|
elif version >= 145 and version < 150: # Crystal Plus
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
||||||
elif version >= 150 and version < 200: # Paradise
|
elif version >= 150 and version < 200: # Paradise
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE
|
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE
|
||||||
elif version >= 200 and version < 205: # New
|
elif version >= 200 and version < 205: # New
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
||||||
elif version >= 205 and version < 210: # New Plus
|
elif version >= 205 and version < 210: # New Plus
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||||
|
|
||||||
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
||||||
# If we get a 32 character long hex string, it's a hash and we're
|
# If we get a 32 character long hex string, it's a hash and we're
|
||||||
# doing encrypted. The likelyhood of false positives is low but
|
# doing encrypted. The likelyhood of false positives is low but
|
||||||
# technically not 0
|
# technically not 0
|
||||||
endpoint = request.getHeader("User-Agent").split("#")[0]
|
if internal_ver < ChuniConstants.VER_CHUNITHM_NEW:
|
||||||
|
endpoint = request.getHeader("User-Agent").split("#")[0]
|
||||||
|
|
||||||
|
else:
|
||||||
|
if internal_ver not in self.hash_table:
|
||||||
|
self.logger.error(f"v{version} does not support encryption or no keys entered")
|
||||||
|
return zlib.compress(b'{"stat": "0"}')
|
||||||
|
|
||||||
|
elif endpoint.lower() not in self.hash_table[internal_ver]:
|
||||||
|
self.logger.error(f"No hash found for v{version} endpoint {endpoint}")
|
||||||
|
return zlib.compress(b'{"stat": "0"}')
|
||||||
|
|
||||||
|
endpoint = self.hash_table[internal_ver][endpoint.lower()]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
crypt = AES.new(
|
crypt = AES.new(
|
||||||
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]),
|
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
|
||||||
AES.MODE_CBC,
|
AES.MODE_CBC,
|
||||||
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1])
|
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
||||||
)
|
)
|
||||||
|
|
||||||
req_raw = crypt.decrypt(req_raw)
|
req_raw = crypt.decrypt(req_raw)
|
||||||
|
|
||||||
except:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to decrypt v{version} request to {endpoint} -> {req_raw}")
|
self.logger.error(
|
||||||
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
|
f"Failed to decrypt v{version} request to {endpoint} -> {e}"
|
||||||
|
)
|
||||||
|
return zlib.compress(b'{"stat": "0"}')
|
||||||
|
|
||||||
encrtped = True
|
encrtped = True
|
||||||
|
|
||||||
if not encrtped and self.game_cfg.crypto.encrypted_only:
|
if not encrtped and self.game_cfg.crypto.encrypted_only and internal_ver >= ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS:
|
||||||
self.logger.error(f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}")
|
self.logger.error(
|
||||||
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
|
f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}"
|
||||||
|
)
|
||||||
|
return zlib.compress(b'{"stat": "0"}')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
unzip = zlib.decompress(req_raw)
|
unzip = zlib.decompress(req_raw)
|
||||||
|
|
||||||
except zlib.error as e:
|
except zlib.error as e:
|
||||||
self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}")
|
self.logger.error(
|
||||||
|
f"Failed to decompress v{version} {endpoint} request -> {e}"
|
||||||
|
)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
req_data = json.loads(unzip)
|
req_data = json.loads(unzip)
|
||||||
|
|
||||||
self.logger.info(f"v{version} {endpoint} request from {request.getClientAddress().host}")
|
self.logger.info(
|
||||||
|
f"v{version} {endpoint} request from {client_ip}"
|
||||||
|
)
|
||||||
self.logger.debug(req_data)
|
self.logger.debug(req_data)
|
||||||
|
|
||||||
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
||||||
|
handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg)
|
||||||
|
|
||||||
try:
|
if not hasattr(handler_cls, func_to_find):
|
||||||
handler = getattr(self.versions[internal_ver], func_to_find)
|
self.logger.warning(f"Unhandled v{version} request {endpoint}")
|
||||||
resp = handler(req_data)
|
resp = {"returnCode": 1}
|
||||||
|
|
||||||
except AttributeError as e:
|
else:
|
||||||
self.logger.warning(f"Unhandled v{version} request {endpoint} - {e}")
|
try:
|
||||||
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
|
handler = getattr(handler_cls, func_to_find)
|
||||||
|
resp = handler(req_data)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
||||||
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
|
return zlib.compress(b'{"stat": "0"}')
|
||||||
|
|
||||||
if resp == None:
|
if resp == None:
|
||||||
resp = {'returnCode': 1}
|
resp = {"returnCode": 1}
|
||||||
|
|
||||||
self.logger.debug(f"Response {resp}")
|
self.logger.debug(f"Response {resp}")
|
||||||
|
|
||||||
@@ -165,9 +244,9 @@ class ChuniServlet():
|
|||||||
padded = pad(zipped, 16)
|
padded = pad(zipped, 16)
|
||||||
|
|
||||||
crypt = AES.new(
|
crypt = AES.new(
|
||||||
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]),
|
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
|
||||||
AES.MODE_CBC,
|
AES.MODE_CBC,
|
||||||
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1])
|
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
||||||
)
|
)
|
||||||
|
|
||||||
return crypt.encrypt(padded)
|
return crypt.encrypt(padded)
|
||||||
+375
-27
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from random import randint
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
@@ -9,13 +9,9 @@ from titles.chuni.database import ChuniData
|
|||||||
from titles.chuni.base import ChuniBase
|
from titles.chuni.base import ChuniBase
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
|
||||||
class ChuniNew(ChuniBase):
|
|
||||||
|
|
||||||
ITEM_TYPE = {
|
class ChuniNew(ChuniBase):
|
||||||
"character": 20,
|
ITEM_TYPE = {"character": 20, "story": 21, "card": 22}
|
||||||
"story": 21,
|
|
||||||
"card": 22
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
self.core_cfg = core_cfg
|
self.core_cfg = core_cfg
|
||||||
@@ -27,10 +23,18 @@ class ChuniNew(ChuniBase):
|
|||||||
self.version = ChuniConstants.VER_CHUNITHM_NEW
|
self.version = ChuniConstants.VER_CHUNITHM_NEW
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
match_start = datetime.strftime(datetime.now() - timedelta(hours=10), self.date_time_format)
|
match_start = datetime.strftime(
|
||||||
match_end = datetime.strftime(datetime.now() + timedelta(hours=10), self.date_time_format)
|
datetime.now() - timedelta(hours=10), self.date_time_format
|
||||||
reboot_start = datetime.strftime(datetime.now() - timedelta(hours=11), self.date_time_format)
|
)
|
||||||
reboot_end = datetime.strftime(datetime.now() - timedelta(hours=10), self.date_time_format)
|
match_end = datetime.strftime(
|
||||||
|
datetime.now() + timedelta(hours=10), self.date_time_format
|
||||||
|
)
|
||||||
|
reboot_start = datetime.strftime(
|
||||||
|
datetime.now() - timedelta(hours=11), self.date_time_format
|
||||||
|
)
|
||||||
|
reboot_end = datetime.strftime(
|
||||||
|
datetime.now() - timedelta(hours=10), self.date_time_format
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"gameSetting": {
|
"gameSetting": {
|
||||||
"isMaintenance": "false",
|
"isMaintenance": "false",
|
||||||
@@ -52,15 +56,18 @@ class ChuniNew(ChuniBase):
|
|||||||
"udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||||
"reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||||
},
|
},
|
||||||
"isDumpUpload": "false",
|
"isDumpUpload": "false",
|
||||||
"isAou": "false",
|
"isAou": "false",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def handle_remove_token_api_request(self, data: Dict) -> Dict:
|
||||||
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_delete_token_api_request(self, data: Dict) -> Dict:
|
def handle_delete_token_api_request(self, data: Dict) -> Dict:
|
||||||
return { "returnCode": "1" }
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_create_token_api_request(self, data: Dict) -> Dict:
|
def handle_create_token_api_request(self, data: Dict) -> Dict:
|
||||||
return { "returnCode": "1" }
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_get_user_map_area_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_map_area_api_request(self, data: Dict) -> Dict:
|
||||||
user_map_areas = self.data.item.get_map_areas(data["userId"])
|
user_map_areas = self.data.item.get_map_areas(data["userId"])
|
||||||
@@ -72,21 +79,18 @@ class ChuniNew(ChuniBase):
|
|||||||
tmp.pop("user")
|
tmp.pop("user")
|
||||||
map_areas.append(tmp)
|
map_areas.append(tmp)
|
||||||
|
|
||||||
return {
|
return {"userId": data["userId"], "userMapAreaList": map_areas}
|
||||||
"userId": data["userId"],
|
|
||||||
"userMapAreaList": map_areas
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict:
|
||||||
return {
|
return {"userId": data["userId"], "symbolCharInfoList": []}
|
||||||
"userId": data["userId"],
|
|
||||||
"symbolCharInfoList": []
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
profile = self.data.profile.get_profile_preview(data["userId"], self.version)
|
profile = self.data.profile.get_profile_preview(data["userId"], self.version)
|
||||||
if profile is None: return None
|
if profile is None:
|
||||||
profile_character = self.data.item.get_character(data["userId"], profile["characterId"])
|
return None
|
||||||
|
profile_character = self.data.item.get_character(
|
||||||
|
data["userId"], profile["characterId"]
|
||||||
|
)
|
||||||
|
|
||||||
if profile_character is None:
|
if profile_character is None:
|
||||||
chara = {}
|
chara = {}
|
||||||
@@ -118,11 +122,355 @@ class ChuniNew(ChuniBase):
|
|||||||
"playerLevel": profile["playerLevel"],
|
"playerLevel": profile["playerLevel"],
|
||||||
"rating": profile["rating"],
|
"rating": profile["rating"],
|
||||||
"headphone": profile["headphone"],
|
"headphone": profile["headphone"],
|
||||||
"chargeState": 0,
|
# Enables favorites and teams
|
||||||
"userNameEx": "0",
|
"chargeState": 1,
|
||||||
|
"userNameEx": "",
|
||||||
"banState": 0,
|
"banState": 0,
|
||||||
"classEmblemMedal": profile["classEmblemMedal"],
|
"classEmblemMedal": profile["classEmblemMedal"],
|
||||||
"classEmblemBase": profile["classEmblemBase"],
|
"classEmblemBase": profile["classEmblemBase"],
|
||||||
"battleRankId": profile["battleRankId"],
|
"battleRankId": profile["battleRankId"],
|
||||||
}
|
}
|
||||||
return data1
|
return data1
|
||||||
|
|
||||||
|
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
|
p = self.data.profile.get_profile_data(data["userId"], self.version)
|
||||||
|
if p is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"userName": p["userName"],
|
||||||
|
"level": p["level"],
|
||||||
|
"medal": p["medal"],
|
||||||
|
"lastDataVersion": "2.00.00",
|
||||||
|
"isLogin": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_printer_login_api_request(self, data: Dict) -> Dict:
|
||||||
|
return {"returnCode": 1}
|
||||||
|
|
||||||
|
def handle_printer_logout_api_request(self, data: Dict) -> Dict:
|
||||||
|
return {"returnCode": 1}
|
||||||
|
|
||||||
|
def handle_get_game_gacha_api_request(self, data: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
returns all current active banners (gachas)
|
||||||
|
"""
|
||||||
|
game_gachas = self.data.static.get_gachas(self.version)
|
||||||
|
|
||||||
|
# clean the database rows
|
||||||
|
game_gacha_list = []
|
||||||
|
for gacha in game_gachas:
|
||||||
|
tmp = gacha._asdict()
|
||||||
|
tmp.pop("id")
|
||||||
|
tmp.pop("version")
|
||||||
|
tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S")
|
||||||
|
tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S")
|
||||||
|
tmp["noticeStartDate"] = datetime.strftime(
|
||||||
|
tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
tmp["noticeEndDate"] = datetime.strftime(
|
||||||
|
tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
|
||||||
|
game_gacha_list.append(tmp)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"length": len(game_gacha_list),
|
||||||
|
"gameGachaList": game_gacha_list,
|
||||||
|
# no clue
|
||||||
|
"registIdList": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
returns all valid cards for a given gachaId
|
||||||
|
"""
|
||||||
|
game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"])
|
||||||
|
|
||||||
|
game_gacha_card_list = []
|
||||||
|
for gacha_card in game_gacha_cards:
|
||||||
|
tmp = gacha_card._asdict()
|
||||||
|
tmp.pop("id")
|
||||||
|
game_gacha_card_list.append(tmp)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"gachaId": data["gachaId"],
|
||||||
|
"length": len(game_gacha_card_list),
|
||||||
|
# check isPickup from the chuni_static_gachas?
|
||||||
|
"isPickup": False,
|
||||||
|
"gameGachaCardList": game_gacha_card_list,
|
||||||
|
# again no clue
|
||||||
|
"emissionList": [],
|
||||||
|
"afterCalcList": [],
|
||||||
|
"ssrBookCalcList": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||||
|
p = self.data.profile.get_profile_data(data["userId"], self.version)
|
||||||
|
if p is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
profile = p._asdict()
|
||||||
|
profile.pop("id")
|
||||||
|
profile.pop("user")
|
||||||
|
profile.pop("version")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"userData": profile,
|
||||||
|
"userEmoney": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"emoneyCredit": 100,
|
||||||
|
"emoneyBrand": 1,
|
||||||
|
"ext1": 0,
|
||||||
|
"ext2": 0,
|
||||||
|
"ext3": 0,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_get_user_gacha_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_gachas = self.data.item.get_user_gachas(data["userId"])
|
||||||
|
if user_gachas is None:
|
||||||
|
return {"userId": data["userId"], "length": 0, "userGachaList": []}
|
||||||
|
|
||||||
|
user_gacha_list = []
|
||||||
|
for gacha in user_gachas:
|
||||||
|
tmp = gacha._asdict()
|
||||||
|
tmp.pop("id")
|
||||||
|
tmp.pop("user")
|
||||||
|
tmp["dailyGachaDate"] = datetime.strftime(tmp["dailyGachaDate"], "%Y-%m-%d")
|
||||||
|
user_gacha_list.append(tmp)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"length": len(user_gacha_list),
|
||||||
|
"userGachaList": user_gacha_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_print_list = self.data.item.get_user_print_states(
|
||||||
|
data["userId"], has_completed=True
|
||||||
|
)
|
||||||
|
if user_print_list is None:
|
||||||
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"length": 0,
|
||||||
|
"nextIndex": -1,
|
||||||
|
"userPrintedCardList": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
print_list = []
|
||||||
|
next_idx = int(data["nextIndex"])
|
||||||
|
max_ct = int(data["maxCount"])
|
||||||
|
|
||||||
|
for x in range(next_idx, len(user_print_list)):
|
||||||
|
tmp = user_print_list[x]._asdict()
|
||||||
|
print_list.append(tmp["cardId"])
|
||||||
|
|
||||||
|
if len(user_print_list) >= max_ct:
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(user_print_list) >= max_ct:
|
||||||
|
next_idx = next_idx + max_ct
|
||||||
|
else:
|
||||||
|
next_idx = -1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"length": len(print_list),
|
||||||
|
"nextIndex": next_idx,
|
||||||
|
"userPrintedCardList": print_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_id = data["userId"]
|
||||||
|
|
||||||
|
user_print_states = self.data.item.get_user_print_states(
|
||||||
|
user_id, has_completed=False
|
||||||
|
)
|
||||||
|
|
||||||
|
card_print_state_list = []
|
||||||
|
for card in user_print_states:
|
||||||
|
tmp = card._asdict()
|
||||||
|
tmp["orderId"] = tmp["id"]
|
||||||
|
tmp.pop("user")
|
||||||
|
tmp["limitDate"] = datetime.strftime(tmp["limitDate"], "%Y-%m-%d")
|
||||||
|
|
||||||
|
card_print_state_list.append(tmp)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"userId": user_id,
|
||||||
|
"length": len(card_print_state_list),
|
||||||
|
"userCardPrintStateList": card_print_state_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||||
|
return super().handle_get_user_character_api_request(data)
|
||||||
|
|
||||||
|
def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||||
|
return super().handle_get_user_item_api_request(data)
|
||||||
|
|
||||||
|
def handle_roll_gacha_api_request(self, data: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
Handle a gacha roll API request, with:
|
||||||
|
gachaId: the gachaId where the cards should be pulled from
|
||||||
|
times: the number of gacha rolls
|
||||||
|
characterId: the character which the user wants
|
||||||
|
"""
|
||||||
|
gacha_id = data["gachaId"]
|
||||||
|
num_rolls = data["times"]
|
||||||
|
chara_id = data["characterId"]
|
||||||
|
|
||||||
|
rolled_cards = []
|
||||||
|
|
||||||
|
# characterId is set after 10 rolls, where the user can select a card
|
||||||
|
# from all gameGachaCards, therefore the correct cardId for a given
|
||||||
|
# characterId should be returned
|
||||||
|
if chara_id != -1:
|
||||||
|
# get the
|
||||||
|
card = self.data.static.get_gacha_card_by_character(gacha_id, chara_id)
|
||||||
|
|
||||||
|
tmp = card._asdict()
|
||||||
|
tmp.pop("id")
|
||||||
|
|
||||||
|
rolled_cards.append(tmp)
|
||||||
|
else:
|
||||||
|
gacha_cards = self.data.static.get_gacha_cards(gacha_id)
|
||||||
|
|
||||||
|
# get the card id for each roll
|
||||||
|
for _ in range(num_rolls):
|
||||||
|
# get the index from all possible cards
|
||||||
|
card_idx = randint(0, len(gacha_cards) - 1)
|
||||||
|
# remove the index from the cards so it wont get pulled again
|
||||||
|
card = gacha_cards.pop(card_idx)
|
||||||
|
|
||||||
|
# remove the "id" fronm the card
|
||||||
|
tmp = card._asdict()
|
||||||
|
tmp.pop("id")
|
||||||
|
|
||||||
|
rolled_cards.append(tmp)
|
||||||
|
|
||||||
|
return {"length": len(rolled_cards), "gameGachaCardList": rolled_cards}
|
||||||
|
|
||||||
|
def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict:
|
||||||
|
upsert = data["cmUpsertUserGacha"]
|
||||||
|
user_id = data["userId"]
|
||||||
|
place_id = data["placeId"]
|
||||||
|
|
||||||
|
# save the user data
|
||||||
|
user_data = upsert["userData"]
|
||||||
|
user_data.pop("rankUpChallengeResults")
|
||||||
|
user_data.pop("userEmoney")
|
||||||
|
|
||||||
|
self.data.profile.put_profile_data(user_id, self.version, user_data)
|
||||||
|
|
||||||
|
# save the user gacha
|
||||||
|
user_gacha = upsert["userGacha"]
|
||||||
|
gacha_id = user_gacha["gachaId"]
|
||||||
|
user_gacha.pop("gachaId")
|
||||||
|
user_gacha.pop("dailyGachaDate")
|
||||||
|
|
||||||
|
self.data.item.put_user_gacha(user_id, gacha_id, user_gacha)
|
||||||
|
|
||||||
|
# save all user items
|
||||||
|
if "userItemList" in upsert:
|
||||||
|
for item in upsert["userItemList"]:
|
||||||
|
self.data.item.put_item(user_id, item)
|
||||||
|
|
||||||
|
# add every gamegachaCard to database
|
||||||
|
for card in upsert["gameGachaCardList"]:
|
||||||
|
self.data.item.put_user_print_state(
|
||||||
|
user_id,
|
||||||
|
hasCompleted=False,
|
||||||
|
placeId=place_id,
|
||||||
|
cardId=card["cardId"],
|
||||||
|
gachaId=card["gachaId"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# retrieve every game gacha card which has been added in order to get
|
||||||
|
# the orderId for the next request
|
||||||
|
user_print_states = self.data.item.get_user_print_states_by_gacha(
|
||||||
|
user_id, gacha_id, has_completed=False
|
||||||
|
)
|
||||||
|
card_print_state_list = []
|
||||||
|
for card in user_print_states:
|
||||||
|
tmp = card._asdict()
|
||||||
|
tmp["orderId"] = tmp["id"]
|
||||||
|
tmp.pop("user")
|
||||||
|
tmp["limitDate"] = datetime.strftime(tmp["limitDate"], "%Y-%m-%d")
|
||||||
|
|
||||||
|
card_print_state_list.append(tmp)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"returnCode": "1",
|
||||||
|
"apiName": "CMUpsertUserGachaApi",
|
||||||
|
"userCardPrintStateList": card_print_state_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
|
||||||
|
return {
|
||||||
|
"returnCode": 1,
|
||||||
|
"orderId": 0,
|
||||||
|
"serialId": "11111111111111111111",
|
||||||
|
"apiName": "CMUpsertUserPrintlogApi",
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_print_detail = data["userPrintDetail"]
|
||||||
|
user_id = data["userId"]
|
||||||
|
|
||||||
|
# generate random serial id
|
||||||
|
serial_id = "".join([str(randint(0, 9)) for _ in range(20)])
|
||||||
|
|
||||||
|
# not needed because are either zero or unset
|
||||||
|
user_print_detail.pop("orderId")
|
||||||
|
user_print_detail.pop("printNumber")
|
||||||
|
user_print_detail.pop("serialId")
|
||||||
|
user_print_detail["printDate"] = datetime.strptime(
|
||||||
|
user_print_detail["printDate"], "%Y-%m-%d"
|
||||||
|
)
|
||||||
|
|
||||||
|
# add the entry to the user print table with the random serialId
|
||||||
|
self.data.item.put_user_print_detail(user_id, serial_id, user_print_detail)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"returnCode": 1,
|
||||||
|
"orderId": 0,
|
||||||
|
"serialId": serial_id,
|
||||||
|
"apiName": "CMUpsertUserPrintApi",
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict:
|
||||||
|
upsert = data["userCardPrintState"]
|
||||||
|
user_id = data["userId"]
|
||||||
|
place_id = data["placeId"]
|
||||||
|
|
||||||
|
# save all user items
|
||||||
|
if "userItemList" in data:
|
||||||
|
for item in data["userItemList"]:
|
||||||
|
self.data.item.put_item(user_id, item)
|
||||||
|
|
||||||
|
# set the card print state to success and use the orderId as the key
|
||||||
|
self.data.item.put_user_print_state(
|
||||||
|
user_id,
|
||||||
|
id=upsert["orderId"],
|
||||||
|
hasCompleted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"}
|
||||||
|
|
||||||
|
def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict:
|
||||||
|
order_ids = data["orderIdList"]
|
||||||
|
user_id = data["userId"]
|
||||||
|
|
||||||
|
# set the card print state to success and use the orderId as the key
|
||||||
|
for order_id in order_ids:
|
||||||
|
self.data.item.put_user_print_state(
|
||||||
|
user_id,
|
||||||
|
id=order_id,
|
||||||
|
hasCompleted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}
|
||||||
+20
-4
@@ -7,6 +7,7 @@ from titles.chuni.new import ChuniNew
|
|||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
|
||||||
|
|
||||||
class ChuniNewPlus(ChuniNew):
|
class ChuniNewPlus(ChuniNew):
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
super().__init__(core_cfg, game_cfg)
|
super().__init__(core_cfg, game_cfg)
|
||||||
@@ -16,8 +17,23 @@ class ChuniNewPlus(ChuniNew):
|
|||||||
ret = super().handle_get_game_setting_api_request(data)
|
ret = super().handle_get_game_setting_api_request(data)
|
||||||
ret["gameSetting"]["romVersion"] = "2.05.00"
|
ret["gameSetting"]["romVersion"] = "2.05.00"
|
||||||
ret["gameSetting"]["dataVersion"] = "2.05.00"
|
ret["gameSetting"]["dataVersion"] = "2.05.00"
|
||||||
ret["gameSetting"]["matchingUri"] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
ret["gameSetting"][
|
||||||
ret["gameSetting"]["matchingUriX"] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
"matchingUri"
|
||||||
ret["gameSetting"]["udpHolePunchUri"] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
||||||
ret["gameSetting"]["reflectorUri"] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
ret["gameSetting"][
|
||||||
|
"matchingUriX"
|
||||||
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
||||||
|
ret["gameSetting"][
|
||||||
|
"udpHolePunchUri"
|
||||||
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
||||||
|
ret["gameSetting"][
|
||||||
|
"reflectorUri"
|
||||||
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_data = super().handle_cm_get_user_preview_api_request(data)
|
||||||
|
|
||||||
|
# hardcode lastDataVersion for CardMaker 1.35
|
||||||
|
user_data["lastDataVersion"] = "2.05.00"
|
||||||
|
return user_data
|
||||||
Loaded 50 of 169 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user