historical/toontown-classic.git/toontown/coderedemption/TTCodeRedemptionDB.py

1246 lines
47 KiB
Python
Raw Normal View History

2024-01-16 11:20:27 -06:00
from direct.directnotify.DirectNotifyGlobal import directNotify
from direct.showbase.DirectObject import DirectObject
from direct.fsm.FSM import FSM
from direct.fsm.StatePush import StateVar, FunctionCall
from direct.task import Task
from toontown.coderedemption.TTCodeDict import TTCodeDict
from toontown.coderedemption import TTCodeRedemptionConsts
from toontown.coderedemption import TTCodeRedemptionDBConsts
from direct.showbase.Job import Job
import os
import json
import datetime
import random
from otp.ai.AIBaseGlobal import *
class TryAgainLater(Exception):
def __init__(self, exception, file):
self._exception = exception
self._file = file
def getJSONException(self):
return self._exception
def __str__(self):
return 'problem using JSON file %s, try again later (%s)' % (self._file, self.exception)
class TTCodeRedemptionDBTester(Job):
notify = directNotify.newCategory('TTCodeRedemptionDBTester')
class TestRewarder:
def _giveReward(self, avId, rewardTypeId, rewardItemId, callback):
callback(0)
def __init__(self, db):
self._db = db
Job.__init__(self, 'TTCodeRedemptionDBTester-%s' % serialNum())
def getRandomSamples(self, callback, numSamples):
samples = []
for i in range(numSamples):
samples.append(int(random.random() * ((1 << 32)-1)))
callback(samples)
@classmethod
def isLotNameValid(cls, lotName):
# make sure a user doesn't create a lot that matches the test lot naming convention
return (TTCodeRedemptionDBConsts.TestLotName not in lotName)
@classmethod
def cleanup(cls, db):
# remove any leftover data from previous tests
db._testing = True
lotNames = db.getLotNames()
for lotName in lotNames:
if TTCodeRedemptionDBConsts.TestLotName in lotName:
db.deleteLot(lotName)
db._testing = False
def _handleRedeemResult(self, result, awardMgrResult):
self._redeemResult.append(result)
self._redeemResult.append(awardMgrResult)
def _getUnusedLotName(self):
lotNames = self._db.getLotNames()
while 1:
lotName = '%s%s' % (TTCodeRedemptionDBConsts.TestLotName, int(random.random() * ((1 << 32)-1)))
if lotName not in lotNames:
break
return lotName
def _getUnusedManualCode(self):
while 1:
code = ''
length = random.randrange(4, 16)
manualCharIndex = random.randrange(length)
for i in range(length):
if i == manualCharIndex:
charSet = TTCodeDict.ManualOnlyCharacters
else:
charSet = TTCodeDict.ManualCharacters
char = random.choice(charSet)
if char in TTCodeDict.IgnoredManualCharacters:
i -= 1
code = code + char
if not self._db.codeExists(code):
break
return code
def _getUnusedUtf8ManualCode(self):
chars = '\u65e5\u672c\u8a9e'
code = str('')
while 1:
code += random.choice(chars)
if not self._db.codeExists(code):
break
return code
def run(self):
self.notify.info('testing started')
retryStartT = None
retryDelay = 5
while 1:
try:
db = self._db
db._testing = True
lotName = self._getUnusedLotName()
# make sure there are at least one manual and one auto lot throughout the tests
phLots = []
phLots.append(self._getUnusedLotName())
for i in self._db.createLot(self.getRandomSamples, phLots[-1], 1, 0, 0):
db._testing = False
yield None
db._testing = True
phLots.append(self._getUnusedLotName())
code = self._getUnusedManualCode()
self._db.createManualLot(phLots[-1], code, 0, 0)
db._testing = False
yield None
db._testing = True
# lot creation
NumCodes = 3
RewardType = 0
RewardItemId = 0
ExpirationDate = '9999-04-01'
for i in self._db.createLot(self.getRandomSamples, lotName, NumCodes, RewardType, RewardItemId, ExpirationDate):
db._testing = False
yield None
db._testing = True
lotNames = self._db.getLotNames()
if lotName not in lotNames:
self.notify.error('could not create code redemption lot \'%s\'' % lotName)
db._testing = False
yield None
db._testing = True
autoLotNames = self._db.getAutoLotNames()
if lotName not in autoLotNames:
self.notify.error('auto lot \'%s\' not found in getAutoLotNames()' % lotName)
db._testing = False
yield None
db._testing = True
manualLotNames = self._db.getManualLotNames()
if lotName in manualLotNames:
self.notify.error('auto lot \'%s\' found in getAutoLotNames()' % lotName)
db._testing = False
yield None
db._testing = True
# get codes in lot
codes = self._db.getCodesInLot(lotName)
if len(codes) != NumCodes:
self.notify.error('incorrect number of codes from getCodesInLot (%s)' % len(codes))
db._testing = False
yield None
db._testing = True
# code existance query
exists = self._db.codeExists(codes[0])
if not exists:
self.notify.error('codeExists returned false for code %s' % codes[0])
db._testing = False
yield None
db._testing = True
# number of redemptions (not yet redeemed)
redemptions = self._db.getRedemptions(codes[0])
if redemptions != 0:
self.notify.error('incorrect number of redemptions (%s) for not-yet-redeemed code %s' % (redemptions, codes[0], ))
db._testing = False
yield None
db._testing = True
# get lot name from code
ln = self._db.getLotNameFromCode(codes[0])
if ln != lotName:
self.notify.error('incorrect lot name (%s) from code (%s)' % (ln, codes[0]))
db._testing = False
yield None
db._testing = True
# get reward from code
rt, rid = self._db.getRewardFromCode(codes[0])
if rt != RewardType or rid != RewardItemId:
self.notify.error('incorrect reward (%s, %s) from code %s' % (rt, rid))
db._testing = False
yield None
db._testing = True
# redeem code
self._redeemResult = []
self._db.redeemCode(codes[0], TTCodeRedemptionDBConsts.FakeAvId, self.TestRewarder(), self._handleRedeemResult)
if self._redeemResult[0] or self._redeemResult[1]:
self.notify.error('error redeeming code %s for fake avatar %s: %s' % (codes[0], TTCodeRedemptionDBConsts.FakeAvId, self._redeemResult))
db._testing = False
yield None
db._testing = True
# number of redemptions (redeemed)
redemptions = self._db.getRedemptions(codes[0])
if redemptions != 1:
self.notify.error('incorrect number of redemptions (%s) for already-redeemed code %s' % (redemptions, codes[0], ))
db._testing = False
yield None
db._testing = True
# redeem code that has already been redeemed
self._redeemResult = []
self._db.redeemCode(codes[0], TTCodeRedemptionDBConsts.FakeAvId, self.TestRewarder(), self._handleRedeemResult)
if self._redeemResult[0] != TTCodeRedemptionConsts.RedeemErrors.CodeAlreadyRedeemed:
self.notify.error('able to redeem code %s twice' % (codes[0]))
db._testing = False
yield None
db._testing = True
# number of redemptions (redeemed)
redemptions = self._db.getRedemptions(codes[0])
if redemptions != 1:
self.notify.error('incorrect number of redemptions (%s) for already-redeemed code %s' % (redemptions, codes[0], ))
db._testing = False
yield None
db._testing = True
# lookup codes redeemed by avId
c = self._db.lookupCodesRedeemedByAvId(TTCodeRedemptionDBConsts.FakeAvId)
if len(c) != 1:
self.notify.error('lookupCodesRedeemedByAvId returned wrong number of codes: %s' % c)
if c[0] != codes[0]:
self.notify.error('lookupCodesRedeemedByAvId returned wrong code: %s' % c[0])
db._testing = False
yield None
db._testing = True
# get code details
details = self._db.getCodeDetails(codes[0])
if details[TTCodeRedemptionDBConsts.CodeFieldName] != codes[0]:
self.notify.error('incorrect %s (%s) returned by getCodeDetails(%s)' % (TTCodeRedemptionDBConsts.CodeFieldName, details[TTCodeRedemptionDBConsts.CodeFieldName], codes[0]))
if details[TTCodeRedemptionDBConsts.AvatarIdFieldName] != TTCodeRedemptionDBConsts.FakeAvId:
self.notify.error('incorrect %s (%s) returned by getCodeDetails(%s)' % (TTCodeRedemptionDBConsts.AvatarIdFieldName, details[TTCodeRedemptionDBConsts.AvatarIdFieldName], codes[0]))
if details[TTCodeRedemptionDBConsts.RewardTypeFieldName] != RewardType:
self.notify.error('incorrect %s (%s) returned by getCodeDetails(%s)' % (TTCodeRedemptionDBConsts.RewardTypeFieldName, details[TTCodeRedemptionDBConsts.RewardTypeFieldName], codes[0]))
if details[TTCodeRedemptionDBConsts.RewardItemIdFieldName] != RewardItemId:
self.notify.error('incorrect %s (%s) returned by getCodeDetails(%s)' % (TTCodeRedemptionDBConsts.RewardItemIdFieldName, details[TTCodeRedemptionDBConsts.RewardItemIdFieldName], codes[0]))
db._testing = False
yield None
db._testing = True
# get expiration date
exp = self._db.getExpiration(lotName)
if exp != ExpirationDate:
self.notify.error('incorrect expiration date: %s' % exp)
db._testing = False
yield None
db._testing = True
# change expiration date
y = 1111
m = 4
d = 1
NewExp = '%s-%02d-%02d' % (y, m, d)
assert datetime.datetime.fromtimestamp(time.time()) > datetime.datetime(y, m, d)
# make sure it doesn't change the expiration date of all lots
controlLotName = self._getUnusedLotName()
controlCode = self._getUnusedManualCode()
controlExp = '%s-%02d-%02d' % (y, m, d+1)
self._db.createManualLot(controlLotName, controlCode, RewardType, RewardItemId, expirationDate=controlExp)
db._testing = False
yield None
db._testing = True
self._db.setExpiration(lotName, NewExp)
db._testing = False
yield None
db._testing = True
exp = self._db.getExpiration(lotName)
if (exp != NewExp):
self.notify.error('could not change expiration date for lot %s' % lotName)
db._testing = False
yield None
db._testing = True
cExp = self._db.getExpiration(controlLotName)
if (cExp != controlExp):
self.notify.error('setExpiration changed control lot expiration!')
db._testing = False
yield None
db._testing = True
self._db.deleteLot(controlLotName)
db._testing = False
yield None
db._testing = True
# redeem code that is expired
self._redeemResult = []
self._db.redeemCode(codes[1], TTCodeRedemptionDBConsts.FakeAvId, self.TestRewarder(), self._handleRedeemResult)
if self._redeemResult[0] != TTCodeRedemptionConsts.RedeemErrors.CodeIsExpired:
self.notify.error('expired code %s was not flagged upon redeem' % (codes[1]))
db._testing = False
yield None
db._testing = True
# lot deletion
self._db.deleteLot(lotName)
db._testing = False
yield None
db._testing = True
codes = (self._getUnusedManualCode(), self._getUnusedUtf8ManualCode())
for code in codes:
# manual code lot
lotName = self._getUnusedLotName()
self.notify.info('manual code: %s' % (code))
self._db.createManualLot(lotName, code, RewardType, RewardItemId)
if not self._db.lotExists(lotName):
self.notify.error('could not create manual lot %s' % lotName)
if not self._db.codeExists(code):
self.notify.error('could not create manual code %s' % code)
db._testing = False
yield None
db._testing = True
autoLotNames = self._db.getAutoLotNames()
if lotName in autoLotNames:
self.notify.error('manual lot \'%s\' found in getAutoLotNames()' % lotName)
db._testing = False
yield None
db._testing = True
manualLotNames = self._db.getManualLotNames()
if lotName not in manualLotNames:
self.notify.error('manual lot \'%s\' not found in getAutoLotNames()' % lotName)
db._testing = False
yield None
db._testing = True
# number of redemptions (not-yet-redeemed)
redemptions = self._db.getRedemptions(code)
if redemptions != 0:
self.notify.error('incorrect number of redemptions (%s) for not-yet-redeemed code %s' % (redemptions, code, ))
db._testing = False
yield None
db._testing = True
# redeem manually-created code
self._redeemResult = []
self._db.redeemCode(code, TTCodeRedemptionDBConsts.FakeAvId, self.TestRewarder(), self._handleRedeemResult)
if self._redeemResult[0] or self._redeemResult[1]:
self.notify.error('error redeeming code %s for fake avatar %s: %s' % (code, TTCodeRedemptionDBConsts.FakeAvId, self._redeemResult))
db._testing = False
yield None
db._testing = True
# number of redemptions (not-yet-redeemed)
self._db.commitOutstandingRedemptions()
redemptions = self._db.getRedemptions(code)
if redemptions != 1:
self.notify.error('incorrect number of redemptions (%s) for redeemed code %s' % (redemptions, code, ))
db._testing = False
yield None
db._testing = True
# redeem manually-created code again
self._redeemResult = []
self._db.redeemCode(code, TTCodeRedemptionDBConsts.FakeAvId, self.TestRewarder(), self._handleRedeemResult)
if self._redeemResult[0] or self._redeemResult[1]:
self.notify.error('error redeeming code %s again for fake avatar %s: %s' % (code, TTCodeRedemptionDBConsts.FakeAvId, self._redeemResult))
db._testing = False
yield None
db._testing = True
# number of redemptions (not-yet-redeemed)
self._db.commitOutstandingRedemptions()
redemptions = self._db.getRedemptions(code)
if redemptions != 2:
self.notify.error('incorrect number of redemptions (%s) for twice-redeemed code %s' % (redemptions, code, ))
db._testing = False
yield None
db._testing = True
self._db.deleteLot(lotName)
db._testing = False
yield None
db._testing = True
lotNames = self._db.getLotNames()
if lotName in lotNames:
self.notify.error('could not delete code redemption lot \'%s\'' % lotName)
db._testing = False
yield None
db._testing = True
# remove placeholder lots
for lotName in phLots:
self._db.deleteLot(lotName)
db._testing = False
yield None
db._testing = True
break
except TryAgainLater as e:
self.notify.warning('caught TryAgainLater exception during self-test, retrying')
retryStartT = globalClock.getRealTime()
while globalClock.getRealTime() < (retryStartT + retryDelay):
yield None
retryDelay *= 2
self.notify.info('testing done')
db._testing = False
yield Job.Done
class NotFound:
pass
class InfoCache:
NotFound = NotFound
def __init__(self):
self._cache = {}
def clear(self):
self._cache = {}
def cacheInfo(self, key, info):
self._cache[key] = info
def hasInfo(self, key):
return key in self._cache
def getInfo(self, key):
return self._cache.get(key, NotFound)
class TTCodeRedemptionDB(DirectObject):
notify = directNotify.newCategory('TTCodeRedemptionDB')
TryAgainLater = TryAgainLater
DoSelfTest = ConfigVariableBool('code-redemption-self-test', False).getValue()
# optimization that reads in all codes and maps them to their lot
# if the code set gets too large this might use up too much RAM
# you can disable the optimization by turning this config off
CacheAllCodes = ConfigVariableBool('code-redemption-cache-all-codes', True).getValue()
class LotFilter:
All = 'all'
Redeemable = 'redeemable'
NonRedeemable = 'nonRedeemable'
Redeemed = 'redeemed'
Expired = 'expired'
def __init__(self, air):
self.air = air
self.filePath = ConfigVariableString('code-redemption-data-folder', 'data/code_redemption/').getValue()
self.lotsFileName = ConfigVariableString('code-redemption-lots-file', 'lots').getValue()
self.codeSpaceFileName = ConfigVariableString('code-redemption-code-space-file', 'code_space').getValue()
self.codeSetFileName = ConfigVariableString('code-redemption-code-set-file', 'code_set_%s').getValue()
# lot name cache
self._code2lotNameCache = InfoCache()
self._lotName2manualCache = InfoCache()
self._code2rewardCache = InfoCache()
self.doMethodLater(5 * 60, self._cacheClearTask, uniqueName('clearLotNameCache'))
self._manualCode2outstandingRedemptions = {}
self.doMethodLater(1 * 60, self._updateRedemptionsTask, uniqueName('updateRedemptions'))
self._code2lotName = {}
# set to true while doing internal tests
self._testing = False
self._initializedSV = StateVar(False)
self._startTime = globalClock.getRealTime()
self._doingCleanup = False
self._dbInitRetryTimeout = 5
self._doInitialCleanup()
self._refreshCode2lotName()
def _doInitialCleanup(self, task=None):
if not self._initializedSV.get():
self._doCleanup()
if not self._initializedSV.get():
self.doMethodLater(self._dbInitRetryTimeout, self._doInitialCleanup,
uniqueName('codeRedemptionInitialCleanup'))
self._dbInitRetryTimeout *= 2
self.notify.warning('could not initialize MySQL db, trying again later...')
return Task.done
def _doCleanup(self):
if self._doingCleanup:
return
self._doingCleanup = True
if not self._initializedSV.get():
try:
TTCodeRedemptionDBTester.cleanup(self)
except TryAgainLater as e:
pass
else:
self._initializedSV.set(True)
self._doingCleanup = False
def _randFuncCallback(self, randList, randSamplesOnOrder, samples):
randSamplesOnOrder[0] -= len(samples)
randList.extend(samples)
def _refreshCode2lotName(self):
if not self.CacheAllCodes:
return
# update the dict of code -> lotName for all codes
self._code2lotName = {}
lotNames = self.getLotNames()
for lotName in lotNames:
codes = self.getCodesInLot(lotName)
for code in codes:
self._code2lotName[code] = lotName
@staticmethod
def _getExpirationString(expiration):
"""
formats expiration date for JSON
"""
return '%s 23:59:59' % str(expiration)
@staticmethod
def _getNowString():
nowStr = str(datetime.datetime.fromtimestamp(time.time()))
# leave off the fractional seconds
if '.' in nowStr:
nowStr = nowStr[:nowStr.index('.')]
return nowStr
def createManualLot(self, name, code, rewardType, rewardItemId, expirationDate=None):
self.notify.info('creating manual code lot \'%s\', code=%s' % (name, (code), ))
self._doCleanup()
code = TTCodeDict.getFromReadableCode(code)
if self.lotExists(name):
self.notify.error('tried to create lot %s that already exists' % name)
if self.codeExists(code):
self.notify.error('tried to create code %s that already exists' % (code))
# First load lots file
lotsFile = self.getFileName(self.lotsFileName)
lotsData = self.loadLotsFile(lotsFile)
lotId = lotsData[TTCodeRedemptionDBConsts.NextLotIdFieldName]
lot = {
TTCodeRedemptionDBConsts.LotIdFieldName: lotId,
TTCodeRedemptionDBConsts.NameFieldName: name,
TTCodeRedemptionDBConsts.ManualFieldName: True,
TTCodeRedemptionDBConsts.RewardTypeFieldName: rewardType,
TTCodeRedemptionDBConsts.RewardItemIdFieldName: rewardItemId,
TTCodeRedemptionDBConsts.SizeFieldName: 1,
TTCodeRedemptionDBConsts.CreationFieldName: self._getNowString()
}
if expirationDate:
lot[TTCodeRedemptionDBConsts.ExpirationFieldName] = self._getExpirationString(expirationDate)
else:
lot[TTCodeRedemptionDBConsts.ExpirationFieldName] = ''
lotsData[TTCodeRedemptionDBConsts.LotsFieldName].append(lot)
lotsData[TTCodeRedemptionDBConsts.NextLotIdFieldName] = lotId + 1
# Then load Code Set file
codeSetFile = self.getFileName(self.codeSetFileName % (name))
codeSetData = self.loadCodeSetFile(codeSetFile)
codeSet = {
TTCodeRedemptionDBConsts.CodeFieldName: code,
TTCodeRedemptionDBConsts.LotIdFieldName: lotId,
TTCodeRedemptionDBConsts.RedemptionsFieldName: 0
}
codeSetData.append(codeSet)
self.saveFile(lotsFile, lotsData)
self.saveFile(codeSetFile, codeSetData)
self._refreshCode2lotName()
self.notify.info('done')
def createLot(self, randFunc, name, numCodes, rewardType, rewardItemId, expirationDate=None):
"""
generator, yields None while working, yields True when finished
randFunc must take a callback and a number of random samples, and must call the callback
with a list of random 32-bit values of length equal to that specified in the call to randFunc
the random values must be truly random and non-repeatable (see NonRepeatableRandomSource)
"""
self.notify.info('creating code lot \'%s\', %s codes' % (name, numCodes, ))
self._doCleanup()
if self.lotExists(name):
self.notify.error('tried to create lot %s that already exists' % name)
randSampleRequestSize = ConfigVariableInt('code-redemption-rand-request-size', 50).getValue()
randSampleRequestThreshold = 2 * randSampleRequestSize
randSamples = []
randSamplesOnOrder = [0, ]
requestSize = min(numCodes, randSampleRequestSize)
randSamplesOnOrder[0] += requestSize
randFunc(Functor(self._randFuncCallback, randSamples, randSamplesOnOrder), requestSize)
# First load code space file
codeSpaceFile = self.getFileName(self.codeSpaceFileName)
codeSpaceData = self.loadCodeSpaceFile(codeSpaceFile)
codeLength = codeSpaceData[TTCodeRedemptionDBConsts.CodeLengthFieldName]
nextCodeValue = codeSpaceData[TTCodeRedemptionDBConsts.NextCodeValueFieldName]
startSerialNum = nextCodeValue
# Second load lots file
lotsFile = self.getFileName(self.lotsFileName)
lotsData = self.loadLotsFile(lotsFile)
lotId = lotsData[TTCodeRedemptionDBConsts.NextLotIdFieldName]
lot = {
TTCodeRedemptionDBConsts.LotIdFieldName: lotId,
TTCodeRedemptionDBConsts.NameFieldName: name,
TTCodeRedemptionDBConsts.ManualFieldName: False,
TTCodeRedemptionDBConsts.RewardTypeFieldName: rewardType,
TTCodeRedemptionDBConsts.RewardItemIdFieldName: rewardItemId,
TTCodeRedemptionDBConsts.SizeFieldName: numCodes,
TTCodeRedemptionDBConsts.CreationFieldName: self._getNowString()
}
if expirationDate:
lot[TTCodeRedemptionDBConsts.ExpirationFieldName] = self._getExpirationString(expirationDate)
else:
lot[TTCodeRedemptionDBConsts.ExpirationFieldName] = ''
lotsData[TTCodeRedemptionDBConsts.LotsFieldName].append(lot)
lotsData[TTCodeRedemptionDBConsts.NextLotIdFieldName] = lotId + 1
# Then load Code Set file
codeSetFile = self.getFileName(self.codeSetFileName % (name))
codeSetData = self.loadCodeSetFile(codeSetFile)
codesLeft = numCodes
curSerialNum = startSerialNum
numCodeValues = TTCodeDict.getNumUsableValuesInCodeSpace(codeLength)
n = 0
while codesLeft:
numCodesRequested = (len(randSamples) + randSamplesOnOrder[0])
if numCodesRequested < codesLeft:
if numCodesRequested < randSampleRequestThreshold:
requestSize = min(codesLeft, randSampleRequestSize)
randSamplesOnOrder[0] += requestSize
randFunc(Functor(self._randFuncCallback, randSamples, randSamplesOnOrder), requestSize)
if len(randSamples) == 0:
yield None
continue
# r in [0,1) but truly random (non-repeatable)
r = randSamples.pop(0) / float(1 << 32)
assert 0. <= r < 1.
# this produces the 1 in N chance of guessing a correct code
# each code is given a chunk of code space, of size N, and the actual value of the
# code is chosen from that section of code space using a true random source
# that means there's no way to guess a valid code based on observation of other codes
randScatter = int(r * TTCodeDict.BruteForceFactor)
assert 0 <= randScatter < TTCodeDict.BruteForceFactor
value = (curSerialNum * TTCodeDict.BruteForceFactor) + randScatter
obfValue = TTCodeDict.getObfuscatedCodeValue(value, codeLength)
code = TTCodeDict.getCodeFromValue(obfValue, codeLength)
codeSet = {
TTCodeRedemptionDBConsts.CodeFieldName: code,
TTCodeRedemptionDBConsts.LotIdFieldName: lotId,
TTCodeRedemptionDBConsts.RedemptionsFieldName: 0
}
codeSetData.append(codeSet)
codesLeft -= 1
curSerialNum += 1
if curSerialNum >= numCodeValues:
curSerialNum = 0
codeLength += 1
numCodeValues = TTCodeDict.getNumUsableValuesInCodeSpace(codeLength)
n = n + 1
if (n % 100) == 0:
yield None
codeSpaceData[TTCodeRedemptionDBConsts.CodeLengthFieldName] = codeLength
codeSpaceData[TTCodeRedemptionDBConsts.NextCodeValueFieldName] = curSerialNum
self.saveFile(codeSpaceFile, codeSpaceData)
self.saveFile(lotsFile, lotsData)
self.saveFile(codeSetFile, codeSetData)
self._refreshCode2lotName()
self.notify.info('done')
yield True
def deleteLot(self, lotName):
self.notify.info('deleting code lot \'%s\'' % (lotName, ))
self._doCleanup()
self._clearCaches()
self.deleteFile(self.getFileName(self.codeSetFileName % (lotName)))
lotsFile = self.getFileName(self.lotsFileName)
lotsData = self.loadLotsFile(lotsFile)
for idx, obj in enumerate(lotsData[TTCodeRedemptionDBConsts.LotsFieldName]):
if obj[TTCodeRedemptionDBConsts.NameFieldName] == lotName:
lotsData[TTCodeRedemptionDBConsts.LotsFieldName].pop(idx)
self.saveFile(lotsFile, lotsData)
self._refreshCode2lotName()
def getLotNames(self):
assert self.notify.debugCall()
self._doCleanup()
lotNames = []
lotsData = self.loadLotsFile(self.getFileName(self.lotsFileName))
for lot in lotsData[TTCodeRedemptionDBConsts.LotsFieldName]:
lotName = lot[TTCodeRedemptionDBConsts.NameFieldName]
if not self._testing:
if TTCodeRedemptionDBConsts.TestLotName in lotName:
continue
lotNames.append(lotName)
return lotNames
def getAutoLotNames(self):
"""
returns names of all code lots that were automatically generated
"""
assert self.notify.debugCall()
self._doCleanup()
autoLotNames = []
lotsData = self.loadLotsFile(self.getFileName(self.lotsFileName))
for lot in lotsData[TTCodeRedemptionDBConsts.LotsFieldName]:
if lot[TTCodeRedemptionDBConsts.ManualFieldName] == False:
lotName = lot[TTCodeRedemptionDBConsts.NameFieldName]
if not self._testing:
if TTCodeRedemptionDBConsts.TestLotName in lotName:
continue
autoLotNames.append(lotName)
return autoLotNames
def getManualLotNames(self):
"""
returns names of all code lots that were manually generated
"""
assert self.notify.debugCall()
self._doCleanup()
manualLotNames = []
lotsData = self.loadLotsFile(self.getFileName(self.lotsFileName))
for lot in lotsData[TTCodeRedemptionDBConsts.LotsFieldName]:
if lot[TTCodeRedemptionDBConsts.ManualFieldName] == True:
lotName = lot[TTCodeRedemptionDBConsts.NameFieldName]
if not self._testing:
if TTCodeRedemptionDBConsts.TestLotName in lotName:
continue
manualLotNames.append(lotName)
return manualLotNames
def getExpirationLotNames(self):
"""
returns names of all code lots that have expiration dates
"""
assert self.notify.debugCall()
self._doCleanup()
lotNames = []
lotsData = self.loadLotsFile(self.getFileName(self.lotsFileName))
for lot in lotsData[TTCodeRedemptionDBConsts.LotsFieldName]:
if lot[TTCodeRedemptionDBConsts.ExpirationFieldName] != '':
lotName = lot[TTCodeRedemptionDBConsts.NameFieldName]
if not self._testing:
if TTCodeRedemptionDBConsts.TestLotName in lotName:
continue
lotNames.append(lotName)
return lotNames
def getCodesInLot(self, lotName, justCode=True, filter=None):
# if justCode, returns list of codes
# if not justCode, returns list of dict of field->value
assert self.notify.debugCall()
self._doCleanup()
if filter is None:
filter = self.LotFilter.All
# TODO: FILTERING
lotsData = self.loadLotsFile(self.getFileName(self.lotsFileName))
codeSetData = self.loadCodeSetFile(self.getFileName(self.codeSetFileName % (lotName)))
if justCode:
codes = []
for codeSet in codeSetData:
code = str(codeSet[TTCodeRedemptionDBConsts.CodeFieldName])
codes.append(code)
result = codes
else:
for codeSet in codeSetData:
codeSet[TTCodeRedemptionDBConsts.CodeFieldName] = str(codeSet[TTCodeRedemptionDBConsts.CodeFieldName])
result = codeSetData
return result
def _clearCaches(self):
self._code2lotNameCache.clear()
self._lotName2manualCache.clear()
self._code2rewardCache.clear()
def _cacheClearTask(self, task):
self._clearCaches()
return Task.again
def commitOutstandingRedemptions(self):
if len(self._manualCode2outstandingRedemptions):
self.notify.info('committing cached manual code redemption counts to DB')
for key in self._manualCode2outstandingRedemptions.keys():
code, lotName = key
count = self._manualCode2outstandingRedemptions[key]
self._updateRedemptionCount(code, True, None, lotName, count)
self._manualCode2outstandingRedemptions = {}
def _updateRedemptionsTask(self, task):
try:
self.commitOutstandingRedemptions()
except TryAgainLater as e:
pass
return Task.again
def getLotNameFromCode(self, code):
assert self.notify.debugCall()
code = TTCodeDict.getFromReadableCode(code)
assert TTCodeDict.isLegalCode(code)
if self.CacheAllCodes:
return self._code2lotName.get(code, None)
cachedLotName = self._code2lotNameCache.getInfo(code)
if cachedLotName is not self._code2lotNameCache.NotFound:
return cachedLotName
assert self.notify.debug('lotNameFromCode CACHE MISS (%s)' % (code))
self._doCleanup()
lotNames = self.getLotNames()
result = None
for lotName in lotNames:
codeSetData = self.loadCodeSetFile(self.getFileName(self.codeSetFileName % (lotName)))
for codeSet in codeSetData:
if codeSet[TTCodeRedemptionDBConsts.CodeFieldName] == code:
result = lotName
break
if result is not None:
self._code2lotNameCache.cacheInfo(code, result)
return result
def getRewardFromCode(self, code):
assert self.notify.debugCall()
code = TTCodeDict.getFromReadableCode(code)
assert TTCodeDict.isLegalCode(code)
lotName = self.getLotNameFromCode(code)
assert lotName is not None
cachedReward = self._code2rewardCache.getInfo(code)
if cachedReward is not self._code2rewardCache.NotFound:
return cachedReward
assert self.notify.debug('reward from code CACHE MISS (%s)' % (code))
self._doCleanup()
lotsData = self.loadLotsFile(self.getFileName(self.lotsFileName))
codeSetData = self.loadCodeSetFile(self.getFileName(self.codeSetFileName % (lotName)))
lotId = -1
rows = []
for codeSet in codeSetData:
if codeSet[TTCodeRedemptionDBConsts.CodeFieldName] == code:
lotId = codeSet[TTCodeRedemptionDBConsts.LotIdFieldName]
break
for lot in lotsData[TTCodeRedemptionDBConsts.LotsFieldName]:
if lot[TTCodeRedemptionDBConsts.LotIdFieldName] == lotId:
rows.append(lot)
assert len(rows) == 1
reward = (int(rows[0][TTCodeRedemptionDBConsts.RewardTypeFieldName]), int(rows[0][TTCodeRedemptionDBConsts.RewardItemIdFieldName]))
self._code2rewardCache.cacheInfo(code, reward)
return reward
def lotExists(self, lotName):
return lotName in self.getLotNames()
def codeExists(self, code):
return self.getLotNameFromCode(code) != None
def getRedemptions(self, code):
assert self.notify.debugCall()
self._doCleanup()
code = TTCodeDict.getFromReadableCode(code)
lotName = self.getLotNameFromCode(code)
if lotName is None:
self.notify.error('getRedemptions: could not find code %s' % (code))
codeSetData = self.loadCodeSetFile(self.getFileName(self.codeSetFileName % (lotName)))
for codeSet in codeSetData:
if codeSet[TTCodeRedemptionDBConsts.CodeFieldName] == code:
return codeSet[TTCodeRedemptionDBConsts.RedemptionsFieldName]
return 0
def redeemCode(self, code, avId, rewarder, callback):
assert self.notify.debugCall()
self._doCleanup()
# callback takes a RedeemError
# 'code' can come from a client, treat with care
origCode = code
code = TTCodeDict.getFromReadableCode(code)
assert TTCodeDict.isLegalCode(code)
lotName = self.getLotNameFromCode(code)
if lotName is None:
self.air.writeServerEvent('invalidCodeRedemption', avId, '%s' % ((origCode), ))
callback(TTCodeRedemptionConsts.RedeemErrors.CodeDoesntExist, 0)
return
lotsData = self.loadLotsFile(self.getFileName(self.lotsFileName))
codeSetData = self.loadCodeSetFile(self.getFileName(self.codeSetFileName % (lotName)))
cachedManual = self._lotName2manualCache.getInfo(lotName)
if cachedManual is not self._lotName2manualCache.NotFound:
manualCode = cachedManual
else:
assert self.notify.debug('manualFromCode CACHE MISS (%s)' % (code))
rows = []
for lot in lotsData[TTCodeRedemptionDBConsts.LotsFieldName]:
if lot[TTCodeRedemptionDBConsts.NameFieldName] == lotName:
rows.append(lot)
assert len(rows) == 1
manualCode = (rows[0][TTCodeRedemptionDBConsts.ManualFieldName] == True)
self._lotName2manualCache.cacheInfo(lotName, manualCode)
if not manualCode:
rows = []
for codeSet in codeSetData:
if codeSet[TTCodeRedemptionDBConsts.CodeFieldName] == code:
codeSet = codeSet
break
for lot in lotsData[TTCodeRedemptionDBConsts.LotsFieldName]:
if lot[TTCodeRedemptionDBConsts.LotIdFieldName] == codeSet[TTCodeRedemptionDBConsts.LotIdFieldName] and (lot[TTCodeRedemptionDBConsts.ExpirationFieldName] == '' or datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S') <= lot[TTCodeRedemptionDBConsts.ExpirationFieldName]):
rows.append(codeSet)
assert len(rows) <= 1
if not manualCode:
if len(rows) == 0:
# code is expired
callback(TTCodeRedemptionConsts.RedeemErrors.CodeIsExpired, 0)
return
redemptions = rows[0][TTCodeRedemptionDBConsts.RedemptionsFieldName]
if redemptions > 0:
callback(TTCodeRedemptionConsts.RedeemErrors.CodeAlreadyRedeemed, 0)
return
rewardTypeId, rewardItemId = self.getRewardFromCode(code)
rewarder._giveReward(avId, rewardTypeId, rewardItemId, Functor(
self._handleRewardResult, code, manualCode, avId, lotName, rewardTypeId, rewardItemId,
callback))
def _updateRedemptionCount(self, code, manualCode, avId, lotName, count):
assert self.notify.debugCall()
codeSetFile = self.getFileName(self.codeSetFileName % (lotName))
codeSetData = self.loadCodeSetFile(codeSetFile)
for codeSet in codeSetData:
if codeSet[TTCodeRedemptionDBConsts.CodeFieldName] == code:
codeSet[TTCodeRedemptionDBConsts.RedemptionsFieldName] = codeSet[TTCodeRedemptionDBConsts.RedemptionsFieldName] + count
if not manualCode:
codeSet[TTCodeRedemptionDBConsts.AvatarIdFieldName] = avId
self.saveFile(codeSetFile, codeSetData)
def _handleRewardResult(self, code, manualCode, avId, lotName, rewardTypeId, rewardItemId, callback, result):
assert self.notify.debugCall()
self._doCleanup()
assert TTCodeDict.isLegalCode(code)
awardMgrResult = result
if awardMgrResult:
callback(TTCodeRedemptionConsts.RedeemErrors.AwardCouldntBeGiven, awardMgrResult)
return
if manualCode:
# queue up redemption count for manual code and write every N minutes
key = (code, lotName)
self._manualCode2outstandingRedemptions.setdefault(key, 0)
self._manualCode2outstandingRedemptions[key] += 1
else:
self._updateRedemptionCount(code, manualCode, avId, lotName, 1)
if not self._testing:
self.air.writeServerEvent('codeRedeemed', avId, '%s|%s|%s|%s' % (
(choice(manualCode, code, TTCodeDict.getReadableCode(code))),
lotName, rewardTypeId, rewardItemId, ))
callback(TTCodeRedemptionConsts.RedeemErrors.Success, awardMgrResult)
def lookupCodesRedeemedByAvId(self, avId):
assert self.notify.debugCall()
self._doCleanup()
codes = []
# manual lots don't record redeemer avIds since they are single-code-multi-toon
for lotName in self.getAutoLotNames():
codeSetData = self.loadCodeSetFile(self.getFileName(self.codeSetFileName % (lotName)))
for codeSet in codeSetData:
if codeSet.get(TTCodeRedemptionDBConsts.AvatarIdFieldName):
if codeSet[TTCodeRedemptionDBConsts.AvatarIdFieldName] == avId:
code = str(codeSet[TTCodeRedemptionDBConsts.CodeFieldName])
codes.append(code)
return codes
def getExpiration(self, lotName):
assert self.notify.debugCall()
self._doCleanup()
lotsData = self.loadLotsFile(self.getFileName(self.lotsFileName))
expiration = ''
for lot in lotsData[TTCodeRedemptionDBConsts.LotsFieldName]:
if lot[TTCodeRedemptionDBConsts.NameFieldName] == lotName:
expiration = str(datetime.datetime.strptime(lot[TTCodeRedemptionDBConsts.ExpirationFieldName], '%Y-%m-%d %H:%M:%S').date())
return expiration
def setExpiration(self, lotName, expiration):
assert self.notify.debugCall()
self._doCleanup()
lotsFile = self.getFileName(self.lotsFileName)
lotsData = self.loadLotsFile(lotsFile)
for lot in lotsData[TTCodeRedemptionDBConsts.LotsFieldName]:
if lot[TTCodeRedemptionDBConsts.NameFieldName] == lotName:
lot[TTCodeRedemptionDBConsts.ExpirationFieldName] = self._getExpirationString(expiration)
self.saveFile(lotsFile, lotsData)
def getCodeDetails(self, code):
assert self.notify.debugCall()
self._doCleanup()
lotsData = self.loadLotsFile(self.getFileName(self.lotsFileName))
for lotName in self.getLotNames():
codeSetData = self.loadCodeSetFile(self.getFileName(self.codeSetFileName % (lotName)))
for codeSet in codeSetData:
if codeSet[TTCodeRedemptionDBConsts.CodeFieldName] == TTCodeDict.getFromReadableCode(code):
codeSet[TTCodeRedemptionDBConsts.CodeFieldName] = str(codeSet[TTCodeRedemptionDBConsts.CodeFieldName])
codeSet[TTCodeRedemptionDBConsts.RewardTypeFieldName] = 0
codeSet[TTCodeRedemptionDBConsts.RewardItemIdFieldName] = 0
for lot in lotsData[TTCodeRedemptionDBConsts.LotsFieldName]:
if lot[TTCodeRedemptionDBConsts.LotIdFieldName] == codeSet[TTCodeRedemptionDBConsts.LotIdFieldName]:
codeSet[TTCodeRedemptionDBConsts.RewardTypeFieldName] = lot[TTCodeRedemptionDBConsts.RewardTypeFieldName]
codeSet[TTCodeRedemptionDBConsts.RewardItemIdFieldName] = lot[TTCodeRedemptionDBConsts.RewardItemIdFieldName]
return codeSet
self.notify.error('code \'%s\' not found' % (code))
if __debug__:
def runTests(self):
self._doRunTests(self._initializedSV.get())
self._runTestsFC = FunctionCall(self._doRunTests, self._initializedSV)
def _doRunTests(self, initialized):
if initialized and self.DoSelfTest:
jobMgr.add(TTCodeRedemptionDBTester(self))
# Custom for JSONs
def loadLotsFile(self, fileName):
data = {TTCodeRedemptionDBConsts.LotsFieldName: [], TTCodeRedemptionDBConsts.NextLotIdFieldName: 0}
try:
with open(self.filePath + fileName, 'r') as f:
data = json.load(f)
fileExists = True
except:
fileExists = False
if not fileExists:
# Use self.update() to setup initial db:
self.saveFile(fileName, data)
return data
def loadCodeSpaceFile(self, fileName):
data = {TTCodeRedemptionDBConsts.CodeLengthFieldName: 4, TTCodeRedemptionDBConsts.NextCodeValueFieldName: 0}
try:
with open(self.filePath + fileName, 'r') as f:
data = json.load(f)
fileExists = True
except:
fileExists = False
if not fileExists:
# Use self.update() to setup initial db:
self.saveFile(fileName, data)
return data
def loadCodeSetFile(self, fileName):
data = []
try:
with open(self.filePath + fileName, 'r') as f:
data = json.load(f)
fileExists = True
except:
fileExists = False
return data
def saveFile(self, fileName, jsonData):
if not os.path.exists(self.filePath):
os.makedirs(self.filePath)
with open(self.filePath + fileName, 'w+') as f:
json.dump(jsonData, f, indent=4)
def deleteFile(self, fileName):
if os.path.exists(self.filePath + fileName):
os.remove(self.filePath + fileName)
def getFileName(self, fileName):
return '%s.json' % (fileName)