aa-killtracker 0.17.0__tar.gz → 1.0.0a1__tar.gz
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.17.0 → aa_killtracker-1.0.0a1}/PKG-INFO +7 -7
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/__init__.py +1 -1
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/admin.py +13 -8
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/app_settings.py +20 -10
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/apps.py +2 -4
- aa_killtracker-1.0.0a1/killtracker/core/discord.py +162 -0
- aa_killtracker-1.0.0a1/killtracker/core/helpers.py +13 -0
- aa_killtracker-0.17.0/killtracker/core/discord_messages.py → aa_killtracker-1.0.0a1/killtracker/core/trackers.py +74 -59
- aa_killtracker-1.0.0a1/killtracker/core/workers.py +46 -0
- aa_killtracker-0.17.0/killtracker/core/killmails.py → aa_killtracker-1.0.0a1/killtracker/core/zkb.py +97 -72
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/forms.py +1 -1
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/managers.py +3 -3
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/models/trackers.py +7 -10
- aa_killtracker-1.0.0a1/killtracker/models/webhooks.py +174 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/providers.py +1 -1
- aa_killtracker-1.0.0a1/killtracker/signals.py +31 -0
- aa_killtracker-1.0.0a1/killtracker/tasks.py +280 -0
- aa_killtracker-1.0.0a1/killtracker/tests/core/test_discord.py +184 -0
- aa_killtracker-1.0.0a1/killtracker/tests/core/test_helpers.py +23 -0
- aa_killtracker-0.17.0/killtracker/tests/core/test_discord_messages_1.py → aa_killtracker-1.0.0a1/killtracker/tests/core/test_tracker_1.py +28 -8
- aa_killtracker-0.17.0/killtracker/tests/core/test_discord_messages_2.py → aa_killtracker-1.0.0a1/killtracker/tests/core/test_tracker_2.py +11 -11
- aa_killtracker-1.0.0a1/killtracker/tests/core/test_workers.py +49 -0
- aa_killtracker-0.17.0/killtracker/tests/core/test_killmails.py → aa_killtracker-1.0.0a1/killtracker/tests/core/test_zkb.py +109 -52
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/models/test_killmails.py +0 -2
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/models/test_trackers_1.py +24 -24
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/models/test_trackers_2.py +6 -5
- aa_killtracker-1.0.0a1/killtracker/tests/models/test_webhooks.py +63 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/test_integration.py +25 -12
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/test_tasks.py +161 -92
- aa_killtracker-1.0.0a1/killtracker/tests/test_utils.py +39 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/factories.py +1 -1
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/helpers.py +1 -1
- aa_killtracker-1.0.0a1/killtracker/tests/utils.py +44 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/pyproject.toml +11 -8
- aa_killtracker-0.17.0/killtracker/exceptions.py +0 -32
- aa_killtracker-0.17.0/killtracker/models/webhooks.py +0 -242
- aa_killtracker-0.17.0/killtracker/tasks.py +0 -231
- aa_killtracker-0.17.0/killtracker/tests/models/test_webhook.py +0 -150
- aa_killtracker-0.17.0/killtracker/tests/test_exceptions.py +0 -12
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/LICENSE +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/README.md +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/checks.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/constants.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/core/__init__.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/management/commands/killtracker_load_eve.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0001_initial_new.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0001_squashed_all.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0002_fix_webhook_notes_field.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0002_tracker_require_attackers_weapon_groups_and_more.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0003_add_state_clauses.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0003_optimize_tracker_form.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0004_add_faction_clauses.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0004_django4_update.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0005_add_final_blow_clause_and_more.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0006_evetypeplus.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0007_restructure_killsmails.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0008_copy_data_to_new_structure.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0009_remove_old_models.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/__init__.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/models/__init__.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/models/killmails.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/static/killtracker/killtracker_logo.png +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/static/killtracker/zkb_icon.png +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/templates/admin/killtracker/tracker/killmail_test.html +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/__init__.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/core/__init__.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/models/__init__.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/test_admin.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/test_admin_2.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/__init__.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/create_eveuniverse.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/evealliances.json +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/evecorporations.json +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/eveentities.json +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/eveuniverse.json +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/killmails.json +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/load_eveuniverse.py +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tools/drop_tables_killtracker.sql +0 -0
- {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tools/generate_conditions_text.py +0 -0
@@ -1,13 +1,12 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: aa-killtracker
|
3
|
-
Version: 0.
|
3
|
+
Version: 1.0.0a1
|
4
4
|
Summary: An app for running killmail trackers with Alliance Auth and Discord.
|
5
5
|
Author-email: Erik Kalkoken <kalkoken87@gmail.com>
|
6
6
|
Requires-Python: >=3.8
|
7
7
|
Description-Content-Type: text/markdown
|
8
8
|
Classifier: Environment :: Web Environment
|
9
9
|
Classifier: Framework :: Django
|
10
|
-
Classifier: Framework :: Django :: 4.0
|
11
10
|
Classifier: Framework :: Django :: 4.2
|
12
11
|
Classifier: Intended Audience :: End Users/Desktop
|
13
12
|
Classifier: License :: OSI Approved :: MIT License
|
@@ -17,15 +16,16 @@ Classifier: Programming Language :: Python :: 3.8
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.9
|
18
17
|
Classifier: Programming Language :: Python :: 3.10
|
19
18
|
Classifier: Programming Language :: Python :: 3.11
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
20
20
|
Classifier: Topic :: Internet :: WWW/HTTP
|
21
21
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
22
22
|
License-File: LICENSE
|
23
|
-
Requires-Dist: allianceauth-app-utils>=1.
|
24
|
-
Requires-Dist: allianceauth>=
|
23
|
+
Requires-Dist: allianceauth-app-utils>=1.26
|
24
|
+
Requires-Dist: allianceauth>=4,<5
|
25
25
|
Requires-Dist: dacite
|
26
|
-
Requires-Dist: dhooks-lite>=1.
|
27
|
-
Requires-Dist: django-eveuniverse>=1.
|
28
|
-
Requires-Dist: redis-simple-mq>=0
|
26
|
+
Requires-Dist: dhooks-lite>=1.1
|
27
|
+
Requires-Dist: django-eveuniverse>=1.5
|
28
|
+
Requires-Dist: redis-simple-mq>=1.0
|
29
29
|
Project-URL: Home, https://gitlab.com/ErikKalkoken/aa-killtracker
|
30
30
|
|
31
31
|
# Killtracker
|
@@ -4,16 +4,21 @@
|
|
4
4
|
|
5
5
|
|
6
6
|
from django.contrib import admin
|
7
|
+
from django.db.models import QuerySet
|
7
8
|
from django.http import HttpResponseRedirect
|
8
9
|
from django.shortcuts import render
|
9
10
|
from django.utils.safestring import mark_safe
|
10
11
|
|
11
12
|
from allianceauth import NAME as site_header
|
12
13
|
|
13
|
-
from
|
14
|
-
from .core.
|
15
|
-
from .forms import
|
16
|
-
|
14
|
+
from killtracker import tasks
|
15
|
+
from killtracker.core.zkb import Killmail
|
16
|
+
from killtracker.forms import (
|
17
|
+
TrackerAdminForm,
|
18
|
+
TrackerAdminKillmailIdForm,
|
19
|
+
field_nice_display,
|
20
|
+
)
|
21
|
+
from killtracker.models import EveKillmail, EveKillmailAttacker, Tracker, Webhook
|
17
22
|
|
18
23
|
|
19
24
|
class EveKillmailAttackerInline(admin.TabularInline):
|
@@ -46,17 +51,17 @@ class WebhookAdmin(admin.ModelAdmin):
|
|
46
51
|
list_filter = ("is_enabled",)
|
47
52
|
ordering = ("name",)
|
48
53
|
|
49
|
-
def _messages_in_queue(self, obj):
|
50
|
-
return obj.
|
54
|
+
def _messages_in_queue(self, obj: Webhook):
|
55
|
+
return obj.messages_queued()
|
51
56
|
|
52
57
|
actions = ["send_test_message", "purge_messages"]
|
53
58
|
|
54
59
|
@admin.display(description="Purge queued messages of selected webhooks")
|
55
|
-
def purge_messages(self, request, queryset):
|
60
|
+
def purge_messages(self, request, queryset: QuerySet[Webhook]):
|
56
61
|
actions_count = 0
|
57
62
|
killmails_deleted = 0
|
58
63
|
for webhook in queryset:
|
59
|
-
killmails_deleted += webhook.
|
64
|
+
killmails_deleted += webhook.delete_queued_messages()
|
60
65
|
actions_count += 1
|
61
66
|
self.message_user(
|
62
67
|
request,
|
@@ -6,20 +6,15 @@ KILLTRACKER_REDISQ_LOCK_TIMEOUT = clean_setting("KILLTRACKER_REDISQ_LOCK_TIMEOUT
|
|
6
6
|
"""Timeout for lock to ensure atomic access to ZKB RedisQ."""
|
7
7
|
|
8
8
|
KILLTRACKER_KILLMAIL_MAX_AGE_FOR_TRACKER = clean_setting(
|
9
|
-
"KILLTRACKER_KILLMAIL_MAX_AGE_FOR_TRACKER",
|
9
|
+
"KILLTRACKER_KILLMAIL_MAX_AGE_FOR_TRACKER", 600
|
10
10
|
)
|
11
11
|
"""Ignore killmails that are older than the given number in minutes
|
12
12
|
sometimes killmails appear belated on ZKB,
|
13
13
|
this feature ensures they don't create new alerts.
|
14
14
|
"""
|
15
15
|
|
16
|
-
KILLTRACKER_MAX_KILLMAILS_PER_RUN = clean_setting(
|
17
|
-
"KILLTRACKER_MAX_KILLMAILS_PER_RUN", 100
|
18
|
-
)
|
19
|
-
"""Maximum number of killmails retrieved from ZKB by task run."""
|
20
|
-
|
21
16
|
KILLTRACKER_PURGE_KILLMAILS_AFTER_DAYS = clean_setting(
|
22
|
-
"KILLTRACKER_PURGE_KILLMAILS_AFTER_DAYS", 30
|
17
|
+
"KILLTRACKER_PURGE_KILLMAILS_AFTER_DAYS", default_value=30, min_value=0
|
23
18
|
)
|
24
19
|
"""Killmails older than set number of days will be purged from the database.
|
25
20
|
If you want to keep all killmails set this to 0.
|
@@ -62,11 +57,14 @@ when creating trackers.
|
|
62
57
|
#####################
|
63
58
|
# INTERNAL SETTINGS
|
64
59
|
|
65
|
-
KILLTRACKER_REDISQ_TTW = clean_setting("KILLTRACKER_REDISQ_TTW",
|
60
|
+
KILLTRACKER_REDISQ_TTW = clean_setting("KILLTRACKER_REDISQ_TTW", 1)
|
66
61
|
"""Max duration to wait for new killmails from redisq in seconds."""
|
67
62
|
|
68
63
|
KILLTRACKER_TASKS_TIMEOUT = clean_setting("KILLTRACKER_TASKS_TIMEOUT", 1_800)
|
69
|
-
"""Tasks hard timeout."""
|
64
|
+
"""Tasks hard timeout in seconds."""
|
65
|
+
|
66
|
+
KILLTRACKER_RUN_TIMEOUT = clean_setting("KILLTRACKER_RUN_TIMEOUT", 55)
|
67
|
+
"""Timeout for killtracker run in seconds."""
|
70
68
|
|
71
69
|
KILLTRACKER_DISCORD_SEND_DELAY = clean_setting(
|
72
70
|
"KILLTRACKER_DISCORD_SEND_DELAY", default_value=2, min_value=1, max_value=900
|
@@ -100,9 +98,21 @@ KILLTRACKER_STORAGE_KILLMAILS_LIFETIME = clean_setting(
|
|
100
98
|
)
|
101
99
|
"""Max lifetime of killmails in temporary storage in seconds."""
|
102
100
|
|
103
|
-
KILLTRACKER_ZKB_REQUEST_DELAY = clean_setting(
|
101
|
+
KILLTRACKER_ZKB_REQUEST_DELAY = clean_setting(
|
102
|
+
"KILLTRACKER_ZKB_REQUEST_DELAY", default_value=500, min_value=500
|
103
|
+
)
|
104
104
|
"""Delay between subsequent calls to ZKB API in milliseconds.
|
105
105
|
|
106
106
|
This delay ensures the app does not breach the CloudFlare rate limit of currently
|
107
107
|
two (2) requests per second per IP address.
|
108
108
|
"""
|
109
|
+
|
110
|
+
KILLTRACKER_MAX_KILLMAILS_PER_RUN = clean_setting(
|
111
|
+
"KILLTRACKER_MAX_KILLMAILS_PER_RUN", default_value=500, min_value=1
|
112
|
+
)
|
113
|
+
"""Maximum number of killmails retrieved from ZKB by task run."""
|
114
|
+
|
115
|
+
KILLTRACKER_MAX_MESSAGES_SENT_PER_RUN = clean_setting(
|
116
|
+
"KILLTRACKER_MAX_MESSAGES_SENT_PER_RUN", default_value=10, min_value=1
|
117
|
+
)
|
118
|
+
"""Maximum number of messages processed per task run."""
|
@@ -9,7 +9,5 @@ class KillmailsConfig(AppConfig):
|
|
9
9
|
verbose_name = f"Killtracker v{__version__}"
|
10
10
|
|
11
11
|
def ready(self) -> None:
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
Killmail.reset_lock_key()
|
12
|
+
import killtracker.checks # noqa: F401 pylint: disable=unused-import
|
13
|
+
import killtracker.signals # noqa: F401 pylint: disable=unused-import
|
@@ -0,0 +1,162 @@
|
|
1
|
+
"""Send messages to Discord webhooks."""
|
2
|
+
|
3
|
+
import datetime as dt
|
4
|
+
import json
|
5
|
+
from copy import copy
|
6
|
+
from dataclasses import dataclass
|
7
|
+
from http import HTTPStatus
|
8
|
+
from time import sleep
|
9
|
+
from typing import List, Optional
|
10
|
+
|
11
|
+
import dhooks_lite
|
12
|
+
|
13
|
+
from django.core.cache import cache
|
14
|
+
from django.utils.timezone import now
|
15
|
+
|
16
|
+
from allianceauth.services.hooks import get_extension_logger
|
17
|
+
from app_utils.json import JSONDateTimeDecoder, JSONDateTimeEncoder
|
18
|
+
from app_utils.logging import LoggerAddTag
|
19
|
+
|
20
|
+
from killtracker import APP_NAME, HOMEPAGE_URL, __title__, __version__
|
21
|
+
from killtracker.app_settings import KILLTRACKER_DISCORD_SEND_DELAY
|
22
|
+
from killtracker.core.helpers import datetime_or_none
|
23
|
+
|
24
|
+
_DEFAULT_429_TIMEOUT = 600
|
25
|
+
|
26
|
+
logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
27
|
+
|
28
|
+
|
29
|
+
class HTTPError(Exception):
|
30
|
+
"""A HTTP error."""
|
31
|
+
|
32
|
+
def __init__(self, status_code: int):
|
33
|
+
self.status_code = status_code
|
34
|
+
|
35
|
+
|
36
|
+
class WebhookRateLimitExhausted(Exception):
|
37
|
+
"""The rate limit of a webhook has been exhausted."""
|
38
|
+
|
39
|
+
def __init__(self, retry_at: dt.datetime, is_original: bool = True):
|
40
|
+
self.retry_at = retry_at
|
41
|
+
self.is_original = is_original
|
42
|
+
|
43
|
+
|
44
|
+
@dataclass
|
45
|
+
class DiscordMessage:
|
46
|
+
"""A Discord message created from a Killmail."""
|
47
|
+
|
48
|
+
avatar_url: Optional[str] = None
|
49
|
+
content: Optional[str] = None
|
50
|
+
embeds: Optional[List[dhooks_lite.Embed]] = None
|
51
|
+
killmail_id: int = 0 # Killmail ID this message from created from
|
52
|
+
username: Optional[str] = None
|
53
|
+
|
54
|
+
def __post_init__(self):
|
55
|
+
if not self.content and not self.embeds:
|
56
|
+
raise ValueError("Message must have content or embeds to be valid")
|
57
|
+
|
58
|
+
def to_json(self) -> str:
|
59
|
+
"""Converts a Discord message into a JSON object and returns it."""
|
60
|
+
|
61
|
+
if self.embeds:
|
62
|
+
embeds_list = [obj.asdict() for obj in self.embeds]
|
63
|
+
else:
|
64
|
+
embeds_list = None
|
65
|
+
|
66
|
+
message = {}
|
67
|
+
if self.killmail_id:
|
68
|
+
message["killmail_id"] = self.killmail_id
|
69
|
+
if self.content:
|
70
|
+
message["content"] = self.content
|
71
|
+
if embeds_list:
|
72
|
+
message["embeds"] = embeds_list
|
73
|
+
if self.username:
|
74
|
+
message["username"] = self.username
|
75
|
+
if self.avatar_url:
|
76
|
+
message["avatar_url"] = self.avatar_url
|
77
|
+
|
78
|
+
return json.dumps(message, cls=JSONDateTimeEncoder)
|
79
|
+
|
80
|
+
@classmethod
|
81
|
+
def from_json(cls, s: str) -> "DiscordMessage":
|
82
|
+
"""Creates a DiscordMessage object from an JSON object and returns it."""
|
83
|
+
message1: dict = json.loads(s, cls=JSONDateTimeDecoder)
|
84
|
+
message2 = copy(message1)
|
85
|
+
if message1.get("embeds"):
|
86
|
+
message2["embeds"] = [
|
87
|
+
dhooks_lite.Embed.from_dict(embed_dict)
|
88
|
+
for embed_dict in message1.get("embeds")
|
89
|
+
]
|
90
|
+
else:
|
91
|
+
message2["embeds"] = None
|
92
|
+
return cls(**message2)
|
93
|
+
|
94
|
+
|
95
|
+
def send_message_to_webhook(name: str, url: str, message: DiscordMessage) -> int:
|
96
|
+
"""Send a message to a Discord webhook and returns the ID of new message."""
|
97
|
+
|
98
|
+
key_retry_at = _make_key_retry_at(url)
|
99
|
+
retry_at = datetime_or_none(cache.get(key_retry_at))
|
100
|
+
if retry_at is not None and retry_at > now():
|
101
|
+
raise WebhookRateLimitExhausted(retry_at=retry_at, is_original=False)
|
102
|
+
|
103
|
+
key_last_request = _make_key_last_request(url)
|
104
|
+
last_request = datetime_or_none(cache.get(key_last_request))
|
105
|
+
if last_request is not None:
|
106
|
+
next_slot = last_request + dt.timedelta(seconds=KILLTRACKER_DISCORD_SEND_DELAY)
|
107
|
+
seconds = (next_slot - now()).total_seconds()
|
108
|
+
if seconds > 0:
|
109
|
+
logger.debug(
|
110
|
+
"%s: Waiting %f seconds for next free slot for webhook", name, seconds
|
111
|
+
)
|
112
|
+
sleep(seconds)
|
113
|
+
|
114
|
+
hook = dhooks_lite.Webhook(
|
115
|
+
url=url,
|
116
|
+
user_agent=dhooks_lite.UserAgent(
|
117
|
+
name=APP_NAME, url=HOMEPAGE_URL, version=__version__
|
118
|
+
),
|
119
|
+
)
|
120
|
+
response = hook.execute(
|
121
|
+
content=message.content,
|
122
|
+
embeds=message.embeds,
|
123
|
+
username=message.username,
|
124
|
+
avatar_url=message.avatar_url,
|
125
|
+
wait_for_response=True,
|
126
|
+
max_retries=0, # we will handle retries ourselves
|
127
|
+
)
|
128
|
+
cache.set(key_last_request, now(), timeout=KILLTRACKER_DISCORD_SEND_DELAY + 30)
|
129
|
+
logger.debug(
|
130
|
+
"%s: Response from Discord for creating message from killmail %d: %s %s %s",
|
131
|
+
name,
|
132
|
+
message.killmail_id,
|
133
|
+
response.status_code,
|
134
|
+
response.headers,
|
135
|
+
response.content,
|
136
|
+
)
|
137
|
+
if not response.status_ok:
|
138
|
+
if response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
|
139
|
+
try:
|
140
|
+
retry_after = int(response.headers["Retry-After"])
|
141
|
+
except KeyError:
|
142
|
+
retry_after = _DEFAULT_429_TIMEOUT
|
143
|
+
retry_at = now() + dt.timedelta(seconds=retry_after)
|
144
|
+
cache.set(key_retry_at, retry_at, timeout=retry_after + 60)
|
145
|
+
raise WebhookRateLimitExhausted(retry_at=retry_at, is_original=True)
|
146
|
+
|
147
|
+
raise HTTPError(response.status_code)
|
148
|
+
|
149
|
+
try:
|
150
|
+
message_id = int(response.content.get("id"))
|
151
|
+
except (AttributeError, ValueError):
|
152
|
+
message_id = 0
|
153
|
+
|
154
|
+
return message_id
|
155
|
+
|
156
|
+
|
157
|
+
def _make_key_last_request(url):
|
158
|
+
return f"killtracker-webhook-last-request-{url}"
|
159
|
+
|
160
|
+
|
161
|
+
def _make_key_retry_at(url):
|
162
|
+
return f"killtracker-webhook-retry-at-{url}"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""Helper functions for core modules."""
|
2
|
+
|
3
|
+
import datetime as dt
|
4
|
+
from typing import Any, Optional
|
5
|
+
|
6
|
+
|
7
|
+
def datetime_or_none(v: Any) -> Optional[dt.datetime]:
|
8
|
+
"""Returns as datetime when it is a datetime else returns None."""
|
9
|
+
if v is None:
|
10
|
+
return None
|
11
|
+
if not isinstance(v, dt.datetime):
|
12
|
+
return None
|
13
|
+
return v
|
@@ -1,9 +1,12 @@
|
|
1
|
-
"""
|
1
|
+
"""Generate Discord messages from tracked killmails."""
|
2
2
|
|
3
|
-
from
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import TYPE_CHECKING, Optional
|
4
7
|
|
5
8
|
import dhooks_lite
|
6
|
-
|
9
|
+
import requests
|
7
10
|
|
8
11
|
from eveuniverse.helpers import EveEntityNameResolver
|
9
12
|
from eveuniverse.models import EveEntity, EveSolarSystem
|
@@ -16,33 +19,54 @@ from app_utils.urls import static_file_absolute_url
|
|
16
19
|
from app_utils.views import humanize_value
|
17
20
|
|
18
21
|
from killtracker import __title__
|
19
|
-
from killtracker.
|
20
|
-
|
21
|
-
from .killmails import ZKB_KILLMAIL_BASEURL, Killmail, TrackerInfo
|
22
|
+
from killtracker.core.discord import DiscordMessage
|
23
|
+
from killtracker.core.zkb import ZKB_KILLMAIL_BASEURL, Killmail, TrackerInfo
|
22
24
|
|
23
|
-
|
25
|
+
if TYPE_CHECKING:
|
26
|
+
from killtracker.models import Tracker
|
24
27
|
|
28
|
+
_ICON_SIZE = 128
|
25
29
|
|
26
30
|
logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
27
31
|
|
28
32
|
|
29
|
-
|
33
|
+
def create_discord_message_from_killmail(
|
34
|
+
tracker: Tracker, killmail: Killmail, intro_text: Optional[str] = None
|
35
|
+
) -> "DiscordMessage":
|
36
|
+
"""Creates a Discord message from a Killmail and returns it."""
|
37
|
+
m = DiscordMessage(
|
38
|
+
killmail_id=killmail.id,
|
39
|
+
content=_create_content(tracker, intro_text),
|
40
|
+
embeds=[_create_embed(tracker, killmail)],
|
41
|
+
)
|
42
|
+
return m
|
43
|
+
|
44
|
+
|
45
|
+
@dataclass(frozen=True)
|
46
|
+
class _MainOrgInfo:
|
30
47
|
"""Infos about a main organization."""
|
31
48
|
|
32
|
-
|
49
|
+
icon_url: str = ""
|
33
50
|
name: str = ""
|
34
|
-
icon_url: str = eveimageserver.alliance_logo_url(1, size=ICON_SIZE)
|
35
51
|
show_as_fleet_kill: bool = False
|
52
|
+
text: str = ""
|
53
|
+
|
54
|
+
def __post_init__(self):
|
55
|
+
if self.icon_url == "":
|
56
|
+
icon_url = eveimageserver.alliance_logo_url(1, size=_ICON_SIZE)
|
57
|
+
object.__setattr__(self, "icon_url", icon_url)
|
36
58
|
|
37
59
|
|
38
|
-
|
60
|
+
@dataclass(frozen=True)
|
61
|
+
class _FinalAttackerInfo:
|
39
62
|
"""Infos about the final attacker on a killmail."""
|
40
63
|
|
41
64
|
name: str = ""
|
42
65
|
ship_type: str = ""
|
43
66
|
|
44
67
|
|
45
|
-
|
68
|
+
@dataclass(frozen=True)
|
69
|
+
class _VictimInfo:
|
46
70
|
"""Infos about the victim of a killmail."""
|
47
71
|
|
48
72
|
name: str
|
@@ -53,9 +77,11 @@ class VictimInfo(NamedTuple):
|
|
53
77
|
ship_type_icon_url: str
|
54
78
|
|
55
79
|
|
56
|
-
def
|
80
|
+
def _create_content(tracker: Tracker, intro_text: Optional[str] = None) -> str:
|
57
81
|
"""Create content for Discord message for a killmail."""
|
58
82
|
|
83
|
+
from killtracker.models import Tracker
|
84
|
+
|
59
85
|
intro_parts = []
|
60
86
|
|
61
87
|
if tracker.ping_type == Tracker.ChannelPingType.EVERYBODY:
|
@@ -69,7 +95,7 @@ def create_content(tracker: Tracker, intro_text: Optional[str] = None) -> str:
|
|
69
95
|
for group in tracker.ping_groups.all():
|
70
96
|
try:
|
71
97
|
role = DiscordUser.objects.group_to_role(group) # type: ignore
|
72
|
-
except HTTPError:
|
98
|
+
except requests.exceptions.HTTPError:
|
73
99
|
logger.warning(
|
74
100
|
"Failed to get Discord roles. Can not ping groups.",
|
75
101
|
exc_info=True,
|
@@ -102,7 +128,7 @@ def _import_discord_user():
|
|
102
128
|
return DiscordUser
|
103
129
|
|
104
130
|
|
105
|
-
def
|
131
|
+
def _create_embed(tracker: Tracker, killmail: Killmail) -> dhooks_lite.Embed:
|
106
132
|
"""Create Discord embed for a killmail."""
|
107
133
|
|
108
134
|
resolver: EveEntityNameResolver = EveEntity.objects.bulk_resolve_names( # type: ignore
|
@@ -111,7 +137,7 @@ def create_embed(tracker: Tracker, killmail: Killmail) -> dhooks_lite.Embed:
|
|
111
137
|
|
112
138
|
# self info
|
113
139
|
distance_text = ""
|
114
|
-
main_org =
|
140
|
+
main_org = _MainOrgInfo()
|
115
141
|
main_ship_group_text = ""
|
116
142
|
tracked_ship_types_text = ""
|
117
143
|
|
@@ -137,10 +163,24 @@ def create_embed(tracker: Tracker, killmail: Killmail) -> dhooks_lite.Embed:
|
|
137
163
|
title = _calc_title(killmail, resolver, main_org, victim)
|
138
164
|
thumbnail_url = _calc_thumbnail_url(victim, main_org)
|
139
165
|
|
140
|
-
|
166
|
+
author = _calc_author(victim)
|
167
|
+
zkb_icon_url = static_file_absolute_url("killtracker/zkb_icon.png")
|
168
|
+
embed_color = int(tracker.color[1:], 16) if tracker and tracker.color else None
|
169
|
+
|
170
|
+
embed = dhooks_lite.Embed(
|
171
|
+
author=author,
|
172
|
+
description=description,
|
173
|
+
title=title,
|
174
|
+
url=f"{ZKB_KILLMAIL_BASEURL}{killmail.id}/",
|
175
|
+
thumbnail=dhooks_lite.Thumbnail(url=thumbnail_url),
|
176
|
+
footer=dhooks_lite.Footer(text="zKillboard", icon_url=zkb_icon_url),
|
177
|
+
timestamp=killmail.time,
|
178
|
+
color=embed_color,
|
179
|
+
)
|
180
|
+
return embed
|
141
181
|
|
142
182
|
|
143
|
-
def _calc_author(victim:
|
183
|
+
def _calc_author(victim: _VictimInfo):
|
144
184
|
# TODO This is a workaround for Embed.Author.name. Address in dhooks_lite
|
145
185
|
return (
|
146
186
|
dhooks_lite.Author(
|
@@ -158,10 +198,10 @@ def _calc_description(
|
|
158
198
|
killmail: Killmail,
|
159
199
|
resolver: EveEntityNameResolver,
|
160
200
|
distance_text: str,
|
161
|
-
main_org:
|
201
|
+
main_org: _MainOrgInfo,
|
162
202
|
main_ship_group_text: str,
|
163
203
|
tracked_ship_types_text: str,
|
164
|
-
victim:
|
204
|
+
victim: _VictimInfo,
|
165
205
|
):
|
166
206
|
solar_system_text = _calc_solar_system(tracker, killmail)
|
167
207
|
total_value = (
|
@@ -186,18 +226,18 @@ def _calc_description(
|
|
186
226
|
|
187
227
|
def _calc_victim(
|
188
228
|
tracker: Tracker, killmail: Killmail, resolver: EveEntityNameResolver
|
189
|
-
) ->
|
229
|
+
) -> _VictimInfo:
|
190
230
|
if killmail.victim.alliance_id:
|
191
231
|
victim_organization = resolver.to_name(killmail.victim.alliance_id)
|
192
232
|
victim_org_url = zkillboard.alliance_url(killmail.victim.alliance_id)
|
193
233
|
victim_org_icon_url = eveimageserver.alliance_logo_url(
|
194
|
-
killmail.victim.alliance_id, size=
|
234
|
+
killmail.victim.alliance_id, size=_ICON_SIZE
|
195
235
|
)
|
196
236
|
elif killmail.victim.corporation_id:
|
197
237
|
victim_organization = resolver.to_name(killmail.victim.corporation_id)
|
198
238
|
victim_org_url = zkillboard.corporation_url(killmail.victim.corporation_id)
|
199
239
|
victim_org_icon_url = eveimageserver.corporation_logo_url(
|
200
|
-
killmail.victim.corporation_id, size=
|
240
|
+
killmail.victim.corporation_id, size=_ICON_SIZE
|
201
241
|
)
|
202
242
|
else:
|
203
243
|
victim_organization = ""
|
@@ -229,12 +269,12 @@ def _calc_victim(
|
|
229
269
|
ship_type = resolver.to_name(ship_type_id) if ship_type_id else ""
|
230
270
|
|
231
271
|
ship_type_icon_url = (
|
232
|
-
eveimageserver.type_icon_url(ship_type_id, size=
|
272
|
+
eveimageserver.type_icon_url(ship_type_id, size=_ICON_SIZE)
|
233
273
|
if ship_type_id
|
234
274
|
else ""
|
235
275
|
)
|
236
276
|
|
237
|
-
return
|
277
|
+
return _VictimInfo(
|
238
278
|
organization=victim_organization,
|
239
279
|
org_url=victim_org_url,
|
240
280
|
org_icon_url=victim_org_icon_url,
|
@@ -246,7 +286,7 @@ def _calc_victim(
|
|
246
286
|
|
247
287
|
def _calc_final_attacker(
|
248
288
|
tracker: Tracker, killmail: Killmail, resolver: EveEntityNameResolver
|
249
|
-
) ->
|
289
|
+
) -> _FinalAttackerInfo:
|
250
290
|
for attacker in killmail.attackers:
|
251
291
|
if attacker.is_final_blow:
|
252
292
|
final_attacker = attacker
|
@@ -255,7 +295,7 @@ def _calc_final_attacker(
|
|
255
295
|
final_attacker = None
|
256
296
|
|
257
297
|
if not final_attacker:
|
258
|
-
return
|
298
|
+
return _FinalAttackerInfo()
|
259
299
|
|
260
300
|
if final_attacker.corporation_id:
|
261
301
|
final_attacker_corporation_zkb_link = _corporation_zkb_link(
|
@@ -286,7 +326,7 @@ def _calc_final_attacker(
|
|
286
326
|
|
287
327
|
ship_type = resolver.to_name(ship_type_id) if ship_type_id else ""
|
288
328
|
|
289
|
-
return
|
329
|
+
return _FinalAttackerInfo(name=final_attacker_str, ship_type=ship_type)
|
290
330
|
|
291
331
|
|
292
332
|
def _calc_solar_system(tracker: Tracker, killmail: Killmail):
|
@@ -338,12 +378,12 @@ def _calc_main_group(
|
|
338
378
|
if main_org_entity.is_corporation:
|
339
379
|
main_org_link = _corporation_zkb_link(tracker, main_org_entity.id, resolver)
|
340
380
|
main_org_icon_url = eveimageserver.corporation_logo_url(
|
341
|
-
main_org_entity.id, size=
|
381
|
+
main_org_entity.id, size=_ICON_SIZE
|
342
382
|
)
|
343
383
|
else:
|
344
384
|
main_org_link = _alliance_zkb_link(tracker, main_org_entity.id, resolver)
|
345
385
|
main_org_icon_url = eveimageserver.alliance_logo_url(
|
346
|
-
main_org_entity.id, size=
|
386
|
+
main_org_entity.id, size=_ICON_SIZE
|
347
387
|
)
|
348
388
|
main_org_text = f" | Main group: {main_org_link} ({main_org_entity.count})"
|
349
389
|
show_as_fleet_kill = tracker.identify_fleets
|
@@ -351,7 +391,7 @@ def _calc_main_group(
|
|
351
391
|
show_as_fleet_kill = False
|
352
392
|
main_org_text = main_org_name = main_org_icon_url = ""
|
353
393
|
|
354
|
-
return
|
394
|
+
return _MainOrgInfo(
|
355
395
|
text=main_org_text,
|
356
396
|
name=main_org_name,
|
357
397
|
icon_url=main_org_icon_url,
|
@@ -380,7 +420,7 @@ def _calc_tracked_ship_types(
|
|
380
420
|
return f"\nTracked ship types involved: **{ship_types_text}**"
|
381
421
|
|
382
422
|
|
383
|
-
def _calc_thumbnail_url(victim:
|
423
|
+
def _calc_thumbnail_url(victim: _VictimInfo, main_org: _MainOrgInfo):
|
384
424
|
if main_org.show_as_fleet_kill:
|
385
425
|
return main_org.icon_url
|
386
426
|
|
@@ -390,8 +430,8 @@ def _calc_thumbnail_url(victim: VictimInfo, main_org: MainOrgInfo):
|
|
390
430
|
def _calc_title(
|
391
431
|
killmail: Killmail,
|
392
432
|
resolver: EveEntityNameResolver,
|
393
|
-
main_org:
|
394
|
-
victim:
|
433
|
+
main_org: _MainOrgInfo,
|
434
|
+
victim: _VictimInfo,
|
395
435
|
):
|
396
436
|
solar_system_name = (
|
397
437
|
resolver.to_name(killmail.solar_system_id) if killmail.solar_system_id else ""
|
@@ -403,31 +443,6 @@ def _calc_title(
|
|
403
443
|
return f"{solar_system_name} | {victim.ship_type} | Killmail"
|
404
444
|
|
405
445
|
|
406
|
-
def _create_embed(
|
407
|
-
killmail: Killmail,
|
408
|
-
tracker: Tracker,
|
409
|
-
victim: VictimInfo,
|
410
|
-
description: str,
|
411
|
-
title: str,
|
412
|
-
thumbnail_url: str,
|
413
|
-
):
|
414
|
-
author = _calc_author(victim)
|
415
|
-
zkb_icon_url = static_file_absolute_url("killtracker/zkb_icon.png")
|
416
|
-
embed_color = int(tracker.color[1:], 16) if tracker and tracker.color else None
|
417
|
-
|
418
|
-
embed = dhooks_lite.Embed(
|
419
|
-
author=author,
|
420
|
-
description=description,
|
421
|
-
title=title,
|
422
|
-
url=f"{ZKB_KILLMAIL_BASEURL}{killmail.id}/",
|
423
|
-
thumbnail=dhooks_lite.Thumbnail(url=thumbnail_url),
|
424
|
-
footer=dhooks_lite.Footer(text="zKillboard", icon_url=zkb_icon_url),
|
425
|
-
timestamp=killmail.time,
|
426
|
-
color=embed_color,
|
427
|
-
)
|
428
|
-
return embed
|
429
|
-
|
430
|
-
|
431
446
|
def _character_zkb_link(
|
432
447
|
tracker: Tracker, entity_id: int, resolver: EveEntityNameResolver
|
433
448
|
) -> str:
|
@@ -0,0 +1,46 @@
|
|
1
|
+
"""Allows tasks to find out whether their worker is currently shutting down.
|
2
|
+
|
3
|
+
This enables long running tasks to abort early,
|
4
|
+
which helps to speed up a warm worker shutdown.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from celery import Task
|
8
|
+
|
9
|
+
from django.core.cache import cache
|
10
|
+
|
11
|
+
from allianceauth.services.hooks import get_extension_logger
|
12
|
+
from app_utils.logging import LoggerAddTag
|
13
|
+
|
14
|
+
from killtracker import __title__
|
15
|
+
|
16
|
+
_TIMEOUT_SECONDS = 120
|
17
|
+
|
18
|
+
logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
19
|
+
|
20
|
+
|
21
|
+
def state_reset(hostname: str) -> None:
|
22
|
+
"""Resets the shutting down state for a worker."""
|
23
|
+
cache.delete(_make_key(hostname))
|
24
|
+
|
25
|
+
|
26
|
+
def state_set(hostname: str) -> None:
|
27
|
+
"""Sets a worker into the shutting down state."""
|
28
|
+
cache.set(_make_key(hostname), "shutting down", timeout=_TIMEOUT_SECONDS)
|
29
|
+
|
30
|
+
|
31
|
+
def is_shutting_down(task: Task) -> bool:
|
32
|
+
"""Reports whether the worker of a celery task is currently shutting down."""
|
33
|
+
try:
|
34
|
+
hostname = str(task.request.hostname)
|
35
|
+
except (AttributeError, TypeError, ValueError):
|
36
|
+
logger.warning("Failed to retrieve hostname: %s", task)
|
37
|
+
return False
|
38
|
+
|
39
|
+
if cache.get(_make_key(hostname)) is None:
|
40
|
+
return False
|
41
|
+
|
42
|
+
return True
|
43
|
+
|
44
|
+
|
45
|
+
def _make_key(hostname: str) -> str:
|
46
|
+
return f"killtracker-worker-shutting-down-{hostname}"
|