interactive bots: Create Front bot.
MODIFY
tools/run-mypy
tools/run-mypy
@@ -93,6 +93,8 @@ force_include = [
"zulip_bots/zulip_bots/bots/trello/test_trello.py",
"zulip_bots/zulip_bots/bots/susi/susi.py",
"zulip_bots/zulip_bots/bots/susi/test_susi.py",
+ "zulip_bots/zulip_bots/bots/front/front.py",
+ "zulip_bots/zulip_bots/bots/front/test_front.py"
]
parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.")

ADD
zulip_bots/zulip_bots/bots/front/__init__.py
zulip_bots/zulip_bots/bots/front/__init__.py
ADD
zulip_bots/zulip_bots/bots/front/assets/usage.png
zulip_bots/zulip_bots/bots/front/assets/usage.png
Binary files /dev/null and b/zulip_bots/zulip_bots/bots/front/assets/usage.png differ

ADD
zulip_bots/zulip_bots/bots/front/fixtures/archive.json
+{
+ "request": {
+ "method": "PATCH",
+ "api_url": "https://api2.frontapp.com/conversations/cnv_kqatm2",
+ "headers": {
+ "Authorization": "Bearer TEST"
+ },
+ "json": {
+ "status": "archived"
+ }
+ },
+ "response": {
+ "error": {
+ "title": "Unauthenticated",
+ "message": "Provided token is not a JSON Web Token"
+ }
+ },
+ "response-headers": {
+ "Status": "401 Unauthorized"
+ }
+}

ADD
zulip_bots/zulip_bots/bots/front/fixtures/comment.json
+{
+ "request": {
+ "method": "POST",
+ "api_url": "https://api2.frontapp.com/conversations/cnv_kqatm2/comments",
+ "headers": {
+ "Authorization": "Bearer TEST"
+ },
+ "json": {
+ "author_id": "alt:email:leela@planet-express.com",
+ "body": "@bender, I thought you were supposed to be cooking for this party."
+ }
+ },
+ "response": {
+ "error": {
+ "title": "Unauthenticated",
+ "message": "Provided token is not a JSON Web Token"
+ }
+ },
+ "response-headers": {
+ "Status": "401 Unauthorized"
+ }
+}

ADD
zulip_bots/zulip_bots/bots/front/fixtures/delete.json
+{
+ "request": {
+ "method": "PATCH",
+ "api_url": "https://api2.frontapp.com/conversations/cnv_kqatm2",
+ "headers": {
+ "Authorization": "Bearer TEST"
+ },
+ "json": {
+ "status": "deleted"
+ }
+ },
+ "response": {
+ "error": {
+ "title": "Unauthenticated",
+ "message": "Provided token is not a JSON Web Token"
+ }
+ },
+ "response-headers": {
+ "Status": "401 Unauthorized"
+ }
+}

ADD
zulip_bots/zulip_bots/bots/front/fixtures/open.json
+{
+ "request": {
+ "method": "PATCH",
+ "api_url": "https://api2.frontapp.com/conversations/cnv_kqatm2",
+ "headers": {
+ "Authorization": "Bearer TEST"
+ },
+ "json": {
+ "status": "open"
+ }
+ },
+ "response": {
+ "error": {
+ "title": "Unauthenticated",
+ "message": "Provided token is not a JSON Web Token"
+ }
+ },
+ "response-headers": {
+ "Status": "401 Unauthorized"
+ }
+}

ADD
zulip_bots/zulip_bots/bots/front/fixtures/spam.json
+{
+ "request": {
+ "method": "PATCH",
+ "api_url": "https://api2.frontapp.com/conversations/cnv_kqatm2",
+ "headers": {
+ "Authorization": "Bearer TEST"
+ },
+ "json": {
+ "status": "spam"
+ }
+ },
+ "response": {
+ "error": {
+ "title": "Unauthenticated",
+ "message": "Provided token is not a JSON Web Token"
+ }
+ },
+ "response-headers": {
+ "Status": "401 Unauthorized"
+ }
+}

ADD
zulip_bots/zulip_bots/bots/front/front.conf
+[front]
+api_key = <api_key>

ADD
zulip_bots/zulip_bots/bots/front/front.py
+import requests
+import re
+from typing import Any, Dict, Optional
+
+class FrontHandler(object):
+ FRONT_API = "https://api2.frontapp.com/conversations/{}"
+ COMMANDS = [
+ ('archive', "Archive a conversation."),
+ ('delete', "Delete a conversation."),
+ ('spam', "Mark a conversation as spam."),
+ ('open', "Restore a conversation."),
+ ('comment <text>', "Leave a comment.")
+ ]
+ CNV_ID_REGEXP = 'cnv_(?P<id>[0-9a-z]+)'
+ COMMENT_PREFIX = "comment "
+
+ def usage(self) -> str:
+ return '''
+ Front Bot uses the Front REST API to interact with Front. In order to use
+ Front Bot, `front.conf` must be set up. See `doc.md` for more details.
+ '''
+
+ def initialize(self, bot_handler: Any) -> None:
+ config = bot_handler.get_config_info('front')
+ api_key = config.get('api_key')
+ if not api_key:
+ raise KeyError("No API key specified.")
+
+ self.auth = "Bearer " + api_key
+
+ def help(self, bot_handler: Any) -> str:
+ response = ""
+ for command, description in self.COMMANDS:
+ response += "`{}` {}\n".format(command, description)
+
+ return response
+
+ def archive(self, bot_handler: Any) -> str:
+ response = requests.patch(self.FRONT_API.format(self.conversation_id),
+ headers={"Authorization": self.auth},
+ json={"status": "archived"})
+
+ if response.status_code not in (200, 204):
+ return "Something went wrong."
+
+ return "Conversation was archived."
+
+ def delete(self, bot_handler: Any) -> str:
+ response = requests.patch(self.FRONT_API.format(self.conversation_id),
+ headers={"Authorization": self.auth},
+ json={"status": "deleted"})
+
+ if response.status_code not in (200, 204):
+ return "Something went wrong."
+
+ return "Conversation was deleted."
+
+ def spam(self, bot_handler: Any) -> str:
+ response = requests.patch(self.FRONT_API.format(self.conversation_id),
+ headers={"Authorization": self.auth},
+ json={"status": "spam"})
+
+ if response.status_code not in (200, 204):
+ return "Something went wrong."
+
+ return "Conversation was marked as spam."
+
+ def restore(self, bot_handler: Any) -> str:
+ response = requests.patch(self.FRONT_API.format(self.conversation_id),
+ headers={"Authorization": self.auth},
+ json={"status": "open"})
+
+ if response.status_code not in (200, 204):
+ return "Something went wrong."
+
+ return "Conversation was restored."
+
+ def comment(self, bot_handler: Any, **kwargs: Any) -> str:
+ response = requests.post(self.FRONT_API.format(self.conversation_id) + "/comments",
+ headers={"Authorization": self.auth}, json=kwargs)
+
+ if response.status_code not in (200, 201):
+ return "Something went wrong."
+
+ return "Comment was sent."
+
+ def handle_message(self, message: Dict[str, str], bot_handler: Any) -> None:
+ command = message['content']
+
+ result = re.search(self.CNV_ID_REGEXP, message['subject'])
+ if not result:
+ bot_handler.send_reply(message, "No coversation ID found. Please make "
+ "sure that the name of the topic "
+ "contains a valid coversation ID.")
+ return None
+
+ self.conversation_id = result.group()
+
+ if command == 'help':
+ bot_handler.send_reply(message, self.help(bot_handler))
+
+ elif command == 'archive':
+ bot_handler.send_reply(message, self.archive(bot_handler))
+
+ elif command == 'delete':
+ bot_handler.send_reply(message, self.delete(bot_handler))
+
+ elif command == 'spam':
+ bot_handler.send_reply(message, self.spam(bot_handler))
+
+ elif command == 'open':
+ bot_handler.send_reply(message, self.restore(bot_handler))
+
+ elif command.startswith(self.COMMENT_PREFIX):
+ kwargs = {
+ 'author_id': "alt:email:" + message['sender_email'],
+ 'body': command[len(self.COMMENT_PREFIX):]
+ }
+ bot_handler.send_reply(message, self.comment(bot_handler, **kwargs))
+ else:
+ bot_handler.send_reply(message, "Unknown command. Use `help` for instructions.")
+
+handler_class = FrontHandler

ADD
zulip_bots/zulip_bots/bots/front/test_front.py
+from typing import Any, Dict
+
+from zulip_bots.test_lib import BotTestCase
+
+class TestFrontBot(BotTestCase):
+ bot_name = 'front'
+
+ def make_request_message(self, content: str) -> Dict[str, Any]:
+ message = super().make_request_message(content)
+ message['subject'] = "cnv_kqatm2"
+ message['sender_email'] = "leela@planet-express.com"
+ return message
+
+ def test_bot_responds_to_empty_message(self) -> None:
+ with self.mock_config_info({'api_key': "TEST"}):
+ self.verify_reply("", "Unknown command. Use `help` for instructions.")
+
+ def test_help(self) -> None:
+ with self.mock_config_info({'api_key': "TEST"}):
+ self.verify_reply('help', "`archive` Archive a conversation.\n"
+ "`delete` Delete a conversation.\n"
+ "`spam` Mark a conversation as spam.\n"
+ "`open` Restore a conversation.\n"
+ "`comment <text>` Leave a comment.\n")
+
+ def test_archive(self) -> None:
+ with self.mock_config_info({'api_key': "TEST"}):
+ with self.mock_http_conversation('archive'):
+ self.verify_reply('archive', "Conversation was archived.")
+
+ def test_delete(self) -> None:
+ with self.mock_config_info({'api_key': "TEST"}):
+ with self.mock_http_conversation('delete'):
+ self.verify_reply('delete', "Conversation was deleted.")
+
+ def test_spam(self) -> None:
+ with self.mock_config_info({'api_key': "TEST"}):
+ with self.mock_http_conversation('spam'):
+ self.verify_reply('spam', "Conversation was marked as spam.")
+
+ def test_restore(self) -> None:
+ with self.mock_config_info({'api_key': "TEST"}):
+ with self.mock_http_conversation('open'):
+ self.verify_reply('open', "Conversation was restored.")
+
+ def test_comment(self) -> None:
+ body = "@bender, I thought you were supposed to be cooking for this party."
+ with self.mock_config_info({'api_key': "TEST"}):
+ with self.mock_http_conversation('comment'):
+ self.verify_reply("comment " + body, "Comment was sent.")
+
+class TestFrontBotWrongTopic(BotTestCase):
+ bot_name = 'front'
+
+ def make_request_message(self, content: str) -> Dict[str, Any]:
+ message = super().make_request_message(content)
+ message['subject'] = "kqatm2"
+ return message
+
+ def test_bot_responds_to_empty_message(self) -> None:
+ pass
+
+ def test_no_conversation_id(self) -> None:
+ with self.mock_config_info({'api_key': "TEST"}):
+ self.verify_reply('archive', "No coversation ID found. Please make "
+ "sure that the name of the topic "
+ "contains a valid coversation ID.")

interactive bots: Create Front bot.