Add more service classes

This commit is contained in:
573dev
2020-10-19 22:15:57 -05:00
parent 7868a04f6d
commit 736b298734
10 changed files with 397 additions and 119 deletions

View File

@@ -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",
]

View File

@@ -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:
<call model="K32:J:B:A:2011033000" srcid="00010203040506070809">
<facility encoding="SHIFT_JIS" method="get"/>
</call>
"""
# Methods
GET = "get"
@classmethod
def get(cls):
"""
Example:
<response>
<facility expire="600">
<location>
<id>US-123</id>
<country>US</country>
<region>.</region>
<name>H</name>
<type __type="u8">0</type>
</location>
<line>
<id>.</id>
<class __type="u8">0</class>
</line>
<portfw>
<globalip __type="ip4" __count="1">127.0.0.1</globalip>
<globalport __type="u16">80</globalport>
<privateport __type="u16">80</privateport>
</portfw>
<public>
<flag __type="u8">1</flag>
<name>.</name>
<latitude>0</latitude>
<longitude>0</longitude>
</public>
<share>
<eacoin>
<notchamount __type="s32">3000</notchamount>
<notchcount __type="s32">3</notchcount>
<supplylimit __type="s32">10000</supplylimit>
</eacoin>
<eapass>
<valid __type="u16">365</valid>
</eapass>
<url>
<eapass>www.ea-pass.konami.net</eapass>
<arcadefan>www.konami.jp/am</arcadefan>
<konaminetdx>http://am.573.jp</konaminetdx>
<konamiid>http://id.konami.net</konamiid>
<eagate>http://eagate.573.jp</eagate>
</url>
</share>
</facility>
</response>
"""
# 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"},
)
)

View File

@@ -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:
<call model="K32:J:B:A:2011033000" srcid="00010203040506070809">
<message method="get"/>
</call>
"""
# Methods
GET = "get"
@classmethod
def get(cls):
"""
Example:
<response>
<message expire="600"/>
</response>
"""
return E.response(E.message({"expire": "600"}))

View File

@@ -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:
<call model="K32:J:B:A:2011033000" srcid="00010203040506070809">
<package method="list" pkgtype="all"/>
</call>
"""
# Methods
LIST = "list"
# PKGTypes
PKGTYPE_ALL = "all"
@classmethod
def list(cls, req: ServiceRequest):
"""
Example:
<response>
<package expire="600"/>
</response>
"""
# 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

View File

@@ -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:
<item>
<name __type="str">K32.mode.std</name>
<value __type="s32">1</value>
<time __type="time">1602992363</time>
</item>
"""
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<name: "{self.name}", value: {self.value}, '
f"time: {self.time.strftime(self.DATE_FMT)}>"
)
class PCBEvent(object):
"""
Handle the PCBEvent request.
It is unknown as to what this does.
Example:
<call model="K32:J:B:A:2011033000" srcid="00010203040506070809">
<pcbevent method="put">
<time __type="time">1602992385</time>
<seq __type="u32">4</seq>
<item>
<name __type="str">K32.mode.std</name>
<value __type="s32">1</value>
<time __type="time">1602992363</time>
</item>
<item>
<name __type="str">K32.playcnt.t</name>
<value __type="s32">1</value>
<time __type="time">1602992363</time>
</item>
<item>
<name __type="str">game.e</name>
<value __type="s32">1</value>
<time __type="time">1602992371</time>
</item>
</pcbevent>
</call>
"""
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:
<response>
<pcbevent expire="600"/>
</response>
"""
return E.response(E.pcbevent({"expire": "600"}))
def __repr__(self) -> str:
return (
f"PCBEvent<time: {self.time.strftime(self.DATE_FMT)}, "
f"seq: {self.seq}, items: {self.items}>"
)

View File

@@ -22,4 +22,17 @@ class PCBTracker(object):
@classmethod
def alive(cls):
"""
Example (if Paseli is not active):
<response>
<pcbtracker ecenable="0"/>
</response>
Potentially if Paseli is active, the response might look like so:
<response>
<pcbtracker time="" limit="" ecenable="1" eclimit=""/>
</response>
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"}))

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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