Repository: opsdroid/opsdroid

Issue:
Ability to send messages to threads
# Description
In Slack there is the ability to do threaded messaging.  It would be nice to be able to send a message into a thread from the original message.   

## Steps to Reproduce
Feature Enhancement

## Expected Functionality
Send a message and be able to say "attach it to this thread".

Possible approach:

```
async def send_message(self, message, thread=None):
```

## Experienced Functionality
Message is attached to the thread

## Versions
- **Opsdroid version:**  v0.16.0+77.g17247f9
- **Python version:** 3.7.3
- **OS/Docker version:**  Windows 10

## Configuration File
Please include your version of the configuration file below.

```yaml
# Your code goes here.

```

## Additional Details
Any other details you wish to include such as screenshots, console messages, etc.


<!-- Love opsdroid? Please consider supporting our collective:
 +👉  https://opencollective.com/opsdroid/donate -->

Make remaining logging messages translatable
## Description

I searched for `_LOGGER` in VS Code and it yielded 322 results, out of which about 100 were relevant and not yet translatable. I wrapped all of them in `_(<string>)` and tested after that. Some of the logging calls with more than two arguments caused some tests to fail after that, so I reverted the changes, ultimately changing about ~50 calls. I also ran `black` manually as per @jos3p's [suggestion](https://github.com/opsdroid/opsdroid/issues/1128#issuecomment-538653664) since I had some issues with `pre-commit`.

Fixes #1128


## Status
**READY**


## Type of change

- New feature (non-breaking change which adds functionality)


# How Has This Been Tested?

I followed the [contributing guide]https://opsdroid.readthedocs.io/en/latest/contributing/l), i.e.:

`pip install -U pre-commit`
`pre-commit install`
`pip install -U tox`
`tox`

As explained in the description, some of the tests failed after changing ALL the logging messages to be wrapped inside `_()`, so I reverted the changes on those to achieve the same testing results as before the changes.


# Checklist:

- [X] I have performed a self-review of my own code
- [X] I have made corresponding changes to the documentation (if applicable)
- [X] I have added tests that prove my fix is effective or that my feature works
- [X] New and existing unit tests pass locally with my changes




Discussion:
The API I would envisage for this is:

```python
connector.send(Message("my message", target="thread_ts"))
```

The question is how we easily expose the original thread_ts to users. It would currently require a connector specific trip into the `raw_event` attribute.

Maybe we should make connectors which support replies / threads to set the target of the event, so you could do:

```python
connector.send(Message("my message", target=message.target))
```

if fact, if we set `message.target` in the slack connector if the message is in a thread this:
```python
message.respond("Message")
```
would reply in the thread, because of [this](https://github.com/opsdroid/opsdroid/blob/master/opsdroid/events.py#L137) line.

So, in summary:

We should add a check for the existence of the `thread_ts` key in the slack event, and if it exists then set the `.target` attribute of the event the connector emits. This would therefore *by default* respond in the thread.
I generally like this approach.

> This would therefore by default respond in the thread.

I'm not sure I like this. I use multiple Slacks on a day to day basis (hooray 😒) and each has its own etiquette. Some like threads, some hate threads. Therefore I think this should be configurable somehow.

Currently the target gets set to the room. Your proposal is to set the target to the `thread_ts` instead. Maybe we could have a configuration option like `respond-in-thread: false` to toggle this functionality?

Another option (which you might actually be suggesting) would be for the bot to respond to the room by default unless the user message is in an existing thread, in which case you respond in the thread.

The configuration option in that case could be whether you want the bot to start a thread by default or not.
# [Codecov](https://codecov.io/gh/opsdroid/opsdroid/pull/1129?src=pr&el=h1) Report
> Merging [#1129](https://codecov.io/gh/opsdroid/opsdroid/pull/1129?src=pr&el=desc) into [master](https://codecov.io/gh/opsdroid/opsdroid/commit/1a4930c48fa7d39819068647c387d6b8dd4f575d?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/opsdroid/opsdroid/pull/1129/graphs/tree.svg?width=650&token=9qJfZL4bOO&height=150&src=pr)](https://codecov.io/gh/opsdroid/opsdroid/pull/1129?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master    #1129   +/-   ##
=======================================
  Coverage   99.91%   99.91%           
=======================================
  Files          46       46           
  Lines        2489     2489           
=======================================
  Hits         2487     2487           
  Misses          2        2
```


| [Impacted Files](https://codecov.io/gh/opsdroid/opsdroid/pull/1129?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [opsdroid/logging.py](https://codecov.io/gh/opsdroid/opsdroid/pull/1129/diff?src=pr&el=tree#diff-b3BzZHJvaWQvbG9nZ2luZy5weQ==) | `100% <ø> (ø)` | :arrow_up: |
| [opsdroid/connector/github/\_\_init\_\_.py](https://codecov.io/gh/opsdroid/opsdroid/pull/1129/diff?src=pr&el=tree#diff-b3BzZHJvaWQvY29ubmVjdG9yL2dpdGh1Yi9fX2luaXRfXy5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [opsdroid/connector/facebook/\_\_init\_\_.py](https://codecov.io/gh/opsdroid/opsdroid/pull/1129/diff?src=pr&el=tree#diff-b3BzZHJvaWQvY29ubmVjdG9yL2ZhY2Vib29rL19faW5pdF9fLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [opsdroid/loader.py](https://codecov.io/gh/opsdroid/opsdroid/pull/1129/diff?src=pr&el=tree#diff-b3BzZHJvaWQvbG9hZGVyLnB5) | `99.31% <100%> (ø)` | :arrow_up: |
| [opsdroid/connector/slack/\_\_init\_\_.py](https://codecov.io/gh/opsdroid/opsdroid/pull/1129/diff?src=pr&el=tree#diff-b3BzZHJvaWQvY29ubmVjdG9yL3NsYWNrL19faW5pdF9fLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [opsdroid/connector/rocketchat/\_\_init\_\_.py](https://codecov.io/gh/opsdroid/opsdroid/pull/1129/diff?src=pr&el=tree#diff-b3BzZHJvaWQvY29ubmVjdG9yL3JvY2tldGNoYXQvX19pbml0X18ucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [opsdroid/connector/websocket/\_\_init\_\_.py](https://codecov.io/gh/opsdroid/opsdroid/pull/1129/diff?src=pr&el=tree#diff-b3BzZHJvaWQvY29ubmVjdG9yL3dlYnNvY2tldC9fX2luaXRfXy5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [opsdroid/connector/gitter/\_\_init\_\_.py](https://codecov.io/gh/opsdroid/opsdroid/pull/1129/diff?src=pr&el=tree#diff-b3BzZHJvaWQvY29ubmVjdG9yL2dpdHRlci9fX2luaXRfXy5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [opsdroid/connector/webexteams/\_\_init\_\_.py](https://codecov.io/gh/opsdroid/opsdroid/pull/1129/diff?src=pr&el=tree#diff-b3BzZHJvaWQvY29ubmVjdG9yL3dlYmV4dGVhbXMvX19pbml0X18ucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [opsdroid/connector/matrix/connector.py](https://codecov.io/gh/opsdroid/opsdroid/pull/1129/diff?src=pr&el=tree#diff-b3BzZHJvaWQvY29ubmVjdG9yL21hdHJpeC9jb25uZWN0b3IucHk=) | `100% <100%> (ø)` | :arrow_up: |
| ... and [2 more](https://codecov.io/gh/opsdroid/opsdroid/pull/1129/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/opsdroid/opsdroid/pull/1129?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/opsdroid/opsdroid/pull/1129?src=pr&el=footer). Last update [1a4930c...4343a3f](https://codecov.io/gh/opsdroid/opsdroid/pull/1129?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

Of course, I'll get to it right away!
It should be all done right now. Apologies if I skipped any, although I'm certain that at least 99% of them are translatable now. I also intentionally skipped calls like `_LOGGER.debug(fbmsg)` since I assume that wouldn't change anything.
All done! When you do merge this PR, please let me know if there are any other issues that would be fitting for a beginner/intermediate to work on!
@FabioRosado I know this PR is closed, but I have one last question. As the commits got squished, how should I keep my fork up-to-date? I needed to re-fork, because it said that my fork is 5 commits ahead of the remote.
The way I do it is to open a new branch when I want to work on an issue and then delete the branch when its done. This will still make your fork outdated, you will need to add the opsdroid repository to your remote with the command:

`git remote add upstream https://github.com/opsdroid/opsdroid.git`

then you can do `git pull upstream master` and finally `git push` this will keep your opsdroid fork up to date.

Note that you can call the main opsdroid remote whatever you like, I just tend to call origin(my repo) and upstream (the main repo).

I hope this makes sense, please let me know if you need any help with anything else 😄

Also I was thinking that since you are from Poland you could work on translating opsdroid into Polish maybe? we don't have the issue open but it will count for your hacktoberfest anyway 😄 👍  

@FabioRosado Thanks, that makes sense!

And yes, I could certainly work on translating opsdroid into Polish. I can work both on this and on the docstrings to mix things up a little bit if that's okay. :P
Haha of course that would be great 😄 
Again let me know if you need any help 👍 

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
@@ -32,6 +32,7 @@
     "icon-emoji": str,
     "connect-timeout": int,
     "chat-as-user": bool,
+    "start_thread": bool,
 }
 
 
@@ -48,6 +49,7 @@ def __init__(self, config, opsdroid=None):
         self.token = config["token"]
         self.timeout = config.get("connect-timeout", 10)
         self.chat_as_user = config.get("chat-as-user", False)
+        self.start_thread = config.get("start_thread", False)
         self.ssl_context = ssl.create_default_context(cafile=certifi.where())
         self.slack = slack.WebClient(
             token=self.token,
@@ -130,15 +132,27 @@ async def _send_message(self, message):
         _LOGGER.debug(
             _("Responding with: '%s' in room  %s."), message.text, message.target
         )
+        data = {
+            "channel": message.target,
+            "text": message.text,
+            "as_user": self.chat_as_user,
+            "username": self.bot_name,
+            "icon_emoji": self.icon_emoji,
+        }
+
+        if message.linked_event:
+            if "thread_ts" in message.linked_event.raw_event:
+                if (
+                    message.linked_event.event_id
+                    != message.linked_event.raw_event["thread_ts"]
+                ):
+                    # Linked Event is inside a thread
+                    data["thread_ts"] = message.linked_event.raw_event["thread_ts"]
+            elif self.start_thread:
+                data["thread_ts"] = message.linked_event.event_id
+
         await self.slack.api_call(
-            "chat.postMessage",
-            data={
-                "channel": message.target,
-                "text": message.text,
-                "as_user": self.chat_as_user,
-                "username": self.bot_name,
-                "icon_emoji": self.icon_emoji,
-            },
+            "chat.postMessage", data=data,
         )
 
     @register_event(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
@@ -163,10 +163,68 @@ async def test_respond(self):
         connector = ConnectorSlack({"token": "abc123"}, opsdroid=self.od)
         connector.slack.api_call = amock.CoroutineMock()
         await connector.send(
-            events.Message(text="test", user="user", target="room", connector=connector)
+            events.Message(
+                text="test", user="user", target="room", connector=connector,
+            )
         )
         self.assertTrue(connector.slack.api_call.called)
 
+    async def test_send_message_inside_thread(self):
+        connector = ConnectorSlack({"token": "abc123"}, opsdroid=self.od)
+        connector.slack.api_call = amock.CoroutineMock()
+        linked_event = events.Message(
+            text="linked text", raw_event={"thread_ts": "1582838099.000600"}
+        )
+        message = events.Message(
+            text="test",
+            user="user",
+            target="room",
+            connector=connector,
+            linked_event=linked_event,
+            event_id="1582838099.000601",
+        )
+
+        await connector.send(message)
+        connector.slack.api_call.assert_called_once_with(
+            "chat.postMessage",
+            data={
+                "channel": "room",
+                "text": "test",
+                "as_user": False,
+                "username": "opsdroid",
+                "icon_emoji": ":robot_face:",
+                "thread_ts": "1582838099.000600",
+            },
+        )
+
+    async def test_send_message_start_thread_is_true(self):
+        connector = ConnectorSlack({"token": "abc123"}, opsdroid=self.od)
+        connector.slack.api_call = amock.CoroutineMock()
+        connector.start_thread = True
+        event_id = "1582838099.000601"
+        linked_event = events.Message(
+            text="linked text", event_id=event_id, raw_event={}
+        )
+        message = events.Message(
+            text="test",
+            user="user",
+            target="room",
+            connector=connector,
+            linked_event=linked_event,
+        )
+        await connector.send(message)
+        connector.slack.api_call.assert_called_once_with(
+            "chat.postMessage",
+            data={
+                "channel": "room",
+                "text": "test",
+                "as_user": False,
+                "username": "opsdroid",
+                "icon_emoji": ":robot_face:",
+                "thread_ts": "1582838099.000601",
+            },
+        )
+
     async def test_send_blocks(self):
         connector = ConnectorSlack({"token": "abc123"}, opsdroid=self.od)
         connector.slack.api_call = amock.CoroutineMock()