Skip to content

Commit d29271b

Browse files
committed
Add error notification when slack invite fails
1 parent a392e08 commit d29271b

9 files changed

Lines changed: 150 additions & 25 deletions

File tree

Pipfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@ pytest-aiohttp = "*"
2323
asynctest = "*"
2424

2525
[requires]
26-
python_version = "3.7"
26+
python_version = "3.7"

pybot/endpoints/api/slack_api.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import logging
2+
23
from sirbot import SirBot
34
from slack import ROOT_URL
45
from slack.exceptions import SlackAPIError
5-
from aiohttp.web_request import Request
66

7-
from pybot.endpoints.api.utils import _slack_info_from_email
7+
from pybot.endpoints.api.utils import _slack_info_from_email, handle_slack_invite_error
8+
from pybot.plugins import APIPlugin
9+
from pybot.plugins.api.request import SlackApiRequest
810

911
logger = logging.getLogger(__name__)
1012

1113

12-
def create_endpoints(plugin):
14+
def create_endpoints(plugin: APIPlugin):
1315
plugin.on_get("verify", verify, wait=True)
1416
plugin.on_get("invite", invite, wait=True)
1517

1618

17-
async def verify(request: Request, app: SirBot) -> any:
19+
async def verify(request: SlackApiRequest, app: SirBot) -> dict:
1820
"""
1921
Verifies whether a user exists in the configured slack group with
2022
the given email
@@ -30,23 +32,34 @@ async def verify(request: Request, app: SirBot) -> any:
3032
return {"exists": False}
3133

3234

33-
async def invite(request: Request, app: SirBot):
35+
async def invite(request: SlackApiRequest, app: SirBot):
3436
"""
3537
Pulls an email out of the querystring and sends it an invite
3638
to the slack team
3739
3840
:return: The request response from slack
3941
"""
40-
try:
41-
slack = app.plugins["admin_slack"].api
42-
email = request.query["email"]
42+
slack = app.plugins["admin_slack"].api
43+
body = await request.json()
44+
45+
if "email" not in body:
46+
return {"error": "Must contain `email` JSON value"}
47+
email = body["email"]
4348

49+
try:
4450
response = await slack.query(
4551
url=ROOT_URL + "users.admin.invite", data={"email": email}
4652
)
47-
# logger.info("Response from slack: ", response)
4853
return response
4954

5055
except SlackAPIError as e:
51-
logger.info(e)
52-
return e.data
56+
logger.info("Slack invite resulted in SlackAPIError: " + e.error)
57+
if e.data["error"] == "already_invited":
58+
return e.data
59+
else:
60+
await handle_slack_invite_error(email, e, slack)
61+
62+
except Exception as e:
63+
logger.exception(e)
64+
await handle_slack_invite_error(email, e, slack)
65+
return e

pybot/endpoints/api/utils.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
from typing import Optional
2-
3-
from slack import ROOT_URL
41
from slack.exceptions import SlackAPIError
2+
from slack.methods import Methods
53
from slack.io.abc import SlackAPI
4+
from slack import ROOT_URL
5+
from typing import Optional
6+
7+
from pybot.endpoints.slack.utils import TICKET_CHANNEL
8+
from pybot.endpoints.slack.utils.action_messages import (
9+
TICKET_OPTIONS,
10+
not_claimed_attachment,
11+
)
612

713

814
async def _slack_info_from_email(
@@ -15,3 +21,45 @@ async def _slack_info_from_email(
1521
return response["user"]
1622
except SlackAPIError:
1723
return fallback
24+
25+
26+
def invite_failure_attachments(email: str, error: str) -> list:
27+
attachments = [
28+
{
29+
"text": "",
30+
"callback_id": "ticket_status",
31+
"response_type": "in_channel",
32+
"fallback": "",
33+
"fields": [
34+
{"title": "Email", "value": f"{email}", "short": True},
35+
{"title": "Error", "value": f"{error}", "short": True},
36+
],
37+
"actions": [
38+
{
39+
"name": "status",
40+
"text": "Current Status",
41+
"type": "select",
42+
"selected_options": [
43+
{"text": "Not Started", "value": "notStarted"}
44+
],
45+
"options": [
46+
{"text": text, "value": value}
47+
for value, text in TICKET_OPTIONS.items()
48+
],
49+
}
50+
],
51+
},
52+
not_claimed_attachment(),
53+
]
54+
return attachments
55+
56+
57+
async def handle_slack_invite_error(email, error, slack):
58+
attachments = invite_failure_attachments(email, error)
59+
response = {
60+
"channel": TICKET_CHANNEL,
61+
"attachments": attachments,
62+
"text": "User Slack Invite Error",
63+
}
64+
65+
return await slack.query(Methods.CHAT_POST_MESSAGE, response)

pybot/plugins/api/plugin.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ def __init__(self):
1717
def load(self, sirbot):
1818
self.session = sirbot.http_session
1919

20-
sirbot.router.add_route("GET", "/pybot/api/v1/slack/{resource}", endpoints.slack_api)
20+
sirbot.router.add_route(
21+
"GET", "/pybot/api/v1/slack/{resource}", endpoints.slack_api
22+
)
23+
sirbot.router.add_route(
24+
"POST", "/pybot/api/v1/slack/{resource}", endpoints.slack_api
25+
)
2126

2227
def on_get(self, request, handler, **kwargs):
2328
if not asyncio.iscoroutinefunction(handler):

pybot/plugins/api/request.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import copy
2+
import json
23
import os
34
from typing import MutableMapping
45

@@ -29,13 +30,11 @@ def __init__(self, raw_request, resource, query):
2930
def authorized(self):
3031
return self.token is not None and self.token in self.auth_tokens
3132

32-
@staticmethod
33-
def __get_token(raw_request):
34-
if "Authorization" in raw_request.headers:
35-
auth_header = raw_request.headers["Authorization"]
36-
if auth_header.startswith("Bearer "):
37-
return auth_header[7:]
38-
return None
33+
async def json(self):
34+
if self.request.can_read_body:
35+
return await self.request.json()
36+
else:
37+
return {}
3938

4039
@classmethod
4140
def from_request(cls, raw_request):
@@ -44,6 +43,14 @@ def from_request(cls, raw_request):
4443

4544
return cls(raw_request, resource, query)
4645

46+
@staticmethod
47+
def __get_token(raw_request):
48+
if "Authorization" in raw_request.headers:
49+
auth_header = raw_request.headers["Authorization"]
50+
if auth_header.startswith("Bearer "):
51+
return auth_header[7:]
52+
return None
53+
4754
def __getitem__(self, item):
4855
return self.request[item]
4956

tests/conftest.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from sirbot.plugins.slack import SlackPlugin
66

77
from pybot import endpoints
8-
from pybot.plugins import AirtablePlugin
8+
from pybot.plugins import AirtablePlugin, APIPlugin
99
from tests import data
1010

1111
pytest_plugins = ("slack.tests.plugin",)
@@ -21,7 +21,7 @@ def action(request):
2121

2222

2323
@pytest.fixture
24-
def bot(loop) -> SirBot:
24+
async def bot(loop) -> SirBot:
2525
b = SirBot()
2626
slack = SlackPlugin(
2727
token="token",
@@ -31,8 +31,13 @@ def bot(loop) -> SirBot:
3131
)
3232
airtable = AirtablePlugin()
3333
endpoints.slack.create_endpoints(slack)
34+
35+
api = APIPlugin()
36+
endpoints.api.create_endpoints(api)
37+
3438
b.load_plugin(slack)
3539
b.load_plugin(airtable)
40+
b.load_plugin(api)
3641

3742
return b
3843

tests/endpoints/__init__.py

Whitespace-only changes.

tests/endpoints/api/__init__.py

Whitespace-only changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import json
2+
3+
import pytest
4+
from asynctest import CoroutineMock
5+
from sirbot import SirBot
6+
7+
MOCK_USER_NAME = "userName"
8+
MOCK_USER_ID = "U8N6XBL7Q"
9+
AUTH_HEADER = {"Authorization": "Bearer devBackendToken"}
10+
11+
VALID_SLACK_RESPONSE = CoroutineMock(
12+
return_value={"user": {"exists": True, "id": MOCK_USER_ID, "name": MOCK_USER_NAME}}
13+
)
14+
15+
16+
@pytest.mark.parametrize(
17+
"headers, status",
18+
[
19+
({"Authorization": "Bearer devBackendToken"}, 200),
20+
({"Authorization": "Bearer abc"}, 403),
21+
(None, 403),
22+
],
23+
)
24+
async def test_detect_credentials(bot: SirBot, aiohttp_client, headers, status):
25+
bot.plugins["slack"].api.query = VALID_SLACK_RESPONSE
26+
client = await aiohttp_client(bot)
27+
28+
res = await client.get(
29+
"/pybot/api/v1/slack/verify?email=test@test.test", headers=headers
30+
)
31+
32+
assert res.status == status
33+
34+
35+
async def test_verify_returns_correct_success_params(bot: SirBot, aiohttp_client):
36+
client = await aiohttp_client(bot)
37+
38+
bot.plugins["slack"].api.query = VALID_SLACK_RESPONSE
39+
40+
res = await client.get(
41+
"/pybot/api/v1/slack/verify?email=test@test.test", headers=AUTH_HEADER
42+
)
43+
body = json.loads(await res.text())
44+
45+
assert body["exists"] is True
46+
assert body["id"] == MOCK_USER_ID
47+
assert body["displayName"] == MOCK_USER_NAME

0 commit comments

Comments
 (0)