aa-killtracker 0.12.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.12.0.dist-info → aa_killtracker-0.12.1.dist-info}/METADATA +1 -1
- {aa_killtracker-0.12.0.dist-info → aa_killtracker-0.12.1.dist-info}/RECORD +18 -20
- killtracker/__init__.py +1 -1
- killtracker/admin.py +18 -160
- killtracker/constants.py +62 -9
- killtracker/managers.py +0 -7
- killtracker/migrations/0003_optimize_tracker_form.py +90 -0
- killtracker/models/__init__.py +3 -2
- killtracker/models/trackers.py +65 -232
- killtracker/models/webhooks.py +242 -0
- killtracker/tests/models/test_trackers_1.py +9 -9
- 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.12.0.dist-info → aa_killtracker-0.12.1.dist-info}/LICENSE +0 -0
- {aa_killtracker-0.12.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."""
|
28
|
+
from killtracker.managers import TrackerManager
|
46
29
|
|
47
|
-
|
48
|
-
proxy = True
|
30
|
+
from .webhooks import Webhook
|
49
31
|
|
50
|
-
|
51
|
-
|
52
|
-
def __str__(self) -> str:
|
53
|
-
return f"{self.name} ({self.eve_group})"
|
54
|
-
|
55
|
-
|
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
|
-
|
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
64
|
|
122
|
-
|
123
|
-
|
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")
|
65
|
+
def _require_attackers_weapon_types_query():
|
66
|
+
return Q(eve_group__in=EveGroupId.weapons())
|
128
67
|
|
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
|
-
)
|
136
68
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
-
)
|
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,
|
176
79
|
)
|
80
|
+
| Q(id=EveGroupId.MINING_DRONE, published=True)
|
81
|
+
| Q(id=EveGroupId.ORBITAL_INFRASTRUCTURE)
|
82
|
+
)
|
177
83
|
|
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
|
-
),
|
235
|
-
)
|
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
|
262
|
-
|
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,6 +352,7 @@ 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. "
|
@@ -534,6 +363,7 @@ class Tracker(models.Model):
|
|
534
363
|
related_name="+",
|
535
364
|
default=None,
|
536
365
|
blank=True,
|
366
|
+
limit_choices_to=_require_attackers_weapon_groups_query,
|
537
367
|
help_text=(
|
538
368
|
"Only include killmails where at least one attacker "
|
539
369
|
"is using one of these weapon groups. "
|
@@ -544,6 +374,7 @@ class Tracker(models.Model):
|
|
544
374
|
related_name="+",
|
545
375
|
default=None,
|
546
376
|
blank=True,
|
377
|
+
limit_choices_to=_require_attackers_weapon_types_query,
|
547
378
|
help_text=(
|
548
379
|
"Only include killmails where at least one attacker "
|
549
380
|
"is using one of these weapon types. "
|
@@ -554,6 +385,7 @@ class Tracker(models.Model):
|
|
554
385
|
related_name="+",
|
555
386
|
default=None,
|
556
387
|
blank=True,
|
388
|
+
limit_choices_to=_require_victim_ship_groups_query,
|
557
389
|
help_text=(
|
558
390
|
"Only include killmails where victim is flying one of these ship groups. "
|
559
391
|
),
|
@@ -563,6 +395,7 @@ class Tracker(models.Model):
|
|
563
395
|
related_name="+",
|
564
396
|
default=None,
|
565
397
|
blank=True,
|
398
|
+
limit_choices_to=_require_victim_ship_types_query,
|
566
399
|
help_text=(
|
567
400
|
"Only include killmails where victim is flying one of these ship types. "
|
568
401
|
),
|
@@ -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)
|
@@ -31,7 +31,7 @@ from ..testdata.factories import (
|
|
31
31
|
)
|
32
32
|
from ..testdata.helpers import LoadTestDataMixin, load_killmail
|
33
33
|
|
34
|
-
|
34
|
+
MODELS_PATH = "killtracker.models"
|
35
35
|
|
36
36
|
|
37
37
|
def esi_get_route_origin_destination(origin, destination, **kwargs) -> list:
|
@@ -81,7 +81,7 @@ class TestTrackerCalculate(LoadTestDataMixin, NoSocketsTestCase):
|
|
81
81
|
expected = {10000001, 10000002, 10000003, 10000004, 10000005}
|
82
82
|
self.assertSetEqual(results, expected)
|
83
83
|
|
84
|
-
@patch(
|
84
|
+
@patch(MODELS_PATH + ".trackers.KILLTRACKER_KILLMAIL_MAX_AGE_FOR_TRACKER", 60)
|
85
85
|
def test_excludes_older_killmails(self):
|
86
86
|
tracker = TrackerFactory(
|
87
87
|
name="Test",
|
@@ -144,7 +144,7 @@ class TestTrackerCalculate(LoadTestDataMixin, NoSocketsTestCase):
|
|
144
144
|
expected = {10000002, 10000003, 10000004}
|
145
145
|
self.assertSetEqual(results, expected)
|
146
146
|
|
147
|
-
@patch("eveuniverse.models.esi")
|
147
|
+
@patch("eveuniverse.models.universe_2.esi")
|
148
148
|
def test_can_filter_max_jumps(self, mock_esi):
|
149
149
|
mock_esi.client.Routes.get_route_origin_destination.side_effect = (
|
150
150
|
esi_get_route_origin_destination
|
@@ -160,7 +160,7 @@ class TestTrackerCalculate(LoadTestDataMixin, NoSocketsTestCase):
|
|
160
160
|
expected = {10000102, 10000103}
|
161
161
|
self.assertSetEqual(results, expected)
|
162
162
|
|
163
|
-
@patch("eveuniverse.models.esi")
|
163
|
+
@patch("eveuniverse.models.universe_2.esi")
|
164
164
|
def test_can_filter_max_distance(self, mock_esi):
|
165
165
|
mock_esi.client.Routes.get_route_origin_destination.side_effect = (
|
166
166
|
esi_get_route_origin_destination
|
@@ -572,7 +572,7 @@ class TestTrackerCalculate2(LoadTestDataMixin, NoSocketsTestCase):
|
|
572
572
|
self.assertIsNone(result)
|
573
573
|
|
574
574
|
|
575
|
-
@patch(
|
575
|
+
@patch(MODELS_PATH + ".trackers.EveSolarSystem.jumps_to")
|
576
576
|
class TestTrackerCalculateTrackerInfo(LoadTestDataMixin, NoSocketsTestCase):
|
577
577
|
def setUp(self) -> None:
|
578
578
|
self.tracker = TrackerFactory(webhook=self.webhook_1)
|
@@ -667,8 +667,8 @@ class TestTrackerEnqueueKillmail(LoadTestDataMixin, TestCase):
|
|
667
667
|
self.tracker = TrackerFactory(name="My Tracker", webhook=self.webhook_1)
|
668
668
|
self.webhook_1.main_queue.clear()
|
669
669
|
|
670
|
-
@patch(
|
671
|
-
@patch("eveuniverse.models.esi")
|
670
|
+
@patch(MODELS_PATH + ".webhooks.KILLTRACKER_WEBHOOK_SET_AVATAR", True)
|
671
|
+
@patch("eveuniverse.models.universe_2.esi")
|
672
672
|
def test_normal(self, mock_esi):
|
673
673
|
mock_esi.client.Routes.get_route_origin_destination.side_effect = (
|
674
674
|
esi_get_route_origin_destination
|
@@ -694,8 +694,8 @@ class TestTrackerEnqueueKillmail(LoadTestDataMixin, TestCase):
|
|
694
694
|
self.assertIn("Combat Battlecruiser", embed["description"])
|
695
695
|
self.assertIn("Tracked ship types", embed["description"])
|
696
696
|
|
697
|
-
@patch(
|
698
|
-
@patch("eveuniverse.models.esi")
|
697
|
+
@patch(MODELS_PATH + ".webhooks.KILLTRACKER_WEBHOOK_SET_AVATAR", False)
|
698
|
+
@patch("eveuniverse.models.universe_2.esi")
|
699
699
|
def test_disabled_avatar(self, mock_esi):
|
700
700
|
mock_esi.client.Routes.get_route_origin_destination.side_effect = (
|
701
701
|
esi_get_route_origin_destination
|