from typing_extensions import Final from bemani.backend.base import Base, Status from bemani.protocol import Node from bemani.common import Time, CardCipher class PASELIHandler(Base): """ A mixin that can be used to provide PASELI services to a game. Handle PASELI requests. The game will check out a session at the beginning of the game, make PASELI purchases against that session, and then close it at the end of of a game. This handler ensures that this works for all games. """ INFINITE_PASELI_AMOUNT: Final[int] = 57300 """ Override this in your subclass if the particular game/series needs a different padding amount to display PASELI transactions on the operator menu. """ paseli_padding: int = 1 def handle_eacoin_checkin_request(self, request: Node) -> Node: if not self.config.paseli.enabled: # Refuse to respond, we don't have PASELI enabled print("PASELI not enabled, ignoring eacoin request") root = Node.void("eacoin") root.set_attribute("status", str(Status.NOT_ALLOWED)) return root root = Node.void("eacoin") cardid = request.child_value("cardid") pin = request.child_value("passwd") if cardid is None or pin is None: # Refuse to return anything print("Invalid eacoin checkin request, missing cardid or pin") root.set_attribute("status", str(Status.NO_PROFILE)) return root userid = self.data.local.user.from_cardid(cardid) if userid is None: # Refuse to do anything print("No user for eacoin checkin request") root.set_attribute("status", str(Status.NO_PROFILE)) return root valid = self.data.local.user.validate_pin(userid, pin) if not valid: # Refuse to do anything print("User entered invalid pin for eacoin checkin request") root.set_attribute("status", str(Status.INVALID_PIN)) return root session = self.data.local.user.create_session(userid) if self.config.paseli.infinite: balance = PASELIHandler.INFINITE_PASELI_AMOUNT else: if self.config.machine.arcade is None: # There's no arcade for this machine, but infinite is not # enabled, so there's no way to find a balance. balance = 0 else: balance = self.data.local.user.get_balance( userid, self.config.machine.arcade ) root.add_child(Node.s16("sequence", 0)) root.add_child(Node.u8("acstatus", 0)) root.add_child(Node.string("acid", "DUMMY_ID")) root.add_child(Node.string("acname", "DUMMY_NAME")) root.add_child(Node.s32("balance", balance)) root.add_child(Node.string("sessid", session)) return root def handle_eacoin_opcheckin_request(self, request: Node) -> Node: if not self.config.paseli.enabled: # Refuse to respond, we don't have PASELI enabled print("PASELI not enabled, ignoring eacoin request") root = Node.void("eacoin") root.set_attribute("status", str(Status.NOT_ALLOWED)) return root root = Node.void("eacoin") passwd = request.child_value("passwd") if passwd is None: # Refuse to return anything print("Invalid eacoin checkin request, missing passwd") root.set_attribute("status", str(Status.NO_PROFILE)) return root if self.config.machine.arcade is None: # Machine doesn't belong to an arcade print("Machine doesn't belong to an arcade") root.set_attribute("status", str(Status.NO_PROFILE)) return root arcade = self.data.local.machine.get_arcade(self.config.machine.arcade) if arcade is None: # Refuse to do anything print("No arcade for operator checkin request") root.set_attribute("status", str(Status.NO_PROFILE)) return root if arcade.pin != passwd: # Refuse to do anything print("User entered invalid pin for operator checkin request") root.set_attribute("status", str(Status.INVALID_PIN)) return root session = self.data.local.machine.create_session(arcade.id) root.add_child(Node.string("sessid", session)) return root def handle_eacoin_consume_request(self, request: Node) -> Node: if not self.config.paseli.enabled: # Refuse to respond, we don't have PASELI enabled print("PASELI not enabled, ignoring eacoin request") root = Node.void("eacoin") root.set_attribute("status", str(Status.NOT_ALLOWED)) return root def make_resp(status: int, balance: int) -> Node: root = Node.void("eacoin") root.add_child(Node.u8("acstatus", status)) root.add_child(Node.u8("autocharge", 0)) root.add_child(Node.s32("balance", balance)) return root session = request.child_value("sessid") payment = request.child_value("payment") service = request.child_value("service") details = request.child_value("detail") if session is None or payment is None: # Refuse to do anything print("Invalid eacoin consume request, missing sessid or payment") return make_resp(2, 0) userid = self.data.local.user.from_session(session) if userid is None: # Refuse to do anything print("Invalid session for eacoin consume request") return make_resp(2, 0) if self.config.paseli.infinite: balance = PASELIHandler.INFINITE_PASELI_AMOUNT - payment else: if self.config.machine.arcade is None: # There's no arcade for this machine, but infinite is not # enabled, so there's no way to find a balance, assume failed # consume payment. balance = None else: # Look up the new balance based on this delta. If there isn't enough, # we will end up returning None here and exit without performing. balance = self.data.local.user.update_balance( userid, self.config.machine.arcade, -payment ) if balance is None: print("Not enough balance for eacoin consume request") return make_resp( 1, self.data.local.user.get_balance( userid, self.config.machine.arcade ), ) else: self.data.local.network.put_event( "paseli_transaction", { "delta": -payment, "balance": balance, "service": -service, "reason": details, "pcbid": self.config.machine.pcbid, }, userid=userid, arcadeid=self.config.machine.arcade, ) return make_resp(0, balance) def handle_eacoin_getlog_request(self, request: Node) -> Node: if not self.config.paseli.enabled: # Refuse to respond, we don't have PASELI enabled print("PASELI not enabled, ignoring eacoin request") root = Node.void("eacoin") root.set_attribute("status", str(Status.NOT_ALLOWED)) return root root = Node.void("eacoin") sessid = request.child_value("sessid") logtype = request.child_value("logtype") target = request.child_value("target") limit = request.child_value("perpage") offset = request.child_value("offset") # Try to determine whether its a user or an arcade session userid = self.data.local.user.from_session(sessid) if userid is None: arcadeid = self.data.local.machine.from_session(sessid) else: arcadeid = None # Bail out if we don't have any idea what session this is if userid is None and arcadeid is None: print("Unable to determine session type") return root # If we're a user session, also look up the current arcade # so we display only entries that happened on this arcade. if userid is not None: arcade = self.data.local.machine.get_arcade(self.config.machine.arcade) if arcade is None: print("Machine doesn't belong to an arcade") return root arcadeid = arcade.id # Now, look up all transactions for this specific group events = self.data.local.network.get_events( userid=userid, arcadeid=arcadeid, event="paseli_transaction", ) # Further filter it down to the current PCBID events = [event for event in events if event.data.get("pcbid") == target] # Grab the end of day today as a timestamp end_of_today = Time.end_of_today() time_format = "%Y-%m-%d %H:%M:%S" date_format = "%Y-%m-%d" # Set up common structure lognode = Node.void(logtype) topic = Node.void("topic") lognode.add_child(topic) summary = Node.void("summary") lognode.add_child(summary) # Display what day we are summed to topic.add_child(Node.string("sumdate", Time.format(Time.now(), date_format))) if logtype == "last7days": # We show today in the today total, last 7 days prior in the week total beginning_of_today = end_of_today - Time.SECONDS_IN_DAY end_of_week = beginning_of_today beginning_of_week = end_of_week - Time.SECONDS_IN_WEEK topic.add_child( Node.string("sumfrom", Time.format(beginning_of_week, date_format)) ) topic.add_child(Node.string("sumto", Time.format(end_of_week, date_format))) today_total = sum( [ -event.data.get_int("delta") for event in events if event.timestamp >= beginning_of_today and event.timestamp < end_of_today ] ) today_total = sum( [ -event.data.get_int("delta") for event in events if event.timestamp >= beginning_of_today and event.timestamp < end_of_today ] ) week_txns = [ -event.data.get_int("delta") for event in events if event.timestamp >= beginning_of_week and event.timestamp < end_of_week ] week_total = sum(week_txns) if len(week_txns) > 0: week_avg = int(sum(week_txns) / len(week_txns)) else: week_avg = 0 # We display the totals for each day starting with yesterday and up through 7 days prior. # Index starts at 0 = yesterday, 1 = the day before, etc... items = [] for days in range(0, 7): end_of_day = end_of_week - (days * Time.SECONDS_IN_DAY) start_of_day = end_of_day - Time.SECONDS_IN_DAY items.append( sum( [ -event.data.get_int("delta") for event in events if event.timestamp >= start_of_day and event.timestamp < end_of_day ] ) ) topic.add_child(Node.s32("today", today_total)) topic.add_child(Node.s32("average", week_avg)) topic.add_child(Node.s32("total", week_total)) summary.add_child(Node.s32_array("items", items)) if logtype == "last52weeks": # Start one week back, since the operator can look at last7days for newer stuff. beginning_of_today = end_of_today - Time.SECONDS_IN_DAY end_of_52_weeks = beginning_of_today - Time.SECONDS_IN_WEEK topic.add_child( Node.string( "sumfrom", Time.format( end_of_52_weeks - (52 * Time.SECONDS_IN_WEEK), date_format ), ) ) topic.add_child( Node.string("sumto", Time.format(end_of_52_weeks, date_format)) ) # We index backwards, where index 0 = the first week back, 1 = the next week back after that, etc... items = [] for weeks in range(0, 52): end_of_range = end_of_52_weeks - (weeks * Time.SECONDS_IN_WEEK) beginning_of_range = end_of_range - Time.SECONDS_IN_WEEK items.append( sum( [ -event.data.get_int("delta") for event in events if event.timestamp >= beginning_of_range and event.timestamp < end_of_range ] ) ) summary.add_child(Node.s32_array("items", items)) if logtype == "eachday": start_ts = Time.now() end_ts = Time.now() weekdays = [0] * 7 for event in events: event_day = Time.days_into_week(event.timestamp) weekdays[event_day] = weekdays[event_day] - event.data.get_int("delta") if event.timestamp < start_ts: start_ts = event.timestamp topic.add_child(Node.string("sumfrom", Time.format(start_ts, date_format))) topic.add_child(Node.string("sumto", Time.format(end_ts, date_format))) summary.add_child(Node.s32_array("items", weekdays)) if logtype == "eachhour": start_ts = Time.now() end_ts = Time.now() hours = [0] * 24 for event in events: event_hour = int( (event.timestamp % Time.SECONDS_IN_DAY) / Time.SECONDS_IN_HOUR ) hours[event_hour] = hours[event_hour] - event.data.get_int("delta") if event.timestamp < start_ts: start_ts = event.timestamp topic.add_child(Node.string("sumfrom", Time.format(start_ts, date_format))) topic.add_child(Node.string("sumto", Time.format(end_ts, date_format))) summary.add_child(Node.s32_array("items", hours)) if logtype == "detail": history = Node.void("history") lognode.add_child(history) # Respect details paging if offset is not None: events = events[offset:] if limit is not None: events = events[:limit] # Output the details themselves for event in events: card_no = "" if event.userid is not None: user = self.data.local.user.get_user(event.userid) if user is not None: cards = self.data.local.user.get_cards(user.id) if len(cards) > 0: card_no = CardCipher.encode(cards[0]) item = Node.void("item") history.add_child(item) item.add_child( Node.string("date", Time.format(event.timestamp, time_format)) ) item.add_child(Node.s32("consume", -event.data.get_int("delta"))) item.add_child(Node.s32("service", -event.data.get_int("service"))) item.add_child(Node.string("cardtype", "")) item.add_child( Node.string("cardno", " " * self.paseli_padding + card_no) ) item.add_child(Node.string("title", "")) item.add_child(Node.string("systemid", "")) if logtype == "lastmonths": year, month, _ = Time.todays_date() this_month = Time.timestamp_from_date(year, month) last_month = Time.timestamp_from_date(year, month - 1) month_before = Time.timestamp_from_date(year, month - 2) topic.add_child( Node.string("sumfrom", Time.format(month_before, date_format)) ) topic.add_child(Node.string("sumto", Time.format(this_month, date_format))) for start, end in [(month_before, last_month), (last_month, this_month)]: year, month, _ = Time.date_from_timestamp(start) items = [] for day in range(0, 31): begin_ts = start + (day * Time.SECONDS_IN_DAY) end_ts = begin_ts + Time.SECONDS_IN_DAY if begin_ts >= end: # Passed the end of this month items.append(0) else: # Sum up all the txns for this day items.append( sum( [ -event.data.get_int("delta") for event in events if event.timestamp >= begin_ts and event.timestamp < end_ts ] ) ) item = Node.void("item") summary.add_child(item) item.add_child(Node.s32("year", year)) item.add_child(Node.s32("month", month)) item.add_child(Node.s32_array("items", items)) root.add_child(Node.u8("processing", 0)) root.add_child(lognode) return root def handle_eacoin_opchpass_request(self, request: Node) -> Node: if not self.config.paseli.enabled: # Refuse to respond, we don't have PASELI enabled print("PASELI not enabled, ignoring eacoin request") root = Node.void("eacoin") root.set_attribute("status", str(Status.NOT_ALLOWED)) return root root = Node.void("eacoin") oldpass = request.child_value("passwd") newpass = request.child_value("newpasswd") if oldpass is None or newpass is None: # Refuse to return anything print("Invalid eacoin pass change request, missing passwd") root.set_attribute("status", str(Status.NO_PROFILE)) return root if self.config.machine.arcade is None: # Machine doesn't belong to an arcade print("Machine doesn't belong to an arcade") root.set_attribute("status", str(Status.NO_PROFILE)) return root arcade = self.data.local.machine.get_arcade(self.config.machine.arcade) if arcade is None: # Refuse to do anything print("No arcade for operator pass change request") root.set_attribute("status", str(Status.NO_PROFILE)) return root if arcade.pin != oldpass: # Refuse to do anything print("User entered invalid pin for operator pass change request") root.set_attribute("status", str(Status.INVALID_PIN)) return root arcade.pin = newpass self.data.local.machine.put_arcade(arcade) return root def handle_eacoin_checkout_request(self, request: Node) -> Node: if not self.config.paseli.enabled: # Refuse to respond, we don't have PASELI enabled print("PASELI not enabled, ignoring eacoin request") root = Node.void("eacoin") root.set_attribute("status", str(Status.NOT_ALLOWED)) return root session = request.child_value("sessid") if session is not None: # Destroy the session so it can't be used for any other purchases self.data.local.user.destroy_session(session) root = Node.void("eacoin") return root def handle_eacoin_opcheckout_request(self, request: Node) -> Node: if not self.config.paseli.enabled: # Refuse to respond, we don't have PASELI enabled print("PASELI not enabled, ignoring eacoin request") root = Node.void("eacoin") root.set_attribute("status", str(Status.NOT_ALLOWED)) return root session = request.child_value("sessid") if session is not None: # Destroy the session so it can't be used for any other purchases self.data.local.machine.destroy_session(session) root = Node.void("eacoin") return root