aa-killtracker 0.17.0__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.
Files changed (39) hide show
  1. {aa_killtracker-0.17.0.dist-info → aa_killtracker-1.0.0.dist-info}/METADATA +7 -8
  2. {aa_killtracker-0.17.0.dist-info → aa_killtracker-1.0.0.dist-info}/RECORD +36 -29
  3. killtracker/__init__.py +1 -1
  4. killtracker/admin.py +13 -8
  5. killtracker/app_settings.py +20 -10
  6. killtracker/apps.py +2 -4
  7. killtracker/core/discord.py +162 -0
  8. killtracker/core/helpers.py +13 -0
  9. killtracker/core/{discord_messages.py → trackers.py} +74 -59
  10. killtracker/core/workers.py +46 -0
  11. killtracker/core/{killmails.py → zkb.py} +97 -72
  12. killtracker/forms.py +1 -1
  13. killtracker/managers.py +3 -3
  14. killtracker/models/trackers.py +7 -10
  15. killtracker/models/webhooks.py +60 -128
  16. killtracker/providers.py +1 -1
  17. killtracker/signals.py +31 -0
  18. killtracker/tasks.py +141 -92
  19. killtracker/tests/core/test_discord.py +184 -0
  20. killtracker/tests/core/test_helpers.py +23 -0
  21. killtracker/tests/core/{test_discord_messages_1.py → test_tracker_1.py} +28 -8
  22. killtracker/tests/core/{test_discord_messages_2.py → test_tracker_2.py} +11 -11
  23. killtracker/tests/core/test_workers.py +49 -0
  24. killtracker/tests/core/{test_killmails.py → test_zkb.py} +109 -52
  25. killtracker/tests/models/test_killmails.py +0 -2
  26. killtracker/tests/models/test_trackers_1.py +24 -24
  27. killtracker/tests/models/test_trackers_2.py +6 -5
  28. killtracker/tests/models/test_webhooks.py +63 -0
  29. killtracker/tests/test_integration.py +25 -12
  30. killtracker/tests/test_tasks.py +161 -92
  31. killtracker/tests/test_utils.py +39 -0
  32. killtracker/tests/testdata/factories.py +1 -1
  33. killtracker/tests/testdata/helpers.py +1 -1
  34. killtracker/tests/utils.py +44 -0
  35. killtracker/exceptions.py +0 -32
  36. killtracker/tests/models/test_webhook.py +0 -150
  37. killtracker/tests/test_exceptions.py +0 -12
  38. {aa_killtracker-0.17.0.dist-info → aa_killtracker-1.0.0.dist-info}/WHEEL +0 -0
  39. {aa_killtracker-0.17.0.dist-info → aa_killtracker-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,24 +1,20 @@
1
1
  """Webhooks models for killtracker."""
2
2
 
3
- import json
4
- from typing import List, Optional
3
+ from typing import Optional
5
4
 
6
- import dhooks_lite
7
5
  from simple_mq import SimpleMQ
8
6
 
9
- from django.core.cache import cache
10
7
  from django.db import models
11
8
  from django.utils.translation import gettext_lazy as _
12
9
 
13
10
  from allianceauth.services.hooks import get_extension_logger
14
11
  from app_utils.allianceauth import get_redis_client
15
- from app_utils.json import JSONDateTimeDecoder, JSONDateTimeEncoder
16
12
  from app_utils.logging import LoggerAddTag
17
13
  from app_utils.urls import static_file_absolute_url
18
14
 
19
- from killtracker import APP_NAME, HOMEPAGE_URL, __title__, __version__
15
+ from killtracker import __title__
20
16
  from killtracker.app_settings import KILLTRACKER_WEBHOOK_SET_AVATAR
21
- from killtracker.exceptions import WebhookTooManyRequests
17
+ from killtracker.core.discord import DiscordMessage, send_message_to_webhook
22
18
  from killtracker.managers import WebhookManager
23
19
 
24
20
  logger = LoggerAddTag(get_extension_logger(__name__), __title__)
@@ -27,8 +23,6 @@ logger = LoggerAddTag(get_extension_logger(__name__), __title__)
27
23
  class Webhook(models.Model):
28
24
  """A webhook to receive messages"""
29
25
 
30
- HTTP_TOO_MANY_REQUESTS = 429
31
-
32
26
  class WebhookType(models.IntegerChoices):
33
27
  """A webhook type."""
34
28
 
@@ -63,8 +57,8 @@ class Webhook(models.Model):
63
57
 
64
58
  def __init__(self, *args, **kwargs) -> None:
65
59
  super().__init__(*args, **kwargs)
66
- self.main_queue = self._create_queue("main")
67
- self.error_queue = self._create_queue("error")
60
+ self._main_queue = self._create_queue("main")
61
+ self._error_queue = self._create_queue("error")
68
62
 
69
63
  def __str__(self) -> str:
70
64
  return self.name
@@ -78,8 +72,8 @@ class Webhook(models.Model):
78
72
  # method to avoid modifying the original state.
79
73
  state = self.__dict__.copy()
80
74
  # Remove the unpicklable entries.
81
- del state["main_queue"]
82
- del state["error_queue"]
75
+ del state["_main_queue"]
76
+ del state["_error_queue"]
83
77
  return state
84
78
 
85
79
  def __setstate__(self, state):
@@ -87,15 +81,15 @@ class Webhook(models.Model):
87
81
  self.__dict__.update(state)
88
82
  # Restore the previously opened file's state. To do so, we need to
89
83
  # reopen it and read from it until the line count is restored.
90
- self.main_queue = self._create_queue("main")
91
- self.error_queue = self._create_queue("error")
84
+ self._main_queue = self._create_queue("main")
85
+ self._error_queue = self._create_queue("error")
92
86
 
93
87
  def save(self, *args, **kwargs):
94
88
  is_new = self.id is None # type: ignore
95
89
  super().save(*args, **kwargs)
96
90
  if is_new:
97
- self.main_queue = self._create_queue("main")
98
- self.error_queue = self._create_queue("error")
91
+ self._main_queue = self._create_queue("main")
92
+ self._error_queue = self._create_queue("error")
99
93
 
100
94
  def _create_queue(self, suffix: str) -> Optional[SimpleMQ]:
101
95
  redis_client = get_redis_client()
@@ -110,129 +104,67 @@ class Webhook(models.Model):
110
104
  returns number of moved messages.
111
105
  """
112
106
  counter = 0
113
- if self.error_queue and self.main_queue:
107
+ if self._error_queue and self._main_queue:
114
108
  while True:
115
- message = self.error_queue.dequeue()
109
+ message = self._error_queue.dequeue()
116
110
  if message is None:
117
111
  break
118
112
 
119
- self.main_queue.enqueue(message)
113
+ self._main_queue.enqueue(message)
120
114
  counter += 1
121
115
 
122
116
  return counter
123
117
 
124
- def enqueue_message(
125
- self,
126
- content: Optional[str] = None,
127
- embeds: Optional[List[dhooks_lite.Embed]] = None,
128
- tts: Optional[bool] = None,
129
- username: Optional[str] = None,
130
- avatar_url: Optional[str] = None,
131
- ) -> int:
132
- """Enqueues a message to be send with this webhook"""
133
- if not self.main_queue:
118
+ def enqueue_message(self, message: DiscordMessage, is_error: bool = False) -> int:
119
+ """Enqueues a discord message to be send with this webhook.
120
+
121
+ Returns the updated number of messages in the main queue.
122
+ """
123
+ q = self._error_queue if is_error else self._main_queue
124
+
125
+ if not q:
134
126
  return 0
135
127
 
136
- username = __title__ if KILLTRACKER_WEBHOOK_SET_AVATAR else username
137
- brand_url = static_file_absolute_url("killtracker/killtracker_logo.png")
138
- avatar_url = brand_url if KILLTRACKER_WEBHOOK_SET_AVATAR else avatar_url
139
- return self.main_queue.enqueue(
140
- self._discord_message_asjson(
141
- content=content,
142
- embeds=embeds,
143
- tts=tts,
144
- username=username,
145
- avatar_url=avatar_url,
146
- )
147
- )
128
+ if KILLTRACKER_WEBHOOK_SET_AVATAR:
129
+ message.username = __title__
148
130
 
149
- @staticmethod
150
- def _discord_message_asjson(
151
- content: Optional[str] = None,
152
- embeds: Optional[List[dhooks_lite.Embed]] = None,
153
- tts: Optional[bool] = None,
154
- username: Optional[str] = None,
155
- avatar_url: Optional[str] = None,
156
- ) -> str:
157
- """Converts a Discord message to JSON and returns it
158
-
159
- Raises ValueError if message is incomplete
160
- """
161
- if not content and not embeds:
162
- raise ValueError("Message must have content or embeds to be valid")
163
-
164
- if embeds:
165
- embeds_list = [obj.asdict() for obj in embeds]
166
- else:
167
- embeds_list = None
168
-
169
- message = {}
170
- if content:
171
- message["content"] = content
172
- if embeds_list:
173
- message["embeds"] = embeds_list
174
- if tts:
175
- message["tts"] = tts
176
- if username:
177
- message["username"] = username
178
- if avatar_url:
179
- message["avatar_url"] = avatar_url
180
-
181
- return json.dumps(message, cls=JSONDateTimeEncoder)
182
-
183
- def send_message_to_webhook(self, message_json: str) -> dhooks_lite.WebhookResponse:
184
- """Send given message to webhook
185
-
186
- Params
187
- message_json: Discord message encoded in JSON
131
+ if KILLTRACKER_WEBHOOK_SET_AVATAR:
132
+ brand_url = static_file_absolute_url("killtracker/killtracker_logo.png")
133
+ message.avatar_url = brand_url
134
+
135
+ return q.enqueue(message.to_json())
136
+
137
+ def dequeue_message(self, is_error: bool = False) -> Optional[DiscordMessage]:
138
+ """Dequeues a message from the main queue and return it.
139
+
140
+ Returns None if the queue is empty.
188
141
  """
189
- timeout = cache.ttl(self._blocked_cache_key()) # type: ignore
190
- if timeout:
191
- raise WebhookTooManyRequests(timeout)
192
-
193
- message = json.loads(message_json, cls=JSONDateTimeDecoder)
194
- if message.get("embeds"):
195
- embeds = [
196
- dhooks_lite.Embed.from_dict(embed_dict)
197
- for embed_dict in message.get("embeds")
198
- ]
199
- else:
200
- embeds = None
201
- hook = dhooks_lite.Webhook(
202
- url=self.url,
203
- user_agent=dhooks_lite.UserAgent(
204
- name=APP_NAME, url=HOMEPAGE_URL, version=__version__
205
- ),
206
- )
207
- response = hook.execute(
208
- content=message.get("content"),
209
- embeds=embeds,
210
- username=message.get("username"),
211
- avatar_url=message.get("avatar_url"),
212
- wait_for_response=True,
213
- max_retries=0, # we will handle retries ourselves
214
- )
215
- logger.debug("headers: %s", response.headers)
216
- logger.debug("status_code: %s", response.status_code)
217
- logger.debug("content: %s", response.content)
218
- if response.status_code == self.HTTP_TOO_MANY_REQUESTS:
219
- logger.error(
220
- "%s: Received too many requests error from API: %s",
221
- self,
222
- response.content,
223
- )
224
- try:
225
- retry_after = int(response.headers["Retry-After"]) + 2
226
- except (ValueError, KeyError):
227
- retry_after = WebhookTooManyRequests.DEFAULT_RESET_AFTER
228
- cache.set(
229
- key=self._blocked_cache_key(), value="BLOCKED", timeout=retry_after
230
- )
231
- raise WebhookTooManyRequests(retry_after)
232
- return response
233
-
234
- def _blocked_cache_key(self) -> str:
235
- return f"{__title__}_webhook_{self.pk}_blocked"
142
+ q = self._error_queue if is_error else self._main_queue
143
+ s = q.dequeue()
144
+ if not s:
145
+ return None
146
+
147
+ return DiscordMessage.from_json(s)
148
+
149
+ def messages_queued(self, is_error: bool = False) -> int:
150
+ """Returns how many message are currently in the queue."""
151
+ q = self._error_queue if is_error else self._main_queue
152
+ if not q:
153
+ return 0
154
+
155
+ return q.size()
156
+
157
+ def delete_queued_messages(self, is_error: bool = False) -> int:
158
+ """Deletes all messages in a queue and returns how many messages where deleted."""
159
+ q = self._error_queue if is_error else self._main_queue
160
+ if not q:
161
+ return 0
162
+
163
+ return q.clear()
164
+
165
+ def send_message(self, message: DiscordMessage) -> int:
166
+ """Send a message to the webhook."""
167
+ return send_message_to_webhook(name=self.name, url=self.url, message=message)
236
168
 
237
169
  @staticmethod
238
170
  def create_message_link(name: str, url: str) -> str:
killtracker/providers.py CHANGED
@@ -5,7 +5,7 @@ from esi.clients import EsiClientProvider
5
5
  from allianceauth.services.hooks import get_extension_logger
6
6
  from app_utils.logging import LoggerAddTag
7
7
 
8
- from . import USER_AGENT_TEXT, __title__
8
+ from killtracker import USER_AGENT_TEXT, __title__
9
9
 
10
10
  logger = LoggerAddTag(get_extension_logger(__name__), __title__)
11
11
 
killtracker/signals.py ADDED
@@ -0,0 +1,31 @@
1
+ """Module signals connects to celery signals."""
2
+
3
+ # pylint: disable=missing-function-docstring
4
+
5
+ from celery import signals
6
+
7
+ from allianceauth.services.hooks import get_extension_logger
8
+ from app_utils.logging import LoggerAddTag
9
+
10
+ from killtracker import __title__
11
+ from killtracker.core import workers
12
+
13
+ logger = LoggerAddTag(get_extension_logger(__name__), __title__)
14
+
15
+
16
+ @signals.worker_ready.connect
17
+ def worker_ready_handler(sender, **kwargs):
18
+ workers.state_reset(sender.hostname)
19
+ logger.debug("worker_ready: %s", sender.hostname)
20
+
21
+
22
+ @signals.worker_shutting_down.connect
23
+ def worker_shutting_down_handler(sender, **kwargs):
24
+ workers.state_set(sender)
25
+ logger.debug("worker_shutting_down: %s", sender)
26
+
27
+
28
+ @signals.worker_shutdown.connect
29
+ def worker_shutdown_handler(sender, **kwargs):
30
+ workers.state_reset(sender.hostname)
31
+ logger.debug("worker_shutdown: %s", sender.hostname)
killtracker/tasks.py CHANGED
@@ -1,8 +1,8 @@
1
1
  """Tasks for killtracker."""
2
2
 
3
- from datetime import timedelta
3
+ import time
4
4
 
5
- from celery import chain, shared_task
5
+ from celery import Task, chain, shared_task
6
6
 
7
7
  from django.db import IntegrityError
8
8
  from django.utils.timezone import now
@@ -11,58 +11,82 @@ from eveuniverse.tasks import update_unresolved_eve_entities
11
11
 
12
12
  from allianceauth.services.hooks import get_extension_logger
13
13
  from allianceauth.services.tasks import QueueOnce
14
- from app_utils.caching import cached_queryset
15
14
  from app_utils.esi import retry_task_if_esi_is_down
16
15
  from app_utils.logging import LoggerAddTag
17
16
 
18
- from . import APP_NAME, __title__
19
- from .app_settings import (
17
+ from killtracker import __title__
18
+ from killtracker.app_settings import (
20
19
  KILLTRACKER_DISCORD_SEND_DELAY,
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
+ KILLTRACKER_RUN_TIMEOUT,
25
26
  KILLTRACKER_STORING_KILLMAILS_ENABLED,
26
27
  KILLTRACKER_TASK_OBJECTS_CACHE_TIMEOUT,
27
28
  KILLTRACKER_TASKS_TIMEOUT,
28
- KILLTRACKER_ZKB_REQUEST_DELAY,
29
29
  )
30
- from .core.killmails import Killmail
31
- from .exceptions import WebhookTooManyRequests
32
- from .models import EveKillmail, Tracker, Webhook
30
+ from killtracker.core import workers
31
+ from killtracker.core.discord import (
32
+ DiscordMessage,
33
+ HTTPError,
34
+ WebhookRateLimitExhausted,
35
+ )
36
+ from killtracker.core.zkb import Killmail, KillmailDoesNotExist, ZKBTooManyRequestsError
37
+ from killtracker.models import EveKillmail, Tracker, Webhook
33
38
 
34
39
  logger = LoggerAddTag(get_extension_logger(__name__), __title__)
35
40
 
36
41
 
37
- @shared_task(timeout=KILLTRACKER_TASKS_TIMEOUT)
38
- def run_killtracker(runs: int = 0) -> None:
39
- """Main task for running the Killtracker.
42
+ @shared_task(bind=True, base=QueueOnce, timeout=KILLTRACKER_TASKS_TIMEOUT)
43
+ def run_killtracker(self: Task) -> int:
44
+ """Fetches and processes new killmails from ZKB API
45
+ and returns how many killmails were processed.
40
46
 
41
- Will fetch new killmails from ZKB and start running trackers for them
47
+ This is the main periodic task for running Killtracker.
42
48
  """
43
49
  if not is_esi_online():
44
50
  logger.warning("ESI is currently offline. Aborting")
45
- return
51
+ return 0
46
52
 
47
- if runs == 0:
48
- logger.info("Killtracker run started...")
49
- qs = cached_queryset(
50
- Webhook.objects.filter(is_enabled=True),
51
- key=f"{APP_NAME}_enabled_webhooks",
52
- timeout=KILLTRACKER_TASK_OBJECTS_CACHE_TIMEOUT,
53
- )
54
- for webhook in qs:
55
- webhook.reset_failed_messages()
53
+ for webhook in Webhook.objects.filter(is_enabled=True):
54
+ webhook.reset_failed_messages()
55
+
56
+ started = time.time()
57
+
58
+ def is_timed_out() -> bool:
59
+ elapsed = time.time() - started
60
+ return KILLTRACKER_RUN_TIMEOUT - elapsed <= 0
56
61
 
57
- killmail = Killmail.create_from_zkb_redisq()
58
- if killmail:
62
+ killmails_count = 0
63
+ for _ in range(KILLTRACKER_MAX_KILLMAILS_PER_RUN):
64
+ if is_timed_out():
65
+ break
66
+
67
+ if workers.is_shutting_down(self):
68
+ logger.debug("Aborting due to worker shutdown")
69
+ break
70
+
71
+ killmail = None
72
+ try:
73
+ killmail = Killmail.create_from_zkb_redisq()
74
+ except ZKBTooManyRequestsError as ex:
75
+ seconds = (ex.retry_at - now()).total_seconds()
76
+ if seconds < 0:
77
+ break
78
+
79
+ logger.warning(
80
+ "Killtracker has been baned from ZKB API for %f seconds", seconds
81
+ )
82
+ raise self.retry(countdown=seconds)
83
+
84
+ if not killmail:
85
+ break
86
+
87
+ killmails_count += 1
59
88
  killmail.save()
60
- qs = cached_queryset(
61
- Tracker.objects.filter(is_enabled=True),
62
- key=f"{APP_NAME}_enabled_trackers",
63
- timeout=KILLTRACKER_TASK_OBJECTS_CACHE_TIMEOUT,
64
- )
65
- for tracker in qs:
89
+ for tracker in Tracker.objects.filter(is_enabled=True):
66
90
  run_tracker.delay(tracker_pk=tracker.pk, killmail_id=killmail.id)
67
91
 
68
92
  if KILLTRACKER_STORING_KILLMAILS_ENABLED:
@@ -71,59 +95,64 @@ def run_killtracker(runs: int = 0) -> None:
71
95
  update_unresolved_eve_entities.si(),
72
96
  ).delay()
73
97
 
74
- total_killmails = runs + (1 if killmail else 0)
75
- if killmail and total_killmails < KILLTRACKER_MAX_KILLMAILS_PER_RUN:
76
- run_killtracker.apply_async(
77
- kwargs={"runs": runs + 1},
78
- eta=now() + timedelta(milliseconds=KILLTRACKER_ZKB_REQUEST_DELAY),
79
- )
80
- else:
81
- if (
82
- KILLTRACKER_STORING_KILLMAILS_ENABLED
83
- and KILLTRACKER_PURGE_KILLMAILS_AFTER_DAYS > 0
84
- ):
85
- delete_stale_killmails.delay()
98
+ elapsed = time.time() - started
99
+ logger.info(
100
+ "Killtracker processed %d new killmails from ZKB in %f seconds",
101
+ killmails_count,
102
+ elapsed,
103
+ )
86
104
 
87
- logger.info(
88
- "Killtracker runs completed. %d killmails received from ZKB",
89
- total_killmails,
90
- )
105
+ if (
106
+ KILLTRACKER_STORING_KILLMAILS_ENABLED
107
+ and KILLTRACKER_PURGE_KILLMAILS_AFTER_DAYS > 0
108
+ ):
109
+ delete_stale_killmails.delay()
110
+
111
+ return killmails_count
91
112
 
92
113
 
93
114
  @shared_task(bind=True, max_retries=None)
94
115
  def run_tracker(
95
- self, tracker_pk: int, killmail_id: int, ignore_max_age: bool = False
116
+ self: Task, tracker_pk: int, killmail_id: int, ignore_max_age: bool = False
96
117
  ) -> None:
97
118
  """Run tracker for given killmail and trigger sending if needed."""
98
119
  retry_task_if_esi_is_down(self)
99
- tracker = Tracker.objects.get_cached(
120
+ tracker: Tracker = Tracker.objects.get_cached(
100
121
  pk=tracker_pk,
101
122
  select_related="webhook",
102
123
  timeout=KILLTRACKER_TASK_OBJECTS_CACHE_TIMEOUT,
103
124
  )
104
- logger.info("%s: Started running tracker", tracker)
105
- killmail = Killmail.get(killmail_id)
125
+ try:
126
+ killmail = Killmail.get(killmail_id)
127
+ except KillmailDoesNotExist as ex:
128
+ logger.error("Aborting. %s", ex)
129
+ return
130
+
106
131
  killmail_new = tracker.process_killmail(
107
132
  killmail=killmail, ignore_max_age=ignore_max_age
108
133
  )
109
134
  if killmail_new:
135
+ logger.info("%s: Killmail %d matches", tracker, killmail_id)
110
136
  killmail_new.save()
111
137
  generate_killmail_message.delay(tracker_pk=tracker_pk, killmail_id=killmail_id)
112
- elif tracker.webhook.main_queue.size():
138
+ elif tracker.webhook.messages_queued():
113
139
  send_messages_to_webhook.delay(webhook_pk=tracker.webhook.pk)
114
140
 
115
141
 
116
142
  @shared_task(bind=True, max_retries=None)
117
- def generate_killmail_message(self, tracker_pk: int, killmail_id: int) -> None:
143
+ def generate_killmail_message(self: Task, tracker_pk: int, killmail_id: int) -> None:
118
144
  """Generate and enqueue message from given killmail and start sending."""
119
145
  retry_task_if_esi_is_down(self)
120
- tracker = Tracker.objects.get_cached(
146
+ tracker: Tracker = Tracker.objects.get_cached(
121
147
  pk=tracker_pk,
122
148
  select_related="webhook",
123
149
  timeout=KILLTRACKER_TASK_OBJECTS_CACHE_TIMEOUT,
124
150
  )
125
- killmail = Killmail.get(killmail_id)
126
- logger.info("%s: Generating message from killmail %s", tracker, killmail.id)
151
+ try:
152
+ killmail = Killmail.get(killmail_id)
153
+ except KillmailDoesNotExist as ex:
154
+ logger.error("Aborting. %s", ex)
155
+ return
127
156
  try:
128
157
  tracker.generate_killmail_message(killmail)
129
158
  except Exception as ex:
@@ -142,12 +171,19 @@ def generate_killmail_message(self, tracker_pk: int, killmail_id: int) -> None:
142
171
  )
143
172
 
144
173
  send_messages_to_webhook.delay(webhook_pk=tracker.webhook.pk)
174
+ logger.info(
175
+ "%s: Added message from killmail %s to send queue", tracker, killmail.id
176
+ )
145
177
 
146
178
 
147
179
  @shared_task(timeout=KILLTRACKER_TASKS_TIMEOUT)
148
180
  def store_killmail(killmail_id: int) -> None:
149
- """stores killmail as EveKillmail object"""
150
- killmail = Killmail.get(killmail_id)
181
+ """Stores killmail as EveKillmail object."""
182
+ try:
183
+ killmail = Killmail.get(killmail_id)
184
+ except KillmailDoesNotExist as ex:
185
+ logger.error("Aborting. %s", ex)
186
+ return
151
187
  try:
152
188
  EveKillmail.objects.create_from_killmail(killmail, resolve_ids=False)
153
189
  except IntegrityError:
@@ -160,63 +196,74 @@ def store_killmail(killmail_id: int) -> None:
160
196
 
161
197
  @shared_task(timeout=KILLTRACKER_TASKS_TIMEOUT)
162
198
  def delete_stale_killmails() -> None:
163
- """deleted all EveKillmail objects that are considered stale"""
199
+ """Deletes all EveKillmail objects that are considered stale."""
164
200
  _, details = EveKillmail.objects.delete_stale()
165
201
  if details:
166
202
  logger.info("Deleted %d stale killmails", details["killtracker.EveKillmail"])
167
203
 
168
204
 
169
205
  @shared_task(
170
- bind=True,
171
- base=QueueOnce, # celery_once locks stay intact during retries
172
- timeout=KILLTRACKER_TASKS_TIMEOUT,
173
- retry_backoff=False,
174
- max_retries=None,
206
+ bind=True, base=QueueOnce, timeout=KILLTRACKER_TASKS_TIMEOUT, max_retries=None
175
207
  )
176
- def send_messages_to_webhook(self, webhook_pk: int) -> None:
177
- """send all queued messages to given Webhook"""
208
+ def send_messages_to_webhook(self: Task, webhook_pk: int) -> None:
209
+ """Sends queued messages to a webhook.
178
210
 
179
- webhook = Webhook.objects.get_cached(
180
- pk=webhook_pk,
181
- timeout=KILLTRACKER_TASK_OBJECTS_CACHE_TIMEOUT,
182
- )
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)
183
216
  if not webhook.is_enabled:
184
217
  logger.info("%s: Webhook is disabled - aborting", webhook)
185
218
  return
186
219
 
187
- message = webhook.main_queue.dequeue()
188
- if message:
189
- logger.info("%s: Sending message to webhook", webhook)
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
224
+
225
+ message = webhook.dequeue_message()
226
+ if not message:
227
+ logger.debug("%s: No more messages to send for webhook", webhook)
228
+ break
229
+
190
230
  try:
191
- response = webhook.send_message_to_webhook(message)
192
- except WebhookTooManyRequests as ex:
193
- webhook.main_queue.enqueue(message)
231
+ message_id = webhook.send_message(message)
232
+
233
+ except WebhookRateLimitExhausted as ex:
234
+ webhook.enqueue_message(message)
194
235
  logger.warning(
195
- "%s: Too many requests for webhook. Blocked for %s seconds. Aborting.",
196
- webhook,
197
- ex.retry_after,
236
+ "%s: Webhook temporarily blocked. Retrying at %s.", webhook, ex.retry_at
198
237
  )
199
- return
238
+ raise self.retry(eta=ex.retry_at)
200
239
 
201
- if not response.status_ok:
202
- webhook.error_queue.enqueue(message)
240
+ except HTTPError as ex:
241
+ webhook.enqueue_message(message, is_error=True)
203
242
  logger.warning(
204
- "%s: Failed to send message to webhook, will retry. "
205
- "HTTP status code: %d, response: %s",
243
+ "%s: Failed to send message for Killmail %d to webhook, will retry. "
244
+ "HTTP status code: %d",
206
245
  webhook,
207
- response.status_code,
208
- response.content,
246
+ message.killmail_id,
247
+ ex.status_code,
209
248
  )
249
+ continue
210
250
 
211
- raise self.retry(countdown=KILLTRACKER_DISCORD_SEND_DELAY)
251
+ logger.info(
252
+ "%s: Discord message %s created for killmail %d",
253
+ webhook,
254
+ message_id,
255
+ message.killmail_id,
256
+ )
212
257
 
213
- logger.debug("%s: No more messages to send for webhook", webhook)
258
+ if webhook.messages_queued() > 0:
259
+ raise self.retry(countdown=KILLTRACKER_DISCORD_SEND_DELAY)
214
260
 
215
261
 
216
262
  @shared_task(timeout=KILLTRACKER_TASKS_TIMEOUT)
217
263
  def send_test_message_to_webhook(webhook_pk: int, count: int = 1) -> None:
218
- """send a test message to given webhook.
219
- Optional inform user about result if user ok is given
264
+ """Send a test message to given webhook.
265
+
266
+ Optional inform user about result if user ok is given.
220
267
  """
221
268
  try:
222
269
  webhook = Webhook.objects.get(pk=webhook_pk)
@@ -224,8 +271,10 @@ def send_test_message_to_webhook(webhook_pk: int, count: int = 1) -> None:
224
271
  logger.error("Webhook with pk = %s does not exist", webhook_pk)
225
272
  return
226
273
 
227
- logger.info("Sending %s test messages to webhook %s", count, webhook)
228
274
  for num in range(count):
229
275
  num_str = f"{num+1}/{count} " if count > 1 else ""
230
- webhook.enqueue_message(content=f"Test message {num_str}from {__title__}.")
276
+ message = DiscordMessage(content=f"Test message {num_str}from {__title__}.")
277
+ webhook.enqueue_message(message)
278
+
231
279
  send_messages_to_webhook.delay(webhook.pk)
280
+ logger.info("%s test messages submitted to webhook %s", count, webhook)