Repository: opsdroid/opsdroid

Issue:
Create a Rocket.Chat connector module
Let's add a [Rocket.Chat connector](https://rocket.chat/) as proposed in #498.

## Steps

 - Make a new submodule directory in [`opsdroid.connector`](https://github.com/opsdroid/opsdroid/tree/master/opsdroid/connector) called `rocketchat` and create an `__init__.py` file inside.
 - Implement the connector according to the [documentation on writing connectors](https://opsdroid.readthedocs.io/en/stable/extending/connectors/).
 - Update the [`requirements.txt`](https://github.com/opsdroid/opsdroid/blob/master/requirements.txt) with any new dependencies from the connector if necessary.
 - Write tests for the connector. (See the [Slack connector tests](https://github.com/jacobtomlinson/opsdroid/blob/master/tests/test_connector_slack.py) for inspiration).
 - Create a [new documentation page](https://github.com/opsdroid/opsdroid/tree/master/docs/connectors).
 - Add the new page to the [mkdocs.yml](https://github.com/opsdroid/opsdroid/blob/master/mkdocs.yml).
 - Add to the [list of connectors](https://github.com/opsdroid/opsdroid/blob/master/docs/configuration-reference.md#connector-modules).

### Resources to get you started
   - [Rockat.Chat REST API](https://rocket.chat/docs/developer-guides/rest-api/)
   - [Rocket.Chat Beta Realtime API](https://rocket.chat/docs/developer-guides/realtime-api/)
   - [jadlog/rocketchat_API](https://github.com/jadolg/rocketchat_API) (bear in mind my comments in #498)

### Asyncio
 Opsdroid uses [asyncio and an event loop](https://docs.python.org/3/library/asyncio.html), so here are some things to bear in mind:
   - Functions should be coroutines which are defined with `async def myfunc(): ...`
   - Coroutines are called with `await`. For example `await myfunc()`. This is how asyncio allows many functions to take it in turns as they swap over when the current thread calls `await`.
   - **Don't block the thread!** If you need to make a call to a blocking function such as `time.sleep()` or any kind of IO request such as `requests.get()` then you should `await` an async variant. For example  you can call `await asyncio.sleep(10)` to sleep without blocking the whole application and you can use `aiohttp` as an async url library.


Discussion:
I have been wanting to tackle this for a while but kept slipping my mind, so I'll give it a try 👍 

Resolving patch:
diff --git a/opsdroid/connector/rocketchat/__init__.py b/opsdroid/connector/rocketchat/__init__.py
new file mode 100644
--- /dev/null
+++ b/opsdroid/connector/rocketchat/__init__.py
@@ -0,0 +1,195 @@
+"""A connector for Rocket.Chat."""
+import asyncio
+import logging
+import datetime
+import aiohttp
+
+from opsdroid.connector import Connector
+from opsdroid.message import Message
+
+_LOGGER = logging.getLogger(__name__)
+API_PATH = '/api/v1/'
+
+
+class RocketChat(Connector):
+    """A connector for the chat service Rocket.Chat."""
+
+    def __init__(self, config):
+        """Create the connector.
+
+        Sets up logic for the Connector class, gets data from
+            the config.yaml or adds default values.
+
+        Args:
+            config (dict): configuration settings from the
+                file config.yaml.
+
+        """
+        super().__init__(config)
+        self.name = "rocket.chat"
+        self.config = config
+        self.default_room = config.get("default-room", "general")
+        self.group = config.get("group", None)
+        self.url = config.get("channel-url", "https://open.rocket.chat")
+        self.update_interval = config.get("update-interval", 1)
+        self.bot_name = config.get("bot-name", "opsdroid")
+        self.listening = True
+        self.latest_update = datetime.datetime.utcnow().isoformat()
+
+        try:
+            self.user_id = config['user-id']
+            self.token = config['token']
+            self.headers = {
+                'X-User-Id': self.user_id,
+                "X-Auth-Token": self.token,
+            }
+        except (KeyError, AttributeError):
+            _LOGGER.error("Unable to login: Access token is missing. "
+                          "Rocket.Chat connector will not be available.")
+
+    def build_url(self, method):
+        """Build the url to connect with api.
+
+        Helper function to build the url to interact with the
+        Rocket.Chat REST API. Uses the global variable API_PATH
+        that points to current api version. (example: /api/v1/)
+
+
+        Args:
+            method (string): Api call endpoint.
+
+        Return:
+            String that represents full API url.
+
+        """
+        return "{}{}{}".format(self.url, API_PATH, method)
+
+    async def connect(self, opsdroid):
+        """Connect to the chat service.
+
+        This method is used to text if the connection to the chat
+        service is successful. If the connection is successful
+        the response is a JSON format containing information
+        about the user. Other than the user username, the
+        information is not used.
+
+        Args:
+            opsdroid (OpsDroid): An instance of opsdroid core.
+
+        """
+        _LOGGER.info("Connecting to Rocket.Chat")
+
+        async with aiohttp.ClientSession() as session:
+            resp = await session.get(self.build_url('me'),
+                                     headers=self.headers)
+            if resp.status != 200:
+                _LOGGER.error("Unable to connect.")
+                _LOGGER.error("Rocket.Chat error %s, %s",
+                              resp.status, resp.text)
+            else:
+                json = await resp.json()
+                _LOGGER.debug("Connected to Rocket.Chat as %s",
+                              json["username"])
+
+    async def _parse_message(self, opsdroid, response):
+        """Parse the message received.
+
+        Args:
+            opsdroid (OpsDroid): An instance of opsdroid core.
+            response (dict): Response returned by aiohttp.Client.
+
+        """
+        if response['messages']:
+            message = Message(
+                response['messages'][0]['msg'],
+                response['messages'][0]['u']['username'],
+                response['messages'][0]['rid'],
+                self)
+            _LOGGER.debug("Received message from Rocket.Chat %s",
+                          response['messages'][0]['msg'])
+
+            await opsdroid.parse(message)
+            self.latest_update = response['messages'][0]['ts']
+
+    async def _get_message(self, opsdroid):
+        """Connect to the API and get messages.
+
+        This method will only listen to either a channel or a
+        private room called groups by Rocket.Chat. If a group
+        is specified in the config then it takes priority
+        over a channel.
+
+        Args:
+            opsdroid (OpsDroid): An instance of opsdroid.core.
+
+        """
+        if self.group:
+            url = self.build_url('groups.history?roomName={}'.format(
+                self.group))
+            self.default_room = self.group
+        else:
+            url = self.build_url('channels.history?roomName={}'.format(
+                self.default_room))
+
+        if self.latest_update:
+            url += '&oldest={}'.format(self.latest_update)
+
+        async with aiohttp.ClientSession() as session:
+            resp = await session.get(url,
+                                     headers=self.headers)
+
+            if resp.status != 200:
+                _LOGGER.error("Rocket.Chat error %s, %s",
+                              resp.status, resp.text)
+                self.listening = False
+            else:
+                json = await resp.json()
+                await self._parse_message(opsdroid, json)
+
+    async def listen(self, opsdroid):
+        """Listen for and parse new messages.
+
+        The method will sleep asynchronously at the end of
+        every loop. The time can either be specified in the
+        config.yaml with the param update-interval - this
+        defaults to 1 second.
+
+        If the channel didn't get any new messages opsdroid
+        will still call the REST API, but won't do anything.
+
+        Args:
+            opsdroid (Opsdroid): An instance of opsdroid core.
+
+        """
+        while self.listening:
+            await self._get_message(opsdroid)
+            await asyncio.sleep(self.update_interval)
+
+    async def respond(self, message, room=None):
+        """Respond with a message.
+
+        The message argument carries both the text to reply with but
+        also which room to reply with depending of the roomId(rid) got
+        from the _parse_message method.
+
+        Args:
+            message (object): An instance of Message
+            room (string, optional): Name of the room to respond to.
+
+        """
+        _LOGGER.debug("Responding with: %s", message.text)
+        async with aiohttp.ClientSession() as session:
+            data = {}
+            data['channel'] = message.room
+            data['alias'] = self.bot_name
+            data['text'] = message.text
+            data['avatar'] = ''
+            resp = await session.post(
+                self.build_url('chat.postMessage'),
+                headers=self.headers,
+                data=data)
+
+            if resp.status == 200:
+                _LOGGER.debug('Successfully responded')
+            else:
+                _LOGGER.debug("Error - %s: Unable to respond", resp.status)


Test patch:
diff --git a/tests/test_connector_rocketchat.py b/tests/test_connector_rocketchat.py
new file mode 100644
--- /dev/null
+++ b/tests/test_connector_rocketchat.py
@@ -0,0 +1,243 @@
+"""Tests for the RocketChat class."""
+import asyncio
+import unittest
+import unittest.mock as mock
+import asynctest
+import asynctest.mock as amock
+
+from opsdroid.core import OpsDroid
+from opsdroid.connector.rocketchat import RocketChat
+from opsdroid.message import Message
+
+
+class TestRocketChat(unittest.TestCase):
+    """Test the opsdroid Slack connector class."""
+
+    def setUp(self):
+        self.loop = asyncio.new_event_loop()
+
+    def test_init(self):
+        """Test that the connector is initialised properly."""
+        connector = RocketChat({
+            'name': 'rocket.chat',
+            'access-token': 'test',
+            'user-id': 'userID'
+        })
+        self.assertEqual("general", connector.default_room)
+        self.assertEqual("rocket.chat", connector.name)
+
+    def test_missing_token(self):
+        """Test that attempt to connect without info raises an error."""
+        with mock.patch('opsdroid.connector.rocketchat._LOGGER.error') \
+                as logmock:
+            RocketChat({})
+            self.assertTrue(logmock.called)
+
+
+class TestConnectorRocketChatAsync(asynctest.TestCase):
+    """Test the async methods of the opsdroid Slack connector class."""
+
+    def setUp(self):
+        self.connector = RocketChat({
+                'name': 'rocket.chat',
+                'token': 'test',
+                'user-id': 'userID',
+                'default_room': "test"
+            })
+        self.connector.latest_update = '2018-10-08T12:57:37.126Z'
+
+    async def test_connect(self):
+        connect_response = amock.Mock()
+        connect_response.status = 200
+        connect_response.json = amock.CoroutineMock()
+        connect_response.return_value = {
+            "_id": "3vABZrQgDzfcz7LZi",
+            "name": "Fábio Rosado",
+            "emails": [
+                {
+                    "address": "fabioglrosado@gmail.com",
+                    "verified": True
+                }
+            ],
+            "status": "online",
+            "statusConnection": "online",
+            "username": "FabioRosado",
+            "utcOffset": 1,
+            "active": True,
+            "roles": [
+                "user"
+            ],
+            "settings": {},
+            "email": "fabioglrosado@gmail.com",
+            "success": True
+        }
+
+        with OpsDroid() as opsdroid, \
+            amock.patch('aiohttp.ClientSession.get') as patched_request, \
+            amock.patch('opsdroid.connector.rocketchat._LOGGER.debug',) \
+                as logmock:
+
+            patched_request.return_value = asyncio.Future()
+            patched_request.return_value.set_result(connect_response)
+
+            await self.connector.connect(opsdroid)
+
+            self.assertTrue(logmock.called)
+            self.assertNotEqual(200, patched_request.status)
+            self.assertTrue(patched_request.called)
+
+    async def test_connect_failure(self):
+        result = amock.MagicMock()
+        result.status = 401
+
+        with OpsDroid() as opsdroid, \
+            amock.patch('aiohttp.ClientSession.get') as patched_request, \
+            amock.patch('opsdroid.connector.rocketchat._LOGGER.error',) \
+                as logmock:
+
+            patched_request.return_value = asyncio.Future()
+            patched_request.return_value.set_result(result)
+
+            await self.connector.connect(opsdroid)
+            self.assertTrue(logmock.called)
+
+    async def test_get_message(self):
+        connector_group = RocketChat({
+                'name': 'rocket.chat',
+                'token': 'test',
+                'user-id': 'userID',
+                'group': "test"
+            })
+        response = amock.Mock()
+        response.status = 200
+        response.json = amock.CoroutineMock()
+        response.return_value = {
+            'messages': [
+                {
+                    "_id": "ZbhuIO764jOIu",
+                    "rid": "Ipej45JSbfjt9",
+                    "msg": "hows it going",
+                    "ts": "2018-05-11T16:05:41.047Z",
+                    "u": {
+                        "_id": "ZbhuIO764jOIu",
+                        "username": "FabioRosado",
+                        "name": "Fábio Rosado"
+                    },
+                    "_updatedAt": "2018-05-11T16:05:41.489Z",
+                    "editedBy": None,
+                    "editedAt": None,
+                    "emoji": None,
+                    "avatar": None,
+                    "alias": None,
+                    "customFields": None,
+                    "attachments": None,
+                    "mentions": [],
+                    "channels": []
+                }
+                ]}
+
+        with OpsDroid() as opsdroid, \
+            amock.patch('aiohttp.ClientSession.get') as patched_request, \
+            amock.patch.object(connector_group, '_parse_message') \
+                as mocked_parse_message:
+
+            patched_request.return_value = asyncio.Future()
+            patched_request.return_value.set_result(response)
+
+            await connector_group._get_message(opsdroid)
+
+            self.assertTrue(patched_request.called)
+            self.assertTrue(mocked_parse_message.called)
+
+    async def test_parse_message(self):
+        response = {
+            'messages': [
+                {
+                    "_id": "ZbhuIO764jOIu",
+                    "rid": "Ipej45JSbfjt9",
+                    "msg": "hows it going",
+                    "ts": "2018-05-11T16:05:41.047Z",
+                    "u": {
+                        "_id": "ZbhuIO764jOIu",
+                        "username": "FabioRosado",
+                        "name": "Fábio Rosado"
+                    },
+                    "_updatedAt": "2018-05-11T16:05:41.489Z",
+                    "editedBy": None,
+                    "editedAt": None,
+                    "emoji": None,
+                    "avatar": None,
+                    "alias": None,
+                    "customFields": None,
+                    "attachments": None,
+                    "mentions": [],
+                    "channels": []
+                }
+                ]}
+
+        with OpsDroid() as opsdroid, \
+                amock.patch('opsdroid.core.OpsDroid.parse') as mocked_parse:
+            await self.connector._parse_message(opsdroid, response)
+            self.assertLogs('_LOGGER', 'debug')
+            self.assertTrue(mocked_parse.called)
+            self.assertEqual("2018-05-11T16:05:41.047Z",
+                             self.connector.latest_update)
+
+
+    async def test_listen(self):
+        self.connector.side_effect = Exception()
+        await self.connector.listen(amock.CoroutineMock())
+
+    async def test_get_message_failure(self):
+        listen_response = amock.Mock()
+        listen_response.status = 401
+
+        with OpsDroid() as opsdroid, \
+            amock.patch('aiohttp.ClientSession.get') as patched_request:
+
+            patched_request.return_value = asyncio.Future()
+            patched_request.return_value.set_result(listen_response)
+            await self.connector._get_message(opsdroid)
+            self.assertLogs('_LOGGER', 'error')
+            self.assertEqual(False, self.connector.listening)
+
+    async def test_respond(self):
+        post_response = amock.Mock()
+        post_response.status = 200
+
+        with OpsDroid() as opsdroid, \
+            amock.patch('aiohttp.ClientSession.post') as patched_request, \
+            amock.patch('opsdroid.connector.rocketchat._LOGGER.debug') \
+                as logmock:
+
+            self.assertTrue(opsdroid.__class__.instances)
+            test_message = Message(text="This is a test",
+                                   user="opsdroid",
+                                   room="test",
+                                   connector=self.connector)
+
+            patched_request.return_value = asyncio.Future()
+            patched_request.return_value.set_result(post_response)
+            await test_message.respond("Response")
+            self.assertTrue(patched_request.called)
+            self.assertTrue(logmock.called)
+
+    async def test_respond_failure(self):
+        post_response = amock.Mock()
+        post_response.status = 401
+
+        with OpsDroid() as opsdroid, \
+            amock.patch('aiohttp.ClientSession.post') as patched_request, \
+            amock.patch('opsdroid.connector.rocketchat._LOGGER.debug') \
+                as logmock:
+
+            self.assertTrue(opsdroid.__class__.instances)
+            test_message = Message(text="This is a test",
+                                   user="opsdroid",
+                                   room="test",
+                                   connector=self.connector)
+
+            patched_request.return_value = asyncio.Future()
+            patched_request.return_value.set_result(post_response)
+            await test_message.respond("Response")
+            self.assertTrue(logmock.called)