diff --git a/integration_tests/web/test_app_manifest.py b/integration_tests/web/test_app_manifest.py new file mode 100644 index 00000000..c7d841ae --- /dev/null +++ b/integration_tests/web/test_app_manifest.py @@ -0,0 +1,200 @@ +import os +import unittest + +from slack_sdk.web import WebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_operations(self): + token = os.environ["SLACK_SDK_TEST_TOOLING_TOKEN"] # xoxe.xoxp-... + client = WebClient(token) + client.apps_manifest_validate(manifest=STR_MANIFEST) + client.apps_manifest_validate(manifest=DICT_MANIFEST) + + response = client.apps_manifest_create(manifest=STR_MANIFEST) + app_id = response["app_id"] + try: + client.apps_manifest_update(app_id=app_id, manifest=DICT_MANIFEST) + client.apps_manifest_export(app_id=app_id) + finally: + client.apps_manifest_delete(app_id=app_id) + + +STR_MANIFEST = """{ + "display_information": { + "name": "manifest-sandbox" + }, + "features": { + "app_home": { + "home_tab_enabled": true, + "messages_tab_enabled": false, + "messages_tab_read_only_enabled": false + }, + "bot_user": { + "display_name": "manifest-sandbox", + "always_online": true + }, + "shortcuts": [ + { + "name": "message one", + "type": "message", + "callback_id": "m", + "description": "message" + }, + { + "name": "global one", + "type": "global", + "callback_id": "g", + "description": "global" + } + ], + "slash_commands": [ + { + "command": "/hey", + "url": "https://www.example.com/", + "description": "What's up?", + "usage_hint": "What's up?", + "should_escape": true + } + ], + "unfurl_domains": [ + "example.com" + ] + }, + "oauth_config": { + "redirect_urls": [ + "https://www.example.com/foo" + ], + "scopes": { + "user": [ + "search:read", + "channels:read", + "groups:read", + "mpim:read" + ], + "bot": [ + "commands", + "incoming-webhook", + "app_mentions:read", + "links:read" + ] + } + }, + "settings": { + "allowed_ip_address_ranges": [ + "123.123.123.123/32" + ], + "event_subscriptions": { + "request_url": "https://www.example.com/slack/events", + "user_events": [ + "member_joined_channel" + ], + "bot_events": [ + "app_mention", + "link_shared" + ] + }, + "interactivity": { + "is_enabled": true, + "request_url": "https://www.example.com/", + "message_menu_options_url": "https://www.example.com/" + }, + "org_deploy_enabled": true, + "socket_mode_enabled": false, + "token_rotation_enabled": true + } +} +""" + +DICT_MANIFEST = { + "display_information": { + "name": "manifest-sandbox" + }, + "features": { + "app_home": { + "home_tab_enabled": True, + "messages_tab_enabled": False, + "messages_tab_read_only_enabled": False + }, + "bot_user": { + "display_name": "manifest-sandbox", + "always_online": True + }, + "shortcuts": [ + { + "name": "message one", + "type": "message", + "callback_id": "m", + "description": "message" + }, + { + "name": "global one", + "type": "global", + "callback_id": "g", + "description": "global" + } + ], + "slash_commands": [ + { + "command": "/hey", + "url": "https://www.example.com/", + "description": "What's up?", + "usage_hint": "What's up?", + "should_escape": True + } + ], + "unfurl_domains": [ + "example.com" + ] + }, + "oauth_config": { + "redirect_urls": [ + "https://www.example.com/foo" + ], + "scopes": { + "user": [ + "search:read", + "channels:read", + "groups:read", + "mpim:read" + ], + "bot": [ + "commands", + "incoming-webhook", + "app_mentions:read", + "links:read" + ] + } + }, + "settings": { + "allowed_ip_address_ranges": [ + "123.123.123.123/32" + ], + "event_subscriptions": { + "request_url": "https://www.example.com/slack/events", + "user_events": [ + "member_joined_channel" + ], + "bot_events": [ + "app_mention", + "link_shared" + ] + }, + "interactivity": { + "is_enabled": True, + "request_url": "https://www.example.com/", + "message_menu_options_url": "https://www.example.com/" + }, + "org_deploy_enabled": True, + "socket_mode_enabled": False, + "token_rotation_enabled": True + } +} diff --git a/slack_sdk/web/async_client.py b/slack_sdk/web/async_client.py index 15767792..099ba32c 100644 --- a/slack_sdk/web/async_client.py +++ b/slack_sdk/web/async_client.py @@ -1928,6 +1928,91 @@ async def apps_uninstall( kwargs.update({"client_id": client_id, "client_secret": client_secret}) return await self.api_call("apps.uninstall", params=kwargs) + async def apps_manifest_create( + self, + *, + manifest: Union[str, Dict[str, Any]], + **kwargs, + ) -> AsyncSlackResponse: + """Create an app from an app manifest + https://api.slack.com/methods/apps.manifest.create + """ + if isinstance(manifest, str): + kwargs.update({"manifest": manifest}) + else: + kwargs.update({"manifest": json.dumps(manifest)}) + return await self.api_call("apps.manifest.create", params=kwargs) + + async def apps_manifest_delete( + self, + *, + app_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Permanently deletes an app created through app manifests + https://api.slack.com/methods/apps.manifest.delete + """ + kwargs.update({"app_id": app_id}) + return await self.api_call("apps.manifest.delete", params=kwargs) + + async def apps_manifest_export( + self, + *, + app_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Export an app manifest from an existing app + https://api.slack.com/methods/apps.manifest.export + """ + kwargs.update({"app_id": app_id}) + return await self.api_call("apps.manifest.export", params=kwargs) + + async def apps_manifest_update( + self, + *, + app_id: str, + manifest: Union[str, Dict[str, Any]], + **kwargs, + ) -> AsyncSlackResponse: + """Update an app from an app manifest + https://api.slack.com/methods/apps.manifest.update + """ + if isinstance(manifest, str): + kwargs.update({"manifest": manifest}) + else: + kwargs.update({"manifest": json.dumps(manifest)}) + kwargs.update({"app_id": app_id}) + return await self.api_call("apps.manifest.update", params=kwargs) + + async def apps_manifest_validate( + self, + *, + manifest: Union[str, Dict[str, Any]], + app_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Validate an app manifest + https://api.slack.com/methods/apps.manifest.validate + """ + if isinstance(manifest, str): + kwargs.update({"manifest": manifest}) + else: + kwargs.update({"manifest": json.dumps(manifest)}) + kwargs.update({"app_id": app_id}) + return await self.api_call("apps.manifest.validate", params=kwargs) + + async def tooling_tokens_rotate( + self, + *, + refresh_token: str, + **kwargs, + ) -> AsyncSlackResponse: + """Exchanges a refresh token for a new app configuration token + https://api.slack.com/methods/tooling.tokens.rotate + """ + kwargs.update({"refresh_token": refresh_token}) + return await self.api_call("tooling.tokens.rotate", params=kwargs) + async def auth_revoke( self, *, diff --git a/slack_sdk/web/client.py b/slack_sdk/web/client.py index 7bad6fe8..b9e87d97 100644 --- a/slack_sdk/web/client.py +++ b/slack_sdk/web/client.py @@ -1919,6 +1919,91 @@ def apps_uninstall( kwargs.update({"client_id": client_id, "client_secret": client_secret}) return self.api_call("apps.uninstall", params=kwargs) + def apps_manifest_create( + self, + *, + manifest: Union[str, Dict[str, Any]], + **kwargs, + ) -> SlackResponse: + """Create an app from an app manifest + https://api.slack.com/methods/apps.manifest.create + """ + if isinstance(manifest, str): + kwargs.update({"manifest": manifest}) + else: + kwargs.update({"manifest": json.dumps(manifest)}) + return self.api_call("apps.manifest.create", params=kwargs) + + def apps_manifest_delete( + self, + *, + app_id: str, + **kwargs, + ) -> SlackResponse: + """Permanently deletes an app created through app manifests + https://api.slack.com/methods/apps.manifest.delete + """ + kwargs.update({"app_id": app_id}) + return self.api_call("apps.manifest.delete", params=kwargs) + + def apps_manifest_export( + self, + *, + app_id: str, + **kwargs, + ) -> SlackResponse: + """Export an app manifest from an existing app + https://api.slack.com/methods/apps.manifest.export + """ + kwargs.update({"app_id": app_id}) + return self.api_call("apps.manifest.export", params=kwargs) + + def apps_manifest_update( + self, + *, + app_id: str, + manifest: Union[str, Dict[str, Any]], + **kwargs, + ) -> SlackResponse: + """Update an app from an app manifest + https://api.slack.com/methods/apps.manifest.update + """ + if isinstance(manifest, str): + kwargs.update({"manifest": manifest}) + else: + kwargs.update({"manifest": json.dumps(manifest)}) + kwargs.update({"app_id": app_id}) + return self.api_call("apps.manifest.update", params=kwargs) + + def apps_manifest_validate( + self, + *, + manifest: Union[str, Dict[str, Any]], + app_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Validate an app manifest + https://api.slack.com/methods/apps.manifest.validate + """ + if isinstance(manifest, str): + kwargs.update({"manifest": manifest}) + else: + kwargs.update({"manifest": json.dumps(manifest)}) + kwargs.update({"app_id": app_id}) + return self.api_call("apps.manifest.validate", params=kwargs) + + def tooling_tokens_rotate( + self, + *, + refresh_token: str, + **kwargs, + ) -> SlackResponse: + """Exchanges a refresh token for a new app configuration token + https://api.slack.com/methods/tooling.tokens.rotate + """ + kwargs.update({"refresh_token": refresh_token}) + return self.api_call("tooling.tokens.rotate", params=kwargs) + def auth_revoke( self, *, diff --git a/slack_sdk/web/legacy_client.py b/slack_sdk/web/legacy_client.py index 09ff7a89..f636d197 100644 --- a/slack_sdk/web/legacy_client.py +++ b/slack_sdk/web/legacy_client.py @@ -1930,6 +1930,91 @@ def apps_uninstall( kwargs.update({"client_id": client_id, "client_secret": client_secret}) return self.api_call("apps.uninstall", params=kwargs) + def apps_manifest_create( + self, + *, + manifest: Union[str, Dict[str, Any]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Create an app from an app manifest + https://api.slack.com/methods/apps.manifest.create + """ + if isinstance(manifest, str): + kwargs.update({"manifest": manifest}) + else: + kwargs.update({"manifest": json.dumps(manifest)}) + return self.api_call("apps.manifest.create", params=kwargs) + + def apps_manifest_delete( + self, + *, + app_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Permanently deletes an app created through app manifests + https://api.slack.com/methods/apps.manifest.delete + """ + kwargs.update({"app_id": app_id}) + return self.api_call("apps.manifest.delete", params=kwargs) + + def apps_manifest_export( + self, + *, + app_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Export an app manifest from an existing app + https://api.slack.com/methods/apps.manifest.export + """ + kwargs.update({"app_id": app_id}) + return self.api_call("apps.manifest.export", params=kwargs) + + def apps_manifest_update( + self, + *, + app_id: str, + manifest: Union[str, Dict[str, Any]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Update an app from an app manifest + https://api.slack.com/methods/apps.manifest.update + """ + if isinstance(manifest, str): + kwargs.update({"manifest": manifest}) + else: + kwargs.update({"manifest": json.dumps(manifest)}) + kwargs.update({"app_id": app_id}) + return self.api_call("apps.manifest.update", params=kwargs) + + def apps_manifest_validate( + self, + *, + manifest: Union[str, Dict[str, Any]], + app_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Validate an app manifest + https://api.slack.com/methods/apps.manifest.validate + """ + if isinstance(manifest, str): + kwargs.update({"manifest": manifest}) + else: + kwargs.update({"manifest": json.dumps(manifest)}) + kwargs.update({"app_id": app_id}) + return self.api_call("apps.manifest.validate", params=kwargs) + + def tooling_tokens_rotate( + self, + *, + refresh_token: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Exchanges a refresh token for a new app configuration token + https://api.slack.com/methods/tooling.tokens.rotate + """ + kwargs.update({"refresh_token": refresh_token}) + return self.api_call("tooling.tokens.rotate", params=kwargs) + def auth_revoke( self, *, diff --git a/tests/slack_sdk_async/web/test_web_client_coverage.py b/tests/slack_sdk_async/web/test_web_client_coverage.py index eecb5fe2..3f3af516 100644 --- a/tests/slack_sdk_async/web/test_web_client_coverage.py +++ b/tests/slack_sdk_async/web/test_web_client_coverage.py @@ -41,13 +41,6 @@ def setUp(self): "openid.connect.token", "openid.connect.userInfo", "users.setActive", - # TODO: will merge https://github.com/slackapi/python-slack-sdk/pull/1123 once the APIs are GAed - "apps.manifest.create", - "apps.manifest.delete", - "apps.manifest.export", - "apps.manifest.update", - "apps.manifest.validate", - "tooling.tokens.rotate", # automation platform token required ones "apps.activities.list", "apps.auth.external.delete", @@ -86,6 +79,21 @@ async def run_method(self, method_name, method, async_method): method(app_id="AID123", enterprise_id="E111", team_ids=["T1", "T2"])["method"] ) await async_method(app_id="AID123", enterprise_id="E111", team_ids=["T1", "T2"]) + elif method_name == "apps_manifest_create": + self.api_methods_to_call.remove(method(manifest="{}")["method"]) + await async_method(manifest="{}") + elif method_name == "apps_manifest_delete": + self.api_methods_to_call.remove(method(app_id="AID123")["method"]) + await async_method(app_id="AID123") + elif method_name == "apps_manifest_export": + self.api_methods_to_call.remove(method(app_id="AID123")["method"]) + await async_method(app_id="AID123") + elif method_name == "apps_manifest_update": + self.api_methods_to_call.remove(method(app_id="AID123", manifest="{}")["method"]) + await async_method(app_id="AID123", manifest="{}") + elif method_name == "apps_manifest_validate": + self.api_methods_to_call.remove(method(manifest="{}")["method"]) + await async_method(manifest="{}") elif method_name == "admin_apps_requests_cancel": self.api_methods_to_call.remove(method(request_id="XXX", enterprise_id="E111", team_id="T123")["method"]) await async_method(request_id="XXX", enterprise_id="E111", team_id="T123") @@ -676,6 +684,9 @@ async def run_method(self, method_name, method, async_method): elif method_name == "search_messages": self.api_methods_to_call.remove(method(query="Slack")["method"]) await async_method(query="Slack") + elif method_name == "tooling_tokens_rotate": + self.api_methods_to_call.remove(method(refresh_token="xoxe-refresh")["method"]) + await async_method(refresh_token="xoxe-refresh") elif method_name == "usergroups_create": self.api_methods_to_call.remove(method(name="Engineering Team")["method"]) await async_method(name="Engineering Team")