Repository: opsdroid/opsdroid

Issue:
Move Facebook Messenger connector into core
This issue covers adding the [Facebook Messenger connector](https://github.com/opsdroid/connector-facebook) to core.

## Background
A while ago we began moving connectors from external plugins into the core of the project (see #185 for more context). We started with [slack](https://github.com/opsdroid/opsdroid/blob/master/opsdroid/connector/slack/__init__.py) and [websockets](https://github.com/opsdroid/opsdroid/blob/master/opsdroid/connector/websocket/__init__.py) but need to go through all the other existing plugins and move them into the core.

## Steps

 - Make a new submodule directory in [`opsdroid.connector`](https://github.com/opsdroid/opsdroid/tree/master/opsdroid/connector) and copy the connector code over.
 - Update the [`requirements.txt`](https://github.com/opsdroid/opsdroid/blob/master/requirements.txt) with any 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).
 - Copy the relevant information from the connector `README.md` into 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).
 - Add a deprecation notice to the old connector. (See [the slack connector](https://github.com/opsdroid/connector-slack))


Discussion:
Happy to help here 👍 

Resolving patch:
diff --git a/opsdroid/connector/facebook/__init__.py b/opsdroid/connector/facebook/__init__.py
new file mode 100644
--- /dev/null
+++ b/opsdroid/connector/facebook/__init__.py
@@ -0,0 +1,129 @@
+"""A connector for Facebook Messenger."""
+import json
+import logging
+
+import aiohttp
+
+from opsdroid.connector import Connector
+from opsdroid.message import Message
+
+
+_LOGGER = logging.getLogger(__name__)
+_FACEBOOK_SEND_URL = "https://graph.facebook.com/v2.6/me/messages" \
+                     "?access_token={}"
+
+
+class ConnectorFacebook(Connector):
+    """A connector for Facebook Messenger.
+
+    It handles the incoming messages from facebook messenger and sends the user
+    messages. It also handles the authentication challenge by verifying the
+    token.
+
+    Attributes:
+        config: The config for this connector specified in the
+            `configuration.yaml` file.
+        name: String name of the connector.
+        opsdroid: opsdroid instance.
+        default_room: String name of default room for chat messages.
+        bot_name: String name for bot.
+
+    """
+
+    def __init__(self, config):
+        """Connector Setup."""
+        super().__init__(config)
+        _LOGGER.debug("Starting facebook connector")
+        self.config = config
+        self.name = self.config.get("name", "facebook")
+        self.opsdroid = None
+        self.default_room = None
+        self.bot_name = config.get("bot-name", 'opsdroid')
+
+    async def connect(self, opsdroid):
+        """Connect to the chat service."""
+        self.opsdroid = opsdroid
+
+        self.opsdroid.web_server.web_app.router.add_post(
+            "/connector/{}".format(self.name),
+            self.facebook_message_handler)
+
+        self.opsdroid.web_server.web_app.router.add_get(
+            "/connector/{}".format(self.name),
+            self.facebook_challenge_handler)
+
+    async def facebook_message_handler(self, request):
+        """Handle incoming message.
+
+        For each entry in request, it will check if the entry is a `messaging`
+        type. Then it will process all the incoming messages.
+
+        Return:
+            A 200 OK response. The Messenger Platform will resend the webhook
+            event every 20 seconds, until a 200 OK response is received.
+            Failing to return a 200 OK may cause your webhook to be
+            unsubscribed by the Messenger Platform.
+
+        """
+        req_data = await request.json()
+
+        if "object" in req_data and req_data["object"] == "page":
+            for entry in req_data["entry"]:
+                for fb_msg in entry["messaging"]:
+                    _LOGGER.debug(fb_msg)
+                    try:
+                        message = Message(fb_msg["message"]["text"],
+                                          fb_msg["sender"]["id"],
+                                          fb_msg["sender"]["id"],
+                                          self)
+                        await self.opsdroid.parse(message)
+                    except KeyError as error:
+                        _LOGGER.error(error)
+
+        return aiohttp.web.Response(
+            text=json.dumps("Received"), status=200)
+
+    async def facebook_challenge_handler(self, request):
+        """Handle auth challenge.
+
+        Return:
+            A response if challenge is a success or failure.
+
+        """
+        _LOGGER.debug(request.query)
+        if request.query["hub.verify_token"] == \
+                self.config.get('verify-token'):
+            return aiohttp.web.Response(
+                text=request.query["hub.challenge"], status=200)
+        return aiohttp.web.Response(
+            text=json.dumps("Bad verify token"), status=403)
+
+    async def listen(self, opsdroid):
+        """Listen for and parse new messages."""
+        pass  # Listening is handled by the aiohttp web server
+
+    async def respond(self, message, room=None):
+        """Respond with a message."""
+        _LOGGER.debug("Responding to facebook")
+        url = _FACEBOOK_SEND_URL.format(self.config.get('page-access-token'))
+        headers = {'content-type': 'application/json'}
+        payload = {
+            "recipient": {
+                "id": message.room
+            },
+            "message": {
+                "text": message.text
+            }
+        }
+        async with aiohttp.ClientSession() as session:
+            resp = await session.post(
+                url,
+                data=json.dumps(payload),
+                headers=headers
+            )
+            if resp.status < 300:
+                _LOGGER.info("Responded with: %s", message.text)
+            else:
+                _LOGGER.debug(resp.status)
+                _LOGGER.debug(await resp.text())
+                _LOGGER.error("Unable to respond to facebook")


Test patch:
diff --git a/tests/test_connector_facebook.py b/tests/test_connector_facebook.py
new file mode 100644
--- /dev/null
+++ b/tests/test_connector_facebook.py
@@ -0,0 +1,170 @@
+import unittest
+import asyncio
+
+import asynctest
+import asynctest.mock as amock
+
+from opsdroid.core import OpsDroid
+from opsdroid.connector.facebook import ConnectorFacebook
+from opsdroid.message import Message
+
+
+class TestConnectorFacebook(unittest.TestCase):
+    """Test the opsdroid Facebook connector class."""
+
+    def setUp(self):
+        self.loop = asyncio.new_event_loop()
+
+    def test_init(self):
+        connector = ConnectorFacebook({})
+        self.assertEqual(None, connector.default_room)
+        self.assertEqual("facebook", connector.name)
+
+    def test_property(self):
+        connector = ConnectorFacebook({})
+        self.assertEqual("facebook", connector.name)
+
+
+class TestConnectorFacebookAsync(asynctest.TestCase):
+    """Test the async methods of the opsdroid Facebook connector class."""
+
+    async def test_connect(self):
+        """Test the connect method adds the handlers."""
+        connector = ConnectorFacebook({})
+        opsdroid = amock.CoroutineMock()
+        opsdroid.web_server = amock.CoroutineMock()
+        opsdroid.web_server.web_app = amock.CoroutineMock()
+        opsdroid.web_server.web_app.router = amock.CoroutineMock()
+        opsdroid.web_server.web_app.router.add_get = amock.CoroutineMock()
+        opsdroid.web_server.web_app.router.add_post = amock.CoroutineMock()
+
+        await connector.connect(opsdroid)
+
+        self.assertTrue(opsdroid.web_server.web_app.router.add_get.called)
+        self.assertTrue(opsdroid.web_server.web_app.router.add_post.called)
+
+    async def test_facebook_message_handler(self):
+        """Test the new facebook message handler."""
+        import aiohttp
+        connector = ConnectorFacebook({})
+        req_ob = {
+            "object": "page",
+            "entry": [{
+                "messaging": [{
+                    "message": {"text": "Hello"},
+                    "sender": {"id": '1234567890'}
+                }]
+            }]
+        }
+        mock_request = amock.CoroutineMock()
+        mock_request.json = amock.CoroutineMock()
+        mock_request.json.return_value = req_ob
+
+        with OpsDroid() as opsdroid:
+            connector.opsdroid = opsdroid
+            connector.opsdroid.parse = amock.CoroutineMock()
+
+            response = await connector.facebook_message_handler(mock_request)
+            self.assertTrue(connector.opsdroid.parse.called)
+            self.assertEqual(type(response), aiohttp.web.Response)
+            self.assertEqual(response.status, 200)
+
+    async def test_facebook_message_handler_invalid(self):
+        """Test the new facebook message handler for invalid message."""
+        import aiohttp
+        connector = ConnectorFacebook({})
+        req_ob = {
+            "object": "page",
+            "entry": [{
+                "messaging": [{
+                    "message": {"text": "Hello"},
+                    "sender": {}
+                }]
+            }]
+        }
+        mock_request = amock.CoroutineMock()
+        mock_request.json = amock.CoroutineMock()
+        mock_request.json.return_value = req_ob
+
+        with OpsDroid() as opsdroid,  \
+                amock.patch('opsdroid.connector.facebook._LOGGER.error') \
+                        as logmock:
+            connector.opsdroid = opsdroid
+            connector.opsdroid.parse = amock.CoroutineMock()
+
+            response = await connector.facebook_message_handler(mock_request)
+            self.assertFalse(connector.opsdroid.parse.called)
+            self.assertTrue(logmock.called)
+            self.assertEqual(type(response), aiohttp.web.Response)
+            self.assertEqual(response.status, 200)
+
+    async def test_facebook_challenge_handler(self):
+        """Test the facebook challenge handler."""
+        import aiohttp
+        connector = ConnectorFacebook({'verify-token': 'token_123'})
+        mock_request = amock.Mock()
+        mock_request.query = {
+            "hub.verify_token": 'token_123',
+            'hub.challenge': 'challenge_123'
+        }
+
+        response = await connector.facebook_challenge_handler(mock_request)
+        self.assertEqual(type(response), aiohttp.web.Response)
+        self.assertEqual(response.text, 'challenge_123')
+        self.assertEqual(response.status, 200)
+
+        mock_request.query = {
+            "hub.verify_token": 'token_abc',
+            'hub.challenge': 'challenge_123'
+        }
+        response = await connector.facebook_challenge_handler(mock_request)
+        self.assertEqual(type(response), aiohttp.web.Response)
+        self.assertEqual(response.status, 403)
+
+    async def test_listen(self):
+        """Test that listen does nothing."""
+        connector = ConnectorFacebook({})
+        await connector.listen(None)
+
+    async def test_respond(self):
+        """Test that responding sends a message."""
+        post_response = amock.Mock()
+        post_response.status = 200
+
+        with OpsDroid() as opsdroid, \
+                amock.patch('aiohttp.ClientSession.post',
+                            new=asynctest.CoroutineMock()) as patched_request:
+            self.assertTrue(opsdroid.__class__.instances)
+            connector = ConnectorFacebook({})
+            room = "a146f52c-548a-11e8-a7d1-28cfe949e12d"
+            test_message = Message(text="Hello world",
+                                   user="Alice",
+                                   room=room,
+                                   connector=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)
+
+    async def test_respond_bad_response(self):
+        """Test that responding sends a message and get bad response."""
+        post_response = amock.Mock()
+        post_response.status = 401
+        post_response.text = amock.CoroutineMock()
+        post_response.text.return_value = "Error"
+
+        with OpsDroid() as opsdroid, \
+                amock.patch('aiohttp.ClientSession.post',
+                            new=asynctest.CoroutineMock()) as patched_request:
+            self.assertTrue(opsdroid.__class__.instances)
+            connector = ConnectorFacebook({})
+            room = "a146f52c-548a-11e8-a7d1-28cfe949e12d"
+            test_message = Message(text="Hello world",
+                                   user="Alice",
+                                   room=room,
+                                   connector=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(post_response.text.called)