diff --git a/app/commands/locales/secret.en-US.yml b/app/commands/locales/secret.en-US.yml new file mode 100644 index 00000000..f93be625 --- /dev/null +++ b/app/commands/locales/secret.en-US.yml @@ -0,0 +1,18 @@ +en-US: + locale_button: "Français" + locale_value: "en-US" + locale_short: "en" + submit: "Submit" + close: "Close" + title: "Share a new secret" + label: "Secret" + placeholder: "Enter your secret here" + ttl: "Time to live" + days: "days" + day: "day" + hours: "hours" + hour: "hour" + minutes: "minutes" + minute: "minute" + error: "There was an error creating your secret" + link_available: "Your secret is available at the following link:" diff --git a/app/commands/locales/secret.fr-FR.yml b/app/commands/locales/secret.fr-FR.yml new file mode 100644 index 00000000..06663456 --- /dev/null +++ b/app/commands/locales/secret.fr-FR.yml @@ -0,0 +1,18 @@ +fr-FR: + locale_button: "English" + locale_value: "fr-FR" + locale_short: "fr" + submit: "Soumettre" + close: "Fermer" + title: "Partager secret" + label: "Secret" + placeholder: "Entrez votre secret ici" + ttl: "Durée de vie" + days: "jours" + day: "jour" + hours: "heures" + hour: "heure" + minutes: "minutes" + minute: "minute" + error: "Il y avait une erreur en créant votre secret" + link_available: "Votre secret est disponible au lien suivant:" diff --git a/app/commands/secret.py b/app/commands/secret.py new file mode 100644 index 00000000..1cf86c7d --- /dev/null +++ b/app/commands/secret.py @@ -0,0 +1,163 @@ +import i18n +import requests +import time +from commands.utils import get_user_locale + +i18n.load_path.append("./commands/locales/") + +i18n.set("locale", "en-US") +i18n.set("fallback", "en-US") + + +def secret_command(client, ack, command, body): + ack() + if "user" in body: + user_id = body["user"]["id"] + else: + user_id = body["user_id"] + locale = get_user_locale(user_id, client) + i18n.set("locale", locale) + view = generate_secret_command_modal_view(command, user_id, locale) + client.views_open(trigger_id=body["trigger_id"], view=view) + + +def secret_view_handler(ack, client, view, logger): + ack() + locale = view["blocks"][0]["elements"][0]["value"] + i18n.set("locale", locale) + secret = view["state"]["values"]["secret_input"]["secret_submission"]["value"] + ttl = view["state"]["values"]["product"]["secret_ttl"]["selected_option"]["value"] + + # Encrypted message API + endpoint = "https://encrypted-message.cdssandbox.xyz/encrypt" + json = {"body": secret, "ttl": int(ttl) + int(time.time())} + response = requests.post( + endpoint, json=json, timeout=10, headers={"Content-Type": "application/json"} + ) + + try: + id = response.json()["id"] + url = f"https://encrypted-message.cdssandbox.xyz/{i18n.t('secret.locale_short')}/view/{id}" + client.chat_postEphemeral( + channel=view["private_metadata"], + user=view["private_metadata"], + text=f"{i18n.t('secret.link_available')} {url}", + ) + except Exception as e: + logger.error(e) + client.chat_postEphemeral( + channel=view["private_metadata"], + user=view["private_metadata"], + text=i18n.t("secret.error"), + ) + return + + +def handle_change_locale_button(ack, client, body): + ack() + if "user" in body: + user_id = body["user"]["id"] + else: + user_id = body["user_id"] + locale = body["actions"][0]["value"] + if locale == "en-US": + locale = "fr-FR" + else: + locale = "en-US" + i18n.set("locale", locale) + command = { + "text": body["view"]["state"]["values"]["secret_input"]["secret_submission"][ + "value" + ] + } + if command["text"] is None: + command["text"] = "" + view = generate_secret_command_modal_view(command, user_id, locale) + client.views_update(view_id=body["view"]["id"], view=view) + + +def generate_secret_command_modal_view(command, user_id, locale="en-US"): + ttl_options = [ + {"name": "7 " + i18n.t("secret.days"), "value": "604800"}, + {"name": "3 " + i18n.t("secret.days"), "value": "259200"}, + {"name": "1 " + i18n.t("secret.day"), "value": "86400"}, + {"name": "12 " + i18n.t("secret.hours"), "value": "43200"}, + {"name": "4 " + i18n.t("secret.hours"), "value": "14400"}, + {"name": "1 " + i18n.t("secret.hour"), "value": "3600"}, + {"name": "30 " + i18n.t("secret.minutes"), "value": "1800"}, + {"name": "5 " + i18n.t("secret.minutes"), "value": "300"}, + ] + + options = [ + { + "text": {"type": "plain_text", "text": i["name"]}, + "value": i["value"], + } + for i in ttl_options + ] + return { + "type": "modal", + "private_metadata": user_id, + "callback_id": "secret_view", + "title": { + "type": "plain_text", + "text": i18n.t("secret.title"), + }, + "submit": { + "type": "plain_text", + "text": i18n.t("secret.submit"), + }, + "blocks": [ + { + "type": "actions", + "block_id": "locale", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": i18n.t("secret.locale_button"), + "emoji": True, + }, + "value": locale, + "action_id": "secret_change_locale", + } + ], + }, + { + "type": "input", + "block_id": "secret_input", + "label": { + "type": "plain_text", + "text": i18n.t("secret.label"), + }, + "element": { + "type": "plain_text_input", + "action_id": "secret_submission", + "initial_value": command["text"], + "placeholder": { + "type": "plain_text", + "text": i18n.t("secret.placeholder"), + }, + }, + }, + { + "block_id": "product", + "type": "input", + "element": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": i18n.t("secret.ttl"), + }, + "options": options, + "action_id": "secret_ttl", + }, + "label": { + "type": "plain_text", + "text": i18n.t("secret.ttl"), + "emoji": True, + }, + }, + ], + } diff --git a/app/main.py b/app/main.py index 55257269..4ea26ac0 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from slack_bolt.adapter.socket_mode import SocketModeHandler from slack_bolt import App from dotenv import load_dotenv -from commands import atip, aws, incident, sre, role +from commands import atip, aws, incident, secret, sre, role from commands.helpers import incident_helper, webhook_helper from server import bot_middleware, server @@ -60,6 +60,11 @@ def main(bot): bot.action("archive_channel")(incident_helper.archive_channel_action) bot.view("view_save_incident_roles")(incident_helper.save_incident_roles) + # Register Secret command + bot.command(f"/{PREFIX}secret")(secret.secret_command) + bot.action("secret_change_locale")(secret.handle_change_locale_button) + bot.view("secret_view")(secret.secret_view_handler) + # Register SRE events bot.command(f"/{PREFIX}sre")(sre.sre_command) diff --git a/app/tests/commands/test_secret.py b/app/tests/commands/test_secret.py new file mode 100644 index 00000000..2e6294a6 --- /dev/null +++ b/app/tests/commands/test_secret.py @@ -0,0 +1,279 @@ +from commands import secret + +from unittest.mock import MagicMock, patch + + +@patch("commands.secret.generate_secret_command_modal_view") +@patch("commands.secret.get_user_locale") +def test_secret_command(mock_get_user_locale, mock_generate_secret_command_modal_view): + client = MagicMock() + ack = MagicMock() + + mock_get_user_locale.return_value = "en-US" + + command = "secret" + + body = { + "trigger_id": "trigger_id", + "user": {"id": "user_id"}, + } + + secret.secret_command(client, ack, command, body) + + mock_get_user_locale.assert_called_once_with("user_id", client) + mock_generate_secret_command_modal_view.assert_called_once_with( + command, "user_id", "en-US" + ) + + client.views_open.assert_called_once_with( + trigger_id="trigger_id", + view=mock_generate_secret_command_modal_view.return_value, + ) + + +@patch("commands.secret.requests") +@patch("commands.secret.time") +def test_secret_view_handler_with_succesfull_request(mock_time, mock_requests): + ack = MagicMock() + client = MagicMock() + logger = MagicMock() + view = { + "blocks": [ + { + "elements": [ + { + "value": "en-US", + } + ] + } + ], + "state": { + "values": { + "secret_input": { + "secret_submission": { + "value": "secret", + } + }, + "product": { + "secret_ttl": { + "selected_option": { + "value": "1", + } + } + }, + } + }, + "private_metadata": "private_metadata", + } + + mock_time.time.return_value = 0 + + mock_requests.post.return_value.json.return_value = {"id": "id"} + + secret.secret_view_handler(ack, client, view, logger) + + ack.assert_called_once_with() + + mock_time.time.assert_called_once_with() + mock_requests.post.assert_called_once_with( + "https://encrypted-message.cdssandbox.xyz/encrypt", + json={"body": "secret", "ttl": 1}, + timeout=10, + headers={"Content-Type": "application/json"}, + ) + + client.chat_postEphemeral.assert_called_once_with( + channel="private_metadata", + user="private_metadata", + text="Your secret is available at the following link: https://encrypted-message.cdssandbox.xyz/en/view/id", + ) + + +@patch("commands.secret.requests") +@patch("commands.secret.time") +def test_secret_view_handler_with_failed_request(mock_time, mock_requests): + ack = MagicMock() + client = MagicMock() + logger = MagicMock() + view = { + "blocks": [ + { + "elements": [ + { + "value": "en-US", + } + ] + } + ], + "state": { + "values": { + "secret_input": { + "secret_submission": { + "value": "secret", + } + }, + "product": { + "secret_ttl": { + "selected_option": { + "value": "1", + } + } + }, + } + }, + "private_metadata": "private_metadata", + } + + mock_time.time.return_value = 0 + + mock_requests.post.return_value.json.return_value = {} + + secret.secret_view_handler(ack, client, view, logger) + + ack.assert_called_once_with() + + mock_time.time.assert_called_once_with() + mock_requests.post.assert_called_once_with( + "https://encrypted-message.cdssandbox.xyz/encrypt", + json={"body": "secret", "ttl": 1}, + timeout=10, + headers={"Content-Type": "application/json"}, + ) + + client.chat_postEphemeral.assert_called_once_with( + channel="private_metadata", + user="private_metadata", + text="There was an error creating your secret", + ) + + +@patch("commands.secret.generate_secret_command_modal_view") +def test_handle_change_locale_button(mock_generate_secret_command_modal_view): + ack = MagicMock() + client = MagicMock() + body = { + "actions": [ + { + "value": "en-US", + } + ], + "user": {"id": "user_id"}, + "view": { + "id": "view_id", + "state": { + "values": { + "secret_input": { + "secret_submission": { + "value": "secret", + } + } + } + }, + }, + } + + secret.handle_change_locale_button(ack, client, body) + + ack.assert_called_once_with() + + mock_generate_secret_command_modal_view.assert_called_once_with( + {"text": "secret"}, + "user_id", + "fr-FR", + ) + + client.views_update.assert_called_once_with( + view_id="view_id", + view=mock_generate_secret_command_modal_view.return_value, + ) + + +def test_generate_secret_command_modal_view(): + command = {"text": "secret"} + user_id = "user_id" + locale = "fr-FR" + + view = secret.generate_secret_command_modal_view(command, user_id, locale) + assert view == { + "type": "modal", + "private_metadata": "user_id", + "callback_id": "secret_view", + "title": {"type": "plain_text", "text": "Partager secret"}, + "submit": {"type": "plain_text", "text": "Soumettre"}, + "blocks": [ + { + "type": "actions", + "block_id": "locale", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "English", + "emoji": True, + }, + "value": "fr-FR", + "action_id": "secret_change_locale", + } + ], + }, + { + "type": "input", + "block_id": "secret_input", + "label": {"type": "plain_text", "text": "Secret"}, + "element": { + "type": "plain_text_input", + "action_id": "secret_submission", + "initial_value": "secret", + "placeholder": { + "type": "plain_text", + "text": "Entrez votre secret ici", + }, + }, + }, + { + "block_id": "product", + "type": "input", + "element": { + "type": "static_select", + "placeholder": {"type": "plain_text", "text": "Durée de vie"}, + "options": [ + { + "text": {"type": "plain_text", "text": "7 jours"}, + "value": "604800", + }, + { + "text": {"type": "plain_text", "text": "3 jours"}, + "value": "259200", + }, + { + "text": {"type": "plain_text", "text": "1 jour"}, + "value": "86400", + }, + { + "text": {"type": "plain_text", "text": "12 heures"}, + "value": "43200", + }, + { + "text": {"type": "plain_text", "text": "4 heures"}, + "value": "14400", + }, + { + "text": {"type": "plain_text", "text": "1 heure"}, + "value": "3600", + }, + { + "text": {"type": "plain_text", "text": "30 minutes"}, + "value": "1800", + }, + { + "text": {"type": "plain_text", "text": "5 minutes"}, + "value": "300", + }, + ], + "action_id": "secret_ttl", + }, + "label": {"type": "plain_text", "text": "Durée de vie", "emoji": True}, + }, + ], + } diff --git a/app/tests/test_main.py b/app/tests/test_main.py index cb057a95..291e8ff3 100644 --- a/app/tests/test_main.py +++ b/app/tests/test_main.py @@ -36,6 +36,10 @@ def test_main_invokes_socket_handler( mock_app.action.assert_any_call("archive_channel") mock_app.view.assert_any_call("view_save_incident_roles") + mock_app.command.assert_any_call("/secret") + mock_app.action.assert_any_call("secret_change_locale") + mock_app.view.assert_any_call("secret_view") + mock_app.command.assert_any_call("/sre") mock_app.view.assert_any_call("create_webhooks_view")