aa-killtracker 0.18.0a2__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aa_killtracker-0.18.0a2.dist-info → aa_killtracker-1.0.0.dist-info}/METADATA +7 -8
- {aa_killtracker-0.18.0a2.dist-info → aa_killtracker-1.0.0.dist-info}/RECORD +33 -30
- killtracker/__init__.py +1 -1
- killtracker/admin.py +1 -1
- killtracker/app_settings.py +14 -7
- killtracker/core/discord.py +162 -0
- killtracker/core/helpers.py +13 -0
- killtracker/core/{discord_messages.py → trackers.py} +16 -75
- killtracker/core/{worker_shutdown.py → workers.py} +3 -4
- killtracker/core/{killmails.py → zkb.py} +32 -40
- killtracker/managers.py +1 -1
- killtracker/models/trackers.py +4 -5
- killtracker/models/webhooks.py +4 -53
- killtracker/signals.py +4 -4
- killtracker/tasks.py +47 -49
- killtracker/tests/core/test_discord.py +184 -0
- killtracker/tests/core/test_helpers.py +23 -0
- killtracker/tests/core/{test_discord_messages_1.py → test_tracker_1.py} +12 -39
- killtracker/tests/core/{test_discord_messages_2.py → test_tracker_2.py} +3 -3
- killtracker/tests/core/test_workers.py +49 -0
- killtracker/tests/core/{test_killmails.py → test_zkb.py} +58 -46
- killtracker/tests/models/test_killmails.py +0 -2
- killtracker/tests/models/test_trackers_1.py +1 -1
- killtracker/tests/models/test_trackers_2.py +2 -2
- killtracker/tests/models/test_webhooks.py +63 -0
- killtracker/tests/test_integration.py +26 -13
- killtracker/tests/test_tasks.py +68 -58
- killtracker/tests/test_utils.py +39 -0
- killtracker/tests/testdata/factories.py +1 -1
- killtracker/tests/testdata/helpers.py +1 -1
- killtracker/tests/utils.py +28 -0
- killtracker/exceptions.py +0 -32
- killtracker/tests/core/test_worker_shutdown.py +0 -34
- killtracker/tests/models/test_webhook.py +0 -164
- killtracker/tests/test_exceptions.py +0 -12
- {aa_killtracker-0.18.0a2.dist-info → aa_killtracker-1.0.0.dist-info}/WHEEL +0 -0
- {aa_killtracker-0.18.0a2.dist-info → aa_killtracker-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
"""
|
1
|
+
"""Fetch killmails from zKillboard."""
|
2
2
|
|
3
3
|
# pylint: disable = redefined-builtin
|
4
4
|
|
@@ -32,19 +32,22 @@ from killtracker.app_settings import (
|
|
32
32
|
KILLTRACKER_STORAGE_KILLMAILS_LIFETIME,
|
33
33
|
KILLTRACKER_ZKB_REQUEST_DELAY,
|
34
34
|
)
|
35
|
-
from killtracker.
|
35
|
+
from killtracker.core.helpers import datetime_or_none
|
36
36
|
from killtracker.providers import esi
|
37
37
|
|
38
|
-
logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
39
|
-
|
40
|
-
ZKB_REDISQ_URL = "https://zkillredisq.stream/listen.php"
|
41
|
-
ZKB_API_URL = "https://zkillboard.com/api/"
|
42
38
|
ZKB_KILLMAIL_BASEURL = "https://zkillboard.com/kill/"
|
43
|
-
REQUESTS_TIMEOUT = (5, 30)
|
44
|
-
DEFAULT_429_TIMEOUT = 10
|
45
39
|
|
46
|
-
|
47
|
-
|
40
|
+
_KEY_RETRY_AT = "killtracker-zkb-retry-at"
|
41
|
+
_KEY_LAST_REQUEST = "killtracker-zkb-last-request"
|
42
|
+
_MAIN_MINIMUM_COUNT = 2
|
43
|
+
_MAIN_MINIMUM_SHARE = 0.25
|
44
|
+
_REQUESTS_TIMEOUT = (5, 30)
|
45
|
+
_ZKB_429_DEFAULT_TIMEOUT = 10
|
46
|
+
_ZKB_API_URL = "https://zkillboard.com/api/"
|
47
|
+
_ZKB_REDISQ_URL = "https://zkillredisq.stream/listen.php"
|
48
|
+
|
49
|
+
logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
50
|
+
|
48
51
|
|
49
52
|
# TODO: Factor out logic for accessing the API to another module
|
50
53
|
|
@@ -57,6 +60,10 @@ class ZKBTooManyRequestsError(Exception):
|
|
57
60
|
self.is_original = is_original
|
58
61
|
|
59
62
|
|
63
|
+
class KillmailDoesNotExist(Exception):
|
64
|
+
"""Killmail does not exist in storage."""
|
65
|
+
|
66
|
+
|
60
67
|
@dataclass
|
61
68
|
class _KillmailBase:
|
62
69
|
"""Base class for all Killmail."""
|
@@ -264,8 +271,8 @@ class Killmail(_KillmailBase):
|
|
264
271
|
jumps: Optional[int] = None,
|
265
272
|
distance: Optional[float] = None,
|
266
273
|
matching_ship_type_ids: Optional[List[int]] = None,
|
267
|
-
minimum_count: int =
|
268
|
-
minimum_share: float =
|
274
|
+
minimum_count: int = _MAIN_MINIMUM_COUNT,
|
275
|
+
minimum_share: float = _MAIN_MINIMUM_SHARE,
|
269
276
|
) -> "Killmail":
|
270
277
|
"""Clone this killmail and add tracker info."""
|
271
278
|
main_ship_group = self._calc_main_attacker_ship_group(
|
@@ -419,45 +426,30 @@ class Killmail(_KillmailBase):
|
|
419
426
|
if "," in KILLTRACKER_QUEUE_ID:
|
420
427
|
raise ImproperlyConfigured("A queue ID must not contains commas.")
|
421
428
|
|
422
|
-
|
423
|
-
if
|
424
|
-
|
425
|
-
retry_at = dt.datetime.fromisoformat(v)
|
426
|
-
except (TypeError, ValueError):
|
427
|
-
cache.delete(key_retry_at)
|
428
|
-
logger.warning("unable to parse timestamp of retry at")
|
429
|
-
return None
|
430
|
-
|
431
|
-
if retry_at > now():
|
432
|
-
raise ZKBTooManyRequestsError(retry_at=retry_at, is_original=False)
|
433
|
-
|
434
|
-
key_last_request = "killtracker-last-request"
|
435
|
-
if (v := cache.get(key_last_request)) is not None:
|
436
|
-
try:
|
437
|
-
last_request = dt.datetime.fromisoformat(v)
|
438
|
-
except (TypeError, ValueError):
|
439
|
-
cache.delete(key_last_request)
|
440
|
-
logger.warning("unable to parse timestamp of last request")
|
441
|
-
last_request = now()
|
429
|
+
retry_at = datetime_or_none(cache.get(_KEY_RETRY_AT))
|
430
|
+
if retry_at is not None and retry_at > now():
|
431
|
+
raise ZKBTooManyRequestsError(retry_at=retry_at, is_original=False)
|
442
432
|
|
433
|
+
last_request = datetime_or_none(cache.get(_KEY_LAST_REQUEST))
|
434
|
+
if last_request is not None:
|
443
435
|
next_slot = last_request + dt.timedelta(
|
444
436
|
milliseconds=KILLTRACKER_ZKB_REQUEST_DELAY
|
445
437
|
)
|
446
438
|
seconds = (next_slot - now()).total_seconds()
|
447
439
|
if seconds > 0:
|
448
|
-
logger.debug("Waiting %f seconds
|
440
|
+
logger.debug("ZKB API: Waiting %f seconds for next free slot", seconds)
|
449
441
|
sleep(seconds)
|
450
442
|
|
451
443
|
response = requests.get(
|
452
|
-
|
444
|
+
_ZKB_REDISQ_URL,
|
453
445
|
params={
|
454
446
|
"queueID": quote_plus(KILLTRACKER_QUEUE_ID),
|
455
447
|
"ttw": KILLTRACKER_REDISQ_TTW,
|
456
448
|
},
|
457
|
-
timeout=
|
449
|
+
timeout=_REQUESTS_TIMEOUT,
|
458
450
|
headers={"User-Agent": USER_AGENT_TEXT},
|
459
451
|
)
|
460
|
-
cache.set(
|
452
|
+
cache.set(_KEY_LAST_REQUEST, now(), timeout=KILLTRACKER_ZKB_REQUEST_DELAY + 30)
|
461
453
|
logger.debug(
|
462
454
|
"Response from ZKB API: %d %s %s",
|
463
455
|
response.status_code,
|
@@ -473,9 +465,9 @@ class Killmail(_KillmailBase):
|
|
473
465
|
try:
|
474
466
|
retry_after = int(response.headers["Retry-After"])
|
475
467
|
except KeyError:
|
476
|
-
retry_after =
|
468
|
+
retry_after = _ZKB_429_DEFAULT_TIMEOUT
|
477
469
|
retry_at = now() + dt.timedelta(seconds=retry_after)
|
478
|
-
cache.set(
|
470
|
+
cache.set(_KEY_RETRY_AT, retry_at, timeout=retry_after + 60)
|
479
471
|
raise ZKBTooManyRequestsError(retry_at=retry_at, is_original=True)
|
480
472
|
|
481
473
|
return None
|
@@ -514,9 +506,9 @@ class Killmail(_KillmailBase):
|
|
514
506
|
"Trying to fetch killmail from ZKB API with killmail ID %d ...",
|
515
507
|
killmail_id,
|
516
508
|
)
|
517
|
-
url = f"{
|
509
|
+
url = f"{_ZKB_API_URL}killID/{killmail_id}/"
|
518
510
|
response = requests.get(
|
519
|
-
url, timeout=
|
511
|
+
url, timeout=_REQUESTS_TIMEOUT, headers={"User-Agent": USER_AGENT_TEXT}
|
520
512
|
)
|
521
513
|
response.raise_for_status()
|
522
514
|
zkb_data = response.json()
|
killtracker/managers.py
CHANGED
@@ -15,7 +15,7 @@ from app_utils.logging import LoggerAddTag
|
|
15
15
|
|
16
16
|
from killtracker import __title__
|
17
17
|
from killtracker.app_settings import KILLTRACKER_PURGE_KILLMAILS_AFTER_DAYS
|
18
|
-
from killtracker.core.
|
18
|
+
from killtracker.core.zkb import Killmail, _KillmailCharacter
|
19
19
|
|
20
20
|
logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
21
21
|
|
killtracker/models/trackers.py
CHANGED
@@ -28,11 +28,10 @@ from app_utils.logging import LoggerAddTag
|
|
28
28
|
from killtracker import __title__
|
29
29
|
from killtracker.app_settings import KILLTRACKER_KILLMAIL_MAX_AGE_FOR_TRACKER
|
30
30
|
from killtracker.constants import EveCategoryId, EveGroupId
|
31
|
-
from killtracker.core.
|
32
|
-
from killtracker.core.
|
31
|
+
from killtracker.core.trackers import create_discord_message_from_killmail
|
32
|
+
from killtracker.core.zkb import Killmail
|
33
33
|
from killtracker.managers import TrackerManager
|
34
|
-
|
35
|
-
from .webhooks import Webhook
|
34
|
+
from killtracker.models.webhooks import Webhook
|
36
35
|
|
37
36
|
logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
38
37
|
|
@@ -886,5 +885,5 @@ class Tracker(models.Model):
|
|
886
885
|
|
887
886
|
Returns the new queue size.
|
888
887
|
"""
|
889
|
-
message =
|
888
|
+
message = create_discord_message_from_killmail(self, killmail, intro_text)
|
890
889
|
return self.webhook.enqueue_message(message)
|
killtracker/models/webhooks.py
CHANGED
@@ -2,10 +2,8 @@
|
|
2
2
|
|
3
3
|
from typing import Optional
|
4
4
|
|
5
|
-
import dhooks_lite
|
6
5
|
from simple_mq import SimpleMQ
|
7
6
|
|
8
|
-
from django.core.cache import cache
|
9
7
|
from django.db import models
|
10
8
|
from django.utils.translation import gettext_lazy as _
|
11
9
|
|
@@ -14,10 +12,9 @@ from app_utils.allianceauth import get_redis_client
|
|
14
12
|
from app_utils.logging import LoggerAddTag
|
15
13
|
from app_utils.urls import static_file_absolute_url
|
16
14
|
|
17
|
-
from killtracker import
|
15
|
+
from killtracker import __title__
|
18
16
|
from killtracker.app_settings import KILLTRACKER_WEBHOOK_SET_AVATAR
|
19
|
-
from killtracker.core.
|
20
|
-
from killtracker.exceptions import WebhookTooManyRequests
|
17
|
+
from killtracker.core.discord import DiscordMessage, send_message_to_webhook
|
21
18
|
from killtracker.managers import WebhookManager
|
22
19
|
|
23
20
|
logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
@@ -26,8 +23,6 @@ logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
|
26
23
|
class Webhook(models.Model):
|
27
24
|
"""A webhook to receive messages"""
|
28
25
|
|
29
|
-
HTTP_TOO_MANY_REQUESTS = 429
|
30
|
-
|
31
26
|
class WebhookType(models.IntegerChoices):
|
32
27
|
"""A webhook type."""
|
33
28
|
|
@@ -167,53 +162,9 @@ class Webhook(models.Model):
|
|
167
162
|
|
168
163
|
return q.clear()
|
169
164
|
|
170
|
-
def send_message(self, message: DiscordMessage) ->
|
165
|
+
def send_message(self, message: DiscordMessage) -> int:
|
171
166
|
"""Send a message to the webhook."""
|
172
|
-
|
173
|
-
if timeout:
|
174
|
-
raise WebhookTooManyRequests(timeout)
|
175
|
-
|
176
|
-
hook = dhooks_lite.Webhook(
|
177
|
-
url=self.url,
|
178
|
-
user_agent=dhooks_lite.UserAgent(
|
179
|
-
name=APP_NAME, url=HOMEPAGE_URL, version=__version__
|
180
|
-
),
|
181
|
-
)
|
182
|
-
response = hook.execute(
|
183
|
-
content=message.content,
|
184
|
-
embeds=message.embeds,
|
185
|
-
username=message.username,
|
186
|
-
avatar_url=message.avatar_url,
|
187
|
-
wait_for_response=True,
|
188
|
-
max_retries=0, # we will handle retries ourselves
|
189
|
-
)
|
190
|
-
logger.debug(
|
191
|
-
"%s: Response from Discord for creating message from killmail %d: %s %s %s",
|
192
|
-
self,
|
193
|
-
message.killmail_id,
|
194
|
-
response.status_code,
|
195
|
-
response.headers,
|
196
|
-
response.content,
|
197
|
-
)
|
198
|
-
if response.status_code == self.HTTP_TOO_MANY_REQUESTS:
|
199
|
-
logger.error(
|
200
|
-
"%s: Received too many requests error from API: %s",
|
201
|
-
self,
|
202
|
-
response.content,
|
203
|
-
)
|
204
|
-
try:
|
205
|
-
retry_after = int(response.headers["Retry-After"]) + 2
|
206
|
-
except (ValueError, KeyError):
|
207
|
-
retry_after = WebhookTooManyRequests.DEFAULT_RESET_AFTER
|
208
|
-
cache.set(
|
209
|
-
key=self._blocked_cache_key(), value="BLOCKED", timeout=retry_after
|
210
|
-
)
|
211
|
-
raise WebhookTooManyRequests(retry_after)
|
212
|
-
|
213
|
-
return response
|
214
|
-
|
215
|
-
def _blocked_cache_key(self) -> str:
|
216
|
-
return f"{__title__}_webhook_{self.pk}_blocked"
|
167
|
+
return send_message_to_webhook(name=self.name, url=self.url, message=message)
|
217
168
|
|
218
169
|
@staticmethod
|
219
170
|
def create_message_link(name: str, url: str) -> str:
|
killtracker/signals.py
CHANGED
@@ -8,24 +8,24 @@ from allianceauth.services.hooks import get_extension_logger
|
|
8
8
|
from app_utils.logging import LoggerAddTag
|
9
9
|
|
10
10
|
from killtracker import __title__
|
11
|
-
from killtracker.core import
|
11
|
+
from killtracker.core import workers
|
12
12
|
|
13
13
|
logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
14
14
|
|
15
15
|
|
16
16
|
@signals.worker_ready.connect
|
17
17
|
def worker_ready_handler(sender, **kwargs):
|
18
|
-
|
18
|
+
workers.state_reset(sender.hostname)
|
19
19
|
logger.debug("worker_ready: %s", sender.hostname)
|
20
20
|
|
21
21
|
|
22
22
|
@signals.worker_shutting_down.connect
|
23
23
|
def worker_shutting_down_handler(sender, **kwargs):
|
24
|
-
|
24
|
+
workers.state_set(sender)
|
25
25
|
logger.debug("worker_shutting_down: %s", sender)
|
26
26
|
|
27
27
|
|
28
28
|
@signals.worker_shutdown.connect
|
29
29
|
def worker_shutdown_handler(sender, **kwargs):
|
30
|
-
|
30
|
+
workers.state_reset(sender.hostname)
|
31
31
|
logger.debug("worker_shutdown: %s", sender.hostname)
|
killtracker/tasks.py
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
import time
|
4
4
|
|
5
|
-
import dhooks_lite
|
6
5
|
from celery import Task, chain, shared_task
|
7
6
|
|
8
7
|
from django.db import IntegrityError
|
@@ -21,20 +20,20 @@ from killtracker.app_settings import (
|
|
21
20
|
KILLTRACKER_GENERATE_MESSAGE_MAX_RETRIES,
|
22
21
|
KILLTRACKER_GENERATE_MESSAGE_RETRY_COUNTDOWN,
|
23
22
|
KILLTRACKER_MAX_KILLMAILS_PER_RUN,
|
23
|
+
KILLTRACKER_MAX_MESSAGES_SENT_PER_RUN,
|
24
24
|
KILLTRACKER_PURGE_KILLMAILS_AFTER_DAYS,
|
25
25
|
KILLTRACKER_RUN_TIMEOUT,
|
26
26
|
KILLTRACKER_STORING_KILLMAILS_ENABLED,
|
27
27
|
KILLTRACKER_TASK_OBJECTS_CACHE_TIMEOUT,
|
28
28
|
KILLTRACKER_TASKS_TIMEOUT,
|
29
29
|
)
|
30
|
-
from killtracker.core import
|
31
|
-
from killtracker.core.
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
ZKBTooManyRequestsError,
|
30
|
+
from killtracker.core import workers
|
31
|
+
from killtracker.core.discord import (
|
32
|
+
DiscordMessage,
|
33
|
+
HTTPError,
|
34
|
+
WebhookRateLimitExhausted,
|
36
35
|
)
|
37
|
-
from killtracker.
|
36
|
+
from killtracker.core.zkb import Killmail, KillmailDoesNotExist, ZKBTooManyRequestsError
|
38
37
|
from killtracker.models import EveKillmail, Tracker, Webhook
|
39
38
|
|
40
39
|
logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
@@ -42,7 +41,8 @@ logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
|
42
41
|
|
43
42
|
@shared_task(bind=True, base=QueueOnce, timeout=KILLTRACKER_TASKS_TIMEOUT)
|
44
43
|
def run_killtracker(self: Task) -> int:
|
45
|
-
"""
|
44
|
+
"""Fetches and processes new killmails from ZKB API
|
45
|
+
and returns how many killmails were processed.
|
46
46
|
|
47
47
|
This is the main periodic task for running Killtracker.
|
48
48
|
"""
|
@@ -64,7 +64,7 @@ def run_killtracker(self: Task) -> int:
|
|
64
64
|
if is_timed_out():
|
65
65
|
break
|
66
66
|
|
67
|
-
if
|
67
|
+
if workers.is_shutting_down(self):
|
68
68
|
logger.debug("Aborting due to worker shutdown")
|
69
69
|
break
|
70
70
|
|
@@ -203,54 +203,51 @@ def delete_stale_killmails() -> None:
|
|
203
203
|
|
204
204
|
|
205
205
|
@shared_task(
|
206
|
-
bind=True,
|
207
|
-
base=QueueOnce, # celery_once locks stay intact during retries
|
208
|
-
timeout=KILLTRACKER_TASKS_TIMEOUT,
|
209
|
-
retry_backoff=False,
|
210
|
-
max_retries=None,
|
206
|
+
bind=True, base=QueueOnce, timeout=KILLTRACKER_TASKS_TIMEOUT, max_retries=None
|
211
207
|
)
|
212
208
|
def send_messages_to_webhook(self: Task, webhook_pk: int) -> None:
|
213
|
-
"""Sends
|
209
|
+
"""Sends queued messages to a webhook.
|
214
210
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
211
|
+
Note: This task will retry after processing a set number of messages
|
212
|
+
to avoid running potentially forever.
|
213
|
+
"""
|
214
|
+
|
215
|
+
webhook: Webhook = Webhook.objects.get(pk=webhook_pk)
|
219
216
|
if not webhook.is_enabled:
|
220
217
|
logger.info("%s: Webhook is disabled - aborting", webhook)
|
221
218
|
return
|
222
219
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
220
|
+
for _ in range(KILLTRACKER_MAX_MESSAGES_SENT_PER_RUN):
|
221
|
+
if workers.is_shutting_down(self):
|
222
|
+
logger.debug("Aborting due to worker shutdown")
|
223
|
+
return
|
227
224
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
logger.warning(
|
233
|
-
"%s: Too many requests for webhook. Blocked for %s seconds. Aborting.",
|
234
|
-
webhook,
|
235
|
-
ex.retry_after,
|
236
|
-
)
|
237
|
-
return
|
225
|
+
message = webhook.dequeue_message()
|
226
|
+
if not message:
|
227
|
+
logger.debug("%s: No more messages to send for webhook", webhook)
|
228
|
+
break
|
238
229
|
|
239
|
-
if not response.status_ok:
|
240
|
-
webhook.enqueue_message(message, is_error=True)
|
241
|
-
logger.warning(
|
242
|
-
"%s: Failed to send message for Killmail %d to webhook, will retry. "
|
243
|
-
"HTTP status code: %d, response: %s",
|
244
|
-
webhook,
|
245
|
-
message.killmail_id,
|
246
|
-
response.status_code,
|
247
|
-
response.content,
|
248
|
-
)
|
249
|
-
else:
|
250
230
|
try:
|
251
|
-
message_id =
|
252
|
-
|
253
|
-
|
231
|
+
message_id = webhook.send_message(message)
|
232
|
+
|
233
|
+
except WebhookRateLimitExhausted as ex:
|
234
|
+
webhook.enqueue_message(message)
|
235
|
+
logger.warning(
|
236
|
+
"%s: Webhook temporarily blocked. Retrying at %s.", webhook, ex.retry_at
|
237
|
+
)
|
238
|
+
raise self.retry(eta=ex.retry_at)
|
239
|
+
|
240
|
+
except HTTPError as ex:
|
241
|
+
webhook.enqueue_message(message, is_error=True)
|
242
|
+
logger.warning(
|
243
|
+
"%s: Failed to send message for Killmail %d to webhook, will retry. "
|
244
|
+
"HTTP status code: %d",
|
245
|
+
webhook,
|
246
|
+
message.killmail_id,
|
247
|
+
ex.status_code,
|
248
|
+
)
|
249
|
+
continue
|
250
|
+
|
254
251
|
logger.info(
|
255
252
|
"%s: Discord message %s created for killmail %d",
|
256
253
|
webhook,
|
@@ -258,7 +255,8 @@ def send_messages_to_webhook(self: Task, webhook_pk: int) -> None:
|
|
258
255
|
message.killmail_id,
|
259
256
|
)
|
260
257
|
|
261
|
-
|
258
|
+
if webhook.messages_queued() > 0:
|
259
|
+
raise self.retry(countdown=KILLTRACKER_DISCORD_SEND_DELAY)
|
262
260
|
|
263
261
|
|
264
262
|
@shared_task(timeout=KILLTRACKER_TASKS_TIMEOUT)
|
@@ -0,0 +1,184 @@
|
|
1
|
+
import datetime as dt
|
2
|
+
from unittest.mock import patch
|
3
|
+
|
4
|
+
import dhooks_lite
|
5
|
+
import requests_mock
|
6
|
+
|
7
|
+
from django.utils.timezone import now
|
8
|
+
|
9
|
+
from app_utils.testing import NoSocketsTestCase
|
10
|
+
|
11
|
+
from killtracker.core.discord import (
|
12
|
+
DiscordMessage,
|
13
|
+
HTTPError,
|
14
|
+
WebhookRateLimitExhausted,
|
15
|
+
_make_key_last_request,
|
16
|
+
_make_key_retry_at,
|
17
|
+
send_message_to_webhook,
|
18
|
+
)
|
19
|
+
from killtracker.tests.utils import CacheFake
|
20
|
+
|
21
|
+
MODULE_PATH = "killtracker.core.discord"
|
22
|
+
|
23
|
+
|
24
|
+
class TestDiscordMessage(NoSocketsTestCase):
|
25
|
+
def test_can_create(self):
|
26
|
+
o = DiscordMessage(content="content")
|
27
|
+
self.assertEqual(o.content, "content")
|
28
|
+
|
29
|
+
def test_should_raise_exception_when_invalid(self):
|
30
|
+
with self.assertRaises(ValueError):
|
31
|
+
DiscordMessage(username="user")
|
32
|
+
|
33
|
+
def test_can_convert_to_and_from_json_1(self):
|
34
|
+
o1 = DiscordMessage(
|
35
|
+
content="content",
|
36
|
+
)
|
37
|
+
s = o1.to_json()
|
38
|
+
o2 = DiscordMessage.from_json(s)
|
39
|
+
self.assertEqual(o1, o2)
|
40
|
+
|
41
|
+
def test_can_convert_to_and_from_json_2(self):
|
42
|
+
o1 = DiscordMessage(
|
43
|
+
avatar_url="avatar_url",
|
44
|
+
content="content",
|
45
|
+
embeds=[dhooks_lite.Embed(description="description")],
|
46
|
+
killmail_id=42,
|
47
|
+
username="username",
|
48
|
+
)
|
49
|
+
s = o1.to_json()
|
50
|
+
o2 = DiscordMessage.from_json(s)
|
51
|
+
self.assertEqual(o1, o2)
|
52
|
+
|
53
|
+
|
54
|
+
@requests_mock.Mocker()
|
55
|
+
@patch(MODULE_PATH + ".cache", new_callable=CacheFake)
|
56
|
+
class TestWebhookSendMessage(NoSocketsTestCase):
|
57
|
+
def setUp(self) -> None:
|
58
|
+
self.name = "webhook"
|
59
|
+
self.message = DiscordMessage(content="Test message")
|
60
|
+
self.url = "https://webhook.example.com/1234"
|
61
|
+
self.message_api = {
|
62
|
+
"name": "test webhook",
|
63
|
+
"type": 1,
|
64
|
+
"channel_id": "199737254929760256",
|
65
|
+
"token": "3d89bb7572e0fb30d8128367b3b1b44fecd1726de135cbe28a41f8b2f777c372ba2939e72279b94526ff5d1bd4358d65cf11",
|
66
|
+
"avatar": None,
|
67
|
+
"guild_id": "199737254929760256",
|
68
|
+
"id": "223704706495545344",
|
69
|
+
"application_id": None,
|
70
|
+
"user": {
|
71
|
+
"username": "test",
|
72
|
+
"discriminator": "7479",
|
73
|
+
"id": "190320984123768832",
|
74
|
+
"avatar": "b004ec1740a63ca06ae2e14c5cee11f3",
|
75
|
+
"public_flags": 131328,
|
76
|
+
},
|
77
|
+
}
|
78
|
+
|
79
|
+
def test_when_send_ok_returns_true(self, requests_mocker, mock_cache):
|
80
|
+
# given
|
81
|
+
requests_mocker.register_uri(
|
82
|
+
"POST", self.url, status_code=200, json=self.message_api
|
83
|
+
)
|
84
|
+
# when
|
85
|
+
got = send_message_to_webhook(
|
86
|
+
name=self.name, url=self.url, message=self.message
|
87
|
+
)
|
88
|
+
# then
|
89
|
+
self.assertEqual(got, 223704706495545344)
|
90
|
+
self.assertTrue(requests_mocker.called)
|
91
|
+
|
92
|
+
def test_should_ignore_invalid_key_for_last_request(
|
93
|
+
self, requests_mocker, mock_cache
|
94
|
+
):
|
95
|
+
# given
|
96
|
+
mock_cache.set(_make_key_last_request(self.url), "invalid")
|
97
|
+
requests_mocker.register_uri(
|
98
|
+
"POST", self.url, status_code=200, json=self.message_api
|
99
|
+
)
|
100
|
+
# when
|
101
|
+
got = send_message_to_webhook(
|
102
|
+
name=self.name, url=self.url, message=self.message
|
103
|
+
)
|
104
|
+
# then
|
105
|
+
self.assertEqual(got, 223704706495545344)
|
106
|
+
self.assertTrue(requests_mocker.called)
|
107
|
+
|
108
|
+
def test_should_ignore_invalid_key_for_retry_at(self, requests_mocker, mock_cache):
|
109
|
+
# given
|
110
|
+
mock_cache.set(_make_key_retry_at(self.url), "invalid")
|
111
|
+
requests_mocker.register_uri(
|
112
|
+
"POST", self.url, status_code=200, json=self.message_api
|
113
|
+
)
|
114
|
+
# when
|
115
|
+
got = send_message_to_webhook(
|
116
|
+
name=self.name, url=self.url, message=self.message
|
117
|
+
)
|
118
|
+
# then
|
119
|
+
self.assertEqual(got, 223704706495545344)
|
120
|
+
self.assertTrue(requests_mocker.called)
|
121
|
+
|
122
|
+
def test_when_send_not_ok_raise_error(self, requests_mocker, mock_cache):
|
123
|
+
# given
|
124
|
+
requests_mocker.register_uri("POST", self.url, status_code=404)
|
125
|
+
# when
|
126
|
+
with self.assertRaises(HTTPError) as ctx:
|
127
|
+
send_message_to_webhook(name=self.name, url=self.url, message=self.message)
|
128
|
+
# then
|
129
|
+
self.assertEqual(ctx.exception.status_code, 404)
|
130
|
+
self.assertTrue(requests_mocker.called)
|
131
|
+
|
132
|
+
def test_raise_too_many_requests_when_received_from_api(
|
133
|
+
self, requests_mocker, mock_cache
|
134
|
+
):
|
135
|
+
# given
|
136
|
+
requests_mocker.register_uri(
|
137
|
+
"POST",
|
138
|
+
self.url,
|
139
|
+
status_code=429,
|
140
|
+
json={
|
141
|
+
"global": False,
|
142
|
+
"message": "You are being rate limited.",
|
143
|
+
"retry_after": 2000,
|
144
|
+
},
|
145
|
+
headers={
|
146
|
+
"x-ratelimit-remaining": "5",
|
147
|
+
"x-ratelimit-reset-after": "60",
|
148
|
+
"Retry-After": "2000",
|
149
|
+
},
|
150
|
+
)
|
151
|
+
# when/then
|
152
|
+
with self.assertRaises(WebhookRateLimitExhausted) as ctx:
|
153
|
+
send_message_to_webhook(name=self.name, url=self.url, message=self.message)
|
154
|
+
|
155
|
+
self.assertTrue(ctx.exception.retry_at)
|
156
|
+
|
157
|
+
def test_too_many_requests_no_retry_value(self, requests_mocker, mock_cache):
|
158
|
+
# given
|
159
|
+
requests_mocker.register_uri(
|
160
|
+
"POST",
|
161
|
+
self.url,
|
162
|
+
status_code=429,
|
163
|
+
headers={
|
164
|
+
"x-ratelimit-remaining": "5",
|
165
|
+
"x-ratelimit-reset-after": "60",
|
166
|
+
},
|
167
|
+
)
|
168
|
+
# when/then
|
169
|
+
with self.assertRaises(WebhookRateLimitExhausted) as ctx:
|
170
|
+
send_message_to_webhook(name=self.name, url=self.url, message=self.message)
|
171
|
+
|
172
|
+
self.assertTrue(ctx.exception.retry_at)
|
173
|
+
|
174
|
+
def test_should_reraise_exception_when_not_expired(
|
175
|
+
self, requests_mocker, mock_cache
|
176
|
+
):
|
177
|
+
# given
|
178
|
+
key = _make_key_retry_at(self.url)
|
179
|
+
mock_cache.set(key, now() + dt.timedelta(hours=1))
|
180
|
+
# when
|
181
|
+
with self.assertRaises(WebhookRateLimitExhausted) as ctx:
|
182
|
+
send_message_to_webhook(name=self.name, url=self.url, message=self.message)
|
183
|
+
# then
|
184
|
+
self.assertTrue(ctx.exception.retry_at)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import datetime as dt
|
2
|
+
from typing import Any, NamedTuple, Optional
|
3
|
+
from unittest import TestCase
|
4
|
+
|
5
|
+
from killtracker.core.helpers import datetime_or_none
|
6
|
+
|
7
|
+
|
8
|
+
class TestDatetimeOrNone(TestCase):
|
9
|
+
def test_should_return_value(self):
|
10
|
+
class Case(NamedTuple):
|
11
|
+
value: Any
|
12
|
+
want: Optional[dt.datetime]
|
13
|
+
|
14
|
+
now = dt.datetime.now()
|
15
|
+
cases = [
|
16
|
+
Case(now, now),
|
17
|
+
Case("abc", None),
|
18
|
+
Case(42, None),
|
19
|
+
Case(None, None),
|
20
|
+
]
|
21
|
+
for tc in cases:
|
22
|
+
got = datetime_or_none(tc.value)
|
23
|
+
self.assertIs(got, tc.want)
|