Skip to content

Commit

Permalink
Merge pull request #704 from seratch/calls-api
Browse files Browse the repository at this point in the history
Fix #695 by adding Calls API support
  • Loading branch information
seratch committed Jun 2, 2020
2 parents 43ceaaa + 6a072c2 commit 86f77bc
Show file tree
Hide file tree
Showing 8 changed files with 357 additions and 17 deletions.
5 changes: 1 addition & 4 deletions integration_tests/web/test_admin_usergroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@


class TestWebClient(unittest.TestCase):
"""Runs integration tests with real Slack API
https://github.com/slackapi/python-slackclient/issues/378
"""
"""Runs integration tests with real Slack API"""

def setUp(self):
self.logger = logging.getLogger(__name__)
Expand Down
128 changes: 128 additions & 0 deletions integration_tests/web/test_calls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import logging
import os
import unittest
import uuid

from integration_tests.env_variable_names import SLACK_SDK_TEST_BOT_TOKEN
from integration_tests.helpers import async_test
from slack import WebClient
from slack.web.classes.blocks import CallBlock

class TestWebClient(unittest.TestCase):
"""Runs integration tests with real Slack API"""

def setUp(self):
self.logger = logging.getLogger(__name__)
self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN]
self.sync_client: WebClient = WebClient(token=self.bot_token)
self.async_client: WebClient = WebClient(token=self.bot_token, run_async=True)

def tearDown(self):
pass

def test_sync(self):
client = self.sync_client
user_id = list(filter(
lambda u: not u["deleted"] and "bot_id" not in u,
client.users_list(limit=50)["members"]
))[0]["id"]

new_call = client.calls_add(
external_unique_id=str(uuid.uuid4()),
join_url="https://www.example.com/calls/12345",
users=[
{
"slack_id": user_id
},
{
"external_id": "anon-111",
"avatar_url": "https://assets.brandfolder.com/pmix53-32t4so-a6439g/original/slackbot.png",
"display_name": "anonymous user 1",
}
]
)
self.assertIsNotNone(new_call)
call_id = new_call["call"]["id"]

channel_message = client.chat_postMessage(channel="#random", blocks=[
{
"type": "call",
"call_id": call_id,
}
])
self.assertIsNotNone(channel_message)

channel_message = client.chat_postMessage(channel="#random", blocks=[
CallBlock(call_id=call_id)
])
self.assertIsNotNone(channel_message)

call_info = client.calls_info(id = call_id)
self.assertIsNotNone(call_info)

new_participants = client.calls_participants_add(id=call_id, users=[
{
"external_id": "anon-222",
"avatar_url": "https://assets.brandfolder.com/pmix53-32t4so-a6439g/original/slackbot.png",
"display_name": "anonymous user 2",
}
])
self.assertIsNotNone(new_participants)

modified_call = client.calls_update(id=call_id, join_url="https://www.example.com/calls/99999")
self.assertIsNotNone(modified_call)

ended_call = client.calls_end(id=call_id)
self.assertIsNotNone(ended_call)

@async_test
async def test_async(self):
client = self.async_client
users = await client.users_list(limit=50)
user_id = list(filter(
lambda u: not u["deleted"] and "bot_id" not in u,
users["members"]
))[0]["id"]

new_call = await client.calls_add(
external_unique_id=str(uuid.uuid4()),
join_url="https://www.example.com/calls/12345",
users=[
{
"slack_id": user_id
},
{
"external_id": "anon-111",
"avatar_url": "https://assets.brandfolder.com/pmix53-32t4so-a6439g/original/slackbot.png",
"display_name": "anonymous user 1",
}
]
)
self.assertIsNotNone(new_call)
call_id = new_call["call"]["id"]

channel_message = await client.chat_postMessage(channel="#random", blocks=[
{
"type": "call",
"call_id": call_id,
}
])
self.assertIsNotNone(channel_message)

call_info = await client.calls_info(id = call_id)
self.assertIsNotNone(call_info)

new_participants = await client.calls_participants_add(id=call_id, users=[
{
"external_id": "anon-222",
"avatar_url": "https://assets.brandfolder.com/pmix53-32t4so-a6439g/original/slackbot.png",
"display_name": "anonymous user 2",
}
])
self.assertIsNotNone(new_participants)

modified_call = await client.calls_update(id=call_id, join_url="https://www.example.com/calls/99999")
self.assertIsNotNone(modified_call)

ended_call = await client.calls_end(id=call_id)
self.assertIsNotNone(ended_call)
26 changes: 26 additions & 0 deletions slack/web/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import slack.version as slack_version
from slack.errors import SlackRequestError
from slack.web import convert_bool_to_0_or_1
from slack.web.classes.blocks import Block
from slack.web.slack_response import SlackResponse


Expand Down Expand Up @@ -651,6 +652,31 @@ def validate_slack_signature(
calculated_signature = f"v0={request_hash}"
return hmac.compare_digest(calculated_signature, signature)

@staticmethod
def _parse_blocks(kwargs):
blocks = kwargs.get("blocks", None)

def to_dict(b: Union[Dict, Block]):
if isinstance(b, Block):
return b.to_dict()
return b

if blocks is not None and isinstance(blocks, list):
dict_blocks = [to_dict(b) for b in blocks]
kwargs.update({"blocks": dict_blocks})

@staticmethod
def _update_call_participants(kwargs, users: Union[str, List[Dict[str, str]]]):
if users is None:
return

if isinstance(users, list):
kwargs.update({"users": json.dumps(users)})
elif isinstance(users, str):
kwargs.update({"users": users})
else:
raise SlackRequestError("users must be either str or List[Dict[str, str]]")


# https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api
deprecated_method_prefixes_2020_01 = ["channels.", "groups.", "im.", "mpim."]
Expand Down
29 changes: 28 additions & 1 deletion slack/web/classes/blocks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import copy
import logging
import warnings
from typing import List, Optional, Set, Union
from typing import List, Optional, Set, Union, Dict

from . import JsonObject, JsonValidator, show_unknown_key_warning
from .elements import (
Expand Down Expand Up @@ -347,3 +347,30 @@ def __init__(

self.external_id = external_id
self.source = source


class CallBlock(Block):
type = "call"

@property
def attributes(self) -> Set[str]:
return super().attributes.union({"call_id", "api_decoration_available", "call"})

def __init__(
self,
*,
call_id: str,
api_decoration_available: Optional[bool] = None,
call: Optional[Dict[str, Dict[str, any]]] = None,
block_id: Optional[str] = None,
**others: dict,
):
"""Displays a call information
https://api.slack.com/reference/block-kit/blocks#call
"""
super().__init__(type=self.type, block_id=block_id)
show_unknown_key_warning(self, others)

self.call_id = call_id
self.api_decoration_available = api_decoration_available
self.call = call
63 changes: 61 additions & 2 deletions slack/web/client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"""A Python module for interacting with Slack's Web API."""

import os
from asyncio import Future
from io import IOBase
from typing import Union, List, Optional
from typing import Union, List, Optional, Dict

import slack.errors as e
from slack.web.base_client import BaseClient, SlackResponse
Expand Down Expand Up @@ -473,6 +472,62 @@ def bots_info(self, **kwargs) -> Union[Future, SlackResponse]:
"""Gets information about a bot user."""
return self.api_call("bots.info", http_verb="GET", params=kwargs)

def calls_add(
self, *, external_unique_id: str, join_url: str, **kwargs
) -> Union[Future, SlackResponse]:
"""Registers a new Call.
Args:
external_unique_id (str): An ID supplied by the 3rd-party Call provider.
It must be unique across all Calls from that service.
e.g. '025169F6-E37A-4E62-BB54-7F93A0FC4C1F'
join_url (str): The URL required for a client to join the Call.
e.g. 'https://example.com/calls/1234567890'
"""
kwargs.update({"external_unique_id": external_unique_id, "join_url": join_url})
self._update_call_participants(kwargs, kwargs.get("users", None))
return self.api_call("calls.add", http_verb="POST", params=kwargs)

def calls_end(self, *, id: str, **kwargs) -> Union[Future, SlackResponse]:
"""Ends a Call.
Args:
id (str): id returned when registering the call using the calls.add method.
"""
kwargs.update({"id": id})
return self.api_call("calls.end", http_verb="POST", params=kwargs)

def calls_info(self, *, id: str, **kwargs) -> Union[Future, SlackResponse]:
"""Returns information about a Call.
Args:
id (str): id returned when registering the call using the calls.add method.
"""
kwargs.update({"id": id})
return self.api_call("calls.info", http_verb="POST", params=kwargs)

def calls_participants_add(
self, *, id: str, users: Union[str, List[Dict[str, str]]], **kwargs
) -> Union[Future, SlackResponse]:
"""Registers new participants added to a Call.
Args:
id (str): id returned when registering the call using the calls.add method.
users: (list): The list of users to add as participants in the Call.
"""
kwargs.update({"id": id})
self._update_call_participants(kwargs, users)
return self.api_call("calls.participants.add", http_verb="POST", params=kwargs)

def calls_update(self, *, id: str, **kwargs) -> Union[Future, SlackResponse]:
"""Updates information about a Call.
Args:
id (str): id returned by the calls.add method.
"""
kwargs.update({"id": id})
return self.api_call("calls.update", http_verb="POST", params=kwargs)

def channels_archive(
self, *, channel: str, **kwargs
) -> Union[Future, SlackResponse]:
Expand Down Expand Up @@ -696,6 +751,7 @@ def chat_postEphemeral(
e.g. [{"type": "section", "text": {"type": "plain_text", "text": "Hello world"}}]
"""
kwargs.update({"channel": channel, "user": user})
self._parse_blocks(kwargs)
return self.api_call("chat.postEphemeral", json=kwargs)

def chat_postMessage(
Expand All @@ -712,6 +768,7 @@ def chat_postMessage(
e.g. [{"type": "section", "text": {"type": "plain_text", "text": "Hello world"}}]
"""
kwargs.update({"channel": channel})
self._parse_blocks(kwargs)
return self.api_call("chat.postMessage", json=kwargs)

def chat_scheduleMessage(
Expand All @@ -725,6 +782,7 @@ def chat_scheduleMessage(
text (str): The message you'd like to send. e.g. 'Hello world'
"""
kwargs.update({"channel": channel, "post_at": post_at, "text": text})
self._parse_blocks(kwargs)
return self.api_call("chat.scheduleMessage", json=kwargs)

def chat_unfurl(
Expand Down Expand Up @@ -756,6 +814,7 @@ def chat_update(
e.g. [{"type": "section", "text": {"type": "plain_text", "text": "Hello world"}}]
"""
kwargs.update({"channel": channel, "ts": ts})
self._parse_blocks(kwargs)
return self.api_call("chat.update", json=kwargs)

def chat_scheduledMessages_list(self, **kwargs) -> Union[Future, SlackResponse]:
Expand Down

0 comments on commit 86f77bc

Please sign in to comment.