aa-killtracker 0.11.0__py3-none-any.whl → 0.12.1__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.
@@ -1,17 +1,12 @@
1
1
  """Tracker models for killtracker."""
2
2
 
3
- import json
4
3
  from datetime import timedelta
5
4
  from typing import List, Optional, Tuple
6
5
 
7
- import dhooks_lite
8
- from simple_mq import SimpleMQ
9
-
10
6
  from django.contrib.auth.models import Group, User
11
- from django.core.cache import cache
12
7
  from django.db import models
8
+ from django.db.models import Q
13
9
  from django.utils.timezone import now
14
- from django.utils.translation import gettext_lazy as _
15
10
  from eveuniverse.helpers import meters_to_ly
16
11
  from eveuniverse.models import (
17
12
  EveConstellation,
@@ -24,251 +19,83 @@ from eveuniverse.models import (
24
19
  from allianceauth.authentication.models import State
25
20
  from allianceauth.eveonline.models import EveAllianceInfo, EveCorporationInfo
26
21
  from allianceauth.services.hooks import get_extension_logger
27
- from app_utils.allianceauth import get_redis_client
28
- from app_utils.json import JSONDateTimeDecoder, JSONDateTimeEncoder
29
22
  from app_utils.logging import LoggerAddTag
30
- from app_utils.urls import static_file_absolute_url
31
23
 
32
- from killtracker import APP_NAME, HOMEPAGE_URL, __title__, __version__
33
- from killtracker.app_settings import (
34
- KILLTRACKER_KILLMAIL_MAX_AGE_FOR_TRACKER,
35
- KILLTRACKER_WEBHOOK_SET_AVATAR,
36
- )
24
+ from killtracker import __title__
25
+ from killtracker.app_settings import KILLTRACKER_KILLMAIL_MAX_AGE_FOR_TRACKER
26
+ from killtracker.constants import EveCategoryId, EveGroupId
37
27
  from killtracker.core.killmails import Killmail
38
- from killtracker.exceptions import WebhookTooManyRequests
39
- from killtracker.managers import EveTypePlusManager, TrackerManager, WebhookManager
40
-
41
- logger = LoggerAddTag(get_extension_logger(__name__), __title__)
42
-
43
-
44
- class EveTypePlus(EveType):
45
- """Variant to show group names with default output."""
46
-
47
- class Meta:
48
- proxy = True
49
-
50
- objects = EveTypePlusManager()
51
-
52
- def __str__(self) -> str:
53
- return f"{self.name} ({self.eve_group})"
28
+ from killtracker.managers import TrackerManager
54
29
 
30
+ from .webhooks import Webhook
55
31
 
56
- class Webhook(models.Model):
57
- """A webhook to receive messages"""
32
+ logger = LoggerAddTag(get_extension_logger(__name__), __title__)
58
33
 
59
- HTTP_TOO_MANY_REQUESTS = 429
60
34
 
61
- class WebhookType(models.IntegerChoices):
62
- """A webhook type."""
35
+ def _require_attackers_ship_groups_query():
36
+ return Q(
37
+ eve_category_id__in=[
38
+ EveCategoryId.STRUCTURE,
39
+ EveCategoryId.SHIP,
40
+ EveCategoryId.FIGHTER,
41
+ ],
42
+ published=True,
43
+ ) | Q(eve_category_id=EveCategoryId.ENTITY)
63
44
 
64
- DISCORD = 1, _("Discord Webhook")
65
45
 
66
- name = models.CharField(
67
- max_length=64, unique=True, help_text="short name to identify this webhook"
68
- )
69
- webhook_type = models.IntegerField(
70
- choices=WebhookType.choices,
71
- default=WebhookType.DISCORD,
72
- help_text="type of this webhook",
73
- )
74
- url = models.CharField(
75
- max_length=255,
76
- unique=True,
77
- help_text=(
78
- "URL of this webhook, e.g. "
79
- "https://discordapp.com/api/webhooks/123456/abcdef"
80
- ),
81
- )
82
- notes = models.TextField(
83
- blank=True,
84
- help_text="you can add notes about this webhook here if you want",
85
- )
86
- is_enabled = models.BooleanField(
87
- default=True,
88
- db_index=True,
89
- help_text="whether notifications are currently sent to this webhook",
46
+ def _require_attackers_ship_types_query():
47
+ return Q(
48
+ eve_group__eve_category_id__in=[
49
+ EveCategoryId.STRUCTURE,
50
+ EveCategoryId.SHIP,
51
+ EveCategoryId.FIGHTER,
52
+ ],
53
+ published=True,
54
+ ) | Q(
55
+ eve_group__eve_category_id=EveCategoryId.ENTITY,
56
+ mass__gt=1,
57
+ volume__gt=1,
90
58
  )
91
- objects = WebhookManager()
92
59
 
93
- def __init__(self, *args, **kwargs) -> None:
94
- super().__init__(*args, **kwargs)
95
- self.main_queue = self._create_queue("main")
96
- self.error_queue = self._create_queue("error")
97
60
 
98
- def __str__(self) -> str:
99
- return self.name
61
+ def _require_attackers_weapon_groups_query():
62
+ return Q(id__in=EveGroupId.weapons())
100
63
 
101
- def __repr__(self) -> str:
102
- return f"{self.__class__.__name__}(id={self.id}, name='{self.name}')" # type: ignore
103
-
104
- def __getstate__(self):
105
- # Copy the object's state from self.__dict__ which contains
106
- # all our instance attributes. Always use the dict.copy()
107
- # method to avoid modifying the original state.
108
- state = self.__dict__.copy()
109
- # Remove the unpicklable entries.
110
- del state["main_queue"]
111
- del state["error_queue"]
112
- return state
113
-
114
- def __setstate__(self, state):
115
- # Restore instance attributes (i.e., filename and lineno).
116
- self.__dict__.update(state)
117
- # Restore the previously opened file's state. To do so, we need to
118
- # reopen it and read from it until the line count is restored.
119
- self.main_queue = self._create_queue("main")
120
- self.error_queue = self._create_queue("error")
121
-
122
- def save(self, *args, **kwargs):
123
- is_new = self.id is None # type: ignore
124
- super().save(*args, **kwargs)
125
- if is_new:
126
- self.main_queue = self._create_queue("main")
127
- self.error_queue = self._create_queue("error")
128
64
 
129
- def _create_queue(self, suffix: str) -> Optional[SimpleMQ]:
130
- redis_client = get_redis_client()
131
- return (
132
- SimpleMQ(redis_client, f"{__title__}_webhook_{self.pk}_{suffix}")
133
- if self.pk
134
- else None
135
- )
65
+ def _require_attackers_weapon_types_query():
66
+ return Q(eve_group__in=EveGroupId.weapons())
136
67
 
137
- def reset_failed_messages(self) -> int:
138
- """moves all messages from error queue into main queue.
139
- returns number of moved messages.
140
- """
141
- counter = 0
142
- if self.error_queue and self.main_queue:
143
- while True:
144
- message = self.error_queue.dequeue()
145
- if message is None:
146
- break
147
-
148
- self.main_queue.enqueue(message)
149
- counter += 1
150
-
151
- return counter
152
-
153
- def enqueue_message(
154
- self,
155
- content: Optional[str] = None,
156
- embeds: Optional[List[dhooks_lite.Embed]] = None,
157
- tts: Optional[bool] = None,
158
- username: Optional[str] = None,
159
- avatar_url: Optional[str] = None,
160
- ) -> int:
161
- """Enqueues a message to be send with this webhook"""
162
- if not self.main_queue:
163
- return 0
164
-
165
- username = __title__ if KILLTRACKER_WEBHOOK_SET_AVATAR else username
166
- brand_url = static_file_absolute_url("killtracker/killtracker_logo.png")
167
- avatar_url = brand_url if KILLTRACKER_WEBHOOK_SET_AVATAR else avatar_url
168
- return self.main_queue.enqueue(
169
- self._discord_message_asjson(
170
- content=content,
171
- embeds=embeds,
172
- tts=tts,
173
- username=username,
174
- avatar_url=avatar_url,
175
- )
176
- )
177
68
 
178
- @staticmethod
179
- def _discord_message_asjson(
180
- content: Optional[str] = None,
181
- embeds: Optional[List[dhooks_lite.Embed]] = None,
182
- tts: Optional[bool] = None,
183
- username: Optional[str] = None,
184
- avatar_url: Optional[str] = None,
185
- ) -> str:
186
- """Converts a Discord message to JSON and returns it
187
-
188
- Raises ValueError if message is incomplete
189
- """
190
- if not content and not embeds:
191
- raise ValueError("Message must have content or embeds to be valid")
192
-
193
- if embeds:
194
- embeds_list = [obj.asdict() for obj in embeds]
195
- else:
196
- embeds_list = None
197
-
198
- message = {}
199
- if content:
200
- message["content"] = content
201
- if embeds_list:
202
- message["embeds"] = embeds_list
203
- if tts:
204
- message["tts"] = tts
205
- if username:
206
- message["username"] = username
207
- if avatar_url:
208
- message["avatar_url"] = avatar_url
209
-
210
- return json.dumps(message, cls=JSONDateTimeEncoder)
211
-
212
- def send_message_to_webhook(self, message_json: str) -> dhooks_lite.WebhookResponse:
213
- """Send given message to webhook
214
-
215
- Params
216
- message_json: Discord message encoded in JSON
217
- """
218
- timeout = cache.ttl(self._blocked_cache_key()) # type: ignore
219
- if timeout:
220
- raise WebhookTooManyRequests(timeout)
221
-
222
- message = json.loads(message_json, cls=JSONDateTimeDecoder)
223
- if message.get("embeds"):
224
- embeds = [
225
- dhooks_lite.Embed.from_dict(embed_dict)
226
- for embed_dict in message.get("embeds")
227
- ]
228
- else:
229
- embeds = None
230
- hook = dhooks_lite.Webhook(
231
- url=self.url,
232
- user_agent=dhooks_lite.UserAgent(
233
- name=APP_NAME, url=HOMEPAGE_URL, version=__version__
234
- ),
69
+ def _require_victim_ship_groups_query():
70
+ return (
71
+ Q(
72
+ eve_category_id__in=[
73
+ EveCategoryId.STRUCTURE,
74
+ EveCategoryId.SHIP,
75
+ EveCategoryId.FIGHTER,
76
+ EveCategoryId.DEPLOYABLE,
77
+ ],
78
+ published=True,
235
79
  )
236
- response = hook.execute(
237
- content=message.get("content"),
238
- embeds=embeds,
239
- username=message.get("username"),
240
- avatar_url=message.get("avatar_url"),
241
- wait_for_response=True,
242
- max_retries=0, # we will handle retries ourselves
243
- )
244
- logger.debug("headers: %s", response.headers)
245
- logger.debug("status_code: %s", response.status_code)
246
- logger.debug("content: %s", response.content)
247
- if response.status_code == self.HTTP_TOO_MANY_REQUESTS:
248
- logger.error(
249
- "%s: Received too many requests error from API: %s",
250
- self,
251
- response.content,
252
- )
253
- try:
254
- retry_after = int(response.headers["Retry-After"]) + 2
255
- except (ValueError, KeyError):
256
- retry_after = WebhookTooManyRequests.DEFAULT_RESET_AFTER
257
- cache.set(
258
- key=self._blocked_cache_key(), value="BLOCKED", timeout=retry_after
259
- )
260
- raise WebhookTooManyRequests(retry_after)
261
- return response
80
+ | Q(id=EveGroupId.MINING_DRONE, published=True)
81
+ | Q(id=EveGroupId.ORBITAL_INFRASTRUCTURE)
82
+ )
262
83
 
263
- def _blocked_cache_key(self) -> str:
264
- return f"{__title__}_webhook_{self.pk}_blocked"
265
84
 
266
- @staticmethod
267
- def create_message_link(name: str, url: str) -> str:
268
- """Create link for a Discord message"""
269
- if name and url:
270
- return f"[{str(name)}]({str(url)})"
271
- return str(name)
85
+ def _require_victim_ship_types_query():
86
+ return (
87
+ Q(
88
+ eve_group__eve_category_id__in=[
89
+ EveCategoryId.STRUCTURE,
90
+ EveCategoryId.SHIP,
91
+ EveCategoryId.FIGHTER,
92
+ EveCategoryId.DEPLOYABLE,
93
+ ],
94
+ published=True,
95
+ )
96
+ | Q(eve_group_id=EveGroupId.MINING_DRONE, published=True)
97
+ | Q(eve_group_id=EveGroupId.ORBITAL_INFRASTRUCTURE)
98
+ )
272
99
 
273
100
 
274
101
  class Tracker(models.Model):
@@ -514,6 +341,7 @@ class Tracker(models.Model):
514
341
  related_name="+",
515
342
  default=None,
516
343
  blank=True,
344
+ limit_choices_to=_require_attackers_ship_groups_query,
517
345
  help_text=(
518
346
  "Only include killmails where at least one attacker "
519
347
  "is flying one of these ship groups. "
@@ -524,16 +352,40 @@ class Tracker(models.Model):
524
352
  related_name="+",
525
353
  default=None,
526
354
  blank=True,
355
+ limit_choices_to=_require_attackers_ship_types_query,
527
356
  help_text=(
528
357
  "Only include killmails where at least one attacker "
529
358
  "is flying one of these ship types. "
530
359
  ),
531
360
  )
361
+ require_attackers_weapon_groups = models.ManyToManyField(
362
+ EveGroup,
363
+ related_name="+",
364
+ default=None,
365
+ blank=True,
366
+ limit_choices_to=_require_attackers_weapon_groups_query,
367
+ help_text=(
368
+ "Only include killmails where at least one attacker "
369
+ "is using one of these weapon groups. "
370
+ ),
371
+ )
372
+ require_attackers_weapon_types = models.ManyToManyField(
373
+ EveType,
374
+ related_name="+",
375
+ default=None,
376
+ blank=True,
377
+ limit_choices_to=_require_attackers_weapon_types_query,
378
+ help_text=(
379
+ "Only include killmails where at least one attacker "
380
+ "is using one of these weapon types. "
381
+ ),
382
+ )
532
383
  require_victim_ship_groups = models.ManyToManyField(
533
384
  EveGroup,
534
385
  related_name="+",
535
386
  default=None,
536
387
  blank=True,
388
+ limit_choices_to=_require_victim_ship_groups_query,
537
389
  help_text=(
538
390
  "Only include killmails where victim is flying one of these ship groups. "
539
391
  ),
@@ -543,6 +395,7 @@ class Tracker(models.Model):
543
395
  related_name="+",
544
396
  default=None,
545
397
  blank=True,
398
+ limit_choices_to=_require_victim_ship_types_query,
546
399
  help_text=(
547
400
  "Only include killmails where victim is flying one of these ship types. "
548
401
  ),
@@ -656,6 +509,7 @@ class Tracker(models.Model):
656
509
  is_matching, matching_ship_type_ids = self._match_attacker_ships(
657
510
  killmail, is_matching, matching_ship_type_ids
658
511
  )
512
+ is_matching = self._match_attacker_weapons(killmail, is_matching)
659
513
  is_matching = self._match_victims(killmail, is_matching)
660
514
  is_matching, matching_ship_type_ids = self._match_victim_ship(
661
515
  killmail, is_matching, matching_ship_type_ids
@@ -865,6 +719,31 @@ class Tracker(models.Model):
865
719
 
866
720
  return is_matching, matching_ship_type_ids
867
721
 
722
+ def _match_attacker_weapons(
723
+ self, killmail: Killmail, is_matching: bool
724
+ ) -> Tuple[bool, List[int]]:
725
+ if is_matching and self.require_attackers_weapon_groups.exists():
726
+ weapon_types_matching_qs = EveType.objects.filter(
727
+ id__in=set(killmail.attackers_weapon_type_ids())
728
+ ).filter(
729
+ eve_group_id__in=list(
730
+ self.require_attackers_weapon_groups.values_list("id", flat=True)
731
+ )
732
+ )
733
+ is_matching = weapon_types_matching_qs.exists()
734
+
735
+ if is_matching and self.require_attackers_weapon_types.exists():
736
+ weapon_types_matching_qs = EveType.objects.filter(
737
+ id__in=set(killmail.attackers_weapon_type_ids())
738
+ ).filter(
739
+ id__in=list(
740
+ self.require_attackers_weapon_types.values_list("id", flat=True)
741
+ )
742
+ )
743
+ is_matching = weapon_types_matching_qs.exists()
744
+
745
+ return is_matching
746
+
868
747
  def _match_victims(self, killmail: Killmail, is_matching: bool) -> bool:
869
748
  if is_matching and self.require_victim_alliances.exists():
870
749
  is_matching = self.require_victim_alliances.filter(
@@ -0,0 +1,242 @@
1
+ """Webhooks models for killtracker."""
2
+
3
+ import json
4
+ from typing import List, Optional
5
+
6
+ import dhooks_lite
7
+ from simple_mq import SimpleMQ
8
+
9
+ from django.core.cache import cache
10
+ from django.db import models
11
+ from django.utils.translation import gettext_lazy as _
12
+
13
+ from allianceauth.services.hooks import get_extension_logger
14
+ from app_utils.allianceauth import get_redis_client
15
+ from app_utils.json import JSONDateTimeDecoder, JSONDateTimeEncoder
16
+ from app_utils.logging import LoggerAddTag
17
+ from app_utils.urls import static_file_absolute_url
18
+
19
+ from killtracker import APP_NAME, HOMEPAGE_URL, __title__, __version__
20
+ from killtracker.app_settings import KILLTRACKER_WEBHOOK_SET_AVATAR
21
+ from killtracker.exceptions import WebhookTooManyRequests
22
+ from killtracker.managers import WebhookManager
23
+
24
+ logger = LoggerAddTag(get_extension_logger(__name__), __title__)
25
+
26
+
27
+ class Webhook(models.Model):
28
+ """A webhook to receive messages"""
29
+
30
+ HTTP_TOO_MANY_REQUESTS = 429
31
+
32
+ class WebhookType(models.IntegerChoices):
33
+ """A webhook type."""
34
+
35
+ DISCORD = 1, _("Discord Webhook")
36
+
37
+ name = models.CharField(
38
+ max_length=64, unique=True, help_text="short name to identify this webhook"
39
+ )
40
+ webhook_type = models.IntegerField(
41
+ choices=WebhookType.choices,
42
+ default=WebhookType.DISCORD,
43
+ help_text="type of this webhook",
44
+ )
45
+ url = models.CharField(
46
+ max_length=255,
47
+ unique=True,
48
+ help_text=(
49
+ "URL of this webhook, e.g. "
50
+ "https://discordapp.com/api/webhooks/123456/abcdef"
51
+ ),
52
+ )
53
+ notes = models.TextField(
54
+ blank=True,
55
+ help_text="you can add notes about this webhook here if you want",
56
+ )
57
+ is_enabled = models.BooleanField(
58
+ default=True,
59
+ db_index=True,
60
+ help_text="whether notifications are currently sent to this webhook",
61
+ )
62
+ objects = WebhookManager()
63
+
64
+ def __init__(self, *args, **kwargs) -> None:
65
+ super().__init__(*args, **kwargs)
66
+ self.main_queue = self._create_queue("main")
67
+ self.error_queue = self._create_queue("error")
68
+
69
+ def __str__(self) -> str:
70
+ return self.name
71
+
72
+ def __repr__(self) -> str:
73
+ return f"{self.__class__.__name__}(id={self.id}, name='{self.name}')" # type: ignore
74
+
75
+ def __getstate__(self):
76
+ # Copy the object's state from self.__dict__ which contains
77
+ # all our instance attributes. Always use the dict.copy()
78
+ # method to avoid modifying the original state.
79
+ state = self.__dict__.copy()
80
+ # Remove the unpicklable entries.
81
+ del state["main_queue"]
82
+ del state["error_queue"]
83
+ return state
84
+
85
+ def __setstate__(self, state):
86
+ # Restore instance attributes (i.e., filename and lineno).
87
+ self.__dict__.update(state)
88
+ # Restore the previously opened file's state. To do so, we need to
89
+ # 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")
92
+
93
+ def save(self, *args, **kwargs):
94
+ is_new = self.id is None # type: ignore
95
+ super().save(*args, **kwargs)
96
+ if is_new:
97
+ self.main_queue = self._create_queue("main")
98
+ self.error_queue = self._create_queue("error")
99
+
100
+ def _create_queue(self, suffix: str) -> Optional[SimpleMQ]:
101
+ redis_client = get_redis_client()
102
+ return (
103
+ SimpleMQ(redis_client, f"{__title__}_webhook_{self.pk}_{suffix}")
104
+ if self.pk
105
+ else None
106
+ )
107
+
108
+ def reset_failed_messages(self) -> int:
109
+ """moves all messages from error queue into main queue.
110
+ returns number of moved messages.
111
+ """
112
+ counter = 0
113
+ if self.error_queue and self.main_queue:
114
+ while True:
115
+ message = self.error_queue.dequeue()
116
+ if message is None:
117
+ break
118
+
119
+ self.main_queue.enqueue(message)
120
+ counter += 1
121
+
122
+ return counter
123
+
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:
134
+ return 0
135
+
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
+ )
148
+
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
188
+ """
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"
236
+
237
+ @staticmethod
238
+ def create_message_link(name: str, url: str) -> str:
239
+ """Create link for a Discord message"""
240
+ if name and url:
241
+ return f"[{str(name)}]({str(url)})"
242
+ return str(name)
@@ -216,11 +216,17 @@ class TestKillmailBasics(NoSocketsTestCase):
216
216
  # then
217
217
  self.assertSetEqual(set(result), {1001, 1002, 1003})
218
218
 
219
- def test_attackers_ships_types(self):
219
+ def test_should_return_attacker_ship_type_ids(self):
220
220
  self.assertListEqual(
221
221
  self.killmail.attackers_ship_type_ids(), [34562, 3756, 3756]
222
222
  )
223
223
 
224
+ def test_should_return_attacker_weapon_ship_type_ids(self):
225
+ self.assertListEqual(
226
+ self.killmail.attackers_weapon_type_ids(),
227
+ [2977, 2488, 2488],
228
+ )
229
+
224
230
  def test_ships_types(self):
225
231
  self.assertSetEqual(self.killmail.ship_type_distinct_ids(), {603, 34562, 3756})
226
232