django-slack-tools 0.1.0__py3-none-any.whl → 0.2.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 +1 -1
- django_slack_tools/app_settings.py +29 -4
- django_slack_tools/slack_messages/admin/message.py +1 -1
- django_slack_tools/slack_messages/apps.py +9 -0
- django_slack_tools/slack_messages/backends/__init__.py +2 -2
- django_slack_tools/slack_messages/backends/base.py +182 -47
- django_slack_tools/slack_messages/backends/dummy.py +8 -9
- django_slack_tools/slack_messages/backends/slack.py +29 -91
- django_slack_tools/slack_messages/message.py +40 -58
- django_slack_tools/slack_messages/migrations/0001_initial.py +0 -2
- django_slack_tools/slack_messages/migrations/0002_default_policy.py +35 -0
- django_slack_tools/slack_messages/migrations/0003_default_recipient.py +35 -0
- django_slack_tools/slack_messages/migrations/0004_slackmessagingpolicy_template_type.py +23 -0
- django_slack_tools/slack_messages/models/messaging_policy.py +29 -3
- django_slack_tools/slack_messages/tasks.py +112 -0
- django_slack_tools/utils/slack/django.py +8 -24
- django_slack_tools/utils/slack/message.py +74 -25
- django_slack_tools/utils/template/__init__.py +5 -0
- django_slack_tools/utils/template/base.py +17 -0
- django_slack_tools/utils/template/dict.py +52 -0
- django_slack_tools/utils/template/django.py +140 -0
- django_slack_tools/views.py +1 -1
- {django_slack_tools-0.1.0.dist-info → django_slack_tools-0.2.0.dist-info}/METADATA +39 -21
- django_slack_tools-0.2.0.dist-info/RECORD +43 -0
- {django_slack_tools-0.1.0.dist-info → django_slack_tools-0.2.0.dist-info}/WHEEL +1 -1
- django_slack_tools/utils/dict_template.py +0 -55
- django_slack_tools-0.1.0.dist-info/RECORD +0 -36
- {django_slack_tools-0.1.0.dist-info → django_slack_tools-0.2.0.dist-info/licenses}/LICENSE +0 -0
django_slack_tools/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -9,8 +9,9 @@ from django.conf import settings
|
|
|
9
9
|
from django.core.exceptions import ImproperlyConfigured
|
|
10
10
|
from django.utils.module_loading import import_string
|
|
11
11
|
from slack_bolt import App
|
|
12
|
+
from typing_extensions import NotRequired
|
|
12
13
|
|
|
13
|
-
from django_slack_tools.slack_messages.backends.base import
|
|
14
|
+
from django_slack_tools.slack_messages.backends.base import BaseBackend
|
|
14
15
|
|
|
15
16
|
APP_SETTINGS_KEY = "DJANGO_SLACK_TOOLS"
|
|
16
17
|
"Django settings key for this application."
|
|
@@ -18,10 +19,11 @@ APP_SETTINGS_KEY = "DJANGO_SLACK_TOOLS"
|
|
|
18
19
|
logger = getLogger(__name__)
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
# TODO(lasuillard): Configuration getting dirty, need refactoring
|
|
21
23
|
class AppSettings:
|
|
22
24
|
"""Application settings."""
|
|
23
25
|
|
|
24
|
-
backend:
|
|
26
|
+
backend: BaseBackend
|
|
25
27
|
|
|
26
28
|
def __init__(self, settings_dict: ConfigDict | None = None) -> None:
|
|
27
29
|
"""Initialize app settings.
|
|
@@ -56,15 +58,26 @@ class AppSettings:
|
|
|
56
58
|
|
|
57
59
|
# Find backend class
|
|
58
60
|
messaging_backend = import_string(settings_dict["BACKEND"]["NAME"])
|
|
59
|
-
if not issubclass(messaging_backend,
|
|
61
|
+
if not issubclass(messaging_backend, BaseBackend):
|
|
60
62
|
msg = "Provided backend is not a subclass of `{qualified_path}` class.".format(
|
|
61
|
-
qualified_path=f"{
|
|
63
|
+
qualified_path=f"{BaseBackend.__module__}.{BaseBackend.__name__}",
|
|
62
64
|
)
|
|
63
65
|
raise ImproperlyConfigured(msg)
|
|
64
66
|
|
|
65
67
|
# Initialize with provided options
|
|
66
68
|
self.backend = messaging_backend(**settings_dict["BACKEND"]["OPTIONS"])
|
|
67
69
|
|
|
70
|
+
# Message delivery default
|
|
71
|
+
self.default_policy_code = settings_dict.get("DEFAULT_POLICY_CODE", "DEFAULT")
|
|
72
|
+
|
|
73
|
+
# Lazy policy defaults
|
|
74
|
+
self.lazy_policy_enabled = settings_dict.get("LAZY_POLICY_ENABLED", False)
|
|
75
|
+
self.default_template = settings_dict.get(
|
|
76
|
+
"DEFAULT_POLICY_CODE",
|
|
77
|
+
{"text": "No template configured for lazily created policy {policy}"},
|
|
78
|
+
)
|
|
79
|
+
self.default_recipient = settings_dict.get("DEFAULT_RECIPIENT", "DEFAULT")
|
|
80
|
+
|
|
68
81
|
@property
|
|
69
82
|
def slack_app(self) -> App:
|
|
70
83
|
"""Registered Slack app or `None`."""
|
|
@@ -83,6 +96,18 @@ class ConfigDict(TypedDict):
|
|
|
83
96
|
BACKEND: BackendConfig
|
|
84
97
|
"Nested backend config."
|
|
85
98
|
|
|
99
|
+
LAZY_POLICY_ENABLED: NotRequired[bool]
|
|
100
|
+
"Whether to enable lazy policy by default."
|
|
101
|
+
|
|
102
|
+
DEFAULT_POLICY_CODE: NotRequired[str]
|
|
103
|
+
"Default policy code used when sending messages via policy with no policy specified."
|
|
104
|
+
|
|
105
|
+
DEFAULT_TEMPLATE: NotRequired[Any]
|
|
106
|
+
"Default template for lazy policy."
|
|
107
|
+
|
|
108
|
+
DEFAULT_RECIPIENT: NotRequired[str]
|
|
109
|
+
"Default recipient alias for lazy policy."
|
|
110
|
+
|
|
86
111
|
|
|
87
112
|
class BackendConfig(TypedDict):
|
|
88
113
|
"""Backend config dict."""
|
|
@@ -61,7 +61,7 @@ class SlackMessageAdmin(admin.ModelAdmin):
|
|
|
61
61
|
n_success = 0
|
|
62
62
|
n_failure = 0
|
|
63
63
|
for message in queryset:
|
|
64
|
-
backend.send_message(message, raise_exception=False)
|
|
64
|
+
backend.send_message(message, raise_exception=False, get_permalink=False)
|
|
65
65
|
if message.ok:
|
|
66
66
|
n_success += 1
|
|
67
67
|
else:
|
|
@@ -9,3 +9,12 @@ class DjangoSlackBotConfig(AppConfig): # noqa: D101
|
|
|
9
9
|
default_auto_field = "django.db.models.BigAutoField"
|
|
10
10
|
name = "django_slack_tools.slack_messages"
|
|
11
11
|
verbose_name = _("Slack Messages")
|
|
12
|
+
|
|
13
|
+
def ready(self) -> None: # pragma: no cover
|
|
14
|
+
"""Auto-discover Celery tasks, if Celery is installed."""
|
|
15
|
+
try:
|
|
16
|
+
import celery # noqa: F401
|
|
17
|
+
except ImportError:
|
|
18
|
+
pass
|
|
19
|
+
else:
|
|
20
|
+
from . import tasks # noqa: F401
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from .base import
|
|
1
|
+
from .base import BaseBackend
|
|
2
2
|
from .dummy import DummyBackend
|
|
3
3
|
from .logging import LoggingBackend
|
|
4
4
|
from .slack import SlackBackend, SlackRedirectBackend
|
|
5
5
|
|
|
6
6
|
__all__ = (
|
|
7
|
-
"
|
|
7
|
+
"BaseBackend",
|
|
8
8
|
"SlackBackend",
|
|
9
9
|
"SlackRedirectBackend",
|
|
10
10
|
"DummyBackend",
|
|
@@ -2,89 +2,224 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import dataclasses
|
|
6
|
+
import traceback
|
|
5
7
|
from abc import ABC, abstractmethod
|
|
6
8
|
from logging import getLogger
|
|
7
|
-
from typing import TYPE_CHECKING, Any,
|
|
9
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
10
|
+
|
|
11
|
+
from slack_sdk.errors import SlackApiError
|
|
8
12
|
|
|
9
13
|
from django_slack_tools.slack_messages.models import SlackMessage, SlackMessagingPolicy
|
|
14
|
+
from django_slack_tools.utils.slack import MessageBody
|
|
15
|
+
from django_slack_tools.utils.template import DictTemplate, DjangoTemplate
|
|
10
16
|
|
|
11
17
|
if TYPE_CHECKING:
|
|
12
18
|
from slack_sdk.web import SlackResponse
|
|
13
19
|
|
|
14
|
-
from django_slack_tools.
|
|
20
|
+
from django_slack_tools.slack_messages.models.mention import SlackMention
|
|
21
|
+
from django_slack_tools.slack_messages.models.message_recipient import SlackMessageRecipient
|
|
22
|
+
from django_slack_tools.utils.slack import MessageHeader
|
|
23
|
+
from django_slack_tools.utils.template import BaseTemplate
|
|
15
24
|
|
|
16
25
|
logger = getLogger(__name__)
|
|
17
26
|
|
|
27
|
+
RESERVED_CONTEXT_KWARGS = frozenset({"policy", "mentions", "mentions_as_str"})
|
|
28
|
+
"""Set of reserved context keys automatically created."""
|
|
18
29
|
|
|
19
|
-
class BackendBase(ABC):
|
|
20
|
-
"""Abstract base class for messaging backends."""
|
|
21
30
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
self,
|
|
25
|
-
message: SlackMessage,
|
|
26
|
-
*,
|
|
27
|
-
raise_exception: bool,
|
|
28
|
-
get_permalink: bool = False,
|
|
29
|
-
) -> SlackMessage: ... # pragma: no cover
|
|
31
|
+
class BaseBackend(ABC):
|
|
32
|
+
"""Abstract base class for messaging backends."""
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
def send_message(
|
|
34
|
+
def prepare_message(
|
|
33
35
|
self,
|
|
34
36
|
*,
|
|
35
37
|
policy: SlackMessagingPolicy | None = None,
|
|
36
38
|
channel: str,
|
|
37
39
|
header: MessageHeader,
|
|
38
40
|
body: MessageBody,
|
|
39
|
-
raise_exception: bool,
|
|
40
|
-
get_permalink: bool = False,
|
|
41
|
-
) -> SlackMessage: ... # pragma: no cover
|
|
42
|
-
|
|
43
|
-
@abstractmethod
|
|
44
|
-
def send_message( # noqa: PLR0913
|
|
45
|
-
self,
|
|
46
|
-
message: SlackMessage | None = None,
|
|
47
|
-
*,
|
|
48
|
-
policy: SlackMessagingPolicy | None = None,
|
|
49
|
-
channel: str | None = None,
|
|
50
|
-
header: MessageHeader | None = None,
|
|
51
|
-
body: MessageBody | None = None,
|
|
52
|
-
raise_exception: bool,
|
|
53
|
-
get_permalink: bool = False,
|
|
54
41
|
) -> SlackMessage:
|
|
55
|
-
"""
|
|
42
|
+
"""Prepare message.
|
|
56
43
|
|
|
57
44
|
Args:
|
|
58
|
-
|
|
59
|
-
If not given, make one using `channel`, `header` and `body` parameters.
|
|
60
|
-
policy: Messaging policy to create message with.
|
|
45
|
+
policy: Related policy instance.
|
|
61
46
|
channel: Channel to send message.
|
|
62
|
-
header:
|
|
63
|
-
body:
|
|
64
|
-
raise_exception: Whether to re-raise caught exception while sending messages.
|
|
65
|
-
get_permalink: Try to get the message permalink via extraneous Slack API calls.
|
|
47
|
+
header: Slack message control header.
|
|
48
|
+
body: Slack message body.
|
|
66
49
|
|
|
67
50
|
Returns:
|
|
68
|
-
|
|
51
|
+
Prepared message.
|
|
69
52
|
"""
|
|
53
|
+
_header: dict = policy.header_defaults if policy else {}
|
|
54
|
+
_header.update(dataclasses.asdict(header))
|
|
70
55
|
|
|
71
|
-
|
|
56
|
+
_body = dataclasses.asdict(body)
|
|
57
|
+
|
|
58
|
+
return SlackMessage(policy=policy, channel=channel, header=_header, body=_body)
|
|
59
|
+
|
|
60
|
+
def prepare_messages_from_policy(
|
|
72
61
|
self,
|
|
62
|
+
policy: SlackMessagingPolicy,
|
|
73
63
|
*,
|
|
74
|
-
policy: SlackMessagingPolicy | None = None,
|
|
75
|
-
channel: str,
|
|
76
64
|
header: MessageHeader,
|
|
77
|
-
|
|
65
|
+
context: dict[str, Any],
|
|
66
|
+
) -> list[SlackMessage]:
|
|
67
|
+
"""Prepare messages from policy.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
policy: Policy to create messages from.
|
|
71
|
+
header: Common message header.
|
|
72
|
+
context: Message context.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Prepared messages.
|
|
76
|
+
"""
|
|
77
|
+
overridden_reserved = RESERVED_CONTEXT_KWARGS & set(context.keys())
|
|
78
|
+
if overridden_reserved:
|
|
79
|
+
logger.warning(
|
|
80
|
+
"Template keyword argument(s) %s reserved for passing mentions, but already exists."
|
|
81
|
+
" User provided value will override it.",
|
|
82
|
+
", ".join(f"`{s}`" for s in overridden_reserved),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
messages: list[SlackMessage] = []
|
|
86
|
+
for recipient in policy.recipients.all():
|
|
87
|
+
logger.debug("Sending message to recipient %s", recipient)
|
|
88
|
+
|
|
89
|
+
# Initialize template instance
|
|
90
|
+
template = self._get_template_instance_from_policy(policy)
|
|
91
|
+
|
|
92
|
+
# Prepare rendering arguments
|
|
93
|
+
render_context = self._get_default_context(policy=policy, recipient=recipient)
|
|
94
|
+
render_context.update(context)
|
|
95
|
+
logger.debug("Context prepared as: %r", render_context)
|
|
96
|
+
|
|
97
|
+
# Render template and parse as body
|
|
98
|
+
rendered = template.render(context=render_context)
|
|
99
|
+
body = MessageBody.from_any(rendered)
|
|
100
|
+
|
|
101
|
+
# Create message instance
|
|
102
|
+
message = self.prepare_message(policy=policy, channel=recipient.channel, header=header, body=body)
|
|
103
|
+
messages.append(message)
|
|
104
|
+
|
|
105
|
+
return SlackMessage.objects.bulk_create(messages)
|
|
106
|
+
|
|
107
|
+
def _get_template_instance_from_policy(self, policy: SlackMessagingPolicy) -> BaseTemplate:
|
|
108
|
+
"""Get template instance."""
|
|
109
|
+
if policy.template_type == SlackMessagingPolicy.TemplateType.DICT:
|
|
110
|
+
return DictTemplate(policy.template)
|
|
111
|
+
|
|
112
|
+
if policy.template_type == SlackMessagingPolicy.TemplateType.DJANGO:
|
|
113
|
+
return DjangoTemplate(file=policy.template)
|
|
114
|
+
|
|
115
|
+
if policy.template_type == SlackMessagingPolicy.TemplateType.DJANGO_INLINE:
|
|
116
|
+
return DjangoTemplate(inline=policy.template)
|
|
117
|
+
|
|
118
|
+
msg = f"Unsupported template type: {policy.template_type!r}"
|
|
119
|
+
raise ValueError(msg)
|
|
120
|
+
|
|
121
|
+
def _get_default_context(self, *, policy: SlackMessagingPolicy, recipient: SlackMessageRecipient) -> dict[str, Any]:
|
|
122
|
+
"""Get default context for rendering.
|
|
123
|
+
|
|
124
|
+
Following default context keys are created:
|
|
125
|
+
|
|
126
|
+
- `policy`: Policy code.
|
|
127
|
+
- `mentions`: List of mentions.
|
|
128
|
+
- `mentions_as_str`: Comma-separated joined string of mentions.
|
|
129
|
+
"""
|
|
130
|
+
mentions: list[SlackMention] = list(recipient.mentions.all())
|
|
131
|
+
mentions_as_str = ", ".join(mention.mention for mention in mentions)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"policy": policy.code,
|
|
135
|
+
"mentions": mentions,
|
|
136
|
+
"mentions_as_str": mentions_as_str,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
def send_messages(self, *messages: SlackMessage, raise_exception: bool, get_permalink: bool) -> int:
|
|
140
|
+
"""Shortcut to send multiple messages.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
messages: Messages to send.
|
|
144
|
+
raise_exception: Whether to propagate exceptions.
|
|
145
|
+
get_permalink: Try to get the message permalink via additional Slack API call.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Count of messages sent successfully.
|
|
149
|
+
"""
|
|
150
|
+
num_sent = 0
|
|
151
|
+
for message in messages:
|
|
152
|
+
sent = self.send_message(message=message, raise_exception=raise_exception, get_permalink=get_permalink)
|
|
153
|
+
num_sent += 1 if sent.ok else 0
|
|
154
|
+
|
|
155
|
+
return num_sent
|
|
156
|
+
|
|
157
|
+
def send_message(
|
|
158
|
+
self,
|
|
159
|
+
message: SlackMessage,
|
|
160
|
+
*,
|
|
161
|
+
raise_exception: bool,
|
|
162
|
+
get_permalink: bool,
|
|
78
163
|
) -> SlackMessage:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
164
|
+
"""Send message.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
message: Prepared message.
|
|
168
|
+
raise_exception: Whether to propagate exceptions.
|
|
169
|
+
get_permalink: Try to get the message permalink via additional Slack API call.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Message sent to Slack.
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
response: SlackResponse
|
|
176
|
+
try:
|
|
177
|
+
response = self._send_message(message)
|
|
178
|
+
except SlackApiError as err:
|
|
179
|
+
if raise_exception:
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
logger.warning(
|
|
183
|
+
"Error occurred while sending message but suppressed because `raise_exception` set.",
|
|
184
|
+
exc_info=err,
|
|
185
|
+
)
|
|
186
|
+
response = err.response
|
|
187
|
+
|
|
188
|
+
message.ok = ok = cast(bool, response.get("ok"))
|
|
189
|
+
if ok:
|
|
190
|
+
# Get message TS if OK
|
|
191
|
+
message.ts = cast(str, response.get("ts"))
|
|
192
|
+
|
|
193
|
+
# Store thread TS if possible
|
|
194
|
+
data: dict[str, Any] = response.get("message", {})
|
|
195
|
+
message.parent_ts = data.get("thread_ts", "")
|
|
196
|
+
|
|
197
|
+
# Get message permalink
|
|
198
|
+
if get_permalink:
|
|
199
|
+
message.permalink = self._get_permalink(message=message, raise_exception=raise_exception)
|
|
200
|
+
|
|
201
|
+
message.request = self._record_request(response)
|
|
202
|
+
message.response = self._record_response(response)
|
|
203
|
+
except: # noqa: E722
|
|
204
|
+
if raise_exception:
|
|
205
|
+
raise
|
|
206
|
+
|
|
207
|
+
logger.exception("Error occurred while sending message but suppressed because `raise_exception` set.")
|
|
208
|
+
message.exception = traceback.format_exc()
|
|
209
|
+
finally:
|
|
210
|
+
message.save()
|
|
211
|
+
|
|
212
|
+
message.refresh_from_db()
|
|
213
|
+
return message
|
|
83
214
|
|
|
84
215
|
@abstractmethod
|
|
85
|
-
def _send_message(self,
|
|
216
|
+
def _send_message(self, message: SlackMessage) -> SlackResponse:
|
|
86
217
|
"""Internal implementation of actual 'send message' behavior."""
|
|
87
218
|
|
|
219
|
+
@abstractmethod
|
|
220
|
+
def _get_permalink(self, *, message: SlackMessage, raise_exception: bool) -> str:
|
|
221
|
+
"""Get a permalink for given message identifier."""
|
|
222
|
+
|
|
88
223
|
@abstractmethod
|
|
89
224
|
def _record_request(self, response: SlackResponse) -> Any:
|
|
90
225
|
"""Extract request data to be recorded. Should return JSON-serializable object."""
|
|
@@ -9,20 +9,16 @@ from slack_sdk.web import SlackResponse
|
|
|
9
9
|
|
|
10
10
|
from django_slack_tools.slack_messages.models import SlackMessage
|
|
11
11
|
|
|
12
|
-
from .base import
|
|
12
|
+
from .base import BaseBackend
|
|
13
13
|
|
|
14
14
|
logger = getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class DummyBackend(
|
|
17
|
+
class DummyBackend(BaseBackend):
|
|
18
18
|
"""An dummy backend that does nothing with message."""
|
|
19
19
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
return SlackMessage()
|
|
23
|
-
|
|
24
|
-
def _prepare_message(self, *args: Any, **kwargs: Any) -> SlackMessage: # noqa: ARG002
|
|
25
|
-
return SlackMessage()
|
|
20
|
+
def prepare_message(self, *args: Any, **kwargs: Any) -> SlackMessage: # noqa: D102, ARG002
|
|
21
|
+
return SlackMessage(header={}, body={})
|
|
26
22
|
|
|
27
23
|
def _send_message(self, *args: Any, **kwargs: Any) -> SlackResponse: # noqa: ARG002
|
|
28
24
|
return SlackResponse(
|
|
@@ -30,11 +26,14 @@ class DummyBackend(BackendBase):
|
|
|
30
26
|
http_verb="POST",
|
|
31
27
|
api_url="https://www.slack.com/api/chat.postMessage",
|
|
32
28
|
req_args={},
|
|
33
|
-
data={"ok":
|
|
29
|
+
data={"ok": True},
|
|
34
30
|
headers={},
|
|
35
31
|
status_code=200,
|
|
36
32
|
)
|
|
37
33
|
|
|
34
|
+
def _get_permalink(self, *, message: SlackMessage, raise_exception: bool) -> str: # noqa: ARG002
|
|
35
|
+
return ""
|
|
36
|
+
|
|
38
37
|
def _record_request(self, *args: Any, **kwargs: Any) -> Any: ...
|
|
39
38
|
|
|
40
39
|
def _record_response(self, *args: Any, **kwargs: Any) -> Any: ...
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import traceback
|
|
6
5
|
from logging import getLogger
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Callable
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
8
7
|
|
|
9
8
|
from django.core.exceptions import ImproperlyConfigured
|
|
10
9
|
from django.utils.module_loading import import_string
|
|
@@ -12,19 +11,19 @@ from django.utils.translation import gettext_lazy as _
|
|
|
12
11
|
from slack_bolt import App
|
|
13
12
|
from slack_sdk.errors import SlackApiError
|
|
14
13
|
|
|
15
|
-
from .base import
|
|
14
|
+
from .base import BaseBackend
|
|
16
15
|
|
|
17
16
|
if TYPE_CHECKING:
|
|
18
17
|
from slack_sdk.web import SlackResponse
|
|
19
18
|
|
|
20
|
-
from django_slack_tools.slack_messages.models import SlackMessage
|
|
21
|
-
from django_slack_tools.utils.slack import MessageBody
|
|
19
|
+
from django_slack_tools.slack_messages.models import SlackMessage
|
|
20
|
+
from django_slack_tools.utils.slack import MessageBody
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
logger = getLogger(__name__)
|
|
25
24
|
|
|
26
25
|
|
|
27
|
-
class SlackBackend(
|
|
26
|
+
class SlackBackend(BaseBackend):
|
|
28
27
|
"""Backend actually sending the messages."""
|
|
29
28
|
|
|
30
29
|
def __init__(
|
|
@@ -49,88 +48,21 @@ class SlackBackend(BackendBase):
|
|
|
49
48
|
|
|
50
49
|
self._slack_app = slack_app
|
|
51
50
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
policy: SlackMessagingPolicy | None = None,
|
|
57
|
-
channel: str | None = None,
|
|
58
|
-
header: MessageHeader | None = None,
|
|
59
|
-
body: MessageBody | None = None,
|
|
60
|
-
raise_exception: bool,
|
|
61
|
-
get_permalink: bool = False,
|
|
62
|
-
) -> SlackMessage:
|
|
63
|
-
"""Send Slack message.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
message: Externally prepared message.
|
|
67
|
-
If not given, make one using `channel`, `header` and `body` parameters.
|
|
68
|
-
policy: Messaging policy to create message with.
|
|
69
|
-
channel: Channel to send message.
|
|
70
|
-
header: Message header that controls how message will sent.
|
|
71
|
-
body: Message body describing content of the message.
|
|
72
|
-
raise_exception: Whether to re-raise caught exception while sending messages.
|
|
73
|
-
get_permalink: Try to get the message permalink via extraneous Slack API calls.
|
|
74
|
-
|
|
75
|
-
Returns:
|
|
76
|
-
Sent Slack message.
|
|
77
|
-
"""
|
|
78
|
-
if not message:
|
|
79
|
-
if not (channel and header and body):
|
|
80
|
-
msg = (
|
|
81
|
-
"Call signature mismatch for overload."
|
|
82
|
-
" If `message` not provided, `channel`, `header` and `body` all must given."
|
|
83
|
-
)
|
|
84
|
-
raise TypeError(msg)
|
|
85
|
-
|
|
86
|
-
message = self._prepare_message(policy=policy, channel=channel, header=header, body=body)
|
|
51
|
+
def _send_message(self, message: SlackMessage) -> SlackResponse:
|
|
52
|
+
header = message.header or {}
|
|
53
|
+
body = message.body or {}
|
|
54
|
+
return self._slack_app.client.chat_postMessage(channel=message.channel, **header, **body)
|
|
87
55
|
|
|
88
|
-
|
|
89
|
-
# Send Slack message
|
|
90
|
-
response: SlackResponse
|
|
91
|
-
try:
|
|
92
|
-
response = self._send_message(message=message)
|
|
93
|
-
except SlackApiError as err:
|
|
94
|
-
if raise_exception:
|
|
95
|
-
raise
|
|
96
|
-
|
|
97
|
-
logger.exception(
|
|
98
|
-
"Error occurred while sending Slack message, but ignored because `raise_exception` not set.",
|
|
99
|
-
)
|
|
100
|
-
response = err.response
|
|
101
|
-
|
|
102
|
-
# Update message detail
|
|
103
|
-
ok: bool | None = response.get("ok")
|
|
104
|
-
message.ok = ok
|
|
105
|
-
if ok:
|
|
106
|
-
# `str` if OK, otherwise `None`
|
|
107
|
-
message.ts = cast(str, response.get("ts"))
|
|
108
|
-
message.parent_ts = response.get("message", {}).get("thread_ts", "") # type: ignore[call-overload]
|
|
109
|
-
if get_permalink:
|
|
110
|
-
message.permalink = self._get_permalink(
|
|
111
|
-
channel=message.channel,
|
|
112
|
-
message_ts=message.ts,
|
|
113
|
-
raise_exception=raise_exception,
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
message.request = self._record_request(response)
|
|
117
|
-
message.response = self._record_response(response)
|
|
118
|
-
except:
|
|
119
|
-
message.exception = traceback.format_exc()
|
|
120
|
-
|
|
121
|
-
# Don't omit raise with flag `raise_exception` here
|
|
122
|
-
raise
|
|
123
|
-
finally:
|
|
124
|
-
message.save()
|
|
125
|
-
|
|
126
|
-
return message
|
|
127
|
-
|
|
128
|
-
def _get_permalink(self, *, channel: str, message_ts: str, raise_exception: bool = False) -> str:
|
|
56
|
+
def _get_permalink(self, *, message: SlackMessage, raise_exception: bool = False) -> str:
|
|
129
57
|
"""Get a permalink for given message identifier."""
|
|
58
|
+
if not message.ts:
|
|
59
|
+
msg = "Message timestamp is not set, can't retrieve permalink."
|
|
60
|
+
raise ValueError(msg)
|
|
61
|
+
|
|
130
62
|
try:
|
|
131
63
|
_permalink_resp = self._slack_app.client.chat_getPermalink(
|
|
132
|
-
channel=channel,
|
|
133
|
-
message_ts=
|
|
64
|
+
channel=message.channel,
|
|
65
|
+
message_ts=message.ts,
|
|
134
66
|
)
|
|
135
67
|
except SlackApiError:
|
|
136
68
|
if raise_exception:
|
|
@@ -144,11 +76,6 @@ class SlackBackend(BackendBase):
|
|
|
144
76
|
|
|
145
77
|
return _permalink_resp.get("permalink", default="")
|
|
146
78
|
|
|
147
|
-
def _send_message(self, *, message: SlackMessage) -> SlackResponse:
|
|
148
|
-
header = message.header or {}
|
|
149
|
-
body = message.body or {}
|
|
150
|
-
return self._slack_app.client.chat_postMessage(channel=message.channel, **header, **body)
|
|
151
|
-
|
|
152
79
|
def _record_request(self, response: SlackResponse) -> dict[str, Any]:
|
|
153
80
|
# Remove auth header (token) from request before recording
|
|
154
81
|
response.req_args.get("headers", {}).pop("Authorization", None)
|
|
@@ -182,7 +109,18 @@ class SlackRedirectBackend(SlackBackend):
|
|
|
182
109
|
|
|
183
110
|
super().__init__(slack_app=slack_app)
|
|
184
111
|
|
|
185
|
-
def
|
|
112
|
+
def prepare_message(self, *args: Any, channel: str, body: MessageBody, **kwargs: Any) -> SlackMessage:
|
|
113
|
+
"""Prepare message to send, with modified for redirection.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
args: Positional arguments to pass to super method.
|
|
117
|
+
channel: Original channel to send message.
|
|
118
|
+
body: Message content.
|
|
119
|
+
kwargs: Keyword arguments to pass to super method.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Prepared message instance.
|
|
123
|
+
"""
|
|
186
124
|
# Modify channel to force messages always sent to specific channel
|
|
187
125
|
# Add an attachment that informing message has been redirected
|
|
188
126
|
if self.inform_redirect:
|
|
@@ -191,7 +129,7 @@ class SlackRedirectBackend(SlackBackend):
|
|
|
191
129
|
*(body.attachments or []),
|
|
192
130
|
]
|
|
193
131
|
|
|
194
|
-
return super().
|
|
132
|
+
return super().prepare_message(*args, channel=self.redirect_channel, body=body, **kwargs)
|
|
195
133
|
|
|
196
134
|
def _make_inform_attachment(self, *, original_channel: str) -> dict[str, Any]:
|
|
197
135
|
msg_redirect_inform = _(
|