Slack Service

The Slack service: the platform’s messages to Slack.

Slack does not depend on a cloud: Smarter posts with the Slack Web API, as a Slack app’s bot, whose token is smarter_settings.slack_bot_token (SMARTER_SLACK_BOT_TOKEN). So SlackService is implemented by SlackWebApiService, for every cloud provider. The bot needs the scopes chat:write, to post messages, and files:write, to upload files, and must be a member of the channel.

The platform uses it through smarter.apps.infrastructure.services.infrastructure .slack. Tests replace it with configure_slack(), e.g. with InMemorySlackService.

from smarter.apps.infrastructure.services import infrastructure

message = infrastructure.slack.post_message("smarter", "*Deployed* :white_check_mark:")
if message:
    infrastructure.slack.upload_file(
        message.channel_id, "job.txt", log.encode("utf-8"), thread_ts=message.ts
    )

A message names its channel by name or id, and SlackService.post_message() returns the channel’s id and the message’s timestamp, ts, which a file upload, or a reply in the message’s thread, needs. Like email, Slack never breaks the work that posts to it: failures are logged and returned as None or False, never raised.

class smarter.apps.infrastructure.services.slack.InMemorySlackService(ready=True, **kwargs)[source]

Bases: SlackService

Slack in memory, for tests and local development: messages are kept in messages, and.

files in files.

A channel’s id is its name, upper-cased, after a C, e.g. CSMARTER.

Parameters:

ready (bool) – Whether the service is ready.

__init__(ready=True, **kwargs)[source]
static channel_id(channel)[source]

The id of a channel, by name or id.

Return type:

str

errors

Set a Web API method’s error, e.g. {"files.getUploadURLExternal": "missing_scope"}, to make it fail.

property ready: bool

Whether the service is authenticated and connected, i.e. it can be used.

class smarter.apps.infrastructure.services.slack.PostedSlackMessage(channel, channel_id, text, ts, thread_ts=None)[source]

Bases: object

A message that InMemorySlackService posted.

__init__(channel, channel_id, text, ts, thread_ts=None)
channel: str

The channel, as the caller named it.

channel_id: str
text: str
thread_ts: str | None = None
ts: str
class smarter.apps.infrastructure.services.slack.SlackApiResponse(**data)[source]

Bases: BaseModel

The fields of a Slack Web API response that the service reads.

Each method returns more, which are allowed, and ignored.

channel: str | None

the id of the channel that the message was posted to.

Type:

chat.postMessage

error: str | None

Why it failed, e.g. channel_not_found, not_in_channel or missing_scope.

file_id: str | None

the id of the file, for files.completeUploadExternal.

Type:

files.getUploadURLExternal

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

needed: str | None

The scope that a missing_scope error needs, e.g. files:write.

ok: bool

Whether the method succeeded.

ts: str | None

the message’s timestamp, which is its id in the channel.

Type:

chat.postMessage

upload_url: str | None

where to send the file’s contents.

Type:

files.getUploadURLExternal

class smarter.apps.infrastructure.services.slack.SlackMessage(channel_id, ts)[source]

Bases: object

A message that was posted to Slack.

__init__(channel_id, ts)
channel_id: str

The channel’s id, e.g. C0123456789, which file uploads need.

ts: str

its id, and the thread_ts of replies in its thread.

Type:

The message’s timestamp, e.g. 1700000000.000100

class smarter.apps.infrastructure.services.slack.SlackService(provider_name, *args, **kwargs)[source]

Bases: InfrastructureService

The platform’s messages to Slack.

error_class

alias of SlackServiceError

log_not_sent(channel)[source]

Log a message or file that was not posted because the service is not ready.

Parameters:

channel (str) – The channel it was for.

Return type:

None

post_message(channel, text, thread_ts=None)[source]

Post a message, formatted with Slack’s mrkdwn, without link or media previews.

Failures are logged rather than raised, so that Slack never breaks the work that posts to it.

Parameters:
  • channel (str) – The channel’s name, e.g. smarter, or its id.

  • text (str) – The message, in mrkdwn. Escape &, < and > in text that is not markup, e.g. with escape().

  • thread_ts (str | None) – The ts of a message to reply to, in its thread.

Return type:

SlackMessage | None

Returns:

The posted message, whose channel id and ts a reply or file upload needs, or None if it was not posted.

service_name: str = 'slack'

The name of the service in signals and logs, see InfrastructureServiceNames.

upload_file(channel_id, filename, content, thread_ts=None, initial_comment='', title=None)[source]

Upload a file to a channel, or to a message’s thread.

Uses Slack’s upload flow: files.getUploadURLExternal, then the file’s contents to the url it returns, then files.completeUploadExternal, which shares the file. The bot needs the scope files:write. Failures are logged rather than raised.

Parameters:
  • channel_id (str) – The channel’s id, e.g. SlackMessage.channel_id. Not its name.

  • filename (str) – The file’s name, e.g. job.txt.

  • content (bytes) – The file’s contents.

  • thread_ts (str | None) – The ts of the message in whose thread to share the file.

  • initial_comment (str) – A message to post with the file.

  • title (str | None) – The file’s title. By default, its name.

Return type:

bool

Returns:

True if the file was uploaded and shared.

class smarter.apps.infrastructure.services.slack.SlackWebApiService(allow_in_tests=False, **kwargs)[source]

Bases: SlackService

Slack, with the Slack Web API, as the bot of smarter_settings.slack_bot_token.

It is ready when the token is set. In the unit tests, it is never ready, unless allow_in_tests is True, so that tests never post to a real workspace.

The token is sent only to SLACK_API_URL, never to an upload url, and is never logged. Redirects are not followed.

Parameters:

allow_in_tests (bool) – Post in the unit tests, e.g. with requests mocked.

__init__(allow_in_tests=False, **kwargs)[source]
property ready: bool

Whether the service is authenticated and connected, i.e. it can be used.

class smarter.apps.infrastructure.services.slack.UploadedSlackFile(channel_id, filename, title, content, thread_ts=None, initial_comment='')[source]

Bases: object

A file that InMemorySlackService uploaded.

__init__(channel_id, filename, title, content, thread_ts=None, initial_comment='')
channel_id: str
content: bytes
filename: str
initial_comment: str = ''
thread_ts: str | None = None
title: str
smarter.apps.infrastructure.services.slack.configure_slack(factory)[source]

Set the factory of the Slack service that get_slack() returns.

Parameters:

factory (Callable[[], SlackService] | None) – Returns the service, or None to restore the default, SlackWebApiService.

Return type:

None

smarter.apps.infrastructure.services.slack.escape(text)[source]

Escape the characters that Slack’s mrkdwn reserves, &, < and >.

Use it on text that is not markup, e.g. a log, so that it is shown as it is, and cannot mention people or channels, e.g. <!channel>.

Parameters:

text (str) – The text.

Return type:

str

Returns:

The text, escaped.

smarter.apps.infrastructure.services.slack.get_slack()[source]

Return the Slack service, which is created once.

Return type:

SlackService