Skip to content

Commit

Permalink
Merge pull request #1 from hacktoolkit/features/slack-threaded-messages
Browse files Browse the repository at this point in the history
Refactors Slack utils to post multiple messages as a thread
  • Loading branch information
jontsai committed Aug 19, 2023
2 parents 7840207 + 66ff2a7 commit d90fe13
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 77 deletions.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 4 additions & 0 deletions src/settings.py → htk/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
from dotenv import load_dotenv


# isort: off


load_dotenv()


##
# Various Settings and Configuration Variables

SLACK_BOT_TOKEN = os.environ.get('HTK_SLACK_BOT_TOKEN', os.environ.get('SLACK_BOT_TOKEN'))
SLACK_WEBHOOK_URL = os.environ.get('HTK_SLACK_WEBHOOK_URL', os.environ.get('SLACK_WEBHOOK_URL'))


Expand Down
File renamed without changes.
File renamed without changes.
16 changes: 16 additions & 0 deletions htk/utils/http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Third Party (PyPI) Imports
from requests.auth import AuthBase


class HTTPBearerAuth(AuthBase):
"""Bearer Authorization
Usage: `requests.post(url, auth=HTTPBearerAuth(...))`
"""

def __init__(self, token):
self.token = token

def __call__(self, r):
r.headers["Authorization"] = "Bearer {}".format(self.token)
return r
212 changes: 212 additions & 0 deletions htk/utils/slack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Future Imports
from __future__ import absolute_import

# Python Standard Library Imports
import json
import typing as T
from dataclasses import dataclass

# Third Party (PyPI) Imports
import requests
from requests.exceptions import JSONDecodeError

# Local Imports
from ..settings import (
SLACK_BOT_TOKEN,
SLACK_WEBHOOK_URL,
)
from .http import HTTPBearerAuth


@dataclass
class SlackMessage:
channel: T.Optional[str] = None
username: T.Optional[str] = None
text: str = ""
attachments: T.Optional[list] = None
icon_emoji: T.Optional[str] = None
unfurl_links: bool = True
unfurl_media: bool = True
thread_ts: T.Optional[str] = None
reply_broadcast: bool = False

@property
def as_payload(self):
payload = {
"text": self.text,
"unfurl_links": self.unfurl_links,
"unfurl_media": self.unfurl_media,
}

if self.channel:
payload["channel"] = self.channel
if self.username:
payload["username"] = self.username
if self.icon_emoji:
payload["icon_emoji"] = self.icon_emoji
if self.attachments:
payload["attachments"] = self.attachments

if self.thread_ts is not None:
payload["thread_ts"] = self.thread_ts
payload["reply_broadcast"] = self.reply_broadcast

return payload


def slack_message(
webhook_url: T.Optional[str] = SLACK_WEBHOOK_URL,
channel: T.Optional[str] = None,
username: T.Optional[str] = None,
text: str = "",
attachments: T.Optional[list] = None,
icon_emoji: T.Optional[str] = None,
unfurl_links: bool = True,
unfurl_media: bool = True,
thread_ts: T.Optional[str] = None,
reply_broadcast: bool = False,
error_response_handlers=None,
) -> requests.Response:
"""Wrapper around `send_webhook_message` + `SlackMessage` for legacy/backwards-compatibility.
New implementations should directly use `send_webhook_message` or `send_message`
"""
return send_webhook_message(
message=SlackMessage(
channel=channel,
username=username,
text=text,
attachments=attachments,
icon_emoji=icon_emoji,
unfurl_links=unfurl_links,
unfurl_media=unfurl_media,
thread_ts=thread_ts,
reply_broadcast=reply_broadcast,
),
webhook_url=webhook_url,
error_response_handlers=error_response_handlers,
)


def send_webhook_message(
message: SlackMessage,
webhook_url: str = None,
error_response_handlers=None,
) -> requests.Response:
"""Performs a webhook call to Slack to send a single message
https://api.slack.com/incoming-webhooks
https://api.slack.com/docs/message-formatting
`channel` override must be a public channel
NOTE: Incoming webhooks is deprecated, and when possible, `send_message` should be used instead
"""
if webhook_url is None:
webhook_url = SLACK_WEBHOOK_URL
if webhook_url is None:
raise Exception("HTK_SLACK_WEBHOOK_URL or SLACK_WEBHOOK_URL not set in ENV")

try:
response = requests.post(webhook_url, json=message.as_payload)
if response.status_code == 200:
# success case, do nothing
pass
elif response.status_code <= 399:
# 200-300, do nothing
pass
else:
print(
"Slack webhook call error: [{}] {}".format(
response.status_code, response.content
)
)
except (requests.exceptions.InvalidSchema, requests.exceptions.MissingSchema) as e:
raise Exception(
"Bad Slack webhook URL: [{}] ({})".format(
webhook_url,
e.__class__.__name__,
)
)
except:
raise

return response


def send_message(
message: SlackMessage,
thread_ts=None,
error_response_handlers=None,
token=None,
) -> requests.Response:
"""Posts a Slack message via HTTP API
POST https://slack.com/api/chat.postMessage
Content-type: application/json
Authorization: Bearer xoxb-your-token
{
"channel": "YOUR_CHANNEL_ID",
"text": "Hello world :tada:"
}
"""
if token is None:
token = SLACK_BOT_TOKEN
if token is None:
raise Exception("HTK_SLACK_BOT_TOKEN or SLACK_BOT_TOKEN not set in ENV")

if thread_ts is not None:
message.thread_ts = thread_ts

url = "https://slack.com/api/chat.postMessage"
auth = HTTPBearerAuth(token)

try:
response = requests.post(url, auth=auth, json=message.as_payload)
response_json = response.json()
except JSONDecodeError as e:
response_json = {
"response": {
"status": response.status_code,
"content": response.content,
},
}
except Exception as e:
response = None
print(e)

return response_json


def send_messages_as_thread(
messages: T.List[SlackMessage], error_response_handlers=None
):
"""Sends a series of `SlackMessage` objects in a thread
The first message is posted, then each message is posted subsequently in the thread of the first message.
"""
is_first = True
thread_ts = None

print(len(messages))

for message in messages:
response = send_message(
message,
thread_ts=thread_ts,
error_response_handlers=error_response_handlers,
)
if is_first:
is_first = False
thread_ts = response.get("ts")
else:
pass


__all__ = [
"SlackMessage",
"send_message",
"send_messages_as_thread",
"send_webhook_message",
"slack_message",
]
File renamed without changes.
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@


about = {}
with open(os.path.join(here, 'src', '__version__.py'), 'r', 'utf-8') as f:
with open(os.path.join(here, 'htk', '__version__.py'), 'r', 'utf-8') as f:
exec(f.read(), about)


Expand Down Expand Up @@ -112,7 +112,7 @@
#'sample': ['package_data.dat'],
},
package_dir={
'htk': 'src',
'htk': 'htk',
},

# Although 'package_data' is the preferred approach, in some case you may
Expand Down
75 changes: 0 additions & 75 deletions src/utils/slack.py

This file was deleted.

0 comments on commit d90fe13

Please sign in to comment.