interactive bots: Create Front bot.
This commit is contained in:
parent
6c0151ab67
commit
ea8393511a
|
@ -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.")
|
||||
|
|
0
zulip_bots/zulip_bots/bots/front/__init__.py
Normal file
0
zulip_bots/zulip_bots/bots/front/__init__.py
Normal file
BIN
zulip_bots/zulip_bots/bots/front/assets/usage.png
Normal file
BIN
zulip_bots/zulip_bots/bots/front/assets/usage.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 126 KiB |
17
zulip_bots/zulip_bots/bots/front/doc.md
Normal file
17
zulip_bots/zulip_bots/bots/front/doc.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Front Bot
|
||||
|
||||
## Setup
|
||||
|
||||
1. Go to the `Setting` of your Front app.
|
||||
2. Copy the `JSON Web Token` from `Plugins & API` → `API`.
|
||||
3. Replace `<api_key>` in `zulip_bots/bots/front/front.conf` with `JSON
|
||||
Web Token`.
|
||||
|
||||
## Usage
|
||||
|
||||
![](assets/usage.png)
|
||||
|
||||
The name of the topic, from which you call the bot, must contain the ID of
|
||||
the corresponding Front conversation. If you have received notifications
|
||||
from this conversation using Front incoming webhook, you can use the topic
|
||||
it has created.
|
21
zulip_bots/zulip_bots/bots/front/fixtures/archive.json
Normal file
21
zulip_bots/zulip_bots/bots/front/fixtures/archive.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
22
zulip_bots/zulip_bots/bots/front/fixtures/comment.json
Normal file
22
zulip_bots/zulip_bots/bots/front/fixtures/comment.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
21
zulip_bots/zulip_bots/bots/front/fixtures/delete.json
Normal file
21
zulip_bots/zulip_bots/bots/front/fixtures/delete.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
21
zulip_bots/zulip_bots/bots/front/fixtures/open.json
Normal file
21
zulip_bots/zulip_bots/bots/front/fixtures/open.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
21
zulip_bots/zulip_bots/bots/front/fixtures/spam.json
Normal file
21
zulip_bots/zulip_bots/bots/front/fixtures/spam.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
2
zulip_bots/zulip_bots/bots/front/front.conf
Normal file
2
zulip_bots/zulip_bots/bots/front/front.conf
Normal file
|
@ -0,0 +1,2 @@
|
|||
[front]
|
||||
api_key = <api_key>
|
123
zulip_bots/zulip_bots/bots/front/front.py
Normal file
123
zulip_bots/zulip_bots/bots/front/front.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
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
|
67
zulip_bots/zulip_bots/bots/front/test_front.py
Normal file
67
zulip_bots/zulip_bots/bots/front/test_front.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
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.")
|
Loading…
Reference in a new issue