Skip to content

Commit

Permalink
COMPINFRA-1590: Initial support of socket mode w/ slack_bolt library
Browse files Browse the repository at this point in the history
  • Loading branch information
jfongatyelp committed Dec 10, 2024
1 parent d70b4e4 commit 996685a
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 32 deletions.
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ pre-commit>=1.0.0
pytest
# this shouldn't be necessary, but tox can't seem to find the installed slackclient in
# tests otherwise
slackclient==1.2.1
slack_bolt
urllib3==1.26.2
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
],
install_requires=[
'pytimeparse',
'slackclient>=1.2.1',
'slack_bolt',
'transitions',
'mypy_extensions',
'signalfx',
Expand Down
123 changes: 96 additions & 27 deletions sticht/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@

import requests
import transitions
from slackclient import SlackClient
from slack_bolt import App
from slack_bolt.adapter.socket_mode.aiohttp import AsyncSocketModeHandler
from slack_bolt.async_app import AsyncApp
from typing_extensions import TypedDict

from sticht.state_machine import DeploymentProcess
Expand Down Expand Up @@ -104,7 +106,7 @@ def is_relevant_event(event):
return False


async def get_slack_events():
async def get_slack_events_from_scribe():
if scribereader is None:
logging.error('Scribereader unavailable. Not tailing slack events.')
return
Expand Down Expand Up @@ -149,6 +151,7 @@ class SlackBlockElement(TypedDict, total=False):
value: str
text: SlackBlockText
confirm: SlackConfirmation
action_id: str


class SlackBlock(TypedDict, total=False):
Expand All @@ -164,18 +167,22 @@ class SlackDeploymentProcess(DeploymentProcess, abc.ABC):
def __init__(self) -> None:
super().__init__()
self.human_readable_status = 'Initializing...'
# Expects a custom wrapper around App which includes both bot_token and app_token
self.slack_client = self.get_slack_client()
self.slack_async_app = AsyncApp(token=self.slack_client.bot_token)
self.slack_async_app.action({'block_id': 'deployment_actions'})(self.handle_block_actions)
self.last_action = None
self.summary_blocks_str = ''
self.detail_blocks_str = ''
self.slack_channel = self.get_slack_channel()
self.send_initial_slack_message()

self.event_queue = asyncio.Queue()
asyncio.ensure_future(self.listen_for_slack_events(), loop=self.event_loop)
asyncio.ensure_future(self.periodically_update_slack(), loop=self.event_loop)

@abc.abstractmethod
def get_slack_client(self) -> SlackClient:
def get_slack_client(self) -> App:
raise NotImplementedError()

@abc.abstractmethod
Expand Down Expand Up @@ -206,6 +213,7 @@ def get_button_element(self, button, is_active) -> SlackBlockElement:
'emoji': True,
},
'value': button,
'action_id': f'button_{button}',
}
if not is_active:
element['confirm'] = self.get_confirmation_object(button)
Expand Down Expand Up @@ -318,8 +326,16 @@ def slack_api_call(self, *args, **kwargs):
return {'ok': False, 'error': 'Slack client does not exist'}
else:
try:
resp = self.slack_client.api_call(*args, **kwargs)
return resp
if 'params' in kwargs:
# WebClient.api_call expects json strings, convert them here
kwargs['params'] = {
k: json.dumps(v) if not isinstance(
v, str,
) else v for k, v in kwargs['params'].items()
}
resp = self.slack_client.slack_app.client.api_call(*args, **kwargs)
# SlackResponse.data is json response dict
return resp.data
except Exception as e:
# leaving error/warning logging to callers, only debug log here.
log.debug(f'Exception encountered when making Slack api call: {e}')
Expand All @@ -334,17 +350,21 @@ def update_slack_thread(self, message, color=None):
print(f'Updating slack thread with: {message}', flush=True)
if color:
resp = self.slack_api_call(
'chat.postMessage',
channel=self.slack_channel,
attachments=[{'text': message, 'color': color}],
thread_ts=self.slack_ts,
api_method='chat.postMessage',
params={
'channel': self.slack_channel,
'attachments': [{'text': message, 'color': color}],
'thread_ts': self.slack_ts,
},
)
else:
resp = self.slack_api_call(
'chat.postMessage',
channel=self.slack_channel,
text=message,
thread_ts=self.slack_ts,
api_method='chat.postMessage',
params={
'channel': self.slack_channel,
'text': message,
'thread_ts': self.slack_ts,
},
)

if resp['ok'] is not True:
Expand All @@ -356,9 +376,14 @@ def send_initial_slack_message(self):
summary_blocks = self.get_summary_blocks_for_deployment()
detail_blocks = self.get_detail_slack_blocks_for_deployment()
resp = self.slack_api_call(
'chat.postMessage', blocks=truncate_blocks_text(summary_blocks), channel=self.slack_channel,
api_method='chat.postMessage',
params={
'blocks': truncate_blocks_text(summary_blocks),
'channel': self.slack_channel,
},
)
self.slack_ts = resp['message']['ts'] if resp and resp['ok'] else None
print(resp)

self.slack_channel_id = resp.get('channel')
if not self.slack_channel_id:
Expand All @@ -384,10 +409,12 @@ def send_initial_slack_message(self):
log_error(f"Posting to slack failed: {resp['error']}")

resp = self.slack_api_call(
'chat.postMessage',
blocks=truncate_blocks_text(detail_blocks),
channel=self.slack_channel,
thread_ts=self.slack_ts,
api_method='chat.postMessage',
params={
'blocks': truncate_blocks_text(detail_blocks),
'channel': self.slack_channel,
'thread_ts': self.slack_ts,
},
)
self.detail_slack_ts = resp['message']['ts'] if resp and resp['ok'] else None

Expand All @@ -407,10 +434,12 @@ def update_slack(self):

if self.summary_blocks_str != summary_blocks_str:
resp = self.slack_api_call(
'chat.update',
channel=self.slack_channel_id,
blocks=truncate_blocks_text(summary_blocks),
ts=self.slack_ts,
api_method='chat.update',
params={
'channel': self.slack_channel_id,
'blocks': truncate_blocks_text(summary_blocks),
'ts': self.slack_ts,
},
)
if resp['ok']:
self.old_summary_blocks_str = summary_blocks_str
Expand All @@ -420,10 +449,12 @@ def update_slack(self):

if self.detail_blocks_str != detail_blocks_str:
resp = self.slack_api_call(
'chat.update',
channel=self.slack_channel_id,
blocks=truncate_blocks_text(detail_blocks),
ts=self.detail_slack_ts,
api_method='chat.update',
params={
'channel': self.slack_channel_id,
'blocks': truncate_blocks_text(detail_blocks),
'ts': self.detail_slack_ts,
},
)
if resp['ok']:
self.old_detail_blocks_str = detail_blocks_str
Expand All @@ -443,10 +474,48 @@ async def periodically_update_slack(self):
def is_relevant_buttonpress(self, buttonpress):
return self.slack_ts == buttonpress.thread_ts

# Not used
def handle_button_press(self, event):
try:
log.debug(f'Got slack event: {event}')
buttonpress = event_to_buttonpress(event)
if self.is_relevant_buttonpress(buttonpress):
self.update_slack_thread(
f'<@{buttonpress.username}> pressed {buttonpress.action}',
)
self.last_action = buttonpress.action

try:
self.trigger(f'{buttonpress.action}_button_clicked')
except (transitions.core.MachineError, AttributeError):
self.update_slack_thread(f'Error: {traceback.format_exc()}')
else:
log.debug(
'But it was not relevant to this instance of mark-for-deployment',
)
except Exception:
log_error(f'Exception while processing event: {traceback.format_exc()}')
log.debug(f'event: {event!r}')

async def handle_block_actions(self, ack, body, client):
await ack()
# TODO: We can now handle button presses and reactions directly instead of just processing events
log.debug(f'handlingblock action {body}')
await self.event_queue.put(body)

async def get_slack_events(self):

handler = AsyncSocketModeHandler(self.slack_async_app, self.slack_client.app_token)
asyncio.create_task(handler.start_async())

while True:
event = await self.event_queue.get()
yield event

async def listen_for_slack_events(self):
log.debug('Listening for slack events...')
try:
async for event in get_slack_events():
async for event in self.get_slack_events():
try:
log.debug(f'Got slack event: {event}')
buttonpress = event_to_buttonpress(event)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.
from unittest import mock

from slackclient import SlackClient
from slack_bolt import App as SlackApp

from sticht import slack

Expand Down Expand Up @@ -150,7 +150,7 @@ def start_state(self):
return '_begin'

def get_slack_client(self):
mock_client = mock.Mock(spec=SlackClient)
mock_client = mock.Mock(spec=SlackApp)
mock_client.api_call.return_value = {
'ok': True,
'message': {'ts': 10},
Expand All @@ -175,7 +175,7 @@ class ErrorSlackDeploymentProcess(DummySlackDeploymentProcess):
default_slack_channel = '#dne'

def get_slack_client(self):
mock_client = mock.Mock(spec=SlackClient)
mock_client = mock.Mock(spec=SlackApp)
mock_client.api_call.return_value = {'ok': False, 'error': 'uh oh'}
return mock_client

Expand Down

0 comments on commit 996685a

Please sign in to comment.