Repository: opsdroid/opsdroid

Issue:
Slack attachment support
Is there any support for sending [slack attachment(s)](https://api.slack.com/docs/message-attachments) in opsdroid? I found [this open issue](https://github.com/opsdroid/connector-slack/issues/13) on the [connector-slack](https://github.com/opsdroid/connector-slack) project but can't seem to find any new information on additional support for attachments since the `connector-slack` project was deprecated.


Discussion:
We have just merged event types to opsdroid, perhaps slack attachments could be supported easier with them, but so far the slack connector doesn't support them.  Would be good to update the slack connector to support this feature, would you like to work on it?
I could probably take a look at it. Maybe something like `message.respond_with_json(dict)` might be the easiest route? That way users could take advantage of any json api's provided by a backend (i.e. not just the `slack-connector`). 
The way we handle events has changed dramatically. It might be useful for @Cadair to give some thoughts on how you imagine this specific example working?
That's an interesting question that I haven't really considered. The events structure is well suited to having an event type for each of the attachments individually. I wonder if it would be worth having a collection event which could pass a group of events to the connector together and then the slack connector could represent them as attachments?
Has there been any update on this?  If I could somehow format my slack messages it would greatly improve my end users interactions. 
The events have been merged and Jacob has a ongoing PR that will implement sending images and files so this will be done in the future.

Slack is a bit funny with permissions so things need to be tested and such
> If I could somehow format my slack messages it would greatly improve my end users interactions.

What do you mean by this? 
Most of our skills, are just interacting with rest API's, returning JSON back to opsdroid.  Something like 'list inventories in Ansible AWX Dev', and returning data like 

`{3: 'xxx-automation-aws-inventory', 2: 'xxx-automation-ent-dev-Inventory'}`


Its ugly, and hard to read, especially as the results get longer and longer.  If we supported attachments, i could use each key/value pair as a field in the attachment.
As a work around you could read the Json and Post the changed message 
@FabioRosado 

I dont follow.
I'm interested in why you are sending the JSON object back to the chat client. I would've thought you would decode the response and extract the bits you want to show to the user. Alternatively you could pretty format the JSON and return it within code fences.

I see how the attachment [fields](https://api.slack.com/docs/message-attachments#fields) could help but if you have a long JSON you may still end up with it hard to read.
Also looks like Slack are [transitioning from attachments to blocks](https://api.slack.com/messaging/attachments-to-blocks).
Like Jacob said I meant that you could perhaps decode the Json object and transform it into a more readable format.

The blocks seem interesting but like it says in the docs we might need to rethink how to implement things with slack.
I started having a quick hack to look at implementing this and it looks like we will need to fix `aioslacker` first as it seems to be out of date with the Slack API.

```
TypeError: post_message() got an unexpected keyword argument 'blocks'
```

https://api.slack.com/methods/chat.postMessage
Did we created our own fork is aioslacker or was it of something else? 
Im an idiot.  I was attempting to send things, like I would direct to the slack client.  I never even thought to just build up a string, with the proper concatenations to format the message.

```
        return_text = f"*{environment} - Accounts*\n"
        for account in accounts:
            return_text = f"{return_text}```ID: {account['id']} Name: {account['name']} Status: {account['status']}```\n"
        await message.respond(f"{return_text}")
```

Works fine.
> Im an idiot.

Don't put yourself down! Could we improve the docs or add examples to help other people avoid these assumptions?

Resolving patch:
diff --git a/opsdroid/connector/slack/__init__.py b/opsdroid/connector/slack/__init__.py
--- a/opsdroid/connector/slack/__init__.py
+++ b/opsdroid/connector/slack/__init__.py
@@ -12,6 +12,7 @@
 
 from opsdroid.connector import Connector, register_event
 from opsdroid.events import Message, Reaction
+from opsdroid.connector.slack.events import Blocks
 
 
 _LOGGER = logging.getLogger(__name__)
@@ -141,6 +142,20 @@ async def send_message(self, message):
             icon_emoji=self.icon_emoji,
         )
 
+    @register_event(Blocks)
+    async def send_blocks(self, blocks):
+        """Respond with structured blocks."""
+        _LOGGER.debug("Responding with interactive blocks in room  %s", blocks.target)
+        await self.slacker.chat.post(
+            "chat.postMessage",
+            data={
+                "channel": blocks.target,
+                "username": self.bot_name,
+                "blocks": blocks.blocks,
+                "icon_emoji": self.icon_emoji,
+            },
+        )
+
     @register_event(Reaction)
     async def send_reaction(self, reaction):
         """React to a message."""
diff --git a/opsdroid/connector/slack/events.py b/opsdroid/connector/slack/events.py
new file mode 100644
--- /dev/null
+++ b/opsdroid/connector/slack/events.py
@@ -0,0 +1,44 @@
+"""Classes to describe different kinds of Slack specific event."""
+
+import json
+
+from opsdroid.events import Message
+
+
+class Blocks(Message):
+    """A blocks object.
+
+    Slack uses blocks to add advenced interactivity and formatting to messages.
+    https://api.slack.com/messaging/interactivity
+    Blocks are provided in JSON format to Slack which renders them.
+
+    Args:
+        blocks (string or dict): String or dict of json for blocks
+        room (string, optional): String name of the room or chat channel in
+                                 which message was sent
+        connector (Connector, optional): Connector object used to interact with
+                                         given chat service
+        raw_event (dict, optional): Raw message as provided by chat service.
+                                    None by default
+
+    Attributes:
+        created: Local date and time that message object was created
+        user: String name of user sending message
+        room: String name of the room or chat channel in which message was sent
+        connector: Connector object used to interact with given chat service
+        blocks: Blocks JSON as string
+        raw_event: Raw message provided by chat service
+        raw_match: A match object for a search against which the message was
+            matched. E.g. a regular expression or natural language intent
+        responded_to: Boolean initialized as False. True if event has been
+            responded to
+
+    """
+
+    def __init__(self, blocks, *args, **kwargs):
+        """Create object with minimum properties."""
+        super().__init__("", *args, **kwargs)
+
+        self.blocks = blocks
+        if isinstance(self.blocks, list):
+            self.blocks = json.dumps(self.blocks)


Test patch:
diff --git a/tests/test_connector_slack.py b/tests/test_connector_slack.py
--- a/tests/test_connector_slack.py
+++ b/tests/test_connector_slack.py
@@ -9,6 +9,7 @@
 
 from opsdroid.core import OpsDroid
 from opsdroid.connector.slack import ConnectorSlack
+from opsdroid.connector.slack.events import Blocks
 from opsdroid.events import Message, Reaction
 from opsdroid.__main__ import configure_lang
 
@@ -232,6 +233,19 @@ async def test_respond(self):
         await connector.send(Message("test", "user", "room", connector))
         self.assertTrue(connector.slacker.chat.post_message.called)
 
+    async def test_send_blocks(self):
+        connector = ConnectorSlack({"api-token": "abc123"}, opsdroid=OpsDroid())
+        connector.slacker.chat.post = amock.CoroutineMock()
+        await connector.send(
+            Blocks(
+                [{"type": "section", "text": {"type": "mrkdwn", "text": "*Test*"}}],
+                "user",
+                "room",
+                connector,
+            )
+        )
+        self.assertTrue(connector.slacker.chat.post.called)
+
     async def test_react(self):
         connector = ConnectorSlack({"api-token": "abc123"})
         connector.slacker.reactions.post = amock.CoroutineMock()