181 lines
7.5 KiB
Python
181 lines
7.5 KiB
Python
from direct.directnotify.DirectNotifyGlobal import directNotify
|
|
from direct.showbase.DirectObject import DirectObject
|
|
from direct.showbase.PythonUtil import formatTimeExact
|
|
from panda3d.core import *
|
|
|
|
Settings = ScratchPad(
|
|
DetectWindow=ConfigVariableDouble('code-redemption-spam-detect-window', 30.).getValue(), # minutes
|
|
DetectThreshold=ConfigVariableInt('code-redemption-spam-detect-threshold', 10).getValue(),
|
|
FirstPenalty=ConfigVariableDouble('code-redemption-spam-first-penalty', .5).getValue(), # minutes
|
|
PenaltyMultiplier=ConfigVariableDouble('code-redemption-spam-penalty-multiplier', 2.).getValue(),
|
|
MaxPenaltyDays=ConfigVariableDouble('code-redemption-spam-max-penalty-days', 2.).getValue(),
|
|
PenaltyResetDays=ConfigVariableDouble('code-redemption-penalty-reset-days', 7.).getValue(),
|
|
)
|
|
|
|
|
|
class TTCodeRedemptionSpamDetector:
|
|
notify = directNotify.newCategory('TTCodeRedemptionSpamDetector')
|
|
|
|
def __init__(self):
|
|
self._avId2tracker = {}
|
|
if __dev__:
|
|
#self._tester = TTCRSDTester(self)
|
|
pass
|
|
self._cullTask = taskMgr.doMethodLater(10 * 60, self._cullTrackers, uniqueName('cullCodeSpamTrackers'))
|
|
|
|
def destroy(self):
|
|
if __dev__:
|
|
#self._tester.destroy()
|
|
self._tester = None
|
|
|
|
def codeSubmitted(self, avId):
|
|
if avId not in self._avId2tracker:
|
|
self._avId2tracker[avId] = TTCRSDTracker(avId)
|
|
self._avId2tracker[avId].codeSubmitted()
|
|
|
|
def avIsBlocked(self, avId):
|
|
tracker = self._avId2tracker.get(avId)
|
|
if tracker:
|
|
return tracker.avIsBlocked()
|
|
return False
|
|
|
|
def _cullTrackers(self, task=None):
|
|
# remove records for avIds that have gone long enough without spamming
|
|
avIds = list(self._avId2tracker.keys())
|
|
for avId in avIds:
|
|
tracker = self._avId2tracker.get(avId)
|
|
if tracker.isExpired():
|
|
self.notify.debug('culling code redemption spam tracker for %s' % avId)
|
|
self._avId2tracker.pop(avId)
|
|
return task.again
|
|
|
|
|
|
class TTCRSDTracker:
|
|
notify = directNotify.newCategory('TTCodeRedemptionSpamDetector')
|
|
|
|
def __init__(self, avId):
|
|
self._avId = avId
|
|
self._timestamps = []
|
|
self._lastTimestamp = None
|
|
self._penaltyDuration = 0
|
|
self._penaltyUntil = 0
|
|
|
|
def codeSubmitted(self):
|
|
now = globalClock.getRealTime()
|
|
self.notify.debug('codeSubmitted by %s @ %s' % (self._avId, now))
|
|
if self._penaltyActive():
|
|
return
|
|
self._timestamps.append(now)
|
|
self._lastTimestamp = now
|
|
self.update()
|
|
|
|
def isExpired(self):
|
|
if self._lastTimestamp is None:
|
|
return True
|
|
now = globalClock.getRealTime()
|
|
# if they've gone for X days without spamming, we can wipe that toon's record
|
|
amnestyDelay = Settings.PenaltyResetDays * 24 * 60 * 60
|
|
return now > (self._lastTimestamp + amnestyDelay)
|
|
|
|
def update(self):
|
|
self._trimTimestamps()
|
|
if (not self._penaltyActive()) and self._overThreshold():
|
|
if self._penaltyDuration == 0:
|
|
self._penaltyDuration = Settings.FirstPenalty * 60 # seconds/min
|
|
else:
|
|
self._penaltyDuration = self._penaltyDuration * Settings.PenaltyMultiplier
|
|
MaxPenaltySecs = Settings.MaxPenaltyDays * 24 * 60 * 60
|
|
if self._penaltyDuration > MaxPenaltySecs:
|
|
self._penaltyDuration = MaxPenaltySecs
|
|
self._penaltyUntil = globalClock.getRealTime() + self._penaltyDuration
|
|
self._timestamps = self._timestamps[Settings.DetectThreshold:]
|
|
durationStr = formatTimeExact(self._penaltyDuration)
|
|
self.notify.info('time penalty for %s: %s' % (self._avId, durationStr))
|
|
|
|
def avIsBlocked(self):
|
|
self.update()
|
|
return self._penaltyActive()
|
|
|
|
def _trimTimestamps(self):
|
|
now = globalClock.getRealTime()
|
|
cutoff = now - (Settings.DetectWindow * 60) # seconds/min
|
|
while len(self._timestamps):
|
|
if self._timestamps[0] < cutoff:
|
|
self._timestamps = self._timestamps[1:]
|
|
else:
|
|
break
|
|
|
|
def _penaltyActive(self):
|
|
return globalClock.getRealTime() < self._penaltyUntil
|
|
|
|
def _overThreshold(self):
|
|
return len(self._timestamps) > Settings.DetectThreshold
|
|
|
|
|
|
if __dev__:
|
|
class TTCRSDTester(DirectObject):
|
|
notify = directNotify.newCategory('TTCodeRedemptionSpamDetector')
|
|
|
|
def __init__(self, detector):
|
|
self._detector = detector
|
|
self._idGen = SerialNumGen()
|
|
self.notify.info('starting tests...')
|
|
self._thresholdTest()
|
|
self._timeoutTest()
|
|
|
|
def destroy(self):
|
|
self._detector = None
|
|
|
|
def _thresholdTest(self):
|
|
avId = next(self._idGen)
|
|
for i in range(Settings.DetectThreshold+1):
|
|
self._detector.codeSubmitted(avId)
|
|
if i < Settings.DetectThreshold:
|
|
assert not self._detector.avIsBlocked(avId)
|
|
else:
|
|
assert self._detector.avIsBlocked(avId)
|
|
self.notify.info('threshold test passed.')
|
|
|
|
def _timeoutTest(self):
|
|
avId = next(self._idGen)
|
|
for i in range(Settings.DetectThreshold+1):
|
|
self._detector.codeSubmitted(avId)
|
|
assert self._detector.avIsBlocked(avId)
|
|
self._timeoutTestStartT = globalClock.getRealTime()
|
|
penaltyDuration = Settings.FirstPenalty * 60
|
|
self._timeoutTestEventT = penaltyDuration
|
|
self.doMethodLater(Settings.FirstPenalty * 60 * .5, Functor(self._timeoutEarlyTest, avId),
|
|
uniqueName('timeoutEarlyTest'))
|
|
self.doMethodLater(Settings.FirstPenalty * 60 * 10, Functor(self._timeoutLateTest, avId),
|
|
uniqueName('timeoutLateTest'))
|
|
|
|
def _timeoutEarlyTest(self, avId, task=None):
|
|
# only do this test if we didn't chug
|
|
if (globalClock.getRealTime() - self._timeoutTestStartT) < (self._timeoutTestEventT * .9):
|
|
assert self._detector.avIsBlocked(avId)
|
|
return task.done
|
|
|
|
def _timeoutLateTest(self, avId, task=None):
|
|
assert not self._detector.avIsBlocked(avId)
|
|
for i in range(Settings.DetectThreshold+1):
|
|
self._detector.codeSubmitted(avId)
|
|
assert self._detector.avIsBlocked(avId)
|
|
self._timeoutLateTestStartT = globalClock.getRealTime()
|
|
penaltyDuration = Settings.PenaltyMultiplier * Settings.FirstPenalty * 60
|
|
self._timeoutLateTestEventT = penaltyDuration
|
|
self.doMethodLater(penaltyDuration * .5, Functor(self._timeoutSecondEarlyTest, avId),
|
|
uniqueName('timeoutSecondEarlyTest'))
|
|
self.doMethodLater(penaltyDuration * 1.5, Functor(self._timeoutSecondLateTest, avId),
|
|
uniqueName('timeoutSecondLateTest'))
|
|
return task.done
|
|
|
|
def _timeoutSecondEarlyTest(self, avId, task=None):
|
|
# only do this test if we didn't chug
|
|
if (globalClock.getRealTime() - self._timeoutLateTestStartT) < (self._timeoutLateTestEventT * .9):
|
|
assert self._detector.avIsBlocked(avId)
|
|
return task.done
|
|
|
|
def _timeoutSecondLateTest(self, avId, task=None):
|
|
assert not self._detector.avIsBlocked(avId)
|
|
self.notify.info('timeout test passed.')
|
|
return task.done
|