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.
Files changed (28) hide show
  1. django_slack_tools/__init__.py +1 -1
  2. django_slack_tools/app_settings.py +29 -4
  3. django_slack_tools/slack_messages/admin/message.py +1 -1
  4. django_slack_tools/slack_messages/apps.py +9 -0
  5. django_slack_tools/slack_messages/backends/__init__.py +2 -2
  6. django_slack_tools/slack_messages/backends/base.py +182 -47
  7. django_slack_tools/slack_messages/backends/dummy.py +8 -9
  8. django_slack_tools/slack_messages/backends/slack.py +29 -91
  9. django_slack_tools/slack_messages/message.py +40 -58
  10. django_slack_tools/slack_messages/migrations/0001_initial.py +0 -2
  11. django_slack_tools/slack_messages/migrations/0002_default_policy.py +35 -0
  12. django_slack_tools/slack_messages/migrations/0003_default_recipient.py +35 -0
  13. django_slack_tools/slack_messages/migrations/0004_slackmessagingpolicy_template_type.py +23 -0
  14. django_slack_tools/slack_messages/models/messaging_policy.py +29 -3
  15. django_slack_tools/slack_messages/tasks.py +112 -0
  16. django_slack_tools/utils/slack/django.py +8 -24
  17. django_slack_tools/utils/slack/message.py +74 -25
  18. django_slack_tools/utils/template/__init__.py +5 -0
  19. django_slack_tools/utils/template/base.py +17 -0
  20. django_slack_tools/utils/template/dict.py +52 -0
  21. django_slack_tools/utils/template/django.py +140 -0
  22. django_slack_tools/views.py +1 -1
  23. {django_slack_tools-0.1.0.dist-info → django_slack_tools-0.2.0.dist-info}/METADATA +39 -21
  24. django_slack_tools-0.2.0.dist-info/RECORD +43 -0
  25. {django_slack_tools-0.1.0.dist-info → django_slack_tools-0.2.0.dist-info}/WHEEL +1 -1
  26. django_slack_tools/utils/dict_template.py +0 -55
  27. django_slack_tools-0.1.0.dist-info/RECORD +0 -36
  28. {django_slack_tools-0.1.0.dist-info → django_slack_tools-0.2.0.dist-info/licenses}/LICENSE +0 -0
@@ -1 +1 @@
1
- __version__ = "0.1.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 BackendBase
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: BackendBase
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, BackendBase):
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"{BackendBase.__module__}.{BackendBase.__name__}",
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 BackendBase
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
- "BackendBase",
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, overload
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.utils.slack import MessageBody, MessageHeader
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
- @overload
23
- def send_message(
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
- @overload
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
- """Send Slack message.
42
+ """Prepare message.
56
43
 
57
44
  Args:
58
- message: Externally prepared message.
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: Message header that controls how message will sent.
63
- body: Message body describing content of the message.
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
- Sent Slack message.
51
+ Prepared message.
69
52
  """
53
+ _header: dict = policy.header_defaults if policy else {}
54
+ _header.update(dataclasses.asdict(header))
70
55
 
71
- def _prepare_message(
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
- body: MessageBody,
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
- _header: dict = policy.header_defaults if policy else {}
80
- _header.update(header.model_dump(exclude_unset=True))
81
- _body = body.model_dump()
82
- return SlackMessage(policy=policy, channel=channel, header=_header, body=_body)
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, *, message: SlackMessage) -> SlackResponse:
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 BackendBase
12
+ from .base import BaseBackend
13
13
 
14
14
  logger = getLogger(__name__)
15
15
 
16
16
 
17
- class DummyBackend(BackendBase):
17
+ class DummyBackend(BaseBackend):
18
18
  """An dummy backend that does nothing with message."""
19
19
 
20
- def send_message(self, *args: Any, **kwargs: Any) -> SlackMessage: # noqa: ARG002
21
- """This backend will not do anything, just like dummy."""
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": False},
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, cast
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 BackendBase
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, SlackMessagingPolicy
21
- from django_slack_tools.utils.slack import MessageBody, MessageHeader
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(BackendBase):
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 send_message( # noqa: PLR0913
53
- self,
54
- message: SlackMessage | None = None,
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
- try:
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=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 _prepare_message(self, *args: Any, channel: str, body: MessageBody, **kwargs: Any) -> SlackMessage:
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()._prepare_message(*args, channel=self.redirect_channel, body=body, **kwargs)
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 = _(