def handle_state(context, trigger_id, title, elements, receiver='', submit_label='Submit', state='n/a'): """Send a dialog into Slack Args: context (dict): The state context object. This is included automatically by Socless Core and SHOULD NOT be supplied by the user when creating a playbook receiver (str): The name of the State in the playbook that will receive the dialog response trigger_id (str): trigger_id required to open a Dialog title (str): Dialog title elements (list): Dialog elements submit_label (str): Label for Dialog's submit button state (str): this string simply echoes back what your app passed to dialog.open. Use it as a pointer that references sensitive data stored elsewhere. Note: - See https://api.slack.com/dialogs for more details on to create elements for a slack dialog - This integration starts a Human Response workflow. When used in a playbook, it needs to be followed by a Task state that uses the _socless_outbound_message_response Activity to receive the response from a user - A user can respond to a dialog by either submitting it or cancelling it. The response payload contains a key named `type` that can be either `dialog_submission` or `dialog_cancellation`. In your playboks, be sure to check what type of response a user provided before acting on it. """ USE_NEW_INTERACTION = 'task_token' in context message_id = gen_id() dialog = { 'title': title, 'elements': elements, 'submit_label': submit_label, 'notify_on_cancel': True, 'callback_id': message_id, 'state': state } payload = {'trigger_id': trigger_id, 'dialog': dialog} url = "https://slack.com/api/dialog.open" headers = { 'content-type': 'application/json', 'Authorization': "Bearer {}".format(SLACK_BOT_TOKEN) } if USE_NEW_INTERACTION: init_human_interaction(context, payload, message_id) resp = requests.post(url, json=payload, headers=headers) json_resp = resp.json() if not json_resp["ok"]: raise Exception(json_resp['error']) if not USE_NEW_INTERACTION: investigation_id = context['artifacts']['event']['investigation_id'] execution_id = context.get('execution_id') socless_dispatch_outbound_message(receiver, message_id, investigation_id, execution_id, payload) return {'response': json_resp, "message_id": message_id}
def handle_state( context, target: str, target_type: str, message_template: str, receiver: str = "", response_desc: str = "[response]", as_user: bool = True, token: str = "", ): """Send a Slack Message and store the message id for the message. Args: target : the username or slack id to send this message to target_type : "slack_id" | "user" | "channel" token : you can pass an alternate token via Jinja template in playbook.json (ssm, environment, etc) Returns: """ helper = SlackHelper(token) if not message_template: raise Exception("No text was supplied to Slack message") USE_NEW_INTERACTION = "task_token" in context message_id = gen_id(6) context["_message_id"] = message_id extended_template = "{message_template}\n```{slash_command} {context[_message_id]} {response_desc}```\n".format( message_template=message_template, slash_command=SLACK_SLASH_COMMAND, context=context, response_desc=response_desc, ) message = socless_template_string(extended_template, context) if USE_NEW_INTERACTION: init_human_interaction(context, message, message_id) resp = helper.slack_post_msg_wrapper(target, target_type, text=message, as_user=as_user) if not USE_NEW_INTERACTION: investigation_id = context["artifacts"]["event"]["investigation_id"] execution_id = context.get("execution_id") socless_dispatch_outbound_message(receiver, message_id, investigation_id, execution_id, message) return { "response": resp.data, # type: ignore "message_id": message_id, "slack_id": resp["channel"], # type: ignore }
def handle_state(context, target, target_type, message_template, receiver="", response_desc='[response]'): """ Send a Slack Message and store the message id for the message """ if not message_template: raise Exception("No text was supplied to Slack message") USE_NEW_INTERACTION = 'task_token' in context message_id = gen_id(6) context['_message_id'] = message_id extended_template = "{message_template}\n```{slash_command} {context[_message_id]} {response_desc}```\n".format( message_template=message_template, slash_command=SLACK_SLASH_COMMAND, context=context, response_desc=response_desc) message = socless_template_string(extended_template, context) target_id = get_channel_id(target, target_type) if USE_NEW_INTERACTION: init_human_interaction(context, message, message_id) r = slack_client.chat_postMessage(channel=target_id, text=message, as_user=True) if not r.data['ok']: raise Exception( f"Human Reponse workflow failed to initiate because slack_client failed to send message: {r.data}" ) if not USE_NEW_INTERACTION: investigation_id = context['artifacts']['event']['investigation_id'] execution_id = context.get('execution_id') socless_dispatch_outbound_message(receiver, message_id, investigation_id, execution_id, message) return { "response": r.data, "message_id": message_id, "slack_id": target_id }
def test_init_human_interaction_fails_on_generic_exceptions(): # test init_human_interaction() to make it fail on exceptions other than KeyError bad_context = { 'task_token': 1.0, #It expects a string, and a float number should make it fail 'execution_id': gen_id(), 'artifacts': { 'event': { 'id': gen_id(), 'created_at': gen_datetimenow(), 'data_types': {}, 'details': { "some": "randon text" }, 'event_type': 'Test sfn', 'event_meta': {}, 'investigation_id': gen_id(), 'status_': 'open', 'is_duplicate': False }, 'execution_id': gen_id() }, 'state_name': gen_id(), 'Parameters': {} } test_message = {"greeting": "Hello, World"} with pytest.raises(Exception): message_id = init_human_interaction(bad_context, test_message)
def test_init_human_interaction_fails_on_invalid_execute_context_key(): # test init_human_interaction() to make it fail on invalid execute context key # it's expected to raise an exception for KeyError bad_context = {'hello': 'world'} test_message = {"greeting": "Hello, World"} with pytest.raises(Exception): message_id = init_human_interaction(bad_context, test_message)
def test_end_human_interaction_fails_on_response_delivery_failed(): # moto step function send_task_success is not implemented yet. Therefore, this is the last line of test that interacts with humaninteraction.py can be written # test end_human_interaction() fails on response_deliver_failed. Expecting it to raise an exception test_response = {"response": "Hello, back"} test_message = {"greeting": "Hello, World"} sfn_item_metadata = mock_sfn_db_context() sfn_context = sfn_item_metadata['sfn_context'] state_handler = StateHandler(sfn_context, MockLambdaContext(), mock_integration_handler) message_id = init_human_interaction(state_handler.context, test_message) with pytest.raises(Exception, match='^response_delivery_failed'): end_human_interaction(message_id, test_response)
def test_end_human_interaction_fails_on_used_message_id(): # test end_human_interaction() fails on message id that has already been used. Expecting it to raise an exception test_response = {"response": "Hello, back"} test_message = {"greeting": "Hello, World"} sfn_item_metadata = mock_sfn_db_context() sfn_context = sfn_item_metadata['sfn_context'] state_handler = StateHandler(sfn_context, MockLambdaContext(), mock_integration_handler) message_id = init_human_interaction(state_handler.context, test_message) response_table = boto3.resource('dynamodb').Table( os.environ['SOCLESS_MESSAGE_RESPONSE_TABLE']) response_table.update_item(Key={"message_id": message_id}, UpdateExpression="SET fulfilled = :fulfilled", ExpressionAttributeValues={":fulfilled": True}) with pytest.raises(Exception, match='message_id_used'): end_human_interaction(message_id, test_response)
def test_end_human_interaction(): # moto step function send_task_success is not implemented yet # test end_human_interaction normally to assert it works as expected test_response = {"response": "Hello, back"} test_message = {"greeting": "Hello, World"} sfn_item_metadata = mock_sfn_db_context() sfn_context = sfn_item_metadata['sfn_context'] db_context = sfn_item_metadata['db_context'] state_name = sfn_context['sfn_context']['State_Config']['Name'] state_handler = StateHandler(sfn_context, MockLambdaContext(), mock_integration_handler) message_id = init_human_interaction(state_handler.context, test_message) with pytest.raises(Exception, match='^response_delivery_failed'): end_human_interaction(message_id, test_response) client = boto3.client('dynamodb') updated_db_context = client.get_item( TableName=os.environ['SOCLESS_RESULTS_TABLE'], Key={'execution_id': dict_to_item(sfn_item_metadata['execution_id'])})['Item'] db_context['results']['results'][state_name] = test_response db_context['results']['results']['_Last_Saved_Results'] = test_response assert dict_to_item(db_context, convert_root=False) == updated_db_context
def test_init_human_interaction(): # test init_human_interaction() normally to assert the item gets saved is the same as expected test_message = {"greeting": "Hello, World"} sfn_item_metadata = mock_sfn_db_context() sfn_context = sfn_item_metadata['sfn_context'] state_handler = StateHandler(sfn_context, MockLambdaContext(), mock_integration_handler) message_id = init_human_interaction(state_handler.context, test_message) mock_message_response_entry = dict_to_item( { "await_token": sfn_context['task_token'], "execution_id": sfn_context['sfn_context']['execution_id'], "fulfilled": False, "investigation_id": sfn_context['sfn_context']['artifacts']['event'] ['investigation_id'], "message": test_message, "receiver": sfn_context['sfn_context']['State_Config']['Name'], }, convert_root=False) client = boto3.client('dynamodb') init_message_response_entry = client.get_item( TableName=os.environ['SOCLESS_MESSAGE_RESPONSE_TABLE'], Key={'message_id': dict_to_item(message_id)})['Item'] mock_message_response_entry['datetime'] = init_message_response_entry[ 'datetime'] mock_message_response_entry['message_id'] = dict_to_item(message_id) assert mock_message_response_entry == init_message_response_entry
def handle_state( context, target_type: str, target: str, text: str, receiver: str = "", prompt_text: str = "", yes_text: str = "Yes", no_text: str = "No", as_user: bool = True, token: str = "", ): """Send a Slack Message and store the message id for the message. Args: target : the username or slack id to send this message to target_type : "slack_id" | "user" | "channel" token : you can pass an alternate token via Jinja template in playbook.json (ssm, environment, etc) Returns: """ helper = SlackHelper(token) USE_NEW_INTERACTION = "task_token" in context if not all([target_type, target, text]): raise Exception( "Incomplete inputs: target, target_type and text must be supplied") ATTACHMENT_YES_ACTION = { "name": "yes_text", "style": "default", "text": "", "type": "button", "value": "true", } ATTACHMENT_NO_ACTION = { "name": "no_text", "style": "danger", "text": "", "type": "button", "value": "false", } ATTACHMENT_TEMPLATE = { "text": "", "mrkdwn_in": ["text"], "fallback": "New message", "callback_id": "", "color": "#3AA3E3", "attachment_type": "default", "actions": [], } message_id = gen_id(6) context["_message_id"] = message_id text = socless_template_string(text, context) prompt_text = socless_template_string(prompt_text, context) ATTACHMENT_TEMPLATE["text"] = "*{}*".format(prompt_text) ATTACHMENT_TEMPLATE["callback_id"] = message_id ATTACHMENT_YES_ACTION["text"] = yes_text ATTACHMENT_NO_ACTION["text"] = no_text ATTACHMENT_TEMPLATE["actions"] = [ ATTACHMENT_YES_ACTION, ATTACHMENT_NO_ACTION ] payload = {"text": text, "ATTACHMENT_TEMPLATE": ATTACHMENT_TEMPLATE} if USE_NEW_INTERACTION: init_human_interaction(context, payload, message_id) resp = helper.slack_post_msg_wrapper( target, target_type, text=text, attachments=[ATTACHMENT_TEMPLATE], as_user=as_user, ) if not USE_NEW_INTERACTION: investigation_id = context["artifacts"]["event"]["investigation_id"] execution_id = context.get("execution_id") socless_dispatch_outbound_message(receiver, message_id, investigation_id, execution_id, payload) return { "response": resp.data, # type: ignore "message_id": message_id, "slack_id": resp["channel"], # type: ignore }
def handle_state(context, target_type, target, text, receiver='', prompt_text='', yes_text='Yes', no_text='No'): """ Send a Slack Message and store the message id for the message """ USE_NEW_INTERACTION = 'task_token' in context if not all([target_type, target, text]): raise Exception( "Incomplete inputs: target, target_type and text must be supplied") target_id = get_channel_id(target, target_type) ATTACHMENT_YES_ACTION = { "name": "yes_text", "style": "default", "text": "", "type": "button", "value": "true" } ATTACHMENT_NO_ACTION = { "name": "no_text", "style": "danger", "text": "", "type": "button", "value": "false" } ATTACHMENT_TEMPLATE = { "text": "", "mrkdwn_in": ["text"], "fallback": "New message", "callback_id": "", "color": "#3AA3E3", "attachment_type": "default", "actions": [] } message_id = gen_id(6) context['_message_id'] = message_id text = socless_template_string(text, context) prompt_text = socless_template_string(prompt_text, context) ATTACHMENT_TEMPLATE['text'] = "*{}*".format(prompt_text) ATTACHMENT_TEMPLATE['callback_id'] = message_id ATTACHMENT_YES_ACTION['text'] = yes_text ATTACHMENT_NO_ACTION['text'] = no_text ATTACHMENT_TEMPLATE['actions'] = [ ATTACHMENT_YES_ACTION, ATTACHMENT_NO_ACTION ] payload = {"text": text, "ATTACHMENT_TEMPLATE": ATTACHMENT_TEMPLATE} if USE_NEW_INTERACTION: init_human_interaction(context, payload, message_id) resp = slack_client.chat_postMessage(channel=target_id, text=text, attachments=[ATTACHMENT_TEMPLATE], as_user=True) if not resp.data['ok']: raise Exception(resp.data['error']) if not USE_NEW_INTERACTION: investigation_id = context['artifacts']['event']['investigation_id'] execution_id = context.get('execution_id') socless_dispatch_outbound_message(receiver, message_id, investigation_id, execution_id, payload) return { 'response': resp.data, "message_id": message_id, "slack_id": target_id }
def handle_state( context, trigger_id: str, title: str, elements: list, receiver: str = "", submit_label: str = "Submit", state: str = "n/a", token: str = "", ): """Send a dialog into Slack Args: receiver : The name of the State in the playbook that will receive the dialog response trigger_id : trigger_id required to open a Dialog title : Dialog title elements : Dialog elements submit_label : Label for Dialog's submit button state : this string simply echoes back what your app passed to dialog.open. Use it as a pointer that references sensitive data stored elsewhere. token : you can pass an alternate token via Jinja template in playbook.json (ssm, environment, etc) Note: - See https://api.slack.com/dialogs for more details on to create elements for a slack dialog - This integration starts a Human Response workflow. When used in a playbook, it needs to be followed by a Task state that uses the _socless_outbound_message_response Activity to receive the response from a user - A user can respond to a dialog by either submitting it or cancelling it. The response payload contains a key named `type` that can be either `dialog_submission` or `dialog_cancellation`. In your playboks, be sure to check what type of response a user provided before acting on it. """ if not token: token = SOCLESS_BOT_TOKEN USE_NEW_INTERACTION = "task_token" in context message_id = gen_id() dialog = { "title": title, "elements": elements, "submit_label": submit_label, "notify_on_cancel": True, "callback_id": message_id, "state": state, } payload = {"trigger_id": trigger_id, "dialog": dialog} url = "https://slack.com/api/dialog.open" headers = { "content-type": "application/json", "Authorization": "Bearer {}".format(token), } if USE_NEW_INTERACTION: init_human_interaction(context, payload, message_id) resp = requests.post(url, json=payload, headers=headers) json_resp = resp.json() if not json_resp["ok"]: raise Exception(json_resp["error"]) if not USE_NEW_INTERACTION: investigation_id = context["artifacts"]["event"]["investigation_id"] execution_id = context.get("execution_id") socless_dispatch_outbound_message(receiver, message_id, investigation_id, execution_id, payload) return {"response": json_resp, "message_id": message_id}