diff --git a/v8_server/eamuse/services/__init__.py b/v8_server/eamuse/services/__init__.py index e69de29..ec86f9d 100644 --- a/v8_server/eamuse/services/__init__.py +++ b/v8_server/eamuse/services/__init__.py @@ -0,0 +1,18 @@ +from v8_server.eamuse.services.facility import Facility +from v8_server.eamuse.services.message import Message +from v8_server.eamuse.services.package import Package +from v8_server.eamuse.services.pcbevent import PCBEvent +from v8_server.eamuse.services.pcbtracker import PCBTracker +from v8_server.eamuse.services.services import ServiceRequest, Services, ServiceType + + +__all__ = [ + "Facility", + "Message", + "PCBEvent", + "PCBTracker", + "Package", + "ServiceRequest", + "ServiceType", + "Services", +] diff --git a/v8_server/eamuse/services/facility.py b/v8_server/eamuse/services/facility.py new file mode 100644 index 0000000..38fd146 --- /dev/null +++ b/v8_server/eamuse/services/facility.py @@ -0,0 +1,113 @@ +from lxml.builder import E + +from v8_server.eamuse.utils.xml import XMLBinTypes as T, e_type + + +class Facility(object): + """ + Handle the Facility request. + + The only method of note is the "get" method. This method expects to return a bunch + of information about the arcade this cabinet is in, as well as some settings for + URLs and the name of the cab. + + Example: + + + + """ + + # Methods + GET = "get" + + @classmethod + def get(cls): + """ + Example: + + + + US-123 + US + . + H + 0 + + + . + 0 + + + 127.0.0.1 + 80 + 80 + + + 1 + . + 0 + 0 + + + + 3000 + 3 + 10000 + + + 365 + + + www.ea-pass.konami.net + www.konami.jp/am + http://am.573.jp + http://id.konami.net + http://eagate.573.jp + + + + + """ + + # TODO: The facility data should be read in from a config file instead of being + # hard coded here + + return E.response( + E.facility( + E.location( + E.id("CA-123"), + E.country("CA"), + E.region("MB"), + E.name("SenPi Arcade"), + E.type("0", e_type(T.u8)), + ), + E.line(E.id("."), E("class", "0", e_type(T.u8))), + E.portfw( + E.globalip("127.0.0.1", e_type(T.ip4, count=1)), + E.globalport("80", e_type(T.u16)), + E.privateport("80", e_type(T.u16)), + ), + E.public( + E.flag("1", e_type(T.u8)), + E.name("Gotem"), + E.latitude("0"), + E.longitude("0"), + ), + E.share( + E.eacoin( + E.notchamount("3000", e_type(T.s32)), + E.notchcount("3", e_type(T.s32)), + E.supplylimit("10000", e_type(T.s32)), + ), + E.eapass(E.valid("365", e_type(T.u16))), + E.url( + E.eapass("www.ea-pass.konami.net"), + E.arcadefan("www.konami.jp/am"), + E.konaminetdx("http://am.573.jp"), + E.konamiid("http://id.konami.net"), + E.eagate("http://eagate.573.jp"), + ), + ), + {"expire": "600"}, + ) + ) diff --git a/v8_server/eamuse/services/message.py b/v8_server/eamuse/services/message.py new file mode 100644 index 0000000..503005d --- /dev/null +++ b/v8_server/eamuse/services/message.py @@ -0,0 +1,27 @@ +from lxml.builder import E + + +class Message(object): + """ + Handle the Message request. + + It is unknown as to what this does. Possibly it's for operator messages + + Example: + + + + """ + + # Methods + GET = "get" + + @classmethod + def get(cls): + """ + Example: + + + + """ + return E.response(E.message({"expire": "600"})) diff --git a/v8_server/eamuse/services/package.py b/v8_server/eamuse/services/package.py new file mode 100644 index 0000000..8740ae1 --- /dev/null +++ b/v8_server/eamuse/services/package.py @@ -0,0 +1,44 @@ +from lxml.builder import E + +from v8_server.eamuse.services.services import ServiceRequest +from v8_server.eamuse.utils.xml import get_xml_attrib + + +class Package(object): + """ + Handle the Package request. + + This is for supporting downloading of updates. We do not support this. + + Example: + + + + """ + + # Methods + LIST = "list" + + # PKGTypes + PKGTYPE_ALL = "all" + + @classmethod + def list(cls, req: ServiceRequest): + """ + Example: + + + + """ + # Grab the pkgtype in case we ever need it + pkgtype = get_xml_attrib(req.xml[0], "pkgtype") + + if pkgtype == cls.PKGTYPE_ALL: + response = E.response(E.package({"expire": "600"})) + else: + raise Exception( + "Not sure how to handle this package request. " + f'pkgtype "{pkgtype}" is unknown for request: {req}' + ) + + return response diff --git a/v8_server/eamuse/services/pcbevent.py b/v8_server/eamuse/services/pcbevent.py new file mode 100644 index 0000000..6736135 --- /dev/null +++ b/v8_server/eamuse/services/pcbevent.py @@ -0,0 +1,96 @@ +from datetime import datetime + +from lxml import etree +from lxml.builder import E + +from v8_server.eamuse.services.services import ServiceRequest + + +class PCBEventItem(object): + """ + Contains the data for a PCBEvent Item + + Example: + + K32.mode.std + 1 + + + """ + + DATE_FMT = "%b/%d/%Y-%H:%M:%S" + + def __init__(self, xml_root: etree) -> None: + # Build our data from the `item` xml root object + self.name = xml_root.find("name").text + self.value = int(xml_root.find("value").text) + self.time = datetime.fromtimestamp(int(xml_root.find("time").text)) + + def __repr__(self) -> str: + return ( + f'PCBEventItem" + ) + + +class PCBEvent(object): + """ + Handle the PCBEvent request. + + It is unknown as to what this does. + + Example: + + + + 4 + + K32.mode.std + 1 + + + + K32.playcnt.t + 1 + + + + game.e + 1 + + + + + """ + + DATE_FMT = "%b/%d/%Y-%H:%M:%S" + + def __init__(self, req: ServiceRequest) -> None: + # Build our item from the data + xml_root = req.xml[0] + + self.time = datetime.fromtimestamp(int(xml_root.find("time").text)) + self.seq = int(xml_root.find("seq").text) + + self.items = [] + for item in xml_root.findall("item"): + self.items.append(PCBEventItem(item)) + + # Methods + PUT = "put" + + @classmethod + def put(cls): + """ + Example: + + + + """ + return E.response(E.pcbevent({"expire": "600"})) + + def __repr__(self) -> str: + return ( + f"PCBEvent" + ) diff --git a/v8_server/eamuse/services/pcbtracker.py b/v8_server/eamuse/services/pcbtracker.py index 9d8603e..8b1d454 100644 --- a/v8_server/eamuse/services/pcbtracker.py +++ b/v8_server/eamuse/services/pcbtracker.py @@ -22,4 +22,17 @@ class PCBTracker(object): @classmethod def alive(cls): + """ + Example (if Paseli is not active): + + + + + Potentially if Paseli is active, the response might look like so: + + + + + I am unsure what the `time`, `limit`, and `eclimit` responses would be. + """ return E.response(E.pcbtracker({"ecenable": "1" if cls.PASELI_ACTIVE else "0"})) diff --git a/v8_server/eamuse/services/services.py b/v8_server/eamuse/services/services.py index b3e0854..6a8e36c 100644 --- a/v8_server/eamuse/services/services.py +++ b/v8_server/eamuse/services/services.py @@ -8,13 +8,13 @@ from random import randint from time import time from typing import Any, Callable, Dict, Optional, Tuple, Union -from flask import Flask, Request +from flask import Request from kbinxml import KBinXML from lxml import etree from lxml.builder import E from lxml.etree import _Element as eElement -from v8_server import LOG_PATH +from v8_server import LOG_PATH, app from v8_server.eamuse.utils.arc4 import EAmuseARC4 from v8_server.eamuse.utils.eamuse import Model from v8_server.eamuse.utils.lz77 import Lz77 @@ -101,7 +101,7 @@ class Services(object): return services @classmethod - def route(cls, app: Flask, _type: ServiceType) -> Callable[[Any], Any]: + def route(cls, _type: ServiceType) -> Callable[[Any], Any]: def decorator(f): func = app.route(f"/{_type.value}", methods=["POST"])(f) return func diff --git a/v8_server/eamuse/utils/eamuse.py b/v8_server/eamuse/utils/eamuse.py index 6a8df62..9bf737b 100644 --- a/v8_server/eamuse/utils/eamuse.py +++ b/v8_server/eamuse/utils/eamuse.py @@ -1,26 +1,6 @@ from __future__ import annotations -from typing import Dict, Optional - - -class XMLBinTypes(object): - s8 = "s8" - u8 = "u8" - s16 = "s16" - u16 = "u16" - s32 = "s32" - u32 = "u32" - s64 = "s64" - u64 = "u64" - ip4 = "ip4" - time = "time" - - -def e_type(_type, count: Optional[int] = None) -> Dict[str, str]: - result = {"__type": _type} - if count is not None: - result["__count"] = str(count) - return result +from typing import Optional class Model: diff --git a/v8_server/eamuse/utils/xml.py b/v8_server/eamuse/utils/xml.py index f39b7c2..99bca84 100644 --- a/v8_server/eamuse/utils/xml.py +++ b/v8_server/eamuse/utils/xml.py @@ -1,9 +1,29 @@ from datetime import datetime -from typing import Optional +from typing import Dict, Optional from lxml import etree +class XMLBinTypes(object): + s8 = "s8" + u8 = "u8" + s16 = "s16" + u16 = "u16" + s32 = "s32" + u32 = "u32" + s64 = "s64" + u64 = "u64" + ip4 = "ip4" + time = "time" + + +def e_type(_type, count: Optional[int] = None) -> Dict[str, str]: + result = {"__type": _type} + if count is not None: + result["__count"] = str(count) + return result + + def get_xml_tag(xml: etree) -> str: return str(xml.tag) diff --git a/v8_server/view/index.py b/v8_server/view/index.py index a021010..1a7c345 100644 --- a/v8_server/view/index.py +++ b/v8_server/view/index.py @@ -2,14 +2,19 @@ import random from typing import Dict, Tuple from flask import request -from lxml import etree as ET # noqa: N812 -from lxml.builder import E from sqlalchemy.orm.exc import MultipleResultsFound from v8_server import app -from v8_server.eamuse.services.pcbtracker import PCBTracker -from v8_server.eamuse.services.services import ServiceRequest, Services, ServiceType -from v8_server.eamuse.utils.eamuse import e_type +from v8_server.eamuse.services import ( + Facility, + Message, + Package, + PCBEvent, + PCBTracker, + ServiceRequest, + Services, + ServiceType, +) from v8_server.eamuse.utils.xml import get_xml_attrib from v8_server.model.connection import Database from v8_server.model.user import Card, ExtID, Profile, RefID, User @@ -43,13 +48,7 @@ def catch_all(u_path: str) -> str: return "You want path: %s" % u_path -def base_response(element: str, attributes: Dict[str, str] = None) -> ET: - if attributes is None: - attributes = {} - return E.response(E(element, {**attributes, "expire": "600"})) - - -@Services.route(app, ServiceType.PCBTRACKER) +@Services.route(ServiceType.PCBTRACKER) def pcbtracker_service() -> FlaskResponse: req = ServiceRequest(request) @@ -61,89 +60,57 @@ def pcbtracker_service() -> FlaskResponse: return req.response(response) +@Services.route(ServiceType.MESSAGE) +def message_service() -> FlaskResponse: + req = ServiceRequest(request) + + if req.method == Message.GET: + response = Message.get() + else: + raise Exception(f"Not sure how to handle this Message Request: {req}") + + return req.response(response) + + +@Services.route(ServiceType.PCBEVENT) +def pcbevent_service() -> FlaskResponse: + req = ServiceRequest(request) + event = PCBEvent(req) + app.logger.info(event) + + if req.method == PCBEvent.PUT: + response = PCBEvent.put() + else: + raise Exception(f"Not sure how to handle this PCBEvent Request: {req}") + + return req.response(response) + + +@Services.route(ServiceType.PACKAGE) +def package_service() -> FlaskResponse: + req = ServiceRequest(request) + + if req.method == Package.LIST: + response = Package.list(req) + else: + raise Exception(f"Not sure how to handle this Package Request: {req}") + + return req.response(response) + + +@Services.route(ServiceType.FACILITY) +def facility_service() -> FlaskResponse: + req = ServiceRequest(request) + + if req.method == Facility.GET: + response = Facility.get() + else: + raise Exception(f"Not sure how to handle this Facility Request: {req}") + + return req.response(response) + + ''' -@app.route("/message/service", methods=["POST"]) -def message() -> Tuple[bytes, Dict[str, str]]: - """ - Unknown what this does. Possibly for operator messages? - """ - _ = eamuse_read_xml(request) - response = base_response("message") - return eamuse_prepare_xml(response, request) - - -@app.route("/pcbevent/service", methods=["POST"]) -def pcbevent() -> Tuple[bytes, Dict[str, str]]: - """ - Handle a PCBEvent request. We do nothing for this aside from logging the event. - """ - _ = eamuse_read_xml(request) - response = base_response("pcbevent") - return eamuse_prepare_xml(response, request) - - -@app.route("/facility/service", methods=["POST"]) -def facility() -> Tuple[bytes, Dict[str, str]]: - """ - Handle a facility request. The only method of note is the "get" request, - which expects to return a bunch of information about the arcade this cabinet is in, - as well as some settings for URLs and the name of the cab. - """ - _ = eamuse_read_xml(request) - - response = E.response( - E.facility( - E.location( - E.id("US-123"), - E.country("US"), - E.region("."), - E.name("H"), - E.type("0", e_type("u8")), - ), - E.line(E.id("."), E("class", "0", e_type("u8"))), - E.portfw( - E.globalip("192.168.1.139", e_type("ip4", count=1)), - E.globalport("80", e_type("u16")), - E.privateport("80", e_type("u16")), - ), - E.public( - E.flag("1", e_type("u8")), - E.name("."), - E.latitude("0"), - E.longitude("0"), - ), - E.share( - E.eacoin( - E.notchamount("3000", e_type("s32")), - E.notchcount("3", e_type("s32")), - E.supplylimit("10000", e_type("s32")), - ), - E.eapass(E.valid("365", e_type("u16"))), - E.url( - E.eapass("www.ea-pass.konami.net"), - E.arcadefan("www.konami.jp/am"), - E.konaminetdx("http://am.573.jp"), - E.konamiid("http://id.konami.net"), - E.eagate("http://eagate.573.jp"), - ), - ), - {"expire": "600"}, - ) - ) - return eamuse_prepare_xml(response, request) - - -@app.route("/package/service", methods=["POST"]) -def package() -> Tuple[bytes, Dict[str, str]]: - """ - This is for supporting downloading of updates. We don't support this. - """ - _ = eamuse_read_xml(request) - - response = base_response("package") - return eamuse_prepare_xml(response, request) - - class CardStatus(object): """ List of statuses we return to the game for various reasons