diff --git a/sopel/bot.py b/sopel/bot.py index 9dc1d78d58..15d50c5c58 100644 --- a/sopel/bot.py +++ b/sopel/bot.py @@ -392,35 +392,43 @@ def say(self, text, recipient, max_messages=1): try: self.sending.acquire() - # No messages within the last 3 seconds? Go ahead! - # Otherwise, wait so it's been at least 0.8 seconds + penalty - recipient_id = Identifier(recipient) - - if recipient_id not in self.stack: - self.stack[recipient_id] = [] - elif self.stack[recipient_id]: - elapsed = time.time() - self.stack[recipient_id][-1][0] - if elapsed < 3: - penalty = float(max(0, len(text) - 40)) / 70 - wait = min(0.8 + penalty, 2) # Never wait more than 2 seconds - if elapsed < wait: - time.sleep(wait - elapsed) - - # Loop detection - messages = [m[1] for m in self.stack[recipient_id][-8:]] - - # If what we about to send repeated at least 5 times in the - # last 2 minutes, replace with '...' - if messages.count(text) >= 5 and elapsed < 120: - text = '...' - if messages.count('...') >= 3: - # If we said '...' 3 times, discard message - return + recipient_stack = self.stack.setdefault(recipient_id, { + 'messages': [], + 'flood_left': self.config.core.flood_burst_lines, + }) + + # If flood bucket is empty, refill the appropriate number of lines + # based on how long it's been since our last message to recipient + if not recipient_stack['flood_left']: + elapsed = time.time() - recipient_stack['messages'][-1][0] + recipient_stack['flood_left'] = min( + self.config.core.flood_burst_lines, + int(elapsed) * self.config.core.flood_refill_rate) + + # If it's too soon to send another message, wait + if not recipient_stack['flood_left']: + elapsed = time.time() - recipient_stack['messages'][-1][0] + penalty = float(max(0, len(text) - 50)) / 70 + wait = min(self.config.core.flood_empty_wait + penalty, 2) # Maximum wait time is 2 sec + if elapsed < wait: + time.sleep(wait - elapsed) + + # Loop detection + messages = [m[1] for m in recipient_stack['messages'][-8:]] + + # If what we're about to send repeated at least 5 times in the last + # two minutes, replace it with '...' + if messages.count(text) >= 5 and elapsed < 120: + text = '...' + if messages.count('...') >= 3: + # If we've already said '...' 3 times, discard message + return self.write(('PRIVMSG', recipient), text) - self.stack[recipient_id].append((time.time(), self.safe(text))) - self.stack[recipient_id] = self.stack[recipient_id][-10:] + recipient_stack['flood_left'] = max(0, recipient_stack['flood_left'] - 1) + recipient_stack['messages'].append((time.time(), self.safe(text))) + recipient_stack['messages'] = recipient_stack['messages'][-10:] finally: self.sending.release() # Now that we've sent the first part, we need to send the rest. Doing diff --git a/sopel/config/core_section.py b/sopel/config/core_section.py index 5cef54cd13..f81617d20a 100644 --- a/sopel/config/core_section.py +++ b/sopel/config/core_section.py @@ -258,3 +258,12 @@ def homedir(self): verify_ssl = ValidatedAttribute('verify_ssl', bool, default=True) """Whether to require a trusted SSL certificate for SSL connections.""" + + flood_burst_lines = ValidatedAttribute('flood_burst_lines', int, default=4) + """How many messages can be sent in burst mode.""" + + flood_empty_wait = ValidatedAttribute('flood_empty_wait', float, default=0.7) + """How long to wait between sending messages when not in burst mode, in seconds.""" + + flood_refill_rate = ValidatedAttribute('flood_refill_rate', int, default=1) + """How quickly burst mode recovers, in messages per second."""