diff --git a/ciscosparkbot/Spark.py b/ciscosparkbot/Spark.py index 3b942b4..b601b15 100644 --- a/ciscosparkbot/Spark.py +++ b/ciscosparkbot/Spark.py @@ -25,7 +25,8 @@ class SparkBot(Flask): def __init__(self, spark_bot_name, spark_bot_token=None, spark_api_url=None, spark_bot_email=None, spark_bot_url=None, - default_action="/help", debug=False): + default_action="/help", debug=False, wh_resource="messages", + wh_event="created"): """ Initialize a new SparkBot @@ -54,6 +55,8 @@ def __init__(self, spark_bot_name, spark_bot_token=None, self.spark_bot_email = spark_bot_email self.spark_bot_url = spark_bot_url self.default_action = default_action + self.wh_resource = wh_resource + self.wh_event = wh_event # Create Spark API Object for interacting with Spark if (spark_api_url): @@ -107,16 +110,20 @@ def spark_setup(self): # Setup the Spark Connection globals()["spark"] = CiscoSparkAPI(access_token=self.spark_bot_token) globals()["webhook"] = self.setup_webhook(self.spark_bot_name, - self.spark_bot_url) + self.spark_bot_url, + self.wh_resource, + self.wh_event) sys.stderr.write("Configuring Webhook. \n") sys.stderr.write("Webhook ID: " + globals()["webhook"].id + "\n") # noinspection PyMethodMayBeStatic - def setup_webhook(self, name, targeturl): + def setup_webhook(self, name, targeturl, wh_resource, wh_event): """ Setup Spark WebHook to send incoming messages to this bot. :param name: Name of the WebHook :param targeturl: Target URL for WebHook + :param wh_resource: WebHook 'resource'; messages, memberships, etc. + :param wh_event: WebHook 'event'; created, or all :return: WebHook """ # Get a list of current webhooks @@ -136,16 +143,19 @@ def setup_webhook(self, name, targeturl): sys.stderr.write("Creating new webhook.\n") wh = self.spark.webhooks.create(name=name, targetUrl=targeturl, - resource="messages", - event="created") + resource=wh_resource, + event=wh_event) - # if we have an existing webhook update it + # if we have an existing webhook, delete and recreate + # (can't update resource/event) else: # Need try block because if there are NO webhooks it throws error try: - wh = self.spark.webhooks.update(webhookId=wh.id, - name=name, - targetUrl=targeturl) + wh = self.spark.webhooks.delete(webhookId=wh.id) + wh = self.spark.webhooks.create(name=name, + targetUrl=targeturl, + resource=wh_resource, + event=wh_event) # https://github.com/CiscoDevNet/ciscosparkapi/blob/master/ciscosparkapi/api/webhooks.py#L237 except Exception as e: msg = "Encountered an error updating webhook: {}" @@ -217,6 +227,7 @@ def process_incoming_message(self): and determine reply. :return: """ + reply = None # Get the webhook data post_data = request.json @@ -224,48 +235,55 @@ def process_incoming_message(self): # Determine the Spark Room to send reply to room_id = post_data["data"]["roomId"] - # Get the details about the message that was sent. - message_id = post_data["data"]["id"] - message = self.spark.messages.get(message_id) - if self.DEBUG: - sys.stderr.write("Message content:" + "\n") - sys.stderr.write(str(message) + "\n") - - # First make sure not processing a message from the bots - # Needed to avoid the bot talking to itself - # We check using IDs instead of emails since the email - # of the bot could change while the bot is running - # for example from bot@sparkbot.io to bot@webex.bot - if message.personId in self.spark.people.me().id: + rsc = post_data["resource"] + if rsc != "messages": + if rsc in self.commands.keys(): + reply = self.commands[rsc]["callback"](self, post_data) + else: + return "" + elif post_data["resource"] == "messages": + # Get the details about the message that was sent. + message_id = post_data["data"]["id"] + message = self.spark.messages.get(message_id) if self.DEBUG: - sys.stderr.write("Ignoring message from our self" + "\n") - return "" - - # Log details on message - sys.stderr.write("Message from: " + message.personEmail + "\n") - - # Find the command that was sent, if any - command = "" - for c in self.commands.items(): - if message.text.find(c[0]) != -1: - command = c[0] - sys.stderr.write("Found command: " + command + "\n") - # If a command was found, stop looking for others - break - - # Build the reply to the user - reply = "" - - # Take action based on command - # If no command found, send the default_action - if command in [""] and self.default_action: - # noinspection PyCallingNonCallable - reply = self.commands[self.default_action]["callback"](message) - elif command in self.commands.keys(): - # noinspection PyCallingNonCallable - reply = self.commands[command]["callback"](message) - else: - pass + sys.stderr.write("Message content:" + "\n") + sys.stderr.write(str(message) + "\n") + + # First make sure not processing a message from the bots + # Needed to avoid the bot talking to itself + # We check using IDs instead of emails since the email + # of the bot could change while the bot is running + # for example from bot@sparkbot.io to bot@webex.bot + if message.personId in self.spark.people.me().id: + if self.DEBUG: + sys.stderr.write("Ignoring message from our self" + "\n") + return "" + + # Log details on message + sys.stderr.write("Message from: " + message.personEmail + "\n") + + # Find the command that was sent, if any + command = "" + for c in self.commands.items(): + if message.text.find(c[0]) != -1: + command = c[0] + sys.stderr.write("Found command: " + command + "\n") + # If a command was found, stop looking for others + break + + # Build the reply to the user + reply = "" + + # Take action based on command + # If no command found, send the default_action + if command in [""] and self.default_action: + # noinspection PyCallingNonCallable + reply = self.commands[self.default_action]["callback"](message) + elif command in self.commands.keys(): + # noinspection PyCallingNonCallable + reply = self.commands[command]["callback"](message) + else: + pass # allow command handlers to craft their own Spark message if reply and isinstance(reply, Response): diff --git a/tests/spark_mock.py b/tests/spark_mock.py index c54b590..e64a59f 100644 --- a/tests/spark_mock.py +++ b/tests/spark_mock.py @@ -67,6 +67,64 @@ def incoming_msg(cls): } return json.dumps(data) + @classmethod + def incoming_membership_fail(cls): + data = { + 'id': 'newwebhook', + 'name': 'My Awesome Webhook', + 'targetUrl': 'https://example.com/mywebhook', + 'resource': 'memberships', + 'event': 'created', + 'orgId': 'OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNkYzlh', + 'createdBy': 'fdsafdsf', + 'appId': 'asdfasdfsadf', + 'ownedBy': 'creator', + 'status': 'active', + 'created': '2018-09-13T19:35:51.248Z', + 'actorId': 'OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNkYzlh', + 'data': { + 'id': 'incoming_membership_id', + 'roomId': 'some_room_id', + 'personId': 'some_person_id', + 'personEmail': 'matt@example.com', + 'personDisplayName': 'Matt', + 'personOrgId': 'OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNk', + 'isModerator': False, + 'isMonitor': False, + 'created': '2018-09-13T19:35:58.803Z' + } + } + return json.dumps(data) + + @classmethod + def incoming_membership_pass(cls): + data = { + 'id': 'newwebhook', + 'name': 'My Awesome Webhook', + 'targetUrl': 'https://example.com/mywebhook', + 'resource': 'memberships', + 'event': 'created', + 'orgId': 'OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNkYzlh', + 'createdBy': 'fdsafdsf', + 'appId': 'asdfasdfsadf', + 'ownedBy': 'creator', + 'status': 'active', + 'created': '2018-09-13T19:35:51.248Z', + 'actorId': 'OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNkYzlh', + 'data': { + 'id': 'incoming_membership_id', + 'roomId': 'some_room_id', + 'personId': 'some_person_id', + 'personEmail': 'matt@cisco.com', + 'personDisplayName': 'Matt', + 'personOrgId': 'OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNk', + 'isModerator': False, + 'isMonitor': False, + 'created': '2018-09-13T19:35:58.803Z' + } + } + return json.dumps(data) + @classmethod def get_message_help(cls): data = { diff --git a/tests/sparkbot.py b/tests/sparkbot.py index a4309ad..04ccdaf 100644 --- a/tests/sparkbot.py +++ b/tests/sparkbot.py @@ -119,6 +119,91 @@ def test_process_incoming_message_send_help(self, m): print(resp.data) self.assertIn(b'I understand the following commands', resp.data) + @requests_mock.mock() + def test_process_incoming_membership_check_sender_fail(self, m): + m.get('https://api.ciscospark.com/v1/webhooks', + json=MockSparkAPI.list_webhooks()) + m.post('https://api.ciscospark.com/v1/webhooks', + json=MockSparkAPI.create_webhook()) + m.post('//api.ciscospark.com/v1/messages', json={}) + bot_email = "test@test.com" + spark_token = "somefaketoken" + bot_url = "http://fakebot.com" + bot_app_name = "testbot" + # Create a new bot + bot = SparkBot(bot_app_name, + spark_bot_token=spark_token, + spark_bot_url=bot_url, + spark_bot_email=bot_email, + debug=True, + wh_resource="memberships", + wh_event="all") + + # Add new command + bot.add_command('memberships', + '*', + self.check_membership) + bot.testing = True + self.app = bot.test_client() + + resp = self.app.post('/', + data=MockSparkAPI.incoming_membership_fail(), + content_type="application/json") + self.assertEqual(resp.status_code, 200) + print(resp.data) + self.assertIn(b"failed", resp.data) + + @requests_mock.mock() + def test_process_incoming_membership_check_sender_pass(self, m): + m.get('https://api.ciscospark.com/v1/webhooks', + json=MockSparkAPI.list_webhooks()) + m.post('https://api.ciscospark.com/v1/webhooks', + json=MockSparkAPI.create_webhook()) + m.post('//api.ciscospark.com/v1/messages', json={}) + bot_email = "test@test.com" + spark_token = "somefaketoken" + bot_url = "http://fakebot.com" + bot_app_name = "testbot" + # Create a new bot + bot = SparkBot(bot_app_name, + spark_bot_token=spark_token, + spark_bot_url=bot_url, + spark_bot_email=bot_email, + debug=True, + wh_resource="memberships", + wh_event="all") + + # Add new command + bot.add_command('memberships', + '*', + self.check_membership) + bot.testing = True + self.app = bot.test_client() + + resp = self.app.post('/', + data=MockSparkAPI.incoming_membership_pass(), + content_type="application/json") + self.assertEqual(resp.status_code, 200) + print(resp.data) + self.assertIn(b"success", resp.data) + + def check_membership(self, ob, incoming_msg): + """ + Sample function to do some action. + :param incoming_msg: The incoming message object from Spark + :param ob: Spark API object + :return: A text or markdown based reply + """ + + whitelist = ["cisco.com"] + pemail = incoming_msg["data"]["personEmail"] + pdom = pemail.split("@")[1] + + if pdom in whitelist: + return "success" + else: + return "failed" + @requests_mock.mock() def test_process_incoming_message_default_command(self, m): m.get('//api.ciscospark.com/v1/people/me', json=MockSparkAPI.me())