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.
Files changed (79) hide show
  1. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/PKG-INFO +7 -7
  2. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/__init__.py +1 -1
  3. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/admin.py +13 -8
  4. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/app_settings.py +20 -10
  5. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/apps.py +2 -4
  6. aa_killtracker-1.0.0a1/killtracker/core/discord.py +162 -0
  7. aa_killtracker-1.0.0a1/killtracker/core/helpers.py +13 -0
  8. aa_killtracker-0.17.0/killtracker/core/discord_messages.py → aa_killtracker-1.0.0a1/killtracker/core/trackers.py +74 -59
  9. aa_killtracker-1.0.0a1/killtracker/core/workers.py +46 -0
  10. aa_killtracker-0.17.0/killtracker/core/killmails.py → aa_killtracker-1.0.0a1/killtracker/core/zkb.py +97 -72
  11. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/forms.py +1 -1
  12. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/managers.py +3 -3
  13. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/models/trackers.py +7 -10
  14. aa_killtracker-1.0.0a1/killtracker/models/webhooks.py +174 -0
  15. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/providers.py +1 -1
  16. aa_killtracker-1.0.0a1/killtracker/signals.py +31 -0
  17. aa_killtracker-1.0.0a1/killtracker/tasks.py +280 -0
  18. aa_killtracker-1.0.0a1/killtracker/tests/core/test_discord.py +184 -0
  19. aa_killtracker-1.0.0a1/killtracker/tests/core/test_helpers.py +23 -0
  20. 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
  21. 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
  22. aa_killtracker-1.0.0a1/killtracker/tests/core/test_workers.py +49 -0
  23. aa_killtracker-0.17.0/killtracker/tests/core/test_killmails.py → aa_killtracker-1.0.0a1/killtracker/tests/core/test_zkb.py +109 -52
  24. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/models/test_killmails.py +0 -2
  25. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/models/test_trackers_1.py +24 -24
  26. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/models/test_trackers_2.py +6 -5
  27. aa_killtracker-1.0.0a1/killtracker/tests/models/test_webhooks.py +63 -0
  28. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/test_integration.py +25 -12
  29. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/test_tasks.py +161 -92
  30. aa_killtracker-1.0.0a1/killtracker/tests/test_utils.py +39 -0
  31. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/factories.py +1 -1
  32. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/helpers.py +1 -1
  33. aa_killtracker-1.0.0a1/killtracker/tests/utils.py +44 -0
  34. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/pyproject.toml +11 -8
  35. aa_killtracker-0.17.0/killtracker/exceptions.py +0 -32
  36. aa_killtracker-0.17.0/killtracker/models/webhooks.py +0 -242
  37. aa_killtracker-0.17.0/killtracker/tasks.py +0 -231
  38. aa_killtracker-0.17.0/killtracker/tests/models/test_webhook.py +0 -150
  39. aa_killtracker-0.17.0/killtracker/tests/test_exceptions.py +0 -12
  40. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/LICENSE +0 -0
  41. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/README.md +0 -0
  42. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/checks.py +0 -0
  43. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/constants.py +0 -0
  44. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/core/__init__.py +0 -0
  45. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/management/commands/killtracker_load_eve.py +0 -0
  46. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0001_initial_new.py +0 -0
  47. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0001_squashed_all.py +0 -0
  48. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0002_fix_webhook_notes_field.py +0 -0
  49. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0002_tracker_require_attackers_weapon_groups_and_more.py +0 -0
  50. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0003_add_state_clauses.py +0 -0
  51. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0003_optimize_tracker_form.py +0 -0
  52. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0004_add_faction_clauses.py +0 -0
  53. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0004_django4_update.py +0 -0
  54. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0005_add_final_blow_clause_and_more.py +0 -0
  55. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0006_evetypeplus.py +0 -0
  56. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0007_restructure_killsmails.py +0 -0
  57. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0008_copy_data_to_new_structure.py +0 -0
  58. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/0009_remove_old_models.py +0 -0
  59. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/migrations/__init__.py +0 -0
  60. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/models/__init__.py +0 -0
  61. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/models/killmails.py +0 -0
  62. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/static/killtracker/killtracker_logo.png +0 -0
  63. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/static/killtracker/zkb_icon.png +0 -0
  64. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/templates/admin/killtracker/tracker/killmail_test.html +0 -0
  65. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/__init__.py +0 -0
  66. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/core/__init__.py +0 -0
  67. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/models/__init__.py +0 -0
  68. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/test_admin.py +0 -0
  69. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/test_admin_2.py +0 -0
  70. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/__init__.py +0 -0
  71. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/create_eveuniverse.py +0 -0
  72. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/evealliances.json +0 -0
  73. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/evecorporations.json +0 -0
  74. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/eveentities.json +0 -0
  75. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/eveuniverse.json +0 -0
  76. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/killmails.json +0 -0
  77. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tests/testdata/load_eveuniverse.py +0 -0
  78. {aa_killtracker-0.17.0 → aa_killtracker-1.0.0a1}/killtracker/tools/drop_tables_killtracker.sql +0 -0
  79. {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.17.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.19.0
24
- Requires-Dist: allianceauth>=3
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.0
27
- Requires-Dist: django-eveuniverse>=1.3
28
- Requires-Dist: redis-simple-mq>=0.5
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
@@ -3,7 +3,7 @@
3
3
  # pylint: disable = invalid-name
4
4
  default_app_config = "killtracker.apps.KillmailsConfig"
5
5
 
6
- __version__ = "0.17.0"
6
+ __version__ = "1.0.0a1"
7
7
  __title__ = "Killtracker"
8
8
 
9
9
  APP_NAME = "aa-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 . import tasks
14
- from .core.killmails import Killmail
15
- from .forms import TrackerAdminForm, TrackerAdminKillmailIdForm, field_nice_display
16
- from .models import EveKillmail, EveKillmailAttacker, Tracker, Webhook
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.main_queue.size()
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.main_queue.clear()
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", 60
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", 5)
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("KILLTRACKER_ZKB_REQUEST_DELAY", 500)
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
- from . import checks # noqa: F401 pylint: disable=unused-import
13
- from .core.killmails import Killmail
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
- """Create discord messages from killmails."""
1
+ """Generate Discord messages from tracked killmails."""
2
2
 
3
- from typing import NamedTuple, Optional
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
- from requests.exceptions import HTTPError
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.models import Tracker
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
- ICON_SIZE = 128
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
- class MainOrgInfo(NamedTuple):
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
- text: str = ""
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
- class FinalAttackerInfo(NamedTuple):
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
- class VictimInfo(NamedTuple):
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 create_content(tracker: Tracker, intro_text: Optional[str] = None) -> str:
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 create_embed(tracker: Tracker, killmail: Killmail) -> dhooks_lite.Embed:
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 = MainOrgInfo()
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
- return _create_embed(killmail, tracker, victim, description, title, thumbnail_url)
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: VictimInfo):
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: MainOrgInfo,
201
+ main_org: _MainOrgInfo,
162
202
  main_ship_group_text: str,
163
203
  tracked_ship_types_text: str,
164
- victim: VictimInfo,
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
- ) -> VictimInfo:
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=ICON_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=ICON_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=ICON_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 VictimInfo(
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
- ) -> FinalAttackerInfo:
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 FinalAttackerInfo()
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 FinalAttackerInfo(name=final_attacker_str, ship_type=ship_type)
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=ICON_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=ICON_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 MainOrgInfo(
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: VictimInfo, main_org: MainOrgInfo):
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: MainOrgInfo,
394
- victim: VictimInfo,
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}"