django-slack-tools 0.2.2__py3-none-any.whl → 0.3.0__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.
- django_slack_tools/__init__.py +0 -1
- django_slack_tools/app_settings.py +54 -90
- django_slack_tools/locale/ko_KR/LC_MESSAGES/django.po +150 -186
- django_slack_tools/py.typed +0 -0
- django_slack_tools/slack_messages/admin/message.py +1 -52
- django_slack_tools/slack_messages/admin/message_recipient.py +2 -2
- django_slack_tools/slack_messages/admin/messaging_policy.py +5 -5
- django_slack_tools/slack_messages/backends/__init__.py +3 -3
- django_slack_tools/slack_messages/backends/base.py +39 -203
- django_slack_tools/slack_messages/backends/dummy.py +0 -12
- django_slack_tools/slack_messages/backends/{logging.py → logging_.py} +0 -6
- django_slack_tools/slack_messages/backends/slack.py +21 -75
- django_slack_tools/slack_messages/message_templates/__init__.py +5 -0
- django_slack_tools/slack_messages/message_templates/base.py +17 -0
- django_slack_tools/{utils/template → slack_messages/message_templates}/django.py +30 -25
- django_slack_tools/slack_messages/message_templates/python.py +38 -0
- django_slack_tools/slack_messages/messenger.py +158 -0
- django_slack_tools/slack_messages/middlewares/__init__.py +4 -0
- django_slack_tools/slack_messages/middlewares/base.py +34 -0
- django_slack_tools/slack_messages/middlewares/django.py +218 -0
- django_slack_tools/slack_messages/migrations/0001_initial.py +4 -4
- django_slack_tools/slack_messages/migrations/0005_alter_slackmessagingpolicy_template_type.py +46 -0
- django_slack_tools/slack_messages/migrations/0006_alter_slackmessage_id.py +26 -0
- django_slack_tools/slack_messages/models/mention.py +1 -1
- django_slack_tools/slack_messages/models/message.py +5 -2
- django_slack_tools/slack_messages/models/message_recipient.py +1 -1
- django_slack_tools/slack_messages/models/messaging_policy.py +4 -4
- django_slack_tools/slack_messages/request.py +91 -0
- django_slack_tools/slack_messages/response.py +21 -0
- django_slack_tools/slack_messages/shortcuts.py +78 -0
- django_slack_tools/slack_messages/tasks.py +16 -49
- django_slack_tools/slack_messages/template_loaders/__init__.py +11 -0
- django_slack_tools/slack_messages/template_loaders/base.py +16 -0
- django_slack_tools/slack_messages/template_loaders/django.py +74 -0
- django_slack_tools/slack_messages/template_loaders/errors.py +9 -0
- django_slack_tools/{utils/slack/django.py → slack_messages/validators.py} +1 -1
- django_slack_tools/utils/django/__init__.py +0 -0
- django_slack_tools/utils/import_helper.py +37 -0
- django_slack_tools/utils/repr.py +10 -0
- django_slack_tools/utils/slack/__init__.py +1 -9
- {django_slack_tools-0.2.2.dist-info → django_slack_tools-0.3.0.dist-info}/METADATA +12 -60
- django_slack_tools-0.3.0.dist-info/RECORD +58 -0
- {django_slack_tools-0.2.2.dist-info → django_slack_tools-0.3.0.dist-info}/WHEEL +1 -1
- django_slack_tools/slack_messages/message.py +0 -110
- django_slack_tools/utils/slack/message.py +0 -93
- django_slack_tools/utils/template/__init__.py +0 -5
- django_slack_tools/utils/template/base.py +0 -17
- django_slack_tools/utils/template/dict.py +0 -52
- django_slack_tools-0.2.2.dist-info/RECORD +0 -43
- /django_slack_tools/utils/{model_mixins.py → django/model_mixins.py} +0 -0
- /django_slack_tools/utils/{widgets.py → django/widgets.py} +0 -0
- {django_slack_tools-0.2.2.dist-info → django_slack_tools-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# noqa: D100
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
from .base import BaseTemplate
|
|
8
|
+
|
|
9
|
+
_PyObj = TypeVar("_PyObj", dict, list, str)
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PythonTemplate(BaseTemplate[_PyObj]):
|
|
15
|
+
"""Template that renders a dictionary."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, template: _PyObj) -> None:
|
|
18
|
+
"""Initialize the template."""
|
|
19
|
+
self.template = template
|
|
20
|
+
|
|
21
|
+
def render(self, context: dict[str, Any]) -> _PyObj: # noqa: D102
|
|
22
|
+
logger.debug("Rendering template %r with context %r", self.template, context)
|
|
23
|
+
result = _format_obj(self.template, context=context)
|
|
24
|
+
logger.debug("Rendered template %r to %r", self.template, result)
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _format_obj(obj: _PyObj, *, context: dict[str, Any]) -> _PyObj:
|
|
29
|
+
if isinstance(obj, dict):
|
|
30
|
+
return {key: _format_obj(value, context=context) for key, value in obj.items()}
|
|
31
|
+
|
|
32
|
+
if isinstance(obj, list):
|
|
33
|
+
return [_format_obj(item, context=context) for item in obj]
|
|
34
|
+
|
|
35
|
+
if isinstance(obj, str):
|
|
36
|
+
return obj.format_map(context)
|
|
37
|
+
|
|
38
|
+
return obj
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# noqa: D100
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from django_slack_tools.slack_messages.backends import BaseBackend
|
|
8
|
+
from django_slack_tools.slack_messages.middlewares import BaseMiddleware
|
|
9
|
+
from django_slack_tools.slack_messages.request import MessageBody, MessageHeader, MessageRequest
|
|
10
|
+
from django_slack_tools.slack_messages.template_loaders import BaseTemplateLoader, TemplateNotFoundError
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
|
|
15
|
+
from django_slack_tools.slack_messages.message_templates import BaseTemplate
|
|
16
|
+
from django_slack_tools.slack_messages.response import MessageResponse
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Messenger:
|
|
22
|
+
"""Messenger class that sends message using templates and middlewares.
|
|
23
|
+
|
|
24
|
+
Components evaluated in order:
|
|
25
|
+
|
|
26
|
+
1. Request processing middlewares
|
|
27
|
+
2. Load template by key and render message in-place
|
|
28
|
+
4. Send message
|
|
29
|
+
5. Response processing middlewares (in reverse order)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
template_loaders: Sequence[BaseTemplateLoader],
|
|
36
|
+
middlewares: Sequence[BaseMiddleware],
|
|
37
|
+
messaging_backend: BaseBackend,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Initialize the Messenger.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
template_loaders: A sequence of template loaders.
|
|
43
|
+
It is tried in order to load the template and the first one that returns a template is used.
|
|
44
|
+
middlewares: A sequence of middlewares.
|
|
45
|
+
Middlewares are applied in the order they are provided for request, and in reverse order for response.
|
|
46
|
+
messaging_backend: The messaging backend to be used.
|
|
47
|
+
"""
|
|
48
|
+
# Validate the template loaders
|
|
49
|
+
for tl in template_loaders:
|
|
50
|
+
if not isinstance(tl, BaseTemplateLoader):
|
|
51
|
+
msg = f"Expected inherited from {BaseTemplateLoader!s}, got {type(tl)}"
|
|
52
|
+
raise TypeError(msg)
|
|
53
|
+
|
|
54
|
+
self.template_loaders = template_loaders
|
|
55
|
+
|
|
56
|
+
# Validate the middlewares
|
|
57
|
+
for mw in middlewares:
|
|
58
|
+
if not isinstance(mw, BaseMiddleware):
|
|
59
|
+
msg = f"Expected inherited from {BaseMiddleware!s}, got {type(mw)}"
|
|
60
|
+
raise TypeError(msg)
|
|
61
|
+
|
|
62
|
+
self.middlewares = middlewares
|
|
63
|
+
|
|
64
|
+
# Validate the messaging backend
|
|
65
|
+
if not isinstance(messaging_backend, BaseBackend):
|
|
66
|
+
msg = f"Expected inherited from {BaseBackend!s}, got {type(messaging_backend)}"
|
|
67
|
+
raise TypeError(msg)
|
|
68
|
+
|
|
69
|
+
self.messaging_backend = messaging_backend
|
|
70
|
+
|
|
71
|
+
def send(
|
|
72
|
+
self,
|
|
73
|
+
to: str,
|
|
74
|
+
*,
|
|
75
|
+
template: str | None = None,
|
|
76
|
+
context: dict[str, str],
|
|
77
|
+
header: MessageHeader | dict[str, Any] | None = None,
|
|
78
|
+
) -> MessageResponse | None:
|
|
79
|
+
"""Simplified shortcut for `.send_request()`."""
|
|
80
|
+
header = MessageHeader.model_validate(header or {})
|
|
81
|
+
request = MessageRequest(template_key=template, channel=to, context=context, header=header)
|
|
82
|
+
return self.send_request(request=request)
|
|
83
|
+
|
|
84
|
+
def send_request(self, request: MessageRequest) -> MessageResponse | None:
|
|
85
|
+
"""Sends a message request and processes the response."""
|
|
86
|
+
logger.info("Sending request: %s", request)
|
|
87
|
+
_request = self._process_request(request)
|
|
88
|
+
if _request is None:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
self._render_message(_request)
|
|
92
|
+
response = self._deliver_message(_request)
|
|
93
|
+
_response = self._process_response(response)
|
|
94
|
+
if _response is None:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
logger.info("Response: %s", _response)
|
|
98
|
+
return response
|
|
99
|
+
|
|
100
|
+
def _process_request(self, request: MessageRequest) -> MessageRequest | None:
|
|
101
|
+
"""Processes the request with middlewares in forward order."""
|
|
102
|
+
for middleware in self.middlewares:
|
|
103
|
+
logger.debug("Processing request (%s) with middleware %s", request, middleware)
|
|
104
|
+
new_request = middleware.process_request(request)
|
|
105
|
+
if new_request is None:
|
|
106
|
+
logger.warning("Middleware %s returned `None`, skipping remaining middlewares", middleware)
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
request = new_request
|
|
110
|
+
|
|
111
|
+
logger.debug("Request after processing: %s", request)
|
|
112
|
+
return request
|
|
113
|
+
|
|
114
|
+
def _render_message(self, request: MessageRequest) -> None:
|
|
115
|
+
"""Updates the request with rendered message, in-place."""
|
|
116
|
+
if request.body is not None:
|
|
117
|
+
logger.debug("Request already has a body, skipping rendering")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if request.template_key is None:
|
|
121
|
+
msg = "Template key is required to render the message"
|
|
122
|
+
raise ValueError(msg)
|
|
123
|
+
|
|
124
|
+
template = self._get_template(request.template_key)
|
|
125
|
+
logger.debug("Rendering request %s with template: %s", request, template)
|
|
126
|
+
rendered = template.render(request.context)
|
|
127
|
+
request.body = MessageBody.model_validate(rendered)
|
|
128
|
+
|
|
129
|
+
def _get_template(self, key: str) -> BaseTemplate:
|
|
130
|
+
"""Loads the template by key."""
|
|
131
|
+
for loader in self.template_loaders:
|
|
132
|
+
template = loader.load(key)
|
|
133
|
+
if template is not None:
|
|
134
|
+
return template
|
|
135
|
+
|
|
136
|
+
msg = f"Template with key '{key}' not found"
|
|
137
|
+
raise TemplateNotFoundError(msg)
|
|
138
|
+
|
|
139
|
+
def _deliver_message(self, request: MessageRequest) -> MessageResponse:
|
|
140
|
+
"""Invoke the messaging backend to deliver the message."""
|
|
141
|
+
logger.debug("Delivering message request: %s", request)
|
|
142
|
+
response = self.messaging_backend.deliver(request)
|
|
143
|
+
logger.debug("Response after delivery: %s", response)
|
|
144
|
+
return response
|
|
145
|
+
|
|
146
|
+
def _process_response(self, response: MessageResponse) -> MessageResponse | None:
|
|
147
|
+
"""Processes the response with middlewares in reverse order."""
|
|
148
|
+
for middleware in reversed(self.middlewares):
|
|
149
|
+
logger.debug("Processing response (%s) with middleware: %s", response, middleware)
|
|
150
|
+
new_response = middleware.process_response(response)
|
|
151
|
+
if new_response is None:
|
|
152
|
+
logger.warning("Middleware %s returned `None`, skipping remaining middlewares", middleware)
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
response = new_response
|
|
156
|
+
|
|
157
|
+
logger.debug("Response after processing: %s", response)
|
|
158
|
+
return response
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# noqa: D100
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from django_slack_tools.slack_messages.request import MessageRequest
|
|
8
|
+
from django_slack_tools.slack_messages.response import MessageResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseMiddleware:
|
|
12
|
+
"""Base class for middleware components."""
|
|
13
|
+
|
|
14
|
+
def process_request(self, request: MessageRequest) -> MessageRequest | None: # pragma: no cover
|
|
15
|
+
"""Process the incoming requests.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
request: Message request.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
MessageRequest objects or `None`.
|
|
22
|
+
"""
|
|
23
|
+
return request
|
|
24
|
+
|
|
25
|
+
def process_response(self, response: MessageResponse) -> MessageResponse | None: # pragma: no cover
|
|
26
|
+
"""Processes a sequence of MessageResponse objects and returns the processed sequence.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
response: Message response.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
MessageResponse objects or `None`.
|
|
33
|
+
"""
|
|
34
|
+
return response
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# noqa: D100
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import TYPE_CHECKING, Literal
|
|
6
|
+
|
|
7
|
+
from slack_bolt import App
|
|
8
|
+
from slack_sdk.errors import SlackApiError
|
|
9
|
+
|
|
10
|
+
from django_slack_tools.slack_messages.models import SlackMessage, SlackMessageRecipient, SlackMessagingPolicy
|
|
11
|
+
from django_slack_tools.slack_messages.request import MessageHeader, MessageRequest
|
|
12
|
+
|
|
13
|
+
from .base import BaseMiddleware
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from django_slack_tools.slack_messages.messenger import Messenger
|
|
17
|
+
from django_slack_tools.slack_messages.response import MessageResponse
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DjangoDatabasePersister(BaseMiddleware):
|
|
23
|
+
"""Persist message history to database. If request is `None`, will do nothing."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, *, slack_app: App | None = None, get_permalink: bool = False) -> None:
|
|
26
|
+
"""Initialize the middleware.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
slack_app: Slack app instance to use for certain tasks, such as getting permalinks.
|
|
30
|
+
get_permalink: If `True`, will try to get the permalink of the message.
|
|
31
|
+
"""
|
|
32
|
+
if get_permalink and not isinstance(slack_app, App):
|
|
33
|
+
msg = "`slack_app` must be an instance of `App` if `get_permalink` is set `True`."
|
|
34
|
+
raise ValueError(msg)
|
|
35
|
+
|
|
36
|
+
self.slack_app = slack_app
|
|
37
|
+
self.get_permalink = get_permalink
|
|
38
|
+
|
|
39
|
+
def process_response(self, response: MessageResponse) -> MessageResponse | None: # noqa: D102
|
|
40
|
+
request = response.request
|
|
41
|
+
if request is None:
|
|
42
|
+
logger.warning("No request found in response, skipping persister.")
|
|
43
|
+
return response
|
|
44
|
+
|
|
45
|
+
logger.debug("Getting permalink for message: %s", response)
|
|
46
|
+
if self.get_permalink: # noqa: SIM108
|
|
47
|
+
permalink = self._get_permalink(channel=request.channel, ts=response.ts)
|
|
48
|
+
else:
|
|
49
|
+
permalink = ""
|
|
50
|
+
|
|
51
|
+
logger.debug("Persisting message history to database: %s", response)
|
|
52
|
+
try:
|
|
53
|
+
history = SlackMessage(
|
|
54
|
+
id=request.id_,
|
|
55
|
+
channel=request.channel,
|
|
56
|
+
header=request.header.model_dump(),
|
|
57
|
+
body=request.body.model_dump() if request.body else {},
|
|
58
|
+
ok=response.ok,
|
|
59
|
+
permalink=permalink,
|
|
60
|
+
ts=response.ts,
|
|
61
|
+
parent_ts=response.parent_ts or "",
|
|
62
|
+
request=request.model_dump(),
|
|
63
|
+
response=response.model_dump(exclude={"request"}),
|
|
64
|
+
exception=response.error or "",
|
|
65
|
+
)
|
|
66
|
+
history.save()
|
|
67
|
+
except Exception:
|
|
68
|
+
logger.exception("Error while saving message history: %s", response)
|
|
69
|
+
|
|
70
|
+
return response
|
|
71
|
+
|
|
72
|
+
def _get_permalink(self, *, channel: str, ts: str | None) -> str:
|
|
73
|
+
"""Get permalink of the message. It returns empty string on error."""
|
|
74
|
+
if not self.slack_app:
|
|
75
|
+
logger.warning("Slack app not provided, cannot get permalink.")
|
|
76
|
+
return ""
|
|
77
|
+
|
|
78
|
+
if not ts:
|
|
79
|
+
logger.warning("No message ts provided, cannot get permalink.")
|
|
80
|
+
return ""
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
response = self.slack_app.client.chat_getPermalink(channel=channel, message_ts=ts)
|
|
84
|
+
return response.get("permalink", default="")
|
|
85
|
+
except SlackApiError as err:
|
|
86
|
+
logger.debug("Error while getting permalink: %s", exc_info=err)
|
|
87
|
+
return ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
OnPolicyNotExists = Literal["create", "default", "error"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DjangoDatabasePolicyHandler(BaseMiddleware):
|
|
94
|
+
"""Middleware to handle Slack messaging policies stored in the database.
|
|
95
|
+
|
|
96
|
+
Be cautious when using this middleware because it includes functionality to distribute messages to multiple recipients,
|
|
97
|
+
which could lead to unwanted infinite loop or recursion if used improperly.
|
|
98
|
+
|
|
99
|
+
This middleware contains a secondary protection against infinite loops by injecting a context key to the message context.
|
|
100
|
+
If the key is found in the context, the middleware will stop the message from being sent. So be careful when modifying the context.
|
|
101
|
+
""" # noqa: E501
|
|
102
|
+
|
|
103
|
+
_RECURSION_DETECTION_CONTEXT_KEY = "__final__"
|
|
104
|
+
"""Recursion detection key injected to message context for fanned-out messages to provide secondary protection against infinite loops.""" # noqa: E501
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
messenger: Messenger | str,
|
|
110
|
+
on_policy_not_exists: OnPolicyNotExists = "error",
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Initialize the middleware.
|
|
113
|
+
|
|
114
|
+
This middleware will load the policy from the database and send the message to all recipients.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
messenger: Messenger instance or name to use for sending messages.
|
|
118
|
+
The messenger instance should be different from the one used in the policy handler,
|
|
119
|
+
because this middleware cannot properly handle fanned-out messages modified by this middleware.
|
|
120
|
+
Also, there are chances of infinite loops if the same messenger is used.
|
|
121
|
+
on_policy_not_exists: Action to take when policy is not found.
|
|
122
|
+
"""
|
|
123
|
+
if on_policy_not_exists not in ("create", "default", "error"):
|
|
124
|
+
msg = f'Unknown value for `on_policy_not_exists`: "{on_policy_not_exists}"'
|
|
125
|
+
raise ValueError(msg)
|
|
126
|
+
|
|
127
|
+
self._messenger = messenger
|
|
128
|
+
self.on_policy_not_exists = on_policy_not_exists
|
|
129
|
+
|
|
130
|
+
# * It's not desirable to put import in the method,
|
|
131
|
+
# * but it's the only way to avoid circular imports for now (what's the fix?)
|
|
132
|
+
@property
|
|
133
|
+
def messenger(self) -> Messenger:
|
|
134
|
+
"""Get the messenger instance. If it's a string, will get the messenger from the app settings."""
|
|
135
|
+
if isinstance(self._messenger, str):
|
|
136
|
+
from django_slack_tools.app_settings import get_messenger
|
|
137
|
+
|
|
138
|
+
self._messenger = get_messenger(self._messenger)
|
|
139
|
+
|
|
140
|
+
return self._messenger
|
|
141
|
+
|
|
142
|
+
def process_request(self, request: MessageRequest) -> MessageRequest | None: # noqa: D102
|
|
143
|
+
# TODO(lasuillard): Hacky way to stop the request, need to find a better way
|
|
144
|
+
# Some extra field (request.meta) could be added to share control context
|
|
145
|
+
if request.context.get(self._RECURSION_DETECTION_CONTEXT_KEY, False):
|
|
146
|
+
return request
|
|
147
|
+
|
|
148
|
+
code = request.channel
|
|
149
|
+
policy = self._get_policy(code=code)
|
|
150
|
+
if not policy.enabled:
|
|
151
|
+
logger.debug("Policy %s is disabled, skipping further messaging", policy)
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
requests: list[MessageRequest] = []
|
|
155
|
+
for recipient in policy.recipients.all():
|
|
156
|
+
default_context = self._get_default_context(recipient)
|
|
157
|
+
context = {
|
|
158
|
+
**default_context,
|
|
159
|
+
**request.context,
|
|
160
|
+
self._RECURSION_DETECTION_CONTEXT_KEY: True,
|
|
161
|
+
}
|
|
162
|
+
header = MessageHeader.model_validate(
|
|
163
|
+
{
|
|
164
|
+
**policy.header_defaults,
|
|
165
|
+
**request.header.model_dump(),
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
req = MessageRequest(channel=recipient.channel, template_key=policy.code, context=context, header=header)
|
|
169
|
+
requests.append(req)
|
|
170
|
+
|
|
171
|
+
# TODO(lasuillard): How to provide users the access the newly created messages?
|
|
172
|
+
# currently, it's possible with persisters but it would require some additional work
|
|
173
|
+
# TODO(lasuillard): Can `sys.setrecursionlimit` be used to prevent spamming if recursion occurs?
|
|
174
|
+
for req in requests:
|
|
175
|
+
self.messenger.send_request(req)
|
|
176
|
+
|
|
177
|
+
# Stop current request
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
def _get_policy(self, *, code: str) -> SlackMessagingPolicy:
|
|
181
|
+
"""Get the policy for the given code."""
|
|
182
|
+
try:
|
|
183
|
+
policy = SlackMessagingPolicy.objects.get(code=code)
|
|
184
|
+
except SlackMessagingPolicy.DoesNotExist:
|
|
185
|
+
if self.on_policy_not_exists == "create":
|
|
186
|
+
logger.warning("No policy found for template key, creating one: %s", code)
|
|
187
|
+
policy = self._create_policy(code=code)
|
|
188
|
+
elif self.on_policy_not_exists == "default":
|
|
189
|
+
policy = SlackMessagingPolicy.objects.get(code="DEFAULT")
|
|
190
|
+
elif self.on_policy_not_exists == "error":
|
|
191
|
+
raise
|
|
192
|
+
else:
|
|
193
|
+
msg = f'Unknown value for `on_policy_not_exists`: "{self.on_policy_not_exists}"'
|
|
194
|
+
raise ValueError(msg) from None
|
|
195
|
+
|
|
196
|
+
return policy
|
|
197
|
+
|
|
198
|
+
def _create_policy(self, *, code: str) -> SlackMessagingPolicy:
|
|
199
|
+
"""Create a policy with the given code.
|
|
200
|
+
|
|
201
|
+
Policy created is disabled by default, thus no message will be sent.
|
|
202
|
+
To modify the default policy creation behavior, simply override this method.
|
|
203
|
+
"""
|
|
204
|
+
policy = SlackMessagingPolicy.objects.create(
|
|
205
|
+
code=code,
|
|
206
|
+
enabled=False,
|
|
207
|
+
template_type=SlackMessagingPolicy.TemplateType.UNKNOWN,
|
|
208
|
+
)
|
|
209
|
+
default_recipients = SlackMessageRecipient.objects.filter(alias="DEFAULT")
|
|
210
|
+
policy.recipients.set(default_recipients)
|
|
211
|
+
return policy
|
|
212
|
+
|
|
213
|
+
def _get_default_context(self, recipient: SlackMessageRecipient) -> dict:
|
|
214
|
+
"""Create default context for the recipient."""
|
|
215
|
+
mentions = [mention.mention for mention in recipient.mentions.all()]
|
|
216
|
+
return {
|
|
217
|
+
"mentions": mentions,
|
|
218
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import django.db.models.deletion
|
|
4
4
|
from django.db import migrations, models
|
|
5
5
|
|
|
6
|
-
import django_slack_tools.
|
|
6
|
+
import django_slack_tools.slack_messages.validators
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class Migration(migrations.Migration):
|
|
@@ -152,7 +152,7 @@ class Migration(migrations.Migration):
|
|
|
152
152
|
blank=True,
|
|
153
153
|
default=dict,
|
|
154
154
|
help_text="Default header values applied to messages on creation.",
|
|
155
|
-
validators=[django_slack_tools.
|
|
155
|
+
validators=[django_slack_tools.slack_messages.validators.header_validator],
|
|
156
156
|
verbose_name="Default header",
|
|
157
157
|
),
|
|
158
158
|
),
|
|
@@ -207,7 +207,7 @@ class Migration(migrations.Migration):
|
|
|
207
207
|
"header",
|
|
208
208
|
models.JSONField(
|
|
209
209
|
help_text="Slack control arguments. Allowed fields are `mrkdwn`, `parse`, `reply_broadcast`, `thread_ts`, `unfurl_links`, `unfurl_media`.", # noqa: E501
|
|
210
|
-
validators=[django_slack_tools.
|
|
210
|
+
validators=[django_slack_tools.slack_messages.validators.header_validator],
|
|
211
211
|
verbose_name="Header",
|
|
212
212
|
),
|
|
213
213
|
),
|
|
@@ -215,7 +215,7 @@ class Migration(migrations.Migration):
|
|
|
215
215
|
"body",
|
|
216
216
|
models.JSONField(
|
|
217
217
|
help_text="Message body. Allowed fields are `attachments`, `body`, `text`, `icon_emoji`, `icon_url`, `metadata`, `username`.", # noqa: E501
|
|
218
|
-
validators=[django_slack_tools.
|
|
218
|
+
validators=[django_slack_tools.slack_messages.validators.body_validator],
|
|
219
219
|
verbose_name="Body",
|
|
220
220
|
),
|
|
221
221
|
),
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Generated by Django 4.2.17 on 2024-12-31 13:40
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|
10
|
+
from django.db.migrations.state import StateApps
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _change_dict_to_python(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
|
14
|
+
db_alias = schema_editor.connection.alias
|
|
15
|
+
model_type = apps.get_model("slack_messages", "SlackMessagingPolicy")
|
|
16
|
+
model_type.objects.using(db_alias).filter(template_type="D").update(template_type="P")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _revert_python_to_dict(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
|
20
|
+
db_alias = schema_editor.connection.alias
|
|
21
|
+
model_type = apps.get_model("slack_messages", "SlackMessagingPolicy")
|
|
22
|
+
model_type.objects.using(db_alias).filter(template_type="P").update(template_type="D")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Migration(migrations.Migration):
|
|
26
|
+
dependencies = [
|
|
27
|
+
("slack_messages", "0004_slackmessagingpolicy_template_type"),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
operations = [
|
|
31
|
+
migrations.AlterField(
|
|
32
|
+
model_name="slackmessagingpolicy",
|
|
33
|
+
name="template_type",
|
|
34
|
+
field=models.CharField(
|
|
35
|
+
choices=[("P", "Python"), ("DJ", "Django"), ("DI", "Django Inline"), ("?", "Unknown")],
|
|
36
|
+
default="P",
|
|
37
|
+
help_text="Type of message template.",
|
|
38
|
+
max_length=2,
|
|
39
|
+
verbose_name="Template type",
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
migrations.RunPython(
|
|
43
|
+
code=_change_dict_to_python,
|
|
44
|
+
reverse_code=_revert_python_to_dict,
|
|
45
|
+
),
|
|
46
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Generated by Django 4.2.18 on 2025-03-13 14:03
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
dependencies = [
|
|
10
|
+
("slack_messages", "0005_alter_slackmessagingpolicy_template_type"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AlterField(
|
|
15
|
+
model_name="slackmessage",
|
|
16
|
+
name="id",
|
|
17
|
+
field=models.CharField(
|
|
18
|
+
default=uuid.uuid4,
|
|
19
|
+
editable=False,
|
|
20
|
+
max_length=255,
|
|
21
|
+
primary_key=True,
|
|
22
|
+
serialize=False,
|
|
23
|
+
unique=True,
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
]
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from django.db import models
|
|
6
6
|
from django.utils.translation import gettext_lazy as _
|
|
7
7
|
|
|
8
|
-
from django_slack_tools.utils.model_mixins import TimestampMixin
|
|
8
|
+
from django_slack_tools.utils.django.model_mixins import TimestampMixin
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class SlackMentionManager(models.Manager["SlackMention"]):
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
5
7
|
from django.db import models
|
|
6
8
|
from django.utils.translation import gettext_lazy as _
|
|
7
9
|
|
|
8
|
-
from django_slack_tools.
|
|
9
|
-
from django_slack_tools.utils.
|
|
10
|
+
from django_slack_tools.slack_messages.validators import body_validator, header_validator
|
|
11
|
+
from django_slack_tools.utils.django.model_mixins import TimestampMixin
|
|
10
12
|
|
|
11
13
|
from .messaging_policy import SlackMessagingPolicy
|
|
12
14
|
|
|
@@ -18,6 +20,7 @@ class SlackMessageManager(models.Manager["SlackMessage"]):
|
|
|
18
20
|
class SlackMessage(TimestampMixin, models.Model):
|
|
19
21
|
"""An Slack message."""
|
|
20
22
|
|
|
23
|
+
id = models.CharField(primary_key=True, unique=True, max_length=255, default=uuid.uuid4, editable=False)
|
|
21
24
|
policy = models.ForeignKey(
|
|
22
25
|
SlackMessagingPolicy,
|
|
23
26
|
verbose_name=_("Messaging Policy"),
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from django.db import models
|
|
6
6
|
from django.utils.translation import gettext_lazy as _
|
|
7
7
|
|
|
8
|
-
from django_slack_tools.utils.model_mixins import TimestampMixin
|
|
8
|
+
from django_slack_tools.utils.django.model_mixins import TimestampMixin
|
|
9
9
|
|
|
10
10
|
from .mention import SlackMention
|
|
11
11
|
|
|
@@ -7,8 +7,8 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
from django.db import models
|
|
8
8
|
from django.utils.translation import gettext_lazy as _
|
|
9
9
|
|
|
10
|
-
from django_slack_tools.
|
|
11
|
-
from django_slack_tools.utils.
|
|
10
|
+
from django_slack_tools.slack_messages.validators import header_validator
|
|
11
|
+
from django_slack_tools.utils.django.model_mixins import TimestampMixin
|
|
12
12
|
|
|
13
13
|
from .message_recipient import SlackMessageRecipient
|
|
14
14
|
|
|
@@ -26,7 +26,7 @@ class SlackMessagingPolicy(TimestampMixin, models.Model):
|
|
|
26
26
|
class TemplateType(models.TextChoices):
|
|
27
27
|
"""Possible template types."""
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
PYTHON = "P", _("Python")
|
|
30
30
|
"Dictionary-based template."
|
|
31
31
|
|
|
32
32
|
DJANGO = "DJ", _("Django")
|
|
@@ -66,7 +66,7 @@ class SlackMessagingPolicy(TimestampMixin, models.Model):
|
|
|
66
66
|
help_text=_("Type of message template."),
|
|
67
67
|
max_length=2,
|
|
68
68
|
choices=TemplateType.choices,
|
|
69
|
-
default=TemplateType.
|
|
69
|
+
default=TemplateType.PYTHON,
|
|
70
70
|
)
|
|
71
71
|
template: models.JSONField[Any] = models.JSONField(
|
|
72
72
|
verbose_name=_("Message template object"),
|