Repository: opsdroid/opsdroid

Issue:
Move Cisco Spark connector into core
This issue covers adding the [Cisco Spark connector](https://github.com/opsdroid/connector-ciscospark) 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:
@jacobtomlinson I would like to solve this
Hello there it would be great if you could take on this task. Please let me know if you need any help

Resolving patch:
diff --git a/opsdroid/connector/webexteams/__init__.py b/opsdroid/connector/webexteams/__init__.py
new file mode 100644
--- /dev/null
+++ b/opsdroid/connector/webexteams/__init__.py
@@ -0,0 +1,108 @@
+"""A connector for Webex Teams."""
+import json
+import logging
+import uuid
+
+import aiohttp
+
+from webexteamssdk import WebexTeamsAPI
+
+from opsdroid.connector import Connector, register_event
+from opsdroid.events import Message
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ConnectorWebexTeams(Connector):
+    """A connector for Webex Teams."""
+
+    def __init__(self, config, opsdroid=None):
+        """Create a connector."""
+        _LOGGER.debug("Loaded webex teams connector")
+        super().__init__(config, opsdroid=opsdroid)
+        self.name = "webexteams"
+        self.config = config
+        self.opsdroid = opsdroid
+        self.default_room = None
+        self.bot_name = config.get("bot-name", "opsdroid")
+        self.bot_webex_id = None
+        self.secret = uuid.uuid4().hex
+        self.people = {}
+
+    async def connect(self):
+        """Connect to the chat service."""
+        try:
+            self.api = WebexTeamsAPI(access_token=self.config["access-token"])
+        except KeyError:
+            _LOGGER.error("Must set accesst-token for webex teams connector!")
+            return
+
+        await self.clean_up_webhooks()
+        await self.subscribe_to_rooms()
+        await self.set_own_id()
+
+    async def webexteams_message_handler(self, request):
+        """Handle webhooks from the Webex Teams api."""
+        _LOGGER.debug("Handling message from Webex Teams")
+        req_data = await request.json()
+
+        _LOGGER.debug(req_data)
+
+        msg = self.api.messages.get(req_data["data"]["id"])
+
+        if req_data["data"]["personId"] != self.bot_webex_id:
+            person = await self.get_person(req_data["data"]["personId"])
+
+            try:
+                message = Message(
+                    msg.text,
+                    person.displayName,
+                    {"id": msg.roomId, "type": msg.roomType},
+                    self,
+                )
+                await self.opsdroid.parse(message)
+            except KeyError as error:
+                _LOGGER.error(error)
+
+        return aiohttp.web.Response(text=json.dumps("Received"), status=201)
+
+    async def clean_up_webhooks(self):
+        """Remove all existing webhooks."""
+        for webhook in self.api.webhooks.list():
+            self.api.webhooks.delete(webhook.id)
+
+    async def subscribe_to_rooms(self):
+        """Create webhooks for all rooms."""
+        _LOGGER.debug("Creating Webex Teams webhook")
+        webhook_endpoint = "/connector/webexteams"
+        self.opsdroid.web_server.web_app.router.add_post(
+            webhook_endpoint, self.webexteams_message_handler
+        )
+
+        self.api.webhooks.create(
+            name="opsdroid",
+            targetUrl="{}{}".format(self.config.get("webhook-url"), webhook_endpoint),
+            resource="messages",
+            event="created",
+            secret=self.secret,
+        )
+
+    async def get_person(self, personId):
+        """Get a person's info from the api or cache."""
+        if personId not in self.people:
+            self.people[personId] = self.api.people.get(personId)
+        return self.people[personId]
+
+    async def set_own_id(self):
+        """Get the bot id and set it in the class."""
+        self.bot_webex_id = self.api.people.me().id
+
+    async def listen(self):
+        """Listen for and parse new messages."""
+        pass  # Listening is handled by the aiohttp web server
+
+    @register_event(Message)
+    async def send_message(self, message):
+        """Respond with a message."""
+        self.api.messages.create(message.target["id"], text=message.text)


Test patch:
diff --git a/tests/test_connector_webexteams.py b/tests/test_connector_webexteams.py
new file mode 100755
--- /dev/null
+++ b/tests/test_connector_webexteams.py
@@ -0,0 +1,163 @@
+"""Tests for the Connector Webex Teams 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.webexteams import ConnectorWebexTeams
+from opsdroid.events import Message, Reaction
+from opsdroid.__main__ import configure_lang
+
+
+class TestConnectorCiscoWebexTeams(unittest.TestCase):
+    """Test the opsdroid Webex Teams connector class."""
+
+    def setUp(self):
+        self.loop = asyncio.new_event_loop()
+        configure_lang({})
+
+    def test_init(self):
+        """Test that the connector is initialised properly."""
+        connector = ConnectorWebexTeams({})
+        self.assertEqual("webexteams", connector.name)
+        self.assertEqual("opsdroid", connector.bot_name)
+
+    def test_missing_api_key(self):
+        """Test that creating without an API without config raises an error."""
+        with self.assertRaises(TypeError):
+            ConnectorWebexTeams()
+
+
+class TestConnectorCiscoSparkAsync(asynctest.TestCase):
+    """Test the async methods of the opsdroid webex teams connector class."""
+
+    async def setUp(self):
+        configure_lang({})
+
+    async def test_connect(self):
+        connector = ConnectorWebexTeams({"access-token": "abc123"}, opsdroid=OpsDroid())
+
+        opsdroid = amock.CoroutineMock()
+        opsdroid.eventloop = self.loop
+        connector.clean_up_webhooks = amock.CoroutineMock()
+        connector.subscribe_to_rooms = amock.CoroutineMock()
+        connector.set_own_id = amock.CoroutineMock()
+
+        with amock.patch(
+            "websockets.connect", new=amock.CoroutineMock()
+        ) as mocked_websocket_connect:
+            await connector.connect()
+
+        self.assertTrue(connector.clean_up_webhooks.called)
+        self.assertTrue(connector.subscribe_to_rooms.called)
+        self.assertTrue(connector.set_own_id.called)
+
+    async def test_message_handler(self):
+        connector = ConnectorWebexTeams({"access-token": "abc123"})
+        connector.opsdroid = OpsDroid()
+        connector.bot_spark_id = "spark123"
+        connector.api = amock.CoroutineMock()
+        request = amock.Mock()
+        request.json = amock.CoroutineMock()
+        request.json.return_value = {
+            "data": {"id": "3vABZrQgDzfcz7LZi", "personId": "21ABZrQgDzfcz7Lsi"}
+        }
+        message = amock.Mock()
+        connector.api.messages.get = amock.Mock()
+        message.text = "Hello"
+        message.roomId = "90ABCrWgrzfcz7LZi"
+        message.roomType = "general"
+        connector.api.messages.get.return_value = message
+        connector.get_person = amock.CoroutineMock()
+        person = amock.CoroutineMock()
+        person.displayName = "Himanshu"
+        connector.get_person.return_value = person
+
+        response = await connector.webexteams_message_handler(request)
+        self.assertLogs("_LOGGER", "debug")
+        self.assertEqual(201, response.status)
+        self.assertEqual('"Received"', response.text)
+        self.assertTrue(connector.api.messages.get.called)
+        self.assertTrue(connector.get_person.called)
+
+        connector.opsdroid = amock.CoroutineMock()
+        connector.opsdroid.parse = amock.CoroutineMock()
+        connector.opsdroid.parse.side_effect = KeyError
+        await connector.webexteams_message_handler(request)
+        self.assertLogs("_LOGGER", "error")
+
+    async def test_connect_fail_keyerror(self):
+        connector = ConnectorWebexTeams({}, opsdroid=OpsDroid())
+        connector.clean_up_webhooks = amock.CoroutineMock()
+        connector.subscribe_to_rooms = amock.CoroutineMock()
+        connector.set_own_id = amock.CoroutineMock()
+        await connector.connect()
+        self.assertLogs("_LOGGER", "error")
+
+    async def test_listen(self):
+        """Test the listen method.
+
+        The Webex Teams connector listens using an API endoint and so the listen
+        method should just pass and do nothing. We just need to test that it
+        does not block.
+
+        """
+        connector = ConnectorWebexTeams({}, opsdroid=OpsDroid())
+        self.assertEqual(await connector.listen(), None)
+
+    async def test_respond(self):
+        connector = ConnectorWebexTeams({"access-token": "abc123"})
+        connector.api = amock.CoroutineMock()
+        connector.api.messages.create = amock.CoroutineMock()
+        message = Message(
+            text="Hello",
+            user="opsdroid",
+            target={"id": "3vABZrQgDzfcz7LZi"},
+            connector=None,
+        )
+        await connector.respond(message)
+        self.assertTrue(connector.api.messages.create.called)
+
+    async def test_get_person(self):
+        connector = ConnectorWebexTeams({"access-token": "abc123"})
+        connector.api = amock.CoroutineMock()
+        connector.api.messages.create = amock.CoroutineMock()
+        connector.api.people.get = amock.CoroutineMock()
+        connector.api.people.get.return_value = "Himanshu"
+        self.assertEqual(len(connector.people), 0)
+        await connector.get_person("3vABZrQgDzfcz7LZi")
+        self.assertEqual(len(connector.people), 1)
+
+    async def test_subscribe_to_rooms(self):
+        connector = ConnectorWebexTeams(
+            {"access-token": "abc123", "webhook-url": "http://127.0.0.1"}
+        )
+        connector.api = amock.CoroutineMock()
+        connector.opsdroid = amock.CoroutineMock()
+        connector.opsdroid.web_server.web_app.router.add_post = amock.CoroutineMock()
+        connector.api.webhooks.create = amock.CoroutineMock()
+        await connector.subscribe_to_rooms()
+        self.assertTrue(connector.api.webhooks.create.called)
+        self.assertTrue(connector.opsdroid.web_server.web_app.router.add_post.called)
+
+    async def test_clean_up_webhooks(self):
+        connector = ConnectorWebexTeams({"access-token": "abc123"})
+        connector.api = amock.CoroutineMock()
+        x = amock.CoroutineMock()
+        x.id = amock.CoroutineMock()
+        connector.api.webhooks.list = amock.Mock()
+        connector.api.webhooks.list.return_value = [x, x]
+        connector.api.webhooks.delete = amock.Mock()
+        await connector.clean_up_webhooks()
+        self.assertTrue(connector.api.webhooks.list.called)
+        self.assertTrue(connector.api.webhooks.delete.called)
+
+    async def test_set_own_id(self):
+        connector = ConnectorWebexTeams({"access-token": "abc123"})
+        connector.api = amock.CoroutineMock()
+        connector.api.people.me().id = "3vABZrQgDzfcz7LZi"
+        await connector.set_own_id()
+        self.assertTrue(connector.bot_webex_id, "3vABZrQgDzfcz7LZi")