1
0
mirror of synced 2024-12-11 05:55:58 +01:00
bemaniutils/bemani/backend/core/eacoin.py

441 lines
19 KiB
Python

from typing import Optional
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.
"""
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_request(self, request: Node) -> Optional[Node]:
"""
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
ad the end of of a game. This handler ensures that this works for all games.
"""
method = request.attribute('method')
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
if method == 'checkin':
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
if method == 'opcheckin':
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
elif method == 'consume':
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)
elif method == 'getlog':
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
elif method == 'opchpass':
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
elif method == 'checkout':
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
elif method == 'opcheckout':
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
# Invalid method
return None