Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Uses settings to control ephemeral state. #120

Merged
merged 11 commits into from
Mar 2, 2022
33 changes: 23 additions & 10 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ The design goal of this plugin is to be able to write chatbot commands once and

2. worker (`nautobot_chatops.workers`)

- This layer is *completely ignorant* of chat platforms. All code in this layer does not know or care about the
- This layer is _completely ignorant_ of chat platforms. All code in this layer does not know or care about the
difference between Slack, WebEx, Microsoft Teams, or any other platform we may support in the future.

- Each `job` worker function acts on the provided parameters, then invokes generic methods on its provided
Expand Down Expand Up @@ -109,24 +109,25 @@ every command as a top-level worker function. (`/nautobot get-device-info <devic
rather than `/nautobot-get-device-info <device>`, `/nautobot-get-vlan-info <vlan>`, etc.) This is because:

a. On platforms such as Slack, each separate slash-command must be enabled and configured separately on the server,
so an excessive number of distinct top-level commands will make the chatbot inconvenient to deploy.
so an excessive number of distinct top-level commands will make the chatbot inconvenient to deploy.
b. Platforms such as Microsoft Teams may limit the number of top-level commands that are displayed to the user in
a chat client, so large numbers of commands may be difficult to discover.
a chat client, so large numbers of commands may be difficult to discover.
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved

That said, the implementation of Nautobot allows it to transparently support both syntaxes (`/command-sub-command` as
well as `/command sub-command`; if the deployer takes the time to set up the bot accordingly.

### Multi-word Parameters

Nautobot dispatchers now allow multi-word arguments to be passed into commands. An example of this is passing city
names to a subcommand parameters. As an example, say we have a command that perfoms a lookup for all sites in Nautobot
that match a city. The command and parameters might look like `/nautobot get-sites location Dallas` where Dallas is the
city we want to search for. For the command to support cities such as `Las Vegas` we would want to quote the city
argument. The new command should look as `/nautobot get-sites location 'Las Vegas'`.
Nautobot dispatchers now allow multi-word arguments to be passed into commands. An example of this is passing city
names to a subcommand parameters. As an example, say we have a command that perfoms a lookup for all sites in Nautobot
that match a city. The command and parameters might look like `/nautobot get-sites location Dallas` where Dallas is the
city we want to search for. For the command to support cities such as `Las Vegas` we would want to quote the city
argument. The new command should look as `/nautobot get-sites location 'Las Vegas'`.

The worker would need to preserve the quoting when prompting for additional parameters. Below is an example:
The worker would need to preserve the quoting when prompting for additional parameters. Below is an example:

Here we use the previous example, but add limit to the site lookup.

```Python
action = f"get-sites location '{city}'" # Adding single quotes around city to preserve quotes.
dispatcher.prompt_for_text(action_id=action, help_text="Please enter the maximum number of sites to return.", label="Number")
Expand Down Expand Up @@ -157,9 +158,21 @@ Some known limitations of currently supported platforms:

- No support for preformatted text in cards (blocks) - while text can be rendered as `monospace`, it still does not
preserve whitespace, so it's not suitable for aligning output into text tables and the like.
- While text-only messages *do* support Markdown preformatted text, the text is wrapped to a max width of 69
- While text-only messages _do_ support Markdown preformatted text, the text is wrapped to a max width of 69
characters regardless of how large the client's window is.
- No table functionality in cards (blocks). The `ColumnSet` card layout feature allows for wrapping content across
multiple columns, but does not provide for any alignment across columns, so it's not suitable for tables.

## Settings

The setting of `send_all_messages_private` within the configuration applied within the Nautobot config is used to send all messages as private messages. The messages will be sent in private when this is set to `True`. The default setting is `False`, which is the default behavior for several message settings.

### Settings - Platform Support of Settings

> This table represents the platform support of particular settings

| Platform | send_all_messages_private |
| Slack | ✅ |
| MS Teams | ❌ |
| WebEx | ❌ |
| Mattermost | ✅ |
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions nautobot_chatops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ class NautobotChatOpsConfig(PluginConfig):
# Mattermost-specific settings
"mattermost_api_token": None,
"mattermost_url": None,
# As requested on https://github.com/nautobot/nautobot-plugin-chatops/issues/114 this setting is used for
# sending all messages as an ephemeral message, meaning only the person interacting with the bot will see the
# responses.
"send_all_messages_private": False,
}

max_version = "1.999"
Expand Down
9 changes: 7 additions & 2 deletions nautobot_chatops/dispatchers/adaptive_cards.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Dispatcher subclass for chat platforms that use Adaptive Cards (https://adaptivecards.io/)."""

from .base import Dispatcher

# pylint: disable=abstract-method
Expand Down Expand Up @@ -121,7 +120,13 @@ def multi_input_dialog(self, command, sub_command, dialog_title, dialog_list):
],
}
blocks.append(buttons)
return self.send_blocks(blocks, callback_id=callback_id, modal=True, ephemeral=False, title=dialog_title)
return self.send_blocks(
blocks,
callback_id=callback_id,
modal=True,
ephemeral=False,
title=dialog_title,
)
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved

def send_warning(self, message):
"""Send a warning message to the user/channel specified by the context."""
Expand Down
40 changes: 37 additions & 3 deletions nautobot_chatops/dispatchers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,28 @@ def ask_permission_to_send_image(self, filename, action_id):

# Send various content to the user or channel

def send_markdown(self, message, ephemeral=False):
def send_markdown(self, message, ephemeral=None):
"""Send a Markdown-formatted text message to the user/channel specified by the context."""
# pylint: disable=unused-argument
if ephemeral is None:
ephemeral = settings.PLUGINS_CONFIG["nautobot_chatops"]["send_all_messages_private"]
raise NotImplementedError

def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=False, title=None):
def send_blocks(
self,
blocks,
callback_id=None,
modal=False,
ephemeral=None,
title=None,
):
"""Send a series of formatting blocks to the user/channel specified by the context."""
# pylint: disable=unused-argument
if ephemeral is None:
ephemeral = settings.PLUGINS_CONFIG["nautobot_chatops"]["send_all_messages_private"]
raise NotImplementedError

def send_snippet(self, text, title=None):
def send_snippet(self, text, title=None, ephemeral=None):
"""Send a longer chunk of text as a snippet or file attachment."""
raise NotImplementedError

Expand Down Expand Up @@ -327,3 +340,24 @@ def select_element(self, action_id, choices, default=(None, None), confirm=False
def text_element(self, text):
"""Construct a simple plaintext element."""
raise NotImplementedError

@staticmethod
def split_messages(text_string: str, max_message_size: int) -> list:
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
"""Method to split a message into smaller messages.

Args:
text_string (str): Text string that should be split
max_message_size (int): Maximum size for a message
"""
return_list = [""]

for line in text_string.splitlines():
# len(line) + 2 to account for a new line character in the character line
# Check to see if the line length of the last item in the list is longer than the max message size
# Once it would be larger than the max size, then start the next line.
if (len(line) + 2) + len(return_list[-1]) < max_message_size:
return_list[-1] += f"{line}\n"
else:
return_list.append(f"{line}\n")

return return_list
18 changes: 14 additions & 4 deletions nautobot_chatops/dispatchers/mattermost.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,10 @@ def command_response_header(self, command, subcommand, args, description="inform
# Send various content to the user or channel

@BACKEND_ACTION_MARKDOWN.time()
def send_markdown(self, message, ephemeral=False):
def send_markdown(self, message, ephemeral=None):
"""Send a Markdown-formatted text message to the user/channel specified by the context."""
if ephemeral is None:
ephemeral = settings.PLUGINS_CONFIG["nautobot_chatops"]["send_all_messages_private"]
try:
if ephemeral:
self.mm_client.chat_post_ephemeral(
Expand All @@ -360,7 +362,7 @@ def send_markdown(self, message, ephemeral=False):

# pylint: disable=arguments-differ
@BACKEND_ACTION_BLOCKS.time()
def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=False, title="Your attention please!"):
def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=None, title="Your attention please!"):
"""Send a series of formatting blocks to the user/channel specified by the context.

Args:
Expand All @@ -370,6 +372,8 @@ def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=False, ti
ephemeral (bool): Whether to send this as an ephemeral message (only visible to the targeted user).
title (str): Title to include on a modal dialog.
"""
if ephemeral is None:
ephemeral = settings.PLUGINS_CONFIG["nautobot_chatops"]["send_all_messages_private"]
logger.info("Sending blocks: %s", json.dumps(blocks, indent=2))
try:
if modal:
Expand Down Expand Up @@ -403,11 +407,17 @@ def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=False, ti
self.send_exception(mm_error)

@BACKEND_ACTION_SNIPPET.time()
def send_snippet(self, text, title=None):
def send_snippet(self, text, title=None, ephemeral=None):
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
"""Send a longer chunk of text as a file snippet."""
channel = [self.context.get("channel_id")]
logger.info("Sending snippet to %s: %s", channel, text)
self.mm_client.chat_post_message(channel_id=self.context.get("channel_id"), snippet=text)
if ephemeral:
for msg in self.split_messages(text, 16383):
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
self.mm_client.chat_post_ephemeral(
channel_id=self.context.get("channel_id"), user_id=self.context.get("user_id"), message=msg
)
else:
self.mm_client.chat_post_message(channel_id=self.context.get("channel_id"), snippet=text)

def send_image(self, image_path):
"""Send an image as a file upload."""
Expand Down
6 changes: 3 additions & 3 deletions nautobot_chatops/dispatchers/ms_teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,12 @@ def ask_permission_to_send_image(self, filename, action_id):
)

@BACKEND_ACTION_MARKDOWN.time()
def send_markdown(self, message, ephemeral=False):
def send_markdown(self, message, ephemeral=None):
"""Send a markdown-formatted text message to the user/channel specified by the context."""
self._send({"text": message, "textFormat": "markdown"})

@BACKEND_ACTION_BLOCKS.time()
def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=False, title=None):
def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=None, title=None):
"""Send a series of formatting blocks to the user/channel specified by the context."""
if title and title not in str(blocks[0]):
blocks.insert(0, self.markdown_element(self.bold(title)))
Expand All @@ -170,7 +170,7 @@ def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=False, ti
)

@BACKEND_ACTION_SNIPPET.time()
def send_snippet(self, text, title=None):
def send_snippet(self, text, title=None, ephemeral=None):
"""Send a longer chunk of text as a snippet or file attachment."""
self.send_markdown(f"```\n{text}\n```")

Expand Down
22 changes: 18 additions & 4 deletions nautobot_chatops/dispatchers/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,12 @@ def command_response_header(self, command, subcommand, args, description="inform
# Send various content to the user or channel

@BACKEND_ACTION_MARKDOWN.time()
def send_markdown(self, message, ephemeral=False):
def send_markdown(self, message, ephemeral=None):
"""Send a Markdown-formatted text message to the user/channel specified by the context."""
try:
if ephemeral is None:
ephemeral = settings.PLUGINS_CONFIG["nautobot_chatops"]["send_all_messages_private"]

if ephemeral:
self.slack_client.chat_postEphemeral(
channel=self.context.get("channel_id"),
Expand All @@ -156,7 +159,7 @@ def send_markdown(self, message, ephemeral=False):

# pylint: disable=arguments-differ
@BACKEND_ACTION_BLOCKS.time()
def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=False, title="Your attention please!"):
def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=None, title="Your attention please!"):
"""Send a series of formatting blocks to the user/channel specified by the context.

Slack distinguishes between simple inline interactive elements and modal dialogs. Modals can contain multiple
Expand All @@ -171,6 +174,8 @@ def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=False, ti
title (str): Title to include on a modal dialog.
"""
logger.info("Sending blocks: %s", json.dumps(blocks, indent=2))
if ephemeral is None:
ephemeral = settings.PLUGINS_CONFIG["nautobot_chatops"]["send_all_messages_private"]
try:
if modal:
if not callback_id:
Expand Down Expand Up @@ -208,16 +213,25 @@ def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=False, ti
self.send_exception(slack_error)

@BACKEND_ACTION_SNIPPET.time()
def send_snippet(self, text, title=None):
def send_snippet(self, text, title=None, ephemeral=None):
"""Send a longer chunk of text as a file snippet."""
if ephemeral is None:
ephemeral = settings.PLUGINS_CONFIG["nautobot_chatops"]["send_all_messages_private"]

if self.context.get("channel_name") == "directmessage":
channels = [self.context.get("user_id")]
else:
channels = [self.context.get("channel_id")]
channels = ",".join(channels)
logger.info("Sending snippet to %s: %s", channels, text)
try:
self.slack_client.files_upload(channels=channels, content=text, title=title)
# Check for the length of the file if the setup is meant to be a private message
if ephemeral:
message_list = self.split_messages(text, 40000)
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
for msg in message_list:
self.send_blocks(self.markdown_block(msg), ephemeral=ephemeral)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want markdown formatting to apply to send_snippet in this case - is it possible to send it as plaintext, or at least wrap it in ```\n...\n``` (similar to WebexDispatcher.send_snippet())?

else:
self.slack_client.files_upload(channels=channels, content=text, title=title)
jvanderaa marked this conversation as resolved.
Show resolved Hide resolved
except SlackClientError as slack_error:
self.send_exception(slack_error)

Expand Down
6 changes: 3 additions & 3 deletions nautobot_chatops/dispatchers/webex.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ def platform_lookup(cls, item_type, item_name):
return None

@BACKEND_ACTION_MARKDOWN.time()
def send_markdown(self, message, ephemeral=False):
def send_markdown(self, message, ephemeral=None):
"""Send a markdown-formatted text message to the user/channel specified by the context."""
self.client.messages.create(roomId=self.context["channel_id"], markdown=message)

@BACKEND_ACTION_BLOCKS.time()
def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=False, title=None):
def send_blocks(self, blocks, callback_id=None, modal=False, ephemeral=None, title=None):
"""Send a series of formatting blocks to the user/channel specified by the context."""
if title and title not in str(blocks[0]):
blocks.insert(0, self.markdown_element(self.bold(title)))
Expand All @@ -119,7 +119,7 @@ def send_image(self, image_path):
self.client.messages.create(roomId=self.context["channel_id"], files=[image_path])

@BACKEND_ACTION_SNIPPET.time()
def send_snippet(self, text, title=None):
def send_snippet(self, text, title=None, ephemeral=None):
"""Send a longer chunk of text as a file snippet."""
return self.send_markdown(f"```\n{text}\n```")

Expand Down
9 changes: 7 additions & 2 deletions tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,17 @@ def is_truthy(arg):
namespace.configure(
{
"nautobot_chatops": {
"nautobot_ver": "1.0.1",
"nautobot_ver": "1.1.6",
"project_name": "nautobot-chatops",
"python_ver": "3.6",
"local": False,
"compose_dir": os.path.join(os.path.dirname(__file__), "development"),
"compose_files": ["docker-compose.requirements.yml", "docker-compose.base.yml", "docker-compose.dev.yml"],
"compose_files": [
"docker-compose.requirements.yml",
# "docker-compose.celery.yml",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If 1.1 is now the default nautobot version, don't we need to include celery by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be. I'm going to do the update to nautobot dev environment in a separate PR. This is a better method to have the Celery components commented out but in the code. It is just easier then to get it up and running.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just an easier method to handle converting over to using 1.1 or later. When Nautobot 1.3 comes out we will make the updates to go to Python3.7 and update to the newer methods.

"docker-compose.base.yml",
"docker-compose.dev.yml",
],
}
}
)
Expand Down