Source code for law.contrib.slack.notification
# coding: utf-8
"""
Slack notifications.
"""
from __future__ import annotations
__all__ = ["notify_slack"]
import os
import json
import pathlib
import threading
import traceback
from law.config import Config
from law.util import escape_markdown
from law.logger import get_logger
from law._types import Any, ModuleType
logger = get_logger(__name__)
[docs]
def notify_slack(
title: str,
content: str | dict[str, str],
attachment_color: str = "#4bb543",
short_threshold: int = 40,
token: str | pathlib.Path | None = None,
channel: str | None = None,
mention_user: str | None = None,
**kwargs,
) -> bool:
"""
Sends a slack notification and returns *True* on success. The communication with the slack API
might have some delays and is therefore handled by a thread. The format of the notification
depends on *content*. If it is a string, a simple text notification is sent. Otherwise, it
should be a dictionary whose fields are used to build a message attachment with two-column
formatting.
"""
# test import
import_slack()
cfg = Config.instance()
# get default token and channel
if not token:
token = cfg.get_expanded("notifications", "slack_token")
if not channel:
channel = cfg.get_expanded("notifications", "slack_channel")
if not token or not channel:
logger.warning(
f"cannot send Slack notification, token ({token}) or channel ({channel}) empty",
)
return False
# append the user to mention to the title
# unless explicitly set to empty string
mention_text = ""
if mention_user is None:
mention_user = cfg.get_expanded("notifications", "slack_mention_user")
if mention_user:
mention_text = f" (@{escape_markdown(mention_user)})"
# request data for the API call
request = {
"channel": channel,
"as_user": True,
"parse": "full",
}
# standard or attachment content?
if isinstance(content, str):
request["text"] = f"{title}{mention_text}\n\n{content}"
else:
# content is a dict, send its data as an attachment
request["text"] = f"{title} {mention_text}"
request["attachments"] = at = {
"color": attachment_color,
"fields": [],
"fallback": f"{title}{mention_text}\n\n",
}
# fill the attachment fields and extend the fallback
for key, value in content.items():
at["fields"].append({ # type: ignore[attr-defined]
"title": key,
"value": value,
"short": len(value) <= short_threshold,
})
at["fallback"] += f"_{key}_: {value}\n" # type: ignore[operator]
# extend by arbitrary kwargs
request.update(kwargs)
# threaded, non-blocking API communication
thread = threading.Thread(target=_notify_slack, args=(token, request))
thread.start()
return True
def _notify_slack(token: str | pathlib.Path, request: dict[str, Any]) -> None:
slack, vslack = import_slack()
try:
# token might be a file
token_file = os.path.expanduser(os.path.expandvars(token))
if os.path.isfile(token_file):
with open(token_file, "r") as f:
token = f.read().strip()
if "attachments" in request and not isinstance(request["attachments"], str):
request["attachments"] = json.dumps([request["attachments"]])
if vslack == 1:
sc = slack.SlackClient(token)
res = sc.api_call("chat.postMessage", **request)
else: # 2
wc = slack.WebClient(token)
res = wc.chat_postMessage(**request)
if not res["ok"]:
logger.warning(f"unsuccessful Slack API call: {res}")
except Exception as e:
t = traceback.format_exc()
logger.warning(f"could not send Slack notification: {e}\n{t}")
def import_slack() -> tuple[ModuleType, int]:
try:
# slackclient 1.x
import slackclient # type: ignore[import-untyped, import-not-found] # noqa
return slackclient, 1
except ImportError:
try:
# slackclient 2.x
import slack # type: ignore[import-untyped, import-not-found] # noqa
return slack, 2
except ImportError as e:
e.args = (
"neither module 'slackclient' nor 'slack' found, run 'pip install slackclient' to "
"install them",
)
raise e