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/trello/test_trello.py",
|
||||||
"zulip_bots/zulip_bots/bots/susi/susi.py",
|
"zulip_bots/zulip_bots/bots/susi/susi.py",
|
||||||
"zulip_bots/zulip_bots/bots/susi/test_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.")
|
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