Skip to content

Commit f7bf3da

Browse files
authored
Merge pull request #1929 from Exirel/flood-penalty-long-message
irc, config: config options for flood penalty on longer messages
2 parents 528e1b8 + 6cdc547 commit f7bf3da

File tree

3 files changed

+160
-12
lines changed

3 files changed

+160
-12
lines changed

docs/source/configuration.rst

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -253,15 +253,26 @@ Flood Prevention
253253
----------------
254254

255255
In order to avoid flooding the server, Sopel has a built-in flood prevention
256-
mechanism. It can be controlled with several directives:
256+
mechanism. The flood burst limit can be controlled with these directives:
257257

258258
* :attr:`~CoreSection.flood_burst_lines`: the number of messages
259259
that can be sent before triggering the throttle mechanism.
260-
* :attr:`~CoreSection.flood_empty_wait`: time to wait once burst limit has been
261-
reached before sending a new message.
262260
* :attr:`~CoreSection.flood_refill_rate`: how much time (in seconds) must be
263261
spent before recovering flood limit.
264262

263+
The wait time when the flood limit is reached can be controlled with these:
264+
265+
* :attr:`~CoreSection.flood_empty_wait`: time to wait once burst limit has been
266+
reached before sending a new message.
267+
* :attr:`~CoreSection.flood_max_wait`: absolute maximum time to wait before
268+
sending a new message once the burst limit has been reached.
269+
270+
And the extra wait penalty for longer messages can be controlled with these:
271+
272+
* :attr:`~CoreSection.flood_text_length`: maximum size of messages before they
273+
start getting an extra wait penalty.
274+
* :attr:`~CoreSection.flood_penalty_ratio`: ratio used to compute said penalty.
275+
265276
For example this configuration::
266277

267278
[core]
@@ -273,15 +284,50 @@ will allow 10 messages at once before triggering the throttle mechanism, then
273284
it'll wait 0.5s before sending a new message, and refill the burst limit every
274285
2 seconds.
275286

287+
The wait time **cannot be longer** than :attr:`~CoreSection.flood_max_wait` (2s
288+
by default). This maximum wait time includes any potential extra penalty for
289+
longer messages.
290+
291+
Messages that are longer than :attr:`~CoreSection.flood_text_length` get an
292+
extra wait penalty. The penalty is computed using a penalty ratio (controlled
293+
by :attr:`~CoreSection.flood_penalty_ratio`, which is 1.4 by default)::
294+
295+
length_overflow = max(0, (len(text) - flood_text_length))
296+
extra_penalty = length_overflow / (flood_text_length * flood_penalty_ratio)
297+
298+
For example with a message of 80 characters, the added extra penalty will be::
299+
300+
length_overflow = max(0, 80 - 50) # == 30
301+
extra_penalty = 30 / (50 * 1.4) # == 0.428s (approximately)
302+
303+
With the default configuration, it means a minimum wait time of 0.928s before
304+
sending any new message (0.5s + 0.428s).
305+
306+
You can **deactivate** this extra wait penalty by setting
307+
:attr:`~CoreSection.flood_penalty_ratio` to 0.
308+
276309
The default configuration works fine with most tested networks, but individual
277310
bots' owners are invited to tweak as necessary to respect their network's flood
278311
policy.
279312

280313
.. versionadded:: 7.0
281314

282-
Flood prevention has been modified in Sopel 7.0 and these configuration
283-
options have been added: ``flood_burst_lines``, ``flood_empty_wait``, and
284-
``flood_refill_rate``.
315+
Additional configuration options: ``flood_burst_lines``, ``flood_empty_wait``,
316+
and ``flood_refill_rate``.
317+
318+
.. versionadded:: 7.1
319+
320+
Even more additional configuration options: ``flood_max_wait``,
321+
``flood_text_length``, and ``flood_penalty_ratio``.
322+
323+
It is now possible to deactivate the extra penalty for longer messages by
324+
setting ``flood_penalty_ratio`` to 0.
325+
326+
.. note::
327+
328+
``@dgw`` said once about Sopel's flood protection logic:
329+
330+
*"It's some arcane magic from AT LEAST a decade ago."*
285331

286332
Perform commands on connect
287333
---------------------------

sopel/config/core_section.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,57 @@ class CoreSection(StaticSection):
526526
.. versionadded:: 7.0
527527
"""
528528

529+
flood_max_wait = ValidatedAttribute('flood_max_wait', float, default=2)
530+
"""How much time to wait at most when flood protection kicks in.
531+
532+
:default: ``2``
533+
534+
This is equivalent to the default value:
535+
536+
.. code-block:: ini
537+
538+
flood_max_wait = 2
539+
540+
.. seealso::
541+
542+
The :ref:`Flood Prevention` chapter to learn what each flood-related
543+
setting does.
544+
545+
.. versionadded:: 7.1
546+
"""
547+
548+
flood_penalty_ratio = ValidatedAttribute('flood_penalty_ratio',
549+
float,
550+
default=1.4)
551+
"""Ratio of the message length used to compute the added wait penalty.
552+
553+
:default: ``1.4``
554+
555+
Messages longer than :attr:`flood_text_length` will get an added
556+
wait penalty (in seconds) that will be computed like this::
557+
558+
overflow = max(0, (len(text) - flood_text_length))
559+
rate = flood_text_length * flood_penalty_ratio
560+
penalty = overflow / rate
561+
562+
.. note::
563+
564+
If the penalty ratio is 0, this penalty will be disabled.
565+
566+
This is equivalent to the default value:
567+
568+
.. code-block:: ini
569+
570+
flood_penalty_ratio = 1.4
571+
572+
.. seealso::
573+
574+
The :ref:`Flood Prevention` chapter to learn what each flood-related
575+
setting does.
576+
577+
.. versionadded:: 7.1
578+
"""
579+
529580
flood_refill_rate = ValidatedAttribute('flood_refill_rate', int, default=1)
530581
"""How quickly burst mode recovers, in messages per second.
531582
@@ -545,6 +596,28 @@ class CoreSection(StaticSection):
545596
.. versionadded:: 7.0
546597
"""
547598

599+
flood_text_length = ValidatedAttribute('flood_text_length', int, default=50)
600+
"""Length of text at which an extra wait penalty is added.
601+
602+
:default: ``50``
603+
604+
Messages longer than this (in bytes) get an added wait penalty if the
605+
flood protection limit is reached.
606+
607+
This is equivalent to the default value:
608+
609+
.. code-block:: ini
610+
611+
flood_text_length = 50
612+
613+
.. seealso::
614+
615+
The :ref:`Flood Prevention` chapter to learn what each flood-related
616+
setting does.
617+
618+
.. versionadded:: 7.1
619+
"""
620+
548621
help_prefix = ValidatedAttribute('help_prefix',
549622
default=COMMAND_DEFAULT_HELP_PREFIX)
550623
"""The prefix to use in help output.

sopel/irc/__init__.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -566,11 +566,19 @@ def say(self, text, recipient, max_messages=1):
566566
# Manage multi-line only when needed
567567
text, excess = tools.get_sendable_message(text)
568568

569+
flood_max_wait = self.settings.core.flood_max_wait
570+
flood_burst_lines = self.settings.core.flood_burst_lines
571+
flood_refill_rate = self.settings.core.flood_refill_rate
572+
flood_empty_wait = self.settings.core.flood_empty_wait
573+
574+
flood_text_length = self.settings.core.flood_text_length
575+
flood_penalty_ratio = self.settings.core.flood_penalty_ratio
576+
569577
with self.sending:
570578
recipient_id = tools.Identifier(recipient)
571579
recipient_stack = self.stack.setdefault(recipient_id, {
572580
'messages': [],
573-
'flood_left': self.config.core.flood_burst_lines,
581+
'flood_left': flood_burst_lines,
574582
})
575583

576584
if recipient_stack['messages']:
@@ -584,15 +592,36 @@ def say(self, text, recipient, max_messages=1):
584592
# based on how long it's been since our last message to recipient
585593
if not recipient_stack['flood_left']:
586594
recipient_stack['flood_left'] = min(
587-
self.config.core.flood_burst_lines,
588-
int(elapsed) * self.config.core.flood_refill_rate)
595+
flood_burst_lines,
596+
int(elapsed) * flood_refill_rate)
589597

590598
# If it's too soon to send another message, wait
591599
if not recipient_stack['flood_left']:
592-
penalty = float(max(0, len(text) - 50)) / 70
593-
wait = min(self.config.core.flood_empty_wait + penalty, 2) # Maximum wait time is 2 sec
600+
penalty = 0
601+
602+
if flood_penalty_ratio > 0:
603+
penalty_ratio = flood_text_length * flood_penalty_ratio
604+
text_length_overflow = float(
605+
max(0, len(text) - flood_text_length))
606+
penalty = text_length_overflow / penalty_ratio
607+
608+
# Maximum wait time is 2 sec by default
609+
initial_wait_time = flood_empty_wait + penalty
610+
wait = min(initial_wait_time, flood_max_wait)
594611
if elapsed < wait:
595-
time.sleep(wait - elapsed)
612+
sleep_time = wait - elapsed
613+
LOGGER.debug(
614+
'Flood protection wait time: %.3fs; '
615+
'elapsed time: %.3fs; '
616+
'initial wait time (limited to %.3fs): %.3fs '
617+
'(including %.3fs of penalty).',
618+
sleep_time,
619+
elapsed,
620+
flood_max_wait,
621+
initial_wait_time,
622+
penalty,
623+
)
624+
time.sleep(sleep_time)
596625

597626
# Loop detection
598627
messages = [m[1] for m in recipient_stack['messages'][-8:]]

0 commit comments

Comments
 (0)