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.
- {aa_killtracker-0.11.0.dist-info → aa_killtracker-0.12.1.dist-info}/METADATA +1 -1
- {aa_killtracker-0.11.0.dist-info → aa_killtracker-0.12.1.dist-info}/RECORD +22 -23
- killtracker/__init__.py +1 -1
- killtracker/admin.py +27 -134
- killtracker/constants.py +66 -3
- killtracker/core/killmails.py +4 -0
- killtracker/management/commands/killtracker_load_eve.py +4 -0
- killtracker/managers.py +0 -7
- killtracker/migrations/0002_tracker_require_attackers_weapon_groups_and_more.py +35 -0
- killtracker/migrations/0003_optimize_tracker_form.py +90 -0
- killtracker/models/__init__.py +3 -2
- killtracker/models/trackers.py +111 -232
- killtracker/models/webhooks.py +242 -0
- killtracker/tests/core/test_killmails.py +7 -1
- killtracker/tests/models/test_trackers_1.py +57 -12
- killtracker/tests/models/test_trackers_2.py +0 -144
- killtracker/tests/models/test_webhook.py +150 -0
- killtracker/tests/test_admin.py +23 -162
- killtracker/tests/test_integration.py +1 -1
- killtracker/tests/test_tasks.py +1 -1
- killtracker/auth_hooks.py +0 -9
- killtracker/templates/admin/killtracker/tracker/change_form.html +0 -16
- killtracker/templates/admin/killtracker/tracker/toogle_npc_button.html +0 -23
- killtracker/urls.py +0 -20
- killtracker/views.py +0 -27
- {aa_killtracker-0.11.0.dist-info → aa_killtracker-0.12.1.dist-info}/LICENSE +0 -0
- {aa_killtracker-0.11.0.dist-info → aa_killtracker-0.12.1.dist-info}/WHEEL +0 -0
killtracker/models/trackers.py
CHANGED
@@ -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
|
33
|
-
from killtracker.app_settings import
|
34
|
-
|
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.
|
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
|
-
|
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
|
-
|
62
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
99
|
-
|
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
|
-
|
130
|
-
|
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
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
-
|
237
|
-
|
238
|
-
|
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
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
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
|
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
|
|